fix dual device and calendar display bug
This commit is contained in:
@@ -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*
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
DUT="CGW453"
|
|
||||||
REF="CGW452"
|
|
||||||
+24
-11
@@ -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
@@ -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],
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user