"""Host-side reproduction of 167 DAC endpoint logic; no firmware changes. Run: py -3 -m unittest discover -s tests -p test_dac167_limit.py -v The source guard ties the model to the inspected C block. These tests reproduce the bug (passing means reproduced), not execution on a C2000 or physical DAC. """ import re import unittest from pathlib import Path SOURCE = Path(__file__).resolve().parents[2] / '167/Source/Internal/DAC.c' def endpoints(start, stop, corrected=False): # Signed C2000 int subtraction must remain representable. Deliberately # exclude inputs -32768..-32765, where subtracting 4 overflows signed int. if not all(-32764 <= value <= 32767 for value in (start, stop)): raise ValueError('Outside modeled, non-overflowing input range') go = float(start - 4) end = float(stop - 4) if go < 0: go = 0.0 if go > 5: go = 5.0 if end > 16: if corrected: end = 16.0 else: go = 16.0 if end <= go: end = go + 1 return go, end def ramp_code(go, end, count, duration=6000): # Source: LOAD_TIME=30, DAC_FREQ=200. int conversion truncates. # Only selected exactly representable / far-from-boundary cases tested. value = (end - go) / 16.0 * count + go / 16.0 * duration if not -32768 <= value < 32768: raise ValueError('C2000 int conversion out of range') return int(value) class Dac167LimitTests(unittest.TestCase): def test_source_matches_reproduced_block(self): code = re.sub(r'\s+', '', SOURCE.read_bytes().decode('latin1')) expected = '''dac_go=DAC_go-4; dac_stop=DAC_stop-4; if(dac_go<0) dac_go=0; if(dac_go>5) dac_go=5; if(dac_stop>16) dac_go=16; if(dac_stop<=dac_go) dac_stop=dac_go+1;''' self.assertIn(re.sub(r'\s+', '', expected), code) def test_defaults_and_threshold(self): for stop, actual, fixed in [ (16, (2, 12), (2, 12)), (19, (2, 15), (2, 15)), (20, (2, 16), (2, 16)), (21, (16, 17), (2, 16)), (22, (16, 18), (2, 16)), (30, (16, 26), (2, 16)), ]: with self.subTest(stop=stop): self.assertEqual(endpoints(6, stop), actual) self.assertEqual(endpoints(6, stop, True), fixed) def test_start_limit_is_overwritten(self): for start in (-100, 0, 4, 6, 9, 10, 20, 100, 32767): with self.subTest(start=start): self.assertEqual(endpoints(start, 21), (16, 17)) self.assertLessEqual(endpoints(start, 21, True)[0], 5) def test_full_nonnegative_stop_range(self): # 327,680 input pairs. Negative stop values do not trigger end > 16. for start in (-100, 0, 4, 6, 8, 9, 10, 20, 100, 32767): for stop in range(32768): actual = endpoints(start, stop) fixed = endpoints(start, stop, True) self.assertEqual(actual != fixed, stop > 20, (start, stop, actual, fixed)) def test_first_and_last_ramp_codes(self): actual = endpoints(6, 21) fixed = endpoints(6, 21, True) self.assertEqual(ramp_code(*actual, 1), 6000) self.assertEqual(ramp_code(*fixed, 1), 750) self.assertEqual(ramp_code(*actual, 6000), 6375) self.assertEqual(ramp_code(*fixed, 6000), 6000) def test_low_stop_guard_is_unchanged(self): self.assertEqual(endpoints(6, 4), (2, 3)) self.assertEqual(endpoints(6, 4), endpoints(6, 4, True)) self.assertEqual(endpoints(20, 0), (5, 6)) def test_model_rejects_signed_subtraction_overflow(self): for value in range(-32768, -32764): with self.assertRaises(ValueError): endpoints(value, 21) with self.assertRaises(ValueError): endpoints(6, value) if __name__ == '__main__': unittest.main(verbosity=2)