|
1 | 1 | """Tests for the SlotClock time-to-slot converter.""" |
2 | 2 |
|
| 3 | +from __future__ import annotations |
| 4 | + |
3 | 5 | import pytest |
4 | 6 |
|
5 | 7 | from lean_spec.subspecs.chain import Interval, SlotClock |
6 | 8 | from lean_spec.subspecs.chain.config import ( |
| 9 | + INTERVALS_PER_SLOT, |
7 | 10 | MILLISECONDS_PER_INTERVAL, |
| 11 | + MILLISECONDS_PER_SLOT, |
8 | 12 | SECONDS_PER_SLOT, |
9 | 13 | ) |
10 | 14 | from lean_spec.subspecs.containers import Slot |
11 | 15 | from lean_spec.types import Uint64 |
12 | 16 |
|
| 17 | +GENESIS_TIME = Uint64(1_700_000_000) |
| 18 | + |
| 19 | + |
| 20 | +class TestIntervalFromUnixTime: |
| 21 | + """Tests for Interval.from_unix_time().""" |
| 22 | + |
| 23 | + def test_at_genesis(self) -> None: |
| 24 | + """Returns interval 0 when unix_seconds equals genesis_time.""" |
| 25 | + assert Interval.from_unix_time(GENESIS_TIME, GENESIS_TIME) == Interval(0) |
| 26 | + |
| 27 | + def test_one_second_after_genesis(self) -> None: |
| 28 | + """One second equals 1000ms, yielding 1000 // 800 = 1 interval.""" |
| 29 | + result = Interval.from_unix_time(GENESIS_TIME + Uint64(1), GENESIS_TIME) |
| 30 | + assert result == Interval(1) |
| 31 | + |
| 32 | + def test_one_slot_after_genesis(self) -> None: |
| 33 | + """One full slot (4s = 4000ms) yields 4000 // 800 = 5 intervals.""" |
| 34 | + result = Interval.from_unix_time(GENESIS_TIME + SECONDS_PER_SLOT, GENESIS_TIME) |
| 35 | + expected = Interval(int(MILLISECONDS_PER_SLOT // MILLISECONDS_PER_INTERVAL)) |
| 36 | + assert result == expected |
| 37 | + |
| 38 | + def test_sub_interval_rounds_down(self) -> None: |
| 39 | + """Partial intervals are truncated by integer division. |
| 40 | +
|
| 41 | + At 0 full seconds past genesis the delta_ms is 0, so the result is 0. |
| 42 | + The method only accepts whole-second Uint64 values, so sub-interval |
| 43 | + precision only surfaces when 1000ms is not a multiple of the interval. |
| 44 | + Here we verify the floor behaviour across the first few seconds. |
| 45 | + """ |
| 46 | + # 0s -> 0ms -> 0 intervals |
| 47 | + assert Interval.from_unix_time(GENESIS_TIME, GENESIS_TIME) == Interval(0) |
| 48 | + # 1s -> 1000ms -> 1000 // 800 = 1 (remainder 200ms truncated) |
| 49 | + assert Interval.from_unix_time(GENESIS_TIME + Uint64(1), GENESIS_TIME) == Interval(1) |
| 50 | + # 2s -> 2000ms -> 2000 // 800 = 2 (remainder 400ms truncated) |
| 51 | + assert Interval.from_unix_time(GENESIS_TIME + Uint64(2), GENESIS_TIME) == Interval(2) |
| 52 | + # 3s -> 3000ms -> 3000 // 800 = 3 (remainder 600ms truncated) |
| 53 | + assert Interval.from_unix_time(GENESIS_TIME + Uint64(3), GENESIS_TIME) == Interval(3) |
| 54 | + |
| 55 | + def test_multiple_slots(self) -> None: |
| 56 | + """Ten slots (40s = 40000ms) yields 40000 // 800 = 50 intervals.""" |
| 57 | + ten_slots = Uint64(10) * SECONDS_PER_SLOT |
| 58 | + result = Interval.from_unix_time(GENESIS_TIME + ten_slots, GENESIS_TIME) |
| 59 | + expected_intervals = (ten_slots * Uint64(1000)) // MILLISECONDS_PER_INTERVAL |
| 60 | + assert result == Interval(expected_intervals) |
| 61 | + |
| 62 | + def test_return_type_is_interval(self) -> None: |
| 63 | + """Return value is an Interval instance, not a plain Uint64.""" |
| 64 | + result = Interval.from_unix_time(GENESIS_TIME + Uint64(5), GENESIS_TIME) |
| 65 | + assert isinstance(result, Interval) |
| 66 | + |
| 67 | + def test_large_time_delta(self) -> None: |
| 68 | + """Works correctly with a large time delta (one day = 86400s).""" |
| 69 | + one_day = Uint64(86400) |
| 70 | + result = Interval.from_unix_time(GENESIS_TIME + one_day, GENESIS_TIME) |
| 71 | + expected = Interval((one_day * Uint64(1000)) // MILLISECONDS_PER_INTERVAL) |
| 72 | + assert result == expected |
| 73 | + |
| 74 | + def test_genesis_time_zero(self) -> None: |
| 75 | + """Works when genesis_time is zero.""" |
| 76 | + result = Interval.from_unix_time(Uint64(4), Uint64(0)) |
| 77 | + # 4s = 4000ms -> 4000 // 800 = 5 |
| 78 | + assert result == Interval(5) |
| 79 | + |
| 80 | + |
| 81 | +class TestIntervalFromSlot: |
| 82 | + """Tests for Interval.from_slot().""" |
| 83 | + |
| 84 | + def test_slot_zero(self) -> None: |
| 85 | + """Slot 0 maps to interval 0.""" |
| 86 | + assert Interval.from_slot(Uint64(0)) == Interval(0) |
| 87 | + |
| 88 | + def test_slot_one(self) -> None: |
| 89 | + """Slot 1 maps to interval equal to INTERVALS_PER_SLOT.""" |
| 90 | + assert Interval.from_slot(Uint64(1)) == Interval(INTERVALS_PER_SLOT) |
| 91 | + |
| 92 | + def test_slot_three(self) -> None: |
| 93 | + """Slot 3 maps to interval 3 * INTERVALS_PER_SLOT.""" |
| 94 | + assert Interval.from_slot(Uint64(3)) == Interval(Uint64(3) * INTERVALS_PER_SLOT) |
| 95 | + |
| 96 | + def test_multiple_slots(self) -> None: |
| 97 | + """Each slot N maps to interval N * INTERVALS_PER_SLOT.""" |
| 98 | + for n in range(10): |
| 99 | + slot = Uint64(n) |
| 100 | + assert Interval.from_slot(slot) == Interval(slot * INTERVALS_PER_SLOT) |
| 101 | + |
| 102 | + def test_return_type_is_interval(self) -> None: |
| 103 | + """Return value is an Interval instance, not a plain Uint64.""" |
| 104 | + result = Interval.from_slot(Uint64(2)) |
| 105 | + assert isinstance(result, Interval) |
| 106 | + |
| 107 | + def test_consistent_with_from_unix_time(self) -> None: |
| 108 | + """from_slot(N) equals from_unix_time at genesis + N * SECONDS_PER_SLOT.""" |
| 109 | + for n in range(5): |
| 110 | + slot = Uint64(n) |
| 111 | + from_slot_result = Interval.from_slot(slot) |
| 112 | + from_unix_result = Interval.from_unix_time( |
| 113 | + GENESIS_TIME + slot * SECONDS_PER_SLOT, GENESIS_TIME |
| 114 | + ) |
| 115 | + assert from_slot_result == from_unix_result |
| 116 | + |
13 | 117 |
|
14 | 118 | class TestCurrentSlot: |
15 | 119 | """Tests for current_slot().""" |
|
0 commit comments