import os import tempfile import unittest from pathlib import Path from unittest.mock import patch from copienator import read_json from copienator.custom_remarks import remarks_for_copy from copienator_gui.app import CopienatorApp from copienator_gui.scores import score_path_for_id, set_score from copienator_gui.workflow import build_workflow class ScoreEditorTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.evaluation = Path(temporary.name) folder = self.evaluation / "A Rendre" / "Student (01)" folder.mkdir(parents=True) self.path = folder / "score.json" self.path.write_text('{"Ex 1": "2", "Ex 2": "4"}') def test_step_is_optional_and_follows_return_preparation_in_both_profiles(self): for personal in (False, True): steps = build_workflow(personal) index = next(index for index, step in enumerate(steps) if step.id == "giving_names") self.assertEqual(steps[index + 1].id, "edit_scores") self.assertTrue(steps[index + 1].optional) self.assertTrue(steps[index + 1].is_manual) def test_update_preserves_other_labels_and_follows_symlinks(self): source = self.evaluation / "source-score.json" self.path.rename(source) self.path.symlink_to(source) path = score_path_for_id(self.evaluation, "1") self.assertEqual(path, score_path_for_id(self.evaluation, "01")) self.assertEqual(set_score(path, "Ex 1", "3,5"), "2") self.assertTrue(path.is_symlink()) self.assertEqual(read_json(source), {"Ex 1": "3.5", "Ex 2": "4"}) self.assertEqual(read_json(path), read_json(source)) def test_invalid_score_or_missing_label_does_not_modify_file(self): before = self.path.read_bytes() for value in ("", "abc", "NaN", "Infinity", "-1"): with self.subTest(value=value), self.assertRaises(ValueError): set_score(self.path, "Ex 1", value) self.assertEqual(self.path.read_bytes(), before) with self.assertRaises(ValueError): set_score(self.path, "Missing", "3") self.assertEqual(self.path.read_bytes(), before) def test_missing_and_ambiguous_ids_are_rejected(self): with self.assertRaises(FileNotFoundError): score_path_for_id(self.evaluation, "2") with self.assertRaises(ValueError): score_path_for_id(self.evaluation, "../1") (self.path.parent.parent / "Other (1)").mkdir() with self.assertRaises(ValueError): score_path_for_id(self.evaluation, "1") @unittest.skipUnless(os.environ.get("DISPLAY"), "Tk requires a display") class ScoreEditorGuiTests(ScoreEditorTests): def setUp(self): super().setUp() self.app = CopienatorApp(Path.cwd(), True, self.evaluation) self.addCleanup(self.close_app) self.app.update() self.app.tree.selection_set("edit_scores") self.app.update() self.panel = self.app.score_editor_panel def close_app(self): for callback in self.app.tk.splitlist(self.app.tk.call("after", "info")): self.app.after_cancel(callback) self.app.destroy() def test_selection_loads_score_and_set_keeps_editor_open(self): self.assertEqual(str(self.panel.set_button.cget("state")), "disabled") self.panel.id_var.set("1") self.assertEqual(tuple(self.panel.label_entry.cget("values")), ("Ex 1", "Ex 2")) self.panel.label_var.set("Ex 1") self.assertEqual(self.panel.score_var.get(), "2") self.app.state_store.update_step("final_score", status="success") self.panel.score_var.set("3,5") self.panel.set_button.invoke() self.app.update() self.assertIs(self.app.score_editor_panel, self.panel) self.assertEqual(self.panel.id_var.get(), "1") self.assertEqual(read_json(self.path), {"Ex 1": "3.5", "Ex 2": "4"}) self.assertEqual(self.app.state_store.step("final_score")["status"], "stale") self.panel.label_var.set("Ex 2") self.assertEqual(self.panel.score_var.get(), "4") self.panel.id_var.set("2") self.assertEqual(self.panel.score_var.get(), "") self.assertEqual(str(self.panel.set_button.cget("state")), "disabled") def test_invalid_score_shows_error_and_running_disables_edits(self): self.panel.id_var.set("01") self.panel.label_var.set("Ex 1") self.panel.score_var.set("bad") with patch("copienator_gui.scores.messagebox.showerror") as error: self.panel.set_button.invoke() error.assert_called_once() self.assertEqual(read_json(self.path)["Ex 1"], "2") self.panel.set_busy(True) self.assertEqual(str(self.panel.set_button.cget("state")), "disabled") def test_skipping_without_changes_preserves_downstream_status(self): self.app.state_store.update_step("final_score", status="success") self.app._skip_step() self.assertEqual(self.app.state_store.step("edit_scores")["status"], "skipped") self.assertEqual(self.app.state_store.step("final_score")["status"], "success") def test_remark_saves_independently_reloads_and_clears(self): self.panel.id_var.set("1") self.panel.label_var.set("Ex 1") self.panel.score_var.set("bad unsaved score") self.panel.remark_entry.insert("1.0", "Justifier.\nDétailler le calcul.") self.panel.remark_button.invoke() self.assertEqual(read_json(self.path)["Ex 1"], "2") self.assertEqual(remarks_for_copy(self.evaluation, "01")["Ex 1"], "Justifier.\nDétailler le calcul.") self.assertEqual(self.app.state_store.step("read_annotations")["status"], "stale") self.panel.label_var.set("Ex 2") self.assertEqual(self.panel.remark_entry.get("1.0", "end-1c"), "") self.panel.label_var.set("Ex 1") self.assertIn("Justifier.", self.panel.remark_entry.get("1.0", "end-1c")) self.panel.remark_entry.delete("1.0", "end") self.panel.remark_button.invoke() self.assertEqual(remarks_for_copy(self.evaluation, "1"), {}) self.panel.set_busy(True) self.assertEqual(str(self.panel.remark_button.cget("state")), "disabled") def test_completing_editor_selects_regeneration_and_preserves_options(self): self.app.state_store.update_step("read_annotations", status="success", variant="grouped", values={"update_score": False, "return_answers_solution": False, "annotation_dir": "BGnot"}) with patch.object(self.app.runner, "start") as start: self.app.run_button.invoke() self.app.update() start.assert_not_called() self.assertEqual(self.app.state_store.step("edit_scores")["status"], "success") self.assertEqual(self.app.current_step.id, "read_annotations") self.assertEqual(self.app.tree.selection(), ("read_annotations",)) self.assertEqual(self.app.tree.item("read_annotations", "values"), ("À revalider",)) self.assertTrue(self.app.arg_vars["update_score"].get()) self.assertEqual(self.app.variant_var.get(), "grouped") self.assertFalse(self.app.arg_vars["return_answers_solution"].get()) self.assertTrue(self.app.state_store.step("read_annotations")["values"]["update_score"]) self.assertIn("--update-score", self.app._make_command())