""" Tests for data/normalizer.py — timestamp normalization and sequence gaps. """ import time from data.normalizer import ( normalize_timestamp, SequenceTracker, detect_sequence_gap, ) def test_normalize_ms_timestamp(): """Millisecond timestamps pass through unchanged.""" ts = 1705312800000 assert normalize_timestamp(ts) == 1705312800000 def test_normalize_seconds_timestamp(): """Second timestamps get multiplied by 1000.""" ts = 1705312800 result = normalize_timestamp(ts) assert result == 1705312800000 def test_normalize_float_timestamp(): """Float seconds get multiplied.""" ts = 1705312800.5 result = normalize_timestamp(ts) assert result == 1705312800500 def test_normalize_large_float_is_ms(): """A float > 1e12 is already in ms.""" ts = 1705312800000.123 result = normalize_timestamp(ts) assert result == 1705312800000 def test_normalize_iso_string(): """ISO 8601 Z string converted to ms.""" result = normalize_timestamp("2024-01-15T12:00:00.000Z") assert result == 1705320000000 def test_normalize_iso_no_z(): """ISO string without trailing Z.""" result = normalize_timestamp("2024-01-15T12:00:00") expected = 1705320000000 assert abs(result - expected) < 1000 def test_normalize_none_returns_now(): """None returns current time (within 1s).""" now_ms = int(time.time() * 1000) result = normalize_timestamp(None) assert abs(result - now_ms) < 2000 def test_normalize_datetime(): """datetime object converted to ms.""" from datetime import datetime dt = datetime(2024, 1, 15, 12, 0, 0) result = normalize_timestamp(dt) assert result == 1705320000000 class TestSequenceTracker: def test_initial_no_gap(self): st = SequenceTracker() assert st.check("l2", "BTC", 100) is None def test_consecutive_no_gap(self): st = SequenceTracker() st.check("l2", "BTC", 100) assert st.check("l2", "BTC", 101) is None assert st.check("l2", "BTC", 102) is None def test_gap_detected(self): st = SequenceTracker() st.check("l2", "BTC", 100) gap = st.check("l2", "BTC", 105) assert gap is not None assert gap["gap_size"] == 4 assert gap["expected"] == 101 def test_multiple_channels_independent(self): st = SequenceTracker() st.check("l2", "BTC", 100) st.check("trades", "BTC", 50) assert st.check("l2", "BTC", 101) is None assert st.check("trades", "BTC", 51) is None def test_reset_clears_state(self): st = SequenceTracker() st.check("l2", "BTC", 100) st.reset("l2", "BTC") assert st.check("l2", "BTC", 200) is None # fresh start def test_gap_counts_accumulate(self): st = SequenceTracker() st.check("l2", "BTC", 100) st.check("l2", "BTC", 105) st.check("l2", "BTC", 110) gaps = st.gap_counts assert gaps["l2:BTC"] == 8 # 4 + 4 def test_detect_sequence_gap_empty(): assert detect_sequence_gap(1, None) == 0 def test_detect_sequence_gap_ok(): assert detect_sequence_gap(102, 101) == 0 def test_detect_sequence_gap_found(): gap = detect_sequence_gap(105, 101) assert gap == 3 def test_detect_sequence_gap_negative(): assert detect_sequence_gap(100, 101) == -1 # dupe or reset