"""VID_02–VID_06 — RationalRate, RationalTime, TimeRange unit tests.""" from __future__ import annotations import math import pytest from muse.plugins.timeline.rational_time import RationalRate, RationalTime, TimeRange class TestVID02RationalRate: def test_reduction(self) -> None: r = RationalRate(48, 2) assert (r.num, r.den) == (24, 1) def test_rejects_non_positive(self) -> None: with pytest.raises(ValueError): RationalRate(0, 1) with pytest.raises(ValueError): RationalRate(1, 0) def test_ntsc_table(self) -> None: assert RationalRate.from_float(23.976) == RationalRate(24000, 1001) assert RationalRate.from_float(29.97) == RationalRate(30000, 1001) def test_rejects_undeclared_fractional(self) -> None: with pytest.raises(ValueError): RationalRate.from_float(23.500001) class TestVID03RationalTimeEquality: def test_equal_instant_different_rate(self) -> None: a = RationalTime(24, RationalRate(24, 1)) b = RationalTime(48, RationalRate(48, 1)) assert a == b def test_ordering(self) -> None: a = RationalTime(10, RationalRate(24, 1)) b = RationalTime(20, RationalRate(24, 1)) assert a < b def test_is_identical(self) -> None: a = RationalTime(24, RationalRate(24, 1)) b = RationalTime(48, RationalRate(48, 1)) assert a == b assert not a.is_identical(b) def test_hash_equal_instant(self) -> None: a = RationalTime(24, RationalRate(24, 1)) b = RationalTime(48, RationalRate(48, 1)) assert hash(a) == hash(b) def test_no_float_in_arithmetic(self) -> None: rt = RationalTime.from_frames(100, RationalRate(24, 1)) frac = rt.to_fraction_seconds() assert isinstance(frac.numerator, int) assert isinstance(frac.denominator, int) class TestVID04TimecodeNonDrop: @pytest.mark.parametrize( ("fps", "tc", "frames"), [ (24, "00:00:10:00", 240), (25, "00:00:10:00", 250), ], ) def test_round_trip(self, fps: int, tc: str, frames: int) -> None: rate = RationalRate(fps, 1) rt = RationalTime.from_timecode(tc, rate, drop_frame=False) assert rt.value == frames assert rt.to_timecode(drop_frame=False) == tc class TestVID05DropFrame: def test_2997_drop_frame_separator(self) -> None: rate = RationalRate.from_float(29.97) rt = RationalTime.from_timecode("00:01:00;00", rate, drop_frame=True) assert ";" in rt.to_timecode(drop_frame=True) def test_2997_round_trip_anchor(self) -> None: rate = RationalRate.from_float(29.97) tc = "00:01:00;00" rt = RationalTime.from_timecode(tc, rate, drop_frame=True) assert rt.to_timecode(drop_frame=True) == tc class TestVID06TimeRange: def test_half_open_contains(self) -> None: rate = RationalRate(24, 1) tr = TimeRange( start_time=RationalTime(0, rate), duration=RationalTime(24, rate), ) assert tr.contains(RationalTime(0, rate)) assert tr.contains(RationalTime(23, rate)) assert not tr.contains(RationalTime(24, rate)) def test_overlaps_boundary(self) -> None: rate = RationalRate(24, 1) a = TimeRange(RationalTime(0, rate), RationalTime(24, rate)) b = TimeRange(RationalTime(24, rate), RationalTime(24, rate)) assert not a.overlaps(b) def test_end_time_exclusive(self) -> None: rate = RationalRate(24, 1) tr = TimeRange(RationalTime(10, rate), RationalTime(5, rate)) end = tr.end_time_exclusive assert end == RationalTime(15, rate) def test_duration_non_negative(self) -> None: rate = RationalRate(24, 1) with pytest.raises(ValueError): TimeRange(RationalTime(0, rate), RationalTime(-1, rate))