test_timeline_rational_time.py
file-level
1
files
1
commits
0
hotspots
0
🧊 dead
0
💥 blast risk
| 1 | """VID_02–VID_06 — RationalRate, RationalTime, TimeRange unit tests.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import math |
| 6 | |
| 7 | import pytest |
| 8 | |
| 9 | from muse.plugins.timeline.rational_time import RationalRate, RationalTime, TimeRange |
| 10 | |
| 11 | |
| 12 | class TestVID02RationalRate: |
| 13 | def test_reduction(self) -> None: |
| 14 | r = RationalRate(48, 2) |
| 15 | assert (r.num, r.den) == (24, 1) |
| 16 | |
| 17 | def test_rejects_non_positive(self) -> None: |
| 18 | with pytest.raises(ValueError): |
| 19 | RationalRate(0, 1) |
| 20 | with pytest.raises(ValueError): |
| 21 | RationalRate(1, 0) |
| 22 | |
| 23 | def test_ntsc_table(self) -> None: |
| 24 | assert RationalRate.from_float(23.976) == RationalRate(24000, 1001) |
| 25 | assert RationalRate.from_float(29.97) == RationalRate(30000, 1001) |
| 26 | |
| 27 | def test_rejects_undeclared_fractional(self) -> None: |
| 28 | with pytest.raises(ValueError): |
| 29 | RationalRate.from_float(23.500001) |
| 30 | |
| 31 | |
| 32 | class TestVID03RationalTimeEquality: |
| 33 | def test_equal_instant_different_rate(self) -> None: |
| 34 | a = RationalTime(24, RationalRate(24, 1)) |
| 35 | b = RationalTime(48, RationalRate(48, 1)) |
| 36 | assert a == b |
| 37 | |
| 38 | def test_ordering(self) -> None: |
| 39 | a = RationalTime(10, RationalRate(24, 1)) |
| 40 | b = RationalTime(20, RationalRate(24, 1)) |
| 41 | assert a < b |
| 42 | |
| 43 | def test_is_identical(self) -> None: |
| 44 | a = RationalTime(24, RationalRate(24, 1)) |
| 45 | b = RationalTime(48, RationalRate(48, 1)) |
| 46 | assert a == b |
| 47 | assert not a.is_identical(b) |
| 48 | |
| 49 | def test_hash_equal_instant(self) -> None: |
| 50 | a = RationalTime(24, RationalRate(24, 1)) |
| 51 | b = RationalTime(48, RationalRate(48, 1)) |
| 52 | assert hash(a) == hash(b) |
| 53 | |
| 54 | def test_no_float_in_arithmetic(self) -> None: |
| 55 | rt = RationalTime.from_frames(100, RationalRate(24, 1)) |
| 56 | frac = rt.to_fraction_seconds() |
| 57 | assert isinstance(frac.numerator, int) |
| 58 | assert isinstance(frac.denominator, int) |
| 59 | |
| 60 | |
| 61 | class TestVID04TimecodeNonDrop: |
| 62 | @pytest.mark.parametrize( |
| 63 | ("fps", "tc", "frames"), |
| 64 | [ |
| 65 | (24, "00:00:10:00", 240), |
| 66 | (25, "00:00:10:00", 250), |
| 67 | ], |
| 68 | ) |
| 69 | def test_round_trip(self, fps: int, tc: str, frames: int) -> None: |
| 70 | rate = RationalRate(fps, 1) |
| 71 | rt = RationalTime.from_timecode(tc, rate, drop_frame=False) |
| 72 | assert rt.value == frames |
| 73 | assert rt.to_timecode(drop_frame=False) == tc |
| 74 | |
| 75 | |
| 76 | class TestVID05DropFrame: |
| 77 | def test_2997_drop_frame_separator(self) -> None: |
| 78 | rate = RationalRate.from_float(29.97) |
| 79 | rt = RationalTime.from_timecode("00:01:00;00", rate, drop_frame=True) |
| 80 | assert ";" in rt.to_timecode(drop_frame=True) |
| 81 | |
| 82 | def test_2997_round_trip_anchor(self) -> None: |
| 83 | rate = RationalRate.from_float(29.97) |
| 84 | tc = "00:01:00;00" |
| 85 | rt = RationalTime.from_timecode(tc, rate, drop_frame=True) |
| 86 | assert rt.to_timecode(drop_frame=True) == tc |
| 87 | |
| 88 | |
| 89 | class TestVID06TimeRange: |
| 90 | def test_half_open_contains(self) -> None: |
| 91 | rate = RationalRate(24, 1) |
| 92 | tr = TimeRange( |
| 93 | start_time=RationalTime(0, rate), |
| 94 | duration=RationalTime(24, rate), |
| 95 | ) |
| 96 | assert tr.contains(RationalTime(0, rate)) |
| 97 | assert tr.contains(RationalTime(23, rate)) |
| 98 | assert not tr.contains(RationalTime(24, rate)) |
| 99 | |
| 100 | def test_overlaps_boundary(self) -> None: |
| 101 | rate = RationalRate(24, 1) |
| 102 | a = TimeRange(RationalTime(0, rate), RationalTime(24, rate)) |
| 103 | b = TimeRange(RationalTime(24, rate), RationalTime(24, rate)) |
| 104 | assert not a.overlaps(b) |
| 105 | |
| 106 | def test_end_time_exclusive(self) -> None: |
| 107 | rate = RationalRate(24, 1) |
| 108 | tr = TimeRange(RationalTime(10, rate), RationalTime(5, rate)) |
| 109 | end = tr.end_time_exclusive |
| 110 | assert end == RationalTime(15, rate) |
| 111 | |
| 112 | def test_duration_non_negative(self) -> None: |
| 113 | rate = RationalRate(24, 1) |
| 114 | with pytest.raises(ValueError): |
| 115 | TimeRange(RationalTime(0, rate), RationalTime(-1, rate)) |