`badblocks -w` cycles through 4 patterns (0xaa, 0x55, 0xff, 0x00), each with a write phase + a verify phase = 8 phases. The output's "XX% done" lines are per-phase, so the dashboard appeared to "rewind" every ~2 hours. Two drives racing each other could look 4× apart in displayed progress despite identical hardware — actually one was just further into a later phase. New _BadblocksProgress state machine watches for "Testing with pattern 0xXX" and "Reading and comparing" headers, advances the phase counter, and reports overall = ((phase-1) * 100 + phase_pct) / 8 clipped to 99. Pure state machine, no I/O. 7 new tests cover phase-header detection, boundary math, monotonicity across a synthetic stream, and the original "two drives at same per-phase % look identical" bug. Image rebuilt and tagged but NOT deployed to the running container — 4 surface-validate jobs are 20-95% through 14TB drives and a recreate would SIGHUP the remote badblocks processes. Deploy with `docker compose up -d` after the current batch finishes.
125 lines
4.7 KiB
Python
125 lines
4.7 KiB
Python
"""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()
|