"""Verifies _BadblocksProgress translates per-phase badblocks output into a monotonic 0-99% overall progress. `badblocks -w` cycles through 4 patterns × {write, verify} = 8 phases. Each phase prints "XX% done" relative to its own 0-100 range. Without this translation the dashboard appeared to "rewind" every ~2 hours when a new phase started — and two drives racing each other could look 4× apart in displayed progress despite identical hardware. Run inside the container image so app deps are present. """ from __future__ import annotations import unittest from app.burnin.stages import _BadblocksProgress class TestBadblocksProgress(unittest.TestCase): def test_default_phase_one(self): """Before any header, treat as start of pattern-1 write.""" p = _BadblocksProgress() self.assertEqual(p.phase, 1) self.assertEqual(p.overall_pct, 0) def test_pattern_headers_set_phase(self): """0xaa→1, 0x55→3, 0xff→5, 0x00→7 (write phases).""" p = _BadblocksProgress() for header, want in [ ("Testing with pattern 0xaa: ", 1), ("Testing with pattern 0x55: ", 3), ("Testing with pattern 0xff: ", 5), ("Testing with pattern 0x00: ", 7), ]: p.update(header) self.assertEqual(p.phase, want, f"after {header!r}") def test_verify_advances_to_next_phase(self): """`Reading and comparing` after `Testing with pattern 0x55` (phase 3) advances to phase 4.""" p = _BadblocksProgress() p.update("Testing with pattern 0x55: 100.00% done") self.assertEqual(p.phase, 3) p.update("Reading and comparing: 0.00% done") self.assertEqual(p.phase, 4) def test_overall_pct_at_phase_boundaries(self): """Verify the math at each phase boundary: phase N at 100% = N * 12.5% overall (clipped to 99 at the end).""" cases = [ (1, 0.0, 0), # start of run (1, 100.0, 12), # 100/800 = 12.5 (2, 100.0, 25), # 200/800 (4, 100.0, 50), # 400/800 (7, 100.0, 87), # 700/800 (8, 100.0, 99), # 800/800 → clipped to 99 ] for phase, phase_pct, want in cases: p = _BadblocksProgress() p.phase = phase p.phase_pct = phase_pct self.assertEqual( p.overall_pct, want, f"phase={phase} phase_pct={phase_pct}", ) def test_realistic_sequence(self): """End-to-end: feed a synthetic badblocks output stream and check the overall percent stays monotonically non-decreasing.""" lines = [ "Testing with pattern 0xaa: ", "10.00% done, 1:00:00 elapsed. (0/0/0 errors)", "50.00% done, 5:00:00 elapsed. (0/0/0 errors)", "99.99% done, 10:00:00 elapsed. (0/0/0 errors)", "Reading and comparing: ", "0.00% done, 10:00:01 elapsed. (0/0/0 errors)", "50.00% done, 12:30:00 elapsed. (0/0/0 errors)", "Testing with pattern 0x55: ", "0.00% done, 15:00:00 elapsed. (0/0/0 errors)", "50.00% done, 17:30:00 elapsed. (0/0/0 errors)", ] p = _BadblocksProgress() seen = [] for line in lines: p.update(line) seen.append(p.overall_pct) self.assertEqual( seen, sorted(seen), f"progress went backwards: {seen}", ) # Sanity: by the time we're halfway through pattern-2 write # (phase 3, 50%), we should report ((3-1)*100 + 50) / 8 = 31%. self.assertEqual(seen[-1], 31) def test_drives_at_different_phases_show_different_overall(self): """The original bug: two drives at the same per-phase 60% but different phases used to look identical (both '60%'). Now they correctly diverge.""" slow = _BadblocksProgress() slow.update("Testing with pattern 0xaa: ") slow.update("60.00% done") fast = _BadblocksProgress() fast.update("Testing with pattern 0xaa: ") fast.update("99.99% done") fast.update("Reading and comparing: ") fast.update("60.00% done") # slow: 60/800 = 7%; fast: (1*100 + 60)/800 = 20% self.assertEqual(slow.overall_pct, 7) self.assertEqual(fast.overall_pct, 20) def test_unknown_pattern_does_not_crash(self): """An unrecognized pattern (e.g. badblocks future versions or custom patterns) just leaves phase unchanged.""" p = _BadblocksProgress() p.update("Testing with pattern 0xab: ") # phase stays at the default 1 self.assertEqual(p.phase, 1) if __name__ == "__main__": unittest.main()