Skip to content

fix: close the score viewer when the score timeline is cleared - #584

Open
FelipeDefensor wants to merge 5 commits into
devfrom
fix/472-score-clear-svg-viewer-v2
Open

fix: close the score viewer when the score timeline is cleared#584
FelipeDefensor wants to merge 5 commits into
devfrom
fix/472-score-clear-svg-viewer-v2

Conversation

@FelipeDefensor

Copy link
Copy Markdown
Collaborator

Alternative to #481, rebuilt on current dev. Same diagnosis, deeper fix.

Closes #472.

The bug is real and reachable

Clearing a score timeline and then letting playback run for one tick reopens the viewer with the score that was just discarded: on_audio_time_change reads ScoreTimelineUI.svg_view, whose getter constructs an SvgViewer when none is registered, loads svg_data into it and shows it. Instrumenting SvgViewer.show confirms one call per clear-plus-tick on dev. The score also survived into the saved file, so reopening a cleared timeline brought it back.

Why not #481 as it stands

#481 guards reset_svg alone. That leaves the auto-creating property in place at five other call sites, and it introduces a state (svg_data == "" on a live timeline) that the rest of the code cannot handle:

  • Undo/redo crashes. With svg_data blanked on clear, restoring a cleared state pushes "" through the update trigger into SvgViewer.load_svg_data, which hands it to etree.fromstringXMLSyntaxError: Document is empty.
  • The new guard catches the wrong thing. Only one callback can be attached per Get member, so with two score timelines the viewer of one answers for the other's id and returns None rather than raising. except NoReplyToRequest does not cover that; reset_svg raises AttributeError. on_audio_time_change had the same latent crash.
  • The viewer still comes back. reset_svg deletes it, and the next playback tick rebuilds it through the property. It stays invisible only because svg_data happens to be empty by then, so the two halves of that PR are load-bearing together rather than independently correct.

What this does instead

528eb264load_svg_data treated only success=False from set_data as a failure, but viewer_beat_x uses validate_pre_validated, so the call always succeeds and SCORE_SVG_CREATE_ERROR was unreachable. A render without usable beat markers silently produced an empty mapping. Check the returned mapping too.

6d391c06svg_view no longer constructs anything; it returns None when there is no viewer, and the explicit get_or_create_svg_view() handles creation. All six read sites updated, plus ScoreAnnotationUI.svg_view, which keeps the creating behaviour it relies on. This is what actually stops the resurrection, and it fixes the None-return crash at the same time.

9a13afb9 — the fix proper. ScoreTLComponentManager.clear discards svg_data (after super().clear(), so the deletion loop still sees the score it is clearing), and update_svg_data closes the viewer instead of loading when there is no score left. load_svg_data refuses empty data outright.

5dd1a7fc — docs; see below.

b106beac_setup_svg_view now goes through get_or_create_svg_view, which sets the measure tracker's visibility from the viewer instead of calling show() unconditionally. Test pins the behaviour that must be preserved.

Tests

16 added, covering: the viewer closing on clear and staying closed through playback, svg_data being discarded, restoring a cleared state, undo/redo, save-and-reload, reset_svg with no viewer and with another timeline owning the slot, and the revived beat-position error. Each fails without its corresponding change.

Full suite: 1847 passed, 0 failed.

Two pre-existing bugs found on the way, left for their own PRs

  • viewer_beat_x is never cleared, so a score imported into a timeline that already had one is laid out against the previous score's beat positions. Clearing it here is not safe yet: _restore_timeline_state restores svg_data before viewer_beat_x, so an undo would reload the already-marker-stripped SVG while the mapping is empty and recomputation yields nothing.
  • components_hash is nondeterministic across undo. TimelineComponentManager.restore_state recreates components in set-iteration order while hash_components() hashes them in list order, so a restored state's hash depends on PYTHONHASHSEED (fails on seeds 0, 1, 4; passes on 2, 3, 5 — same on clean dev). That makes undoable() unusable on any timeline with several components and can mark an unmodified file as modified. The undo/redo test here asserts restored values directly and carries a comment pointing at this.

Unrelated: parallel test runs

pytest-xdist was already a dev dependency but nothing documented it, so the documented way to run the suite takes about eight minutes instead of one and a half. 5dd1a7fc adds pytest -n auto to CLAUDE.md, along with the two intermittent-failure sources that look like regressions but are not — cross-test interference under -n auto, and the PYTHONHASHSEED dependency above.

`load_svg_data` only treated `set_data` returning success=False as a
failure, but `viewer_beat_x` uses `validate_pre_validated`, so the call
always succeeds and SCORE_SVG_CREATE_ERROR was unreachable. A musicXML
render without usable beat markers silently produced an empty beat-to-x
mapping instead of telling the user the file was not set up properly.

Check the returned mapping as well.
Reading `svg_view` constructed an SvgViewer whenever none was registered,
so any consumer could resurrect a viewer as a side effect of a lookup --
including `on_audio_time_change`, which runs on every playback tick, and
`reset_svg`, whose whole job is to get rid of the viewer.

`svg_view` now returns None when there is no viewer, and the explicit
`get_or_create_svg_view()` handles construction. That also covers the case
where the request returns None instead of raising: only one callback can be
attached per Get member, so with two score timelines the viewer of one
answers for the other's id and returns None. `reset_svg` and
`on_audio_time_change` raised AttributeError on that path.
Clearing a score timeline left `svg_data` in place, so the viewer came
back with the discarded score: `reset_svg` closed it, but the next
playback tick reopened and reloaded it, and even without that the score
was still in the saved file and reappeared on the next open.

`ScoreTLComponentManager.clear` now discards `svg_data`, and
`update_svg_data` closes the viewer instead of loading when there is no
score left. That second half also matters for undo/redo: restoring a
cleared state pushes an empty string through the update trigger, which
`SvgViewer.load_svg_data` fed straight to `etree.fromstring` and raised
`XMLSyntaxError: Document is empty`. It now refuses empty data outright.

Closes #472
pytest-xdist is already a dev dependency but nothing said to use it, so
the documented way to run the suite takes minutes instead of about one.

Also record the two intermittent-failure sources that look like real
regressions but are not: cross-test interference under -n auto, and
undoable() depending on PYTHONHASHSEED through the component ordering in
restore_state.
_setup_svg_view now goes through get_or_create_svg_view, which sets the
measure tracker's visibility from the viewer instead of calling show()
unconditionally. Pin the behaviour it is meant to preserve.
Comment thread CLAUDE.md
Comment on lines +46 to +50
`-n auto`, and which ones fail varies between runs; this predates any given change, so confirm a
failure reproduces serially before investigating it. The same goes for `undoable()` on a timeline
with several components: `TimelineComponentManager.restore_state` recreates components in
set-iteration order while `hash_components()` hashes them in list order, so whether the assertion
passes depends on `PYTHONHASHSEED`.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

though also good to check which tests are leaking data / affecting the other tests. could potentially indicate a real world user interaction sequence issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants