fix dual device and calendar display bug
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
/backend/__pycache__
|
||||
/backend/_run_sched_test.py
|
||||
/backend/output.txt
|
||||
/backend/.env
|
||||
IMPLEMENTATIONPLAN.md
|
||||
scheduler.db
|
||||
scheduler.db*
|
||||
@@ -1,2 +0,0 @@
|
||||
DUT="CGW453"
|
||||
REF="CGW452"
|
||||
+23
-10
@@ -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 >= ?
|
||||
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] = []
|
||||
|
||||
+21
-8
@@ -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],
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -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 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}
|
||||
|
||||
@@ -259,9 +259,9 @@ export default function SettingsModal({ isOpen, onClose, settings, onSave }) {
|
||||
Holidays
|
||||
</p>
|
||||
<Field label="Holiday Dates" hint="(comma-separated, YYYY-MM-DD)">
|
||||
<input
|
||||
type="text"
|
||||
<textarea
|
||||
className={INPUT_CLS}
|
||||
rows={4}
|
||||
placeholder="2026-07-04, 2026-12-25…"
|
||||
value={form.holidays ?? ''}
|
||||
onChange={(e) => set('holidays', e.target.value)}
|
||||
|
||||
@@ -42,7 +42,12 @@ export default function ShiftSlot({
|
||||
style={{ minHeight: `${minContentHeightPx}px` }}
|
||||
>
|
||||
{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>
|
||||
|
||||
@@ -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
|
||||
<div className="mt-5">
|
||||
<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>
|
||||
<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>
|
||||
{windowDetails.tests?.length ? (
|
||||
{windowTests.length ? (
|
||||
<div className="flex flex-col gap-3">
|
||||
{windowDetails.tests.map((test) => (
|
||||
<TestRow key={`${test.test_id}-${test.device}-${test.scheduled_date}-${test.shift_index}`} test={test} />
|
||||
{windowTests.map((test, index) => (
|
||||
<TestRow
|
||||
key={`${test.test_id}-${test.device}-${test.scheduled_date ?? 'na'}-${test.shift_index ?? 'na'}-${test.sequence_in_shift ?? index}`}
|
||||
test={test}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user