Files
noteman-slicer/tests/test_project.py
T
Esa Kataja b6847a06ee Treat a project as spent once its song has been exported
Opening an exported song starts a fresh session from detection instead
of resuming: cuts, discards and metadata do not carry over, so a re-cut
never inherits decisions that have already shipped. --resume overrides
it on edit, export and project.

This reverses what was agreed in planning and written into docs/spec.md
and CONTEXT.md, which promised resume-across-sessions and re-export.
Both are corrected. The cost is deliberate and worth stating: changing
the width cap or adding the SVG renderer later now means re-cutting each
song by hand rather than regenerating every bundle from its project
file.

Export records the flag in bundle.write, so no caller can forget it.

Also removed --refit and the Auto-fit buttons, which were added without
being asked for and whose only purpose - migrating projects made before
the content rectangle was proposed - disappears once exported projects
start fresh. Reset now restores detection's proposal rather than the
whole page: clearing to full width would undo the thing the rectangle
exists for, so one button covers it.

open_project() replaces four copies of load-or-detect across the CLI
and the editor.
2026-07-28 23:49:03 +03:00

89 lines
3.1 KiB
Python

"""Runnable check for project state: round-trip, cut edits, discard pre-set."""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
from noteman_slicer.detect import PageDetection, System # noqa: E402
from noteman_slicer.project import Cut, Project, default_path # noqa: E402
def main() -> int:
tmp = Path(__file__).with_name("_tmp")
tmp.mkdir(exist_ok=True)
pdf = tmp / "song.pdf"
pdf.write_bytes(b"%PDF-1.7 not really a pdf, only its bytes are hashed")
height = 1000
detection = PageDetection(
skew=-1.1,
systems=[System(200, 400, 60.0), System(600, 800, 60.0)],
cuts=[500],
)
project = Project.from_detection(pdf, [detection], [height])
page = project.pages[0]
# One cut between the systems, plus a boundary cut above the first and
# below the last — so the header and footer become their own slices.
assert len(page.cuts) == 3, [c.points for c in page.cuts]
assert page.discards == [True, False, False, True], page.discards
assert page.slice_count == 4
assert project.kept_slices() == [(0, 1), (0, 2)], project.kept_slices()
# Geometry is normalised, so it survives any change of resolution.
assert all(0.0 <= y <= 1.0 for cut in page.cuts for _, y in cut.points)
assert page.cuts[1].straight_y == 0.5
# A straight cut is flat; a stepped one is not, and interpolates.
step = Cut([(0.0, 0.20), (0.35, 0.20), (0.35, 0.40), (1.0, 0.40)])
assert step.straight_y is None
assert step.y_at(0.0) == 0.20
assert step.y_at(1.0) == 0.40
assert step.y_at(0.35) == 0.20 or step.y_at(0.35) == 0.40
assert step.highest == 0.20 and step.lowest == 0.40
# Adding a cut splits a slice and keeps that slice's flag on both halves.
before = page.slice_count
index = page.add_cut(Cut.straight(0.65))
assert page.slice_count == before + 1
assert index == 2, index
assert page.discards == [True, False, False, False, True], page.discards
# Removing it merges them again.
page.remove_cut(index)
assert page.slice_count == before
assert page.discards == [True, False, False, True], page.discards
# Round-trip.
saved = project.save()
assert saved == default_path(pdf), saved
reloaded = Project.load(saved)
assert reloaded.pages[0].skew == -1.1
assert reloaded.pages[0].discards == page.discards
assert [c.points for c in reloaded.pages[0].cuts] == [c.points for c in page.cuts]
assert reloaded.source_hash == project.source_hash
assert not reloaded.source_changed()
# A project is spent once exported: reopening starts fresh.
assert not reloaded.exported
reloaded.exported = True
reloaded.save()
assert Project.load(saved).exported
# A PDF edited underneath must be reported, not silently re-cut.
pdf.write_bytes(b"%PDF-1.7 different bytes entirely")
assert reloaded.source_changed()
for f in (pdf, saved):
f.unlink()
tmp.rmdir()
print("ok")
return 0
if __name__ == "__main__":
sys.exit(main())