fix dual device and calendar display bug

This commit is contained in:
2026-07-13 04:02:23 -04:00
parent 42f75fecef
commit 96a6309ff6
10 changed files with 71 additions and 202 deletions
+1
View File
@@ -2,6 +2,7 @@
/backend/__pycache__ /backend/__pycache__
/backend/_run_sched_test.py /backend/_run_sched_test.py
/backend/output.txt /backend/output.txt
/backend/.env
IMPLEMENTATIONPLAN.md IMPLEMENTATIONPLAN.md
scheduler.db scheduler.db
scheduler.db* scheduler.db*
-2
View File
@@ -1,2 +0,0 @@
DUT="CGW453"
REF="CGW452"
+24 -11
View File
@@ -123,14 +123,6 @@ def init_db(db_path: str | Path = DB_PATH) -> None:
minutes INTEGER NOT NULL minutes INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS rerun_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
detected_date TEXT NOT NULL,
failed_test_ids_json TEXT NOT NULL,
estimated_rerun_minutes INTEGER NOT NULL,
rerun_during_day INTEGER
);
CREATE INDEX IF NOT EXISTS idx_tests_status ON tests(status); CREATE INDEX IF NOT EXISTS idx_tests_status ON tests(status);
CREATE INDEX IF NOT EXISTS idx_tests_priority ON tests(priority); CREATE INDEX IF NOT EXISTS idx_tests_priority ON tests(priority);
CREATE INDEX IF NOT EXISTS idx_schedules_date_shift ON schedules(scheduled_date, shift_index); CREATE INDEX IF NOT EXISTS idx_schedules_date_shift ON schedules(scheduled_date, shift_index);
@@ -503,11 +495,32 @@ def get_schedule_week(start_date: str, db_path: str | Path = DB_PATH) -> list[Sc
FROM schedules s FROM schedules s
JOIN tests t ON t.test_id = s.test_id AND t.device = s.device JOIN tests t ON t.test_id = s.test_id AND t.device = s.device
WHERE s.schedule_version = ? WHERE s.schedule_version = ?
AND s.scheduled_date >= ? AND (
AND s.scheduled_date < date(?, '+7 day') (
s.scheduled_date >= ?
AND s.scheduled_date < date(?, '+7 day')
AND s.shift_index IN (2, 3)
)
OR (
s.scheduled_date > ?
AND s.scheduled_date < date(?, '+7 day')
AND s.shift_index = 1
)
OR (
s.scheduled_date = date(?, '+7 day')
AND s.shift_index = 1
)
)
ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift ORDER BY s.scheduled_date, s.shift_index, s.sequence_in_shift
""", """,
(latest, start_date, start_date), (
latest,
start_date,
start_date,
start_date,
start_date,
start_date,
),
).fetchall() ).fetchall()
result: list[ScheduleRow] = [] result: list[ScheduleRow] = []
+21 -8
View File
@@ -293,7 +293,7 @@ class Scheduler:
for day, shift_idx in window.shifts for day, shift_idx in window.shifts
] ]
current_shift_pos = 0 current_shift_pos = 0
sequence_in_shift = 0 sequence_in_shift = [0] * len(window.shifts)
for bundle in bundles: for bundle in bundles:
bundle_key = (bundle.index, bundle.device) bundle_key = (bundle.index, bundle.device)
@@ -309,26 +309,39 @@ class Scheduler:
test_minutes = self._get_test_minutes(test, bundle.device) test_minutes = self._get_test_minutes(test, bundle.device)
# Advance past any shift that can't fit this test while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] <= 0:
while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] < test_minutes:
current_shift_pos += 1 current_shift_pos += 1
sequence_in_shift = 0
if current_shift_pos >= len(window.shifts): if current_shift_pos >= len(window.shifts):
break break
shift_day, shift_idx = window.shifts[current_shift_pos] if sum(shift_remaining[current_shift_pos:]) < test_minutes:
sequence_in_shift += 1 break
shift_remaining[current_shift_pos] -= test_minutes
start_shift_pos = current_shift_pos
shift_day, shift_idx = window.shifts[start_shift_pos]
sequence_in_shift[start_shift_pos] += 1
remaining_test_minutes = test_minutes
consume_shift_pos = start_shift_pos
while remaining_test_minutes > 0 and consume_shift_pos < len(window.shifts):
consumed_minutes = min(shift_remaining[consume_shift_pos], remaining_test_minutes)
shift_remaining[consume_shift_pos] -= consumed_minutes
remaining_test_minutes -= consumed_minutes
consume_shift_pos += 1
window.remaining_minutes -= test_minutes window.remaining_minutes -= test_minutes
while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] <= 0:
current_shift_pos += 1
self.schedule.append( self.schedule.append(
ScheduleEntry( ScheduleEntry(
test_id=test, test_id=test,
device=bundle.device, device=bundle.device,
scheduled_date=str(shift_day), scheduled_date=str(shift_day),
shift_index=shift_idx, shift_index=shift_idx,
sequence_in_shift=sequence_in_shift, sequence_in_shift=sequence_in_shift[start_shift_pos],
) )
) )
-62
View File
@@ -1,62 +0,0 @@
from __future__ import annotations
import csv
from pathlib import Path
import sys
from tempfile import TemporaryDirectory
import unittest
sys.path.append(str(Path(__file__).resolve().parent))
from db import DEVICE_DUT, DEVICE_REF
from parser import P2P_COE_REQUIRED_COLUMNS, parse_target_csv
def _write_p2p_csv(directory: str, test_id: str = "P2PRXAX001") -> Path:
path = Path(directory) / "runtime-overrides.csv"
row = {column: "" for column in P2P_COE_REQUIRED_COLUMNS}
row["Priority"] = "1"
row["Index"] = "1"
row["TC ID"] = test_id
with path.open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=P2P_COE_REQUIRED_COLUMNS)
writer.writeheader()
writer.writerow(row)
return path
class RuntimeOverridesParseTests(unittest.TestCase):
def test_parse_target_csv_applies_device_specific_runtime_overrides(self) -> None:
with TemporaryDirectory() as tmpdir:
csv_path = _write_p2p_csv(tmpdir)
parsed = parse_target_csv(
csv_path,
runtime_overrides={
DEVICE_DUT: {"P2P": 91},
DEVICE_REF: {"P2P": 123},
},
)
by_device = {record.device: record for record in parsed.tests}
self.assertEqual(by_device[DEVICE_DUT].estimated_minutes, 91)
self.assertEqual(by_device[DEVICE_REF].estimated_minutes, 123)
def test_parse_target_csv_keeps_legacy_runtime_override_for_both_devices(self) -> None:
with TemporaryDirectory() as tmpdir:
csv_path = _write_p2p_csv(tmpdir)
parsed = parse_target_csv(
csv_path,
runtime_overrides={"P2P": 88},
)
by_device = {record.device: record for record in parsed.tests}
self.assertEqual(by_device[DEVICE_DUT].estimated_minutes, 88)
self.assertEqual(by_device[DEVICE_REF].estimated_minutes, 88)
if __name__ == "__main__":
unittest.main()
-111
View File
@@ -1,111 +0,0 @@
from __future__ import annotations
from pathlib import Path
import sys
import unittest
sys.path.append(str(Path(__file__).resolve().parent))
from scheduler import DUT, REF, Scheduler
from test_bundle import Test
def _test_config() -> dict[str, dict[str, str | None]]:
return {
"Station 1": {
"test_point": "T1D",
"sta": "STA5",
}
}
def _make_test(test_id: str, device: str, minutes: int) -> Test:
return Test(
test_id=test_id,
device=device,
test_type="P2P",
rotation=None,
rx_tx=None,
has_coe_pair=False,
coe_pairing=[],
config=_test_config(),
throttled=False,
estimated_minutes=minutes,
)
class SchedulerDualDeviceWeekendStartTests(unittest.TestCase):
def _first_window_devices(self, entries, start_date: str) -> set[str]:
return {
entry.device
for entry in entries
if entry.scheduled_date == start_date and entry.shift_index == 3
}
def test_weekend_start_window_stays_single_device_when_disabled(self) -> None:
start_date = "2026-07-10" # Friday
tests = [
_make_test("P2PRXAX001", DUT, 900),
_make_test("P2PRXAX001", REF, 300),
]
scheduler = Scheduler(
tests=tests,
top_priority_tests=set(),
start_date=start_date,
holiday_dates=set(),
dual_device_weekend_start_enabled=False,
)
err = scheduler.compile_schedule()
self.assertIsNone(err)
entries = scheduler.get_schedule()
self.assertEqual(self._first_window_devices(entries, start_date), {DUT})
def test_weekend_start_window_runs_both_devices_when_enabled(self) -> None:
start_date = "2026-07-10" # Friday
tests = [
_make_test("P2PRXAX001", DUT, 900),
_make_test("P2PRXAX001", REF, 300),
]
scheduler = Scheduler(
tests=tests,
top_priority_tests=set(),
start_date=start_date,
holiday_dates=set(),
dual_device_weekend_start_enabled=True,
)
err = scheduler.compile_schedule()
self.assertIsNone(err)
entries = scheduler.get_schedule()
self.assertEqual(self._first_window_devices(entries, start_date), {DUT, REF})
def test_holiday_adjusted_weekend_start_runs_both_devices(self) -> None:
start_date = "2026-07-09" # Thursday with Friday as holiday => weekend-start window
holidays = {"2026-07-10"}
tests = [
_make_test("P2PRXAX001", DUT, 700),
_make_test("P2PRXAX001", REF, 400),
]
scheduler = Scheduler(
tests=tests,
top_priority_tests=set(),
start_date=start_date,
holiday_dates=holidays,
dual_device_weekend_start_enabled=True,
)
err = scheduler.compile_schedule()
self.assertIsNone(err)
entries = scheduler.get_schedule()
self.assertEqual(self._first_window_devices(entries, start_date), {DUT, REF})
if __name__ == "__main__":
unittest.main()
+6 -1
View File
@@ -18,11 +18,16 @@ export default function DayColumn({
const dayName = DAY_NAMES[date.getDay()] const dayName = DAY_NAMES[date.getDay()]
const dayNum = date.getDate() const dayNum = date.getDate()
const dateKey = date.toISOString().slice(0, 10) const dateKey = date.toISOString().slice(0, 10)
const nextDateKey = new Date(date.getTime() + (24 * 60 * 60 * 1000)).toISOString().slice(0, 10)
function getWindowForShift(shiftIndex) { function getWindowForShift(shiftIndex) {
return windowLookup.get(`${dateKey}::shift${shiftIndex}`) ?? null return windowLookup.get(`${dateKey}::shift${shiftIndex}`) ?? null
} }
function getNextDayShift1Window() {
return windowLookup.get(`${nextDateKey}::shift1`) ?? null
}
const shift2Visible = showShift2 || shifts.shift2.length > 0 || Boolean(getWindowForShift(2)) const shift2Visible = showShift2 || shifts.shift2.length > 0 || Boolean(getWindowForShift(2))
return ( return (
@@ -65,7 +70,7 @@ export default function DayColumn({
tests={nextDayShift1} tests={nextDayShift1}
visible={true} visible={true}
active={nextDayShift1Active} active={nextDayShift1Active}
windowDetails={null} windowDetails={getNextDayShift1Window()}
minContentRows={shiftMinRows.nextDayShift1} minContentRows={shiftMinRows.nextDayShift1}
slotHeightPx={slotHeights.nextDayShift1} slotHeightPx={slotHeights.nextDayShift1}
onWindowSelect={onWindowSelect} onWindowSelect={onWindowSelect}
+2 -2
View File
@@ -259,9 +259,9 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
Holidays Holidays
</p> </p>
<Field label="Holiday Dates" hint="(comma-separated, YYYY-MM-DD)"> <Field label="Holiday Dates" hint="(comma-separated, YYYY-MM-DD)">
<input <textarea
type="text"
className={INPUT_CLS} className={INPUT_CLS}
rows={4}
placeholder="2026-07-04, 2026-12-25…" placeholder="2026-07-04, 2026-12-25…"
value={form.holidays ?? ''} value={form.holidays ?? ''}
onChange={(e) => set('holidays', e.target.value)} onChange={(e) => set('holidays', e.target.value)}
+6 -1
View File
@@ -42,7 +42,12 @@ export default function ShiftSlot({
style={{ minHeight: `${minContentHeightPx}px` }} style={{ minHeight: `${minContentHeightPx}px` }}
> >
{tests.length === 0 ? null : ( {tests.length === 0 ? null : (
tests.map((test) => <TestCard key={`${test.test_id}-${test.device}`} test={test} />) tests.map((test, index) => (
<TestCard
key={`${test.test_id}-${test.device}-${test.scheduled_date ?? 'na'}-${test.shift_index ?? 'na'}-${test.sequence_in_shift ?? index}`}
test={test}
/>
))
)} )}
</div> </div>
</div> </div>
@@ -183,6 +183,10 @@ function TestRow({ test }) {
export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose }) { export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose }) {
const [showConfigMap, setShowConfigMap] = useState(false) const [showConfigMap, setShowConfigMap] = useState(false)
const windowTests = useMemo(
() => (Array.isArray(windowDetails?.tests) ? windowDetails.tests : []),
[windowDetails],
)
const windowTestConfig = useMemo(() => getWindowTestConfig(windowDetails), [windowDetails]) const windowTestConfig = useMemo(() => getWindowTestConfig(windowDetails), [windowDetails])
const windowConfigMapKey = useMemo( const windowConfigMapKey = useMemo(
() => (windowTestConfig.length > 0 ? String(windowTestConfig[0]).toUpperCase() : null), () => (windowTestConfig.length > 0 ? String(windowTestConfig[0]).toUpperCase() : null),
@@ -311,12 +315,15 @@ export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose
<div className="mt-5"> <div className="mt-5">
<div className="flex items-center justify-between gap-3 mb-3"> <div className="flex items-center justify-between gap-3 mb-3">
<p className="text-sm font-semibold text-white">Tests Scheduled In This Window</p> <p className="text-sm font-semibold text-white">Tests Scheduled In This Window</p>
<span className="text-xs text-gray-500">{windowDetails.tests?.length ?? 0} tests</span> <span className="text-xs text-gray-500">{windowTests.length} tests</span>
</div> </div>
{windowDetails.tests?.length ? ( {windowTests.length ? (
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{windowDetails.tests.map((test) => ( {windowTests.map((test, index) => (
<TestRow key={`${test.test_id}-${test.device}-${test.scheduled_date}-${test.shift_index}`} test={test} /> <TestRow
key={`${test.test_id}-${test.device}-${test.scheduled_date ?? 'na'}-${test.shift_index ?? 'na'}-${test.sequence_in_shift ?? index}`}
test={test}
/>
))} ))}
</div> </div>
) : ( ) : (