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()