diff --git a/.gitignore b/.gitignore index ded0387..b479b1c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /backend/__pycache__ /backend/_run_sched_test.py /backend/output.txt +/backend/.env IMPLEMENTATIONPLAN.md scheduler.db scheduler.db* \ No newline at end of file diff --git a/backend/.env b/backend/.env deleted file mode 100644 index bc8f05d..0000000 --- a/backend/.env +++ /dev/null @@ -1,2 +0,0 @@ -DUT="CGW453" -REF="CGW452" \ No newline at end of file diff --git a/backend/db.py b/backend/db.py index 3927705..225ecf3 100644 --- a/backend/db.py +++ b/backend/db.py @@ -123,14 +123,6 @@ def init_db(db_path: str | Path = DB_PATH) -> None: 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_priority ON tests(priority); 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 JOIN tests t ON t.test_id = s.test_id AND t.device = s.device WHERE s.schedule_version = ? - AND s.scheduled_date >= ? - AND s.scheduled_date < date(?, '+7 day') + AND ( + ( + 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 """, - (latest, start_date, start_date), + ( + latest, + start_date, + start_date, + start_date, + start_date, + start_date, + ), ).fetchall() result: list[ScheduleRow] = [] diff --git a/backend/scheduler.py b/backend/scheduler.py index e3494bf..6832feb 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -293,7 +293,7 @@ class Scheduler: for day, shift_idx in window.shifts ] current_shift_pos = 0 - sequence_in_shift = 0 + sequence_in_shift = [0] * len(window.shifts) for bundle in bundles: bundle_key = (bundle.index, bundle.device) @@ -309,26 +309,39 @@ class Scheduler: 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] < test_minutes: + while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] <= 0: current_shift_pos += 1 - sequence_in_shift = 0 if current_shift_pos >= len(window.shifts): break - shift_day, shift_idx = window.shifts[current_shift_pos] - sequence_in_shift += 1 - shift_remaining[current_shift_pos] -= test_minutes + if sum(shift_remaining[current_shift_pos:]) < test_minutes: + break + + 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 + while current_shift_pos < len(window.shifts) and shift_remaining[current_shift_pos] <= 0: + current_shift_pos += 1 + self.schedule.append( ScheduleEntry( test_id=test, device=bundle.device, scheduled_date=str(shift_day), shift_index=shift_idx, - sequence_in_shift=sequence_in_shift, + sequence_in_shift=sequence_in_shift[start_shift_pos], ) ) diff --git a/backend/test_runtime_overrides.py b/backend/test_runtime_overrides.py deleted file mode 100644 index 5757ba7..0000000 --- a/backend/test_runtime_overrides.py +++ /dev/null @@ -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() \ No newline at end of file diff --git a/backend/test_scheduler_dual_device.py b/backend/test_scheduler_dual_device.py deleted file mode 100644 index cd70e0c..0000000 --- a/backend/test_scheduler_dual_device.py +++ /dev/null @@ -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() diff --git a/frontend/src/components/DayColumn.jsx b/frontend/src/components/DayColumn.jsx index 120349f..e360dee 100644 --- a/frontend/src/components/DayColumn.jsx +++ b/frontend/src/components/DayColumn.jsx @@ -18,11 +18,16 @@ export default function DayColumn({ const dayName = DAY_NAMES[date.getDay()] const dayNum = date.getDate() const dateKey = date.toISOString().slice(0, 10) + const nextDateKey = new Date(date.getTime() + (24 * 60 * 60 * 1000)).toISOString().slice(0, 10) function getWindowForShift(shiftIndex) { 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)) return ( @@ -65,7 +70,7 @@ export default function DayColumn({ tests={nextDayShift1} visible={true} active={nextDayShift1Active} - windowDetails={null} + windowDetails={getNextDayShift1Window()} minContentRows={shiftMinRows.nextDayShift1} slotHeightPx={slotHeights.nextDayShift1} onWindowSelect={onWindowSelect} diff --git a/frontend/src/components/SettingsModal.jsx b/frontend/src/components/SettingsModal.jsx index cb41c11..924fb71 100644 --- a/frontend/src/components/SettingsModal.jsx +++ b/frontend/src/components/SettingsModal.jsx @@ -259,9 +259,9 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) { Holidays

- set('holidays', e.target.value)} diff --git a/frontend/src/components/ShiftSlot.jsx b/frontend/src/components/ShiftSlot.jsx index e7a2551..3bc5756 100644 --- a/frontend/src/components/ShiftSlot.jsx +++ b/frontend/src/components/ShiftSlot.jsx @@ -42,7 +42,12 @@ export default function ShiftSlot({ style={{ minHeight: `${minContentHeightPx}px` }} > {tests.length === 0 ? null : ( - tests.map((test) => ) + tests.map((test, index) => ( + + )) )} diff --git a/frontend/src/components/TestWindowDetailsPanel.jsx b/frontend/src/components/TestWindowDetailsPanel.jsx index fefe262..e2275e4 100644 --- a/frontend/src/components/TestWindowDetailsPanel.jsx +++ b/frontend/src/components/TestWindowDetailsPanel.jsx @@ -183,6 +183,10 @@ function TestRow({ test }) { export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose }) { const [showConfigMap, setShowConfigMap] = useState(false) + const windowTests = useMemo( + () => (Array.isArray(windowDetails?.tests) ? windowDetails.tests : []), + [windowDetails], + ) const windowTestConfig = useMemo(() => getWindowTestConfig(windowDetails), [windowDetails]) const windowConfigMapKey = useMemo( () => (windowTestConfig.length > 0 ? String(windowTestConfig[0]).toUpperCase() : null), @@ -311,12 +315,15 @@ export default function TestWindowDetailsPanel({ isOpen, windowDetails, onClose

Tests Scheduled In This Window

- {windowDetails.tests?.length ?? 0} tests + {windowTests.length} tests
- {windowDetails.tests?.length ? ( + {windowTests.length ? (
- {windowDetails.tests.map((test) => ( - + {windowTests.map((test, index) => ( + ))}
) : (