Refaire fixes and GUI support

This commit is contained in:
2026-09-06 19:06:42 +02:00
parent a9897606ac
commit 2ff1a9b7b9
11 changed files with 1664 additions and 116 deletions
+265
View File
@@ -0,0 +1,265 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from copienator import atomic_write_json, read_json
from copienator_gui.app import CopienatorApp
from copienator_gui.refaire import (
ALL_COPIES,
SECTION,
available_copies,
resolve_layout,
validate_selection,
)
from copienator_gui.workflow import build_command, build_workflow
class SelectionTests(unittest.TestCase):
def test_selection_validates_and_deduplicates_labels(self):
copies = {"Copie01": Path("/tmp/Copie01.pdf")}
self.assertEqual(
validate_selection(
[["Copie01", ["Ex 2", "Ex 1", "Ex 2"]]], copies, ["Ex 1", "Ex 2"]
),
[["Copie01", ["Ex 1", "Ex 2"]]],
)
for invalid in (
[],
[["missing", []]],
[["Copie01", ["unknown"]]],
[["../Copie01", []]],
):
with self.subTest(invalid=invalid), self.assertRaises(ValueError):
validate_selection(invalid, copies, ["Ex 1"])
def test_automatic_layout_groups_shared_labels_and_honors_explicit_choice(self):
selection = [["Copie01", ["Ex 1"]], ["Copie02", ["Ex 1"]]]
self.assertEqual(resolve_layout(selection, ["Ex 1", "Ex 2"]), "grouped")
self.assertEqual(resolve_layout(selection, ["Ex 1"], "copies"), "copies")
self.assertEqual(resolve_layout([["Copie01", ["Ex 1"]]], ["Ex 1"]), "copies")
self.assertEqual(
resolve_layout([["Copie01", ["Ex 1"]], ["Copie02", []]], ["Ex 1"]),
"grouped",
)
def test_redo_steps_are_in_both_profiles_and_never_autostart(self):
for personal in (False, True):
steps = [
step for step in build_workflow(personal) if step.section == SECTION
]
self.assertEqual(len(steps), 9)
self.assertEqual(steps[0].id, "refaire_selection")
self.assertTrue(all(not step.auto_start_first_visit for step in steps))
merge = steps[-1]
for mode in ("BGnot", "Bnot", "Anot"):
command = build_command(
Path.cwd(),
merge,
merge.variants[0],
{"target": "/tmp/Exam", "annotation_dir": mode},
"/tmp/Exam",
)
self.assertEqual(
command[4:],
[
"read-grouped",
"/tmp/Exam",
"--refaire",
"--annotation-dir",
mode,
],
)
@unittest.skipUnless(
os.environ.get("DISPLAY"), "Tk tests require a display (use xvfb-run)"
)
class RefaireGuiTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.root = Path(self.temp.name)
(self.root / "Copies").mkdir()
(self.root / "Anot").mkdir()
(self.root / "Par label").mkdir()
(self.root / "BRnot").mkdir()
for name in ("Copie01", "Copie02"):
(self.root / "Copies" / f"{name}.pdf").touch()
(self.root / "labels").write_text("Ex 1\nEx 2\n", encoding="utf-8")
atomic_write_json(self.root / "correction.json", {})
self.app = CopienatorApp(Path.cwd(), False, self.root)
self.app.update()
def tearDown(self):
for callback in self.app.tk.splitlist(self.app.tk.call("after", "info")):
self.app.after_cancel(callback)
self.app.destroy()
self.temp.cleanup()
def select(self, ident):
self.app.tree.selection_set(ident)
self.app.tree.see(ident)
self.app.update()
def save_selection(self):
self.select("refaire_selection")
panel = self.app.refaire_panel
panel.label_var.set("Ex 1")
panel.add()
panel.label_var.set("Ex 2")
panel.add()
panel.copy_var.set("Copie02")
panel.label_var.set("Toute la copie")
panel.add()
self.app._run_current_step()
self.app.update()
def test_collapsed_branch_is_separate_and_stays_open_when_refreshed(self):
section = self.app.tree.parent("refaire_selection")
self.assertFalse(self.app.tree.item(section, "open"))
self.assertNotIn("refaire_selection", self.app._progression_ids("giving_names"))
self.assertNotIn("clean", self.app._progression_ids("refaire_merge"))
self.select("refaire_selection")
self.app._populate_tree()
self.assertTrue(self.app.tree.item(section, "open"))
def test_dropdown_selection_saves_json_and_drives_all_commands(self):
self.save_selection()
self.assertEqual(
read_json(self.root / "refaire.json"),
[["Copie01", ["Ex 1", "Ex 2"]], ["Copie02", []]],
)
self.assertEqual(self.app.current_step.id, "refaire_review")
commands = self.app._refaire_commands()
self.assertEqual(
[command[5] for command in commands],
[str(path) for path in available_copies(self.root).values()],
)
self.select("refaire_annotate")
self.assertEqual(
self.app._make_command()[4:],
["annotate-grouped", str(self.root), "--refaire", "--overwrite"],
)
self.select("refaire_merge")
self.assertEqual(
self.app._make_command()[4:],
["read-grouped", str(self.root), "--refaire", "--annotation-dir", "Anot"],
)
self.assertEqual(self.app.state_store.step("annotation").get("status"), None)
def test_one_label_for_all_copies_and_layout_override(self):
for name in ("Copie01", "Copie02"):
directory = self.root / "Copies" / name
directory.mkdir()
(directory / "Ex 1.pdf").touch()
self.select("refaire_selection")
panel = self.app.refaire_panel
panel.copy_var.set(ALL_COPIES)
panel.label_var.set("Ex 1")
panel.add()
self.assertEqual(panel.entries, {"Copie01": ["Ex 1"], "Copie02": ["Ex 1"]})
panel.layout_var.set("Par copie")
self.app._run_current_step()
self.app.update()
self.select("refaire_annotate")
self.assertEqual(self.app._make_command()[4], "annotate-checks")
def test_bulk_add_skips_absent_answers_and_preserves_whole_copy_selection(self):
directory = self.root / "Copies/Copie01"
directory.mkdir()
(directory / "Ex 1_new.pdf").touch()
self.select("refaire_selection")
panel = self.app.refaire_panel
panel.copy_var.set("Copie01")
panel.add()
panel.copy_var.set(ALL_COPIES)
panel.label_var.set("Ex 1")
panel.add()
self.assertEqual(panel.entries, {"Copie01": []})
self.assertIn("1 sans réponse", panel.message_var.get())
def test_correction_folder_buttons_open_expected_folders(self):
for name in ("Sol", "Persp"):
(self.root / name).mkdir()
self.select("refaire_selection")
buttons = self.app.form.winfo_children()[0].winfo_children()
with patch("copienator_gui.app.open_path") as opened:
for button in buttons:
button.invoke()
self.assertEqual(
[call.args[0] for call in opened.call_args_list],
[self.root / "Sol", self.root / "Persp"],
)
def test_small_window_keeps_selection_accessible_by_scrolling(self):
self.select("refaire_selection")
self.app.geometry("900x640")
self.app.update()
self.app.form_canvas.yview_moveto(1)
self.app.update()
self.assertAlmostEqual(self.app.form_canvas.yview()[1], 1.0)
self.assertLess(
self.app.run_button.winfo_rooty() + self.app.run_button.winfo_height(),
self.app.winfo_rooty() + self.app.winfo_height(),
)
def test_queue_runs_copies_in_order_and_stops_on_failure(self):
self.save_selection()
self.select("refaire_split")
with patch.object(self.app.runner, "start") as start:
self.app._run_current_step()
self.assertEqual(start.call_count, 1)
self.assertEqual(len(self.app.pending_refaire_commands), 1)
self.app._finish_process(0, False)
self.assertEqual(start.call_count, 2)
self.assertEqual(self.app.active_step_id, "refaire_split")
self.app._finish_process(1, False)
self.assertEqual(
self.app.state_store.step("refaire_split")["status"], "failed"
)
self.assertIsNone(self.app.active_step_id)
self.assertFalse(self.app.pending_refaire_commands)
def test_interruption_does_not_launch_next_copy(self):
self.save_selection()
self.select("refaire_split")
with patch.object(self.app.runner, "start") as start:
self.app._run_current_step()
self.app._finish_process(130, True)
self.assertEqual(start.call_count, 1)
self.assertFalse(self.app.pending_refaire_commands)
self.assertEqual(
self.app.state_store.step("refaire_split")["status"], "interrupted"
)
def test_unsaved_or_external_changes_block_commands(self):
self.save_selection()
atomic_write_json(self.root / "refaire.json", [["Copie02", []]])
with self.assertRaisesRegex(ValueError, "sélection a changé"):
self.app._refaire_commands()
def test_selection_reload_and_finalization_invalidation(self):
self.save_selection()
self.app.state_store.update_step("giving_names", status="success")
self.app.state_store.update_step(
"annotation", status="success", last_run_variant="simple"
)
self.select("refaire_merge")
with patch.object(self.app.runner, "start"):
self.app._run_current_step()
self.app._finish_process(0, False)
self.app.update()
self.assertEqual(self.app.state_store.step("giving_names")["status"], "stale")
self.assertEqual(self.app.state_store.step("annotation")["status"], "success")
self.assertEqual(self.app.current_step.id, "refaire_merge")
self.select("refaire_selection")
self.assertEqual(
self.app.refaire_panel.entries, {"Copie01": ["Ex 1", "Ex 2"], "Copie02": []}
)
if __name__ == "__main__":
unittest.main()