309 lines
13 KiB
Python
309 lines
13 KiB
Python
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_restart_actions_warn_reset_and_preserve_or_clear_selection(self):
|
||
self.save_selection()
|
||
self.app.state_store.update_step("refaire_merge", status="success")
|
||
self.app.state_store.update_step("annotation", status="success")
|
||
selection = read_json(self.root / "refaire.json")
|
||
with patch(
|
||
"copienator_gui.app.messagebox.askyesno", return_value=False
|
||
) as confirm:
|
||
self.app._start_refaire_pass(False)
|
||
self.assertIn("importé", confirm.call_args.args[1])
|
||
self.assertIn("Mettre à jour", confirm.call_args.args[1])
|
||
self.assertEqual(read_json(self.root / "refaire.json"), selection)
|
||
with patch("copienator_gui.app.messagebox.askyesno", return_value=True):
|
||
self.app._start_refaire_pass(True)
|
||
self.app.update()
|
||
self.assertEqual(self.app.current_step.id, "refaire_selection")
|
||
self.assertEqual(self.app.refaire_panel.values()["selection"], selection)
|
||
self.assertIsNone(self.app.state_store.step("refaire_merge").get("status"))
|
||
self.assertEqual(self.app.state_store.step("annotation")["status"], "success")
|
||
first = self.app.state_store.workspace.refaire_session_id
|
||
with patch(
|
||
"copienator_gui.app.messagebox.askyesno", return_value=True
|
||
) as confirm:
|
||
self.app._start_refaire_pass(False)
|
||
self.app.update()
|
||
self.assertIn("n’est pas marquée comme fusionnée", confirm.call_args.args[1])
|
||
self.assertEqual(self.app.refaire_panel.entries, {})
|
||
self.assertEqual(read_json(self.root / "refaire.json"), [])
|
||
self.assertNotEqual(self.app.state_store.workspace.refaire_session_id, first)
|
||
self.assertEqual(self.app.refaire_panel.source_var.get(), "Anot")
|
||
|
||
def test_restart_is_blocked_while_a_command_is_active(self):
|
||
self.save_selection()
|
||
self.app.active_step_id = "refaire_correct"
|
||
with (
|
||
patch("copienator_gui.app.messagebox.showwarning") as warning,
|
||
patch("copienator_gui.app.messagebox.askyesno") as confirm,
|
||
):
|
||
self.app._start_refaire_pass(False)
|
||
warning.assert_called_once()
|
||
confirm.assert_not_called()
|
||
self.assertIsNone(self.app.state_store.workspace.refaire_session_id)
|
||
|
||
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()
|