From e7b2175b893a35f61f9885bc643c98b65a0c7c61 Mon Sep 17 00:00:00 2001 From: Felipe Defensor <87732202+FelipeDefensor@users.noreply.github.com> Date: Tue, 25 Aug 2026 11:26:06 -0300 Subject: [PATCH] fix: don't crash when a measure holds more beats than the beat pattern "Set amount in measure" puts an arbitrary number of beats in a measure, so beats_in_measure[-1] can hold more than the beat pattern prescribes for that slot. get_extension_from_beat_pattern treated that as impossible and raised, and extend_beats_in_measure only recognised an exactly-full measure as complete. Adding a beat in that state raised out of recalculate_measures. The command handler recovers by restoring the snapshot it took beforehand, but restoring re-runs the same code, so the recovery failed too and left the timeline empty -- every beat on it was lost. Both places now treat a measure at or over its prescribed size as full, so the extension starts a new measure instead. --- tests/timelines/beat/test_beat_timeline.py | 25 +++++++++++++++++++ .../timelines/beat/test_beat_timeline_ui.py | 23 +++++++++++++++++ tilia/timelines/beat/timeline.py | 12 ++++----- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/tests/timelines/beat/test_beat_timeline.py b/tests/timelines/beat/test_beat_timeline.py index a47b927e7..6f08fa041 100644 --- a/tests/timelines/beat/test_beat_timeline.py +++ b/tests/timelines/beat/test_beat_timeline.py @@ -1,5 +1,6 @@ import pytest +from tilia.timelines.beat.timeline import BeatTimeline from tilia.ui import commands @@ -436,3 +437,27 @@ def test_crop_timeline_with_many_beats(self, resources, tilia, tilia_state): assert len(tilia.timelines[0]) == 11 assert tilia.timelines[0][-1].get_data("time") == 10 + + +class TestGetExtensionFromBeatPattern: + def test_starting_measure_emptier_than_beat_pattern(self): + extension = BeatTimeline.get_extension_from_beat_pattern( + [4], 3, beats_on_starting_measure=2 + ) + assert extension == [2, 1] + + def test_starting_measure_as_full_as_beat_pattern(self): + extension = BeatTimeline.get_extension_from_beat_pattern( + [4], 3, beats_on_starting_measure=4 + ) + assert extension == [3] + + def test_starting_measure_fuller_than_beat_pattern(self): + """ "Set amount in measure" can put more beats in a measure than the + beat pattern prescribes for it. There is nothing left to fill in that + case, so the extension has to start a new measure. + """ + extension = BeatTimeline.get_extension_from_beat_pattern( + [4], 3, beats_on_starting_measure=5 + ) + assert extension == [3] diff --git a/tests/ui/timelines/beat/test_beat_timeline_ui.py b/tests/ui/timelines/beat/test_beat_timeline_ui.py index b9150a686..10715fa16 100644 --- a/tests/ui/timelines/beat/test_beat_timeline_ui.py +++ b/tests/ui/timelines/beat/test_beat_timeline_ui.py @@ -509,6 +509,29 @@ def test_updates_measure_numbers(self, beat_tlui): assert [get_displayed_measure_number(b) for b in beat_tlui] == ["1", "", "2"] + def test_add_beat_when_last_measure_is_fuller_than_beat_pattern(self, beat_tlui): + beat_tlui.timeline.beat_pattern = [4] + for i in range(24): + beat_tlui.create_beat(i / 10) + + # give a middle measure one beat more than the beat pattern prescribes + beat_tlui.select_element(beat_tlui[16]) + with Serve(Get.FROM_USER_INT, (True, 5)): + commands.execute("timeline.beat.set_amount_in_measure") + + # then make that measure the last one + beat_tlui.deselect_all_elements() + for element in list(beat_tlui)[-3:]: + beat_tlui.select_element(element) + commands.execute("timeline.component.delete") + assert beat_tlui.timeline.beats_in_measure == [4, 4, 4, 4, 5] + + commands.execute("media.seek", 3.0) + commands.execute("timeline.beat.add") + + assert len(beat_tlui) == 22 + assert beat_tlui.timeline.beats_in_measure == [4, 4, 4, 4, 5, 1] + class TestFillWithBeats: @pytest.fixture(autouse=True) diff --git a/tilia/timelines/beat/timeline.py b/tilia/timelines/beat/timeline.py index 01c564337..4567a3a7a 100644 --- a/tilia/timelines/beat/timeline.py +++ b/tilia/timelines/beat/timeline.py @@ -417,13 +417,13 @@ def get_extension_from_beat_pattern( diff = beats - beats_on_starting_measure extension = [min(diff, amount)] remaining_beats = amount - diff - elif beats_on_starting_measure == beats: + else: + # A measure may hold more beats than the beat pattern + # prescribes for it, as "Set amount in measure" sets an + # arbitrary amount. There is nothing left to fill in that + # case, so the extension starts a new measure. extension = [] remaining_beats = amount - else: - raise ValueError( - "More beats on starting measure than found in the iterator" - ) else: extension = [] remaining_beats = amount @@ -472,7 +472,7 @@ def extend_beats_in_measure(self, amount: int) -> None: bp_index = (self.measure_count % len(self.beat_pattern)) - 1 is_last_measure_complete = ( - self._beats_in_measure[-1] == self.beat_pattern[bp_index] + self._beats_in_measure[-1] >= self.beat_pattern[bp_index] ) if is_last_measure_complete: