import importlib.util from pathlib import Path import unittest MODULE_PATH = (Path(__file__).resolve().parents[3] / 'python/logic_analyzer/decoders/gate_timing.py') SPEC = importlib.util.spec_from_file_location('gate_driver_timing_core', MODULE_PATH) TIMING = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(TIMING) def kinds(events): return [item['kind'] for item in events] class TimingCheckerTests(unittest.TestCase): def test_1sp0635_nominal_ack_at_100_mhz(self): checker = TIMING.TimingChecker(100_000_000, profile='1SP0635') checker.on_control_edge(100, 1) self.assertEqual(kinds(checker.on_status_edge(125, 1)), ['delay_ok']) result = checker.on_status_edge(195, 0) self.assertEqual(kinds(result), ['width_ok']) self.assertEqual(result[0]['width_ns'], 700.0) def test_1sd536f2_nominal_ack_at_100_mhz_active_low(self): checker = TIMING.TimingChecker( 100_000_000, profile='1SD536F2', vstat_active_high=False) checker.on_control_edge(20, 1) self.assertEqual(kinds(checker.on_status_edge(58, 0)), ['delay_ok']) result = checker.on_status_edge(148, 1) self.assertEqual(kinds(result), ['width_ok']) self.assertEqual(result[0]['width_ns'], 900.0) def test_late_delay_is_warning_but_valid_width_passes(self): checker = TIMING.TimingChecker(100_000_000, profile='1SP0635') checker.on_control_edge(0, 1) self.assertEqual(kinds(checker.on_status_edge(50, 1)), ['delay_warn']) self.assertEqual(kinds(checker.on_status_edge(120, 0)), ['width_ok']) def test_too_short_ack_fails(self): checker = TIMING.TimingChecker(100_000_000, profile='1SP0635') checker.on_control_edge(0, 1) checker.on_status_edge(25, 1) self.assertEqual(kinds(checker.on_status_edge(55, 0)), ['width_fail']) def test_long_status_is_fault(self): checker = TIMING.TimingChecker(100_000_000, profile='1SP0635') checker.on_control_edge(0, 1) checker.on_status_edge(25, 1) self.assertEqual(kinds(checker.on_status_edge(225, 0)), ['fault']) def test_missing_ack_expires(self): checker = TIMING.TimingChecker(100_000_000, profile='1SP0635') checker.on_control_edge(10, 0) deadline = checker.next_deadline() self.assertIsNotNone(deadline) self.assertEqual(kinds(checker.expire(deadline)), ['missing']) if __name__ == '__main__': unittest.main() class InputTimingTests(unittest.TestCase): def test_both_directions_and_independent_minimums(self): c = TIMING.InputTimingChecker(1e9, vin1_mintime_ns=100, vin2_mintime_ns=200) c.update(0, 0, 0) c.update(10, 1, 0) self.assertEqual(kinds(c.update(110, 0, 0)), ['mintime_ok']) event = c.update(140, 0, 1)[0] self.assertEqual(event['duration_ns'], 30) self.assertIn('Vin1 -> Vin2', event['text']) self.assertEqual(kinds(c.update(240, 0, 0)), ['mintime_fail']) event = c.update(280, 1, 0)[0] self.assertEqual(event['duration_ns'], 40) self.assertIn('Vin2 -> Vin1', event['text']) def test_atomic_handover_and_active_low(self): c = TIMING.InputTimingChecker(1e9, False, False) c.update(0, 1, 1) c.update(10, 0, 1) events = c.update(20, 1, 0) self.assertEqual(kinds(events), ['mintime_ok', 'deadtime']) self.assertEqual(events[1]['duration_ns'], 0) self.assertEqual(kinds(c.update(30, 0, 1)), ['mintime_ok', 'deadtime']) def test_overlap_and_capture_boundaries(self): c = TIMING.InputTimingChecker(1e9) c.update(0, 1, 0) c.update(10, 1, 1) events = c.update(25, 0, 1) self.assertEqual(kinds(events), ['overlap']) self.assertEqual(events[0]['duration_ns'], 15) c.update(30, 1, 1) self.assertEqual(kinds(c.finish(50)), ['overlap']) c = TIMING.InputTimingChecker(1e9) c.update(0, 0, 0) self.assertEqual(c.update(10, 1, 0), []) self.assertEqual(c.finish(20), []) def test_no_stale_deadtime_after_same_input_restarts(self): c = TIMING.InputTimingChecker(1e9) for t, a, b in [(0, 0, 0), (10, 1, 0), (20, 0, 0), (30, 1, 0), (40, 0, 0)]: c.update(t, a, b) self.assertEqual(c.update(50, 0, 1)[0]['duration_ns'], 10) def test_invalid_minimums(self): for value in (-1, float('nan'), float('inf')): with self.assertRaises(ValueError): TIMING.InputTimingChecker(1e9, vin2_mintime_ns=value) class CycleResultTests(unittest.TestCase): def cycle(self, checker, origin=0, delay=250, width=700, inverted=False): events = checker.on_control_edge(origin + 100, not inverted) events += checker.on_status_edge(origin + 100 + delay, 1) events += checker.on_status_edge(origin + 100 + delay + width, 0) events += checker.on_control_edge(origin + 3000, inverted) events += checker.on_status_edge(origin + 3250, 1) events += checker.on_status_edge(origin + 3950, 0) return [e for e in events if e['kind'].startswith('cycle_')] def test_complete_cycle_range_and_active_low(self): for inverted in (False, True): checker = TIMING.TimingChecker(1e9, cycle_results=True, vin_active_high=not inverted) events = self.cycle(checker, inverted=inverted) self.assertEqual(len(events), 1) self.assertEqual((events[0]['kind'], events[0]['start'], events[0]['end']), ('cycle_ok', 100, 3950)) def test_warning_fault_and_next_cycle_recovery(self): for delay, width, verdict in [(500, 700, 'warning'), (250, 100, 'fault'), (250, 2000, 'fault')]: checker = TIMING.TimingChecker(1e9, cycle_results=True) self.assertEqual(self.cycle(checker, delay=delay, width=width)[0]['kind'], 'cycle_' + verdict) self.assertEqual(self.cycle(checker, origin=10000)[0]['kind'], 'cycle_ok') def test_missing_ack_and_clipped_capture(self): checker = TIMING.TimingChecker(1e9, cycle_results=True) checker.on_control_edge(100, 1) checker.expire(1500) checker.on_control_edge(3000, 0) checker.on_status_edge(3250, 1) events = checker.on_status_edge(3950, 0) self.assertEqual(events[-1]['kind'], 'cycle_fault') checker.on_control_edge(10000, 1) self.assertEqual(checker.finish_cycles(10100)[0]['kind'], 'cycle_warning') self.assertEqual(checker.finish_cycles(10100), []) def test_no_ok_before_second_ack_finishes(self): checker = TIMING.TimingChecker(1e9, cycle_results=True) events = checker.on_control_edge(100, 1) events += checker.on_status_edge(350, 1) events += checker.on_status_edge(1050, 0) events += checker.on_control_edge(3000, 0) events += checker.on_status_edge(3250, 1) self.assertFalse(any(e['kind'].startswith('cycle_') for e in events)) self.assertEqual(checker.on_status_edge(3950, 0)[-1]['kind'], 'cycle_ok')