User asked for one meter per badblocks pattern. The drawer now shows
4 meters (one per pattern: 0xaa / 0x55 / 0xff / 0x00), each split
into write (left, blue) + verify (right, green) halves so a glance
shows both which pattern is current AND whether you're writing or
verifying within it.
Backend:
- New columns burnin_stages.bb_phase (1-8) + bb_phase_pct (0-100)
via idempotent ALTER TABLE migration
- _update_stage_bb_phase() helper called from the badblocks parser
on every tick (when phase or percent changes)
- /api/v1/drives/{id}/drawer SELECT now returns the new fields
Frontend (app.js + app.css):
- _drawerRenderBadblocksMeters(phase, phasePct) computes per-pattern
fill state and emits 4-meter HTML with W/V sub-labels
- Conditional render: only shows when stage_name === 'surface_validate'
AND bb_phase is set, so historical pre-1.0.0-44 stage rows render
unchanged (single percent, no meters)
3 new tests cover the migration columns, single-tick persistence,
and overwrite-on-second-tick. Total suite: 75 tests.
Image rebuilt and tagged but NOT deployed — 4 burn-ins are running
right now and a recreate would SIGHUP them. Deploy with
`docker compose up -d` after the current batch finishes; the
migration runs at init and the meters light up for the next batch.
100 lines
3.4 KiB
Python
100 lines
3.4 KiB
Python
"""Verifies _update_stage_bb_phase actually writes to burnin_stages
|
|
and the migration adds the columns idempotently.
|
|
|
|
The drive-drawer's 4-meter UI depends on these columns being populated
|
|
on every parser tick. If a future refactor drops the call or breaks
|
|
the migration, this test catches it before users see the meters
|
|
go blank.
|
|
|
|
Run inside the container image so app deps are present.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import tempfile
|
|
import unittest
|
|
|
|
import aiosqlite
|
|
|
|
|
|
async def _setup_db_with_stage() -> str:
|
|
fd, path = tempfile.mkstemp(suffix=".db")
|
|
os.close(fd)
|
|
from app.config import settings
|
|
settings.db_path = path
|
|
|
|
from app.database import init_db
|
|
await init_db()
|
|
|
|
async with aiosqlite.connect(path) as db:
|
|
await db.execute(
|
|
"INSERT INTO drives "
|
|
"(truenas_disk_id, devname, serial, model, size_bytes, "
|
|
" temperature_c, smart_health, last_seen_at, last_polled_at) "
|
|
"VALUES ('id-1', 'sda', 'SER1', 'TestModel', 14000000000000, "
|
|
" 30, 'PASSED', '2026-05-09T00:00:00+00:00', "
|
|
" '2026-05-09T00:00:00+00:00')"
|
|
)
|
|
await db.execute(
|
|
"INSERT INTO burnin_jobs "
|
|
"(drive_id, profile, state, operator, created_at) "
|
|
"VALUES (1, 'surface', 'running', 'op', "
|
|
" '2026-05-09T00:00:00+00:00')"
|
|
)
|
|
await db.execute(
|
|
"INSERT INTO burnin_stages "
|
|
"(burnin_job_id, stage_name, state) "
|
|
"VALUES (1, 'surface_validate', 'running')"
|
|
)
|
|
await db.commit()
|
|
return path
|
|
|
|
|
|
class TestBBPhasePersistence(unittest.IsolatedAsyncioTestCase):
|
|
|
|
async def asyncSetUp(self):
|
|
self.path = await _setup_db_with_stage()
|
|
|
|
async def asyncTearDown(self):
|
|
try:
|
|
os.unlink(self.path)
|
|
except OSError:
|
|
pass
|
|
|
|
async def test_columns_exist_after_init(self):
|
|
async with aiosqlite.connect(self.path) as db:
|
|
cur = await db.execute("PRAGMA table_info(burnin_stages)")
|
|
cols = {r[1] for r in await cur.fetchall()}
|
|
self.assertIn("bb_phase", cols)
|
|
self.assertIn("bb_phase_pct", cols)
|
|
|
|
async def test_update_writes_phase_and_pct(self):
|
|
from app.burnin._common import _update_stage_bb_phase
|
|
await _update_stage_bb_phase(1, "surface_validate", 3, 47.5)
|
|
async with aiosqlite.connect(self.path) as db:
|
|
cur = await db.execute(
|
|
"SELECT bb_phase, bb_phase_pct FROM burnin_stages "
|
|
"WHERE burnin_job_id=1 AND stage_name='surface_validate'"
|
|
)
|
|
row = await cur.fetchone()
|
|
self.assertEqual(row[0], 3)
|
|
self.assertAlmostEqual(row[1], 47.5)
|
|
|
|
async def test_update_overwrites(self):
|
|
"""Each tick should replace the previous value, not accumulate."""
|
|
from app.burnin._common import _update_stage_bb_phase
|
|
await _update_stage_bb_phase(1, "surface_validate", 1, 10.0)
|
|
await _update_stage_bb_phase(1, "surface_validate", 2, 80.0)
|
|
async with aiosqlite.connect(self.path) as db:
|
|
cur = await db.execute(
|
|
"SELECT bb_phase, bb_phase_pct FROM burnin_stages "
|
|
"WHERE burnin_job_id=1 AND stage_name='surface_validate'"
|
|
)
|
|
row = await cur.fetchone()
|
|
self.assertEqual(row[0], 2)
|
|
self.assertAlmostEqual(row[1], 80.0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|