Refaire fixes and GUI support
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from copienator import EvaluationWorkspace, ExitCode, atomic_write_json, read_json
|
||||
from copienator.annotation_data import AnnotationLoadResult
|
||||
from copienator.commands import annotating_by_label as grouped
|
||||
from copienator.commands import annotating_with_checks as checks
|
||||
from copienator.commands import export, import_annotations
|
||||
from copienator.commands import reading_grouped_annotations as reader
|
||||
|
||||
|
||||
class GroupedRedoTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.root = Path(self.temp.name) / "Exam"
|
||||
for directory in ("Copies", "Par label", "BGnot", "BRnot"):
|
||||
(self.root / directory).mkdir(parents=True)
|
||||
(self.root / "labels").write_text("Ex 1\nEx 2\n")
|
||||
atomic_write_json(self.root / "correction.json", {})
|
||||
atomic_write_json(
|
||||
self.root / "refaire.json", [["Copie01", ["Ex 1"]], ["Copie02", ["Ex 1"]]]
|
||||
)
|
||||
(self.root / "BRnot/previous.txt").write_text("previous redo")
|
||||
(self.root / "BGnot/main.txt").write_text("main run")
|
||||
self.workspace = EvaluationWorkspace(self.root)
|
||||
self.data = {"01": {"Ex 1": {}}, "02": {"Ex 1": {}}}
|
||||
|
||||
def tearDown(self):
|
||||
self.temp.cleanup()
|
||||
|
||||
@staticmethod
|
||||
def render(item):
|
||||
student_id, label, _content = item
|
||||
image = Image.new("RGB", (100, 100), "white")
|
||||
ImageDraw.Draw(image).rectangle((10, 10, 50, 50), outline="black", width=2)
|
||||
return (
|
||||
student_id,
|
||||
label,
|
||||
image,
|
||||
0,
|
||||
[
|
||||
{
|
||||
"type": "score",
|
||||
"label": label,
|
||||
"value": 3,
|
||||
"final_box": [10, 10, 50, 50],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def generate(self):
|
||||
with (
|
||||
patch.object(
|
||||
grouped,
|
||||
"load_annotation_data",
|
||||
return_value=AnnotationLoadResult(self.data, []),
|
||||
) as load,
|
||||
patch.object(grouped, "render_item", side_effect=self.render),
|
||||
patch.object(grouped, "_load_label_groups") as label_groups,
|
||||
):
|
||||
self.assertEqual(
|
||||
grouped.run(self.workspace, refaire=True, overwrite=True),
|
||||
ExitCode.SUCCESS,
|
||||
)
|
||||
self.assertEqual(
|
||||
load.call_args.kwargs["refaire_list"],
|
||||
read_json(self.root / "refaire.json"),
|
||||
)
|
||||
label_groups.assert_not_called()
|
||||
directories = list((self.root / "BRnot").iterdir())
|
||||
self.assertEqual(len(directories), 1)
|
||||
self.assertTrue(directories[0].is_dir())
|
||||
self.assertEqual((self.root / "BGnot/main.txt").read_text(), "main run")
|
||||
return directories[0]
|
||||
|
||||
def test_grouped_redo_export_import_and_actual_annotation_detection(self):
|
||||
directory = self.generate()
|
||||
metadata = read_json(directory / "bnote.json")["images"]
|
||||
self.assertEqual(
|
||||
[(item["id"], item["label"]) for item in metadata],
|
||||
[("01", "Ex 1"), ("02", "Ex 1")],
|
||||
)
|
||||
export_root = Path(self.temp.name) / "Export"
|
||||
with patch.object(export, "EXPORT_DIR", export_root):
|
||||
self.assertEqual(export.run(self.workspace, refaire=True), ExitCode.SUCCESS)
|
||||
self.assertEqual(len(list((export_root / "Exam").glob("*.pdf"))), 1)
|
||||
imported_root = Path(self.temp.name) / "Import"
|
||||
imported_root.mkdir()
|
||||
with Image.open(directory / "Reference.jpg") as reference:
|
||||
annotated = reference.convert("RGB")
|
||||
draw = ImageDraw.Draw(annotated)
|
||||
draw.rectangle((17, 17, 43, 43), fill="black")
|
||||
draw.rectangle((70, 170, 90, 190), fill="black")
|
||||
annotated.save(imported_root / f"{directory.name}.pdf", "PDF", resolution=72)
|
||||
with patch.object(import_annotations, "IMPORT_DIR", imported_root):
|
||||
self.assertEqual(
|
||||
import_annotations.run(self.workspace, refaire=True), ExitCode.SUCCESS
|
||||
)
|
||||
actions, notes, incomplete = reader._scan_redo_annotations(
|
||||
self.root / "BRnot", {"01": {"Ex 1"}, "02": {"Ex 1"}}
|
||||
)
|
||||
self.assertFalse(incomplete)
|
||||
self.assertEqual(actions["01"][0]["value"], 3)
|
||||
self.assertFalse(actions.get("02"))
|
||||
self.assertIn("Ex 1", notes["02"])
|
||||
full_data = {student: {"Ex 1": {}, "Ex 2": {}} for student in ("01", "02")}
|
||||
for mode in ("BGnot", "Bnot", "Anot"):
|
||||
(self.root / mode).mkdir(exist_ok=True)
|
||||
with (
|
||||
self.subTest(mode=mode),
|
||||
patch.object(
|
||||
reader,
|
||||
"load_annotation_data",
|
||||
return_value=AnnotationLoadResult(full_data, []),
|
||||
),
|
||||
patch.object(
|
||||
reader,
|
||||
"apply_actions_and_regenerate_grouped",
|
||||
return_value=(ExitCode.SUCCESS, ""),
|
||||
) as regenerate,
|
||||
):
|
||||
self.assertEqual(
|
||||
reader.run(self.workspace, refaire=True, annotation_dir=mode),
|
||||
ExitCode.SUCCESS,
|
||||
)
|
||||
calls = {call.args[2]: call for call in regenerate.call_args_list}
|
||||
self.assertEqual(set(calls), {"01", "02"})
|
||||
self.assertEqual(set(calls["01"].args[1]["01"]), {"Ex 1", "Ex 2"})
|
||||
self.assertEqual(calls["01"].args[3][0]["value"], 3)
|
||||
self.assertIn("Ex 1", calls["02"].args[4])
|
||||
|
||||
def test_missing_group_leaves_affected_copy_incomplete(self):
|
||||
directory = self.generate()
|
||||
shutil.copy2(directory / "Concat.pdf", directory / "Concat_annotated.pdf")
|
||||
extra = self.root / "BRnot/Ex 2 G1"
|
||||
extra.mkdir()
|
||||
atomic_write_json(
|
||||
extra / "bnote.json", {"images": [{"id": "01", "label": "Ex 2"}]}
|
||||
)
|
||||
_actions, _notes, incomplete = reader._scan_redo_annotations(
|
||||
self.root / "BRnot", {"01": {"Ex 1", "Ex 2"}, "02": {"Ex 1"}}
|
||||
)
|
||||
self.assertEqual(incomplete, {"01"})
|
||||
|
||||
def test_failed_generation_preserves_previous_redo_for_both_layouts(self):
|
||||
for module, worker in ((grouped, "render_item"), (checks, "_render_student")):
|
||||
with (
|
||||
self.subTest(module=module),
|
||||
patch.object(
|
||||
module,
|
||||
"load_annotation_data",
|
||||
return_value=AnnotationLoadResult(self.data, []),
|
||||
),
|
||||
patch.object(
|
||||
module,
|
||||
worker,
|
||||
return_value=None if module is grouped else "partial",
|
||||
),
|
||||
):
|
||||
if module is grouped:
|
||||
status = module.run(self.workspace, refaire=True, overwrite=True)
|
||||
else:
|
||||
status = module.run(
|
||||
self.workspace, self.root, refaire=True, overwrite=True
|
||||
)
|
||||
self.assertEqual(status, ExitCode.PARTIAL)
|
||||
self.assertEqual(
|
||||
(self.root / "BRnot/previous.txt").read_text(), "previous redo"
|
||||
)
|
||||
|
||||
def test_switching_to_per_copy_removes_previous_group_layout(self):
|
||||
self.generate()
|
||||
|
||||
def render(_workspace, student_id, _labels, **kwargs):
|
||||
output = kwargs["output_root"] / f"Copie{student_id}"
|
||||
output.mkdir()
|
||||
(output / "Concat.pdf").touch()
|
||||
return "success"
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
checks,
|
||||
"load_annotation_data",
|
||||
return_value=AnnotationLoadResult(self.data, []),
|
||||
),
|
||||
patch.object(checks, "_render_student", side_effect=render),
|
||||
):
|
||||
self.assertEqual(
|
||||
checks.run(self.workspace, self.root, refaire=True, overwrite=True),
|
||||
ExitCode.SUCCESS,
|
||||
)
|
||||
self.assertEqual(
|
||||
{path.name for path in (self.root / "BRnot").iterdir()},
|
||||
{"Copie01", "Copie02"},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,280 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from copienator import EvaluationWorkspace, ExitCode, atomic_write_json, read_json
|
||||
from copienator.annotation_data import AnnotationLoadResult, _coordinate_index
|
||||
from copienator.commands import reading_grouped_annotations as reader
|
||||
|
||||
|
||||
class RefaireTests(unittest.TestCase):
|
||||
def workspace(self, root, mode):
|
||||
for name in ("Copies", "Par label", mode, "BRnot/Copie01"):
|
||||
(root / name).mkdir(parents=True)
|
||||
(root / "labels").write_text("Ex 1\nEx 2\n")
|
||||
atomic_write_json(root / "correction.json", {})
|
||||
atomic_write_json(root / "refaire.json", [["Copie01", ["Ex 2"]]])
|
||||
for name in (
|
||||
"bnote.json",
|
||||
"checkboxes.json",
|
||||
"Reference.jpg",
|
||||
"Concat_annotated.pdf",
|
||||
):
|
||||
(root / "BRnot/Copie01" / name).touch()
|
||||
atomic_write_json(
|
||||
root / "BRnot/Copie01/bnote.json", {"images": [{"label": "Ex 2"}]}
|
||||
)
|
||||
return EvaluationWorkspace(root)
|
||||
|
||||
def test_reader_merges_full_copy_for_every_original_mode(self):
|
||||
for mode in ("BGnot", "Bnot", "Anot"):
|
||||
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as tmp:
|
||||
workspace = self.workspace(Path(tmp), mode)
|
||||
data = {"01": {"Ex 1": {}, "Ex 2": {}}, "02": {"Ex 1": {}}}
|
||||
|
||||
def load(_workspace, data=data, **kwargs):
|
||||
return AnnotationLoadResult(
|
||||
{"01": {"Ex 2": {}}} if kwargs else data, []
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(reader, "load_annotation_data", side_effect=load),
|
||||
patch.object(
|
||||
reader,
|
||||
"_scan_annotation_directory",
|
||||
return_value=(
|
||||
{"01": [{"label": "Ex 2", "type": "score", "value": 3}]},
|
||||
{},
|
||||
),
|
||||
),
|
||||
patch.object(
|
||||
reader,
|
||||
"apply_actions_and_regenerate_grouped",
|
||||
return_value=(ExitCode.SUCCESS, ""),
|
||||
) as render,
|
||||
):
|
||||
self.assertEqual(
|
||||
reader.run(workspace, refaire=True, annotation_dir=mode),
|
||||
ExitCode.SUCCESS,
|
||||
)
|
||||
render.assert_called_once()
|
||||
self.assertEqual(set(render.call_args.args[1]["01"]), {"Ex 1", "Ex 2"})
|
||||
self.assertEqual(render.call_args.args[2], "01")
|
||||
self.assertEqual(render.call_args.kwargs["selected_labels"], {"Ex 2"})
|
||||
self.assertEqual(render.call_args.kwargs["annotation_dir"], mode)
|
||||
|
||||
def test_whole_copy_selection_replaces_all_old_actions(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
workspace = self.workspace(root, "BGnot")
|
||||
(root / "BGnot/Ex 1").mkdir()
|
||||
atomic_write_json(root / "refaire.json", [["Copie01", []]])
|
||||
atomic_write_json(
|
||||
root / "BRnot/Copie01/bnote.json",
|
||||
{"images": [{"label": label} for label in ("Ex 1", "Ex 2")]},
|
||||
)
|
||||
loaded = AnnotationLoadResult({"01": {"Ex 1": {}, "Ex 2": {}}}, [])
|
||||
|
||||
def scan(directory, *args, **kwargs):
|
||||
return (
|
||||
({"01": [{"label": "Ex 1", "type": "score", "value": 1}]}, {})
|
||||
if directory.parent.name == "BGnot"
|
||||
else ({"01": [{"label": "Ex 2", "type": "score", "value": 4}]}, {})
|
||||
)
|
||||
|
||||
with (
|
||||
patch.object(reader, "load_annotation_data", return_value=loaded),
|
||||
patch.object(reader, "_scan_annotation_directory", side_effect=scan),
|
||||
patch.object(
|
||||
reader,
|
||||
"apply_actions_and_regenerate_grouped",
|
||||
return_value=(ExitCode.SUCCESS, ""),
|
||||
) as render,
|
||||
):
|
||||
self.assertEqual(reader.run(workspace, refaire=True), ExitCode.SUCCESS)
|
||||
self.assertEqual(
|
||||
render.call_args.kwargs["selected_labels"], {"Ex 1", "Ex 2"}
|
||||
)
|
||||
self.assertEqual(
|
||||
render.call_args.args[3],
|
||||
[{"label": "Ex 2", "type": "score", "value": 4}],
|
||||
)
|
||||
|
||||
def test_stale_redo_selection_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
workspace = self.workspace(root, "Anot")
|
||||
atomic_write_json(
|
||||
root / "BRnot/Copie01/bnote.json", {"images": [{"label": "Ex 1"}]}
|
||||
)
|
||||
with (
|
||||
patch.object(
|
||||
reader,
|
||||
"load_annotation_data",
|
||||
return_value=AnnotationLoadResult({"01": {"Ex 2": {}}}, []),
|
||||
),
|
||||
patch.object(reader, "apply_actions_and_regenerate_grouped") as render,
|
||||
):
|
||||
self.assertEqual(
|
||||
reader.run(workspace, refaire=True, annotation_dir="Anot"),
|
||||
ExitCode.PARTIAL,
|
||||
)
|
||||
render.assert_not_called()
|
||||
|
||||
def test_missing_redo_return_leaves_copy_untouched(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
workspace = self.workspace(root, "BGnot")
|
||||
(root / "BRnot/Copie01/Concat_annotated.pdf").unlink()
|
||||
with (
|
||||
patch.object(
|
||||
reader,
|
||||
"load_annotation_data",
|
||||
return_value=AnnotationLoadResult({"01": {"Ex 2": {}}}, []),
|
||||
),
|
||||
patch.object(reader, "apply_actions_and_regenerate_grouped") as render,
|
||||
):
|
||||
self.assertEqual(reader.run(workspace, refaire=True), ExitCode.PARTIAL)
|
||||
render.assert_not_called()
|
||||
|
||||
def test_regeneration_preserves_untouched_image_and_score_and_saves_redo(self):
|
||||
for mode in ("BGnot", "Bnot", "Anot"):
|
||||
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
workspace = self.workspace(root, mode)
|
||||
output = root / mode / "Copie01"
|
||||
output.mkdir()
|
||||
Image.new("RGB", (40, 30), "red").save(output / "Ex 1.jpg")
|
||||
before = (output / "Ex 1.jpg").read_bytes()
|
||||
atomic_write_json(output / "score.json", {"Ex 1": "3.5", "Ex 2": "0"})
|
||||
answer = root / "answer.pdf"
|
||||
answer.touch()
|
||||
data = {
|
||||
"01": {
|
||||
label: {
|
||||
"result": {"score": 2, "feedback": []},
|
||||
"pdf_path": answer,
|
||||
"coordinates": (0, 0),
|
||||
}
|
||||
for label in ("Ex 1", "Ex 2")
|
||||
}
|
||||
}
|
||||
with (
|
||||
patch.object(
|
||||
reader.annotating,
|
||||
"make_base_image",
|
||||
return_value=(Image.new("RGB", (40, 20)), 0, 0),
|
||||
),
|
||||
patch.object(
|
||||
reader.annotating,
|
||||
"compose_label_image",
|
||||
return_value=(Image.new("RGB", (40, 20), "blue"), 0),
|
||||
),
|
||||
patch.object(reader, "get_extra_pdfs_as_images", return_value=[]),
|
||||
):
|
||||
status, _ = reader.apply_actions_and_regenerate_grouped(
|
||||
workspace,
|
||||
data,
|
||||
"01",
|
||||
[],
|
||||
{},
|
||||
["Ex 1", "Ex 2"],
|
||||
annotation_dir=mode,
|
||||
selected_labels={"Ex 2"},
|
||||
)
|
||||
self.assertEqual(status, ExitCode.SUCCESS)
|
||||
self.assertEqual(
|
||||
read_json(output / "score.json"), {"Ex 1": "3.5", "Ex 2": "2"}
|
||||
)
|
||||
self.assertEqual((output / "Ex 1.jpg").read_bytes(), before)
|
||||
self.assertTrue((output / "Ex 2.jpg").is_file())
|
||||
with Image.open(output / "Concat.jpg") as concat:
|
||||
self.assertEqual(concat.size, (40, 50))
|
||||
|
||||
def test_simple_import_survives_successive_redos(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
workspace = self.workspace(root, "Anot")
|
||||
output = root / "Anot/Copie01"
|
||||
output.mkdir()
|
||||
for label in ("Ex 1", "Ex 2"):
|
||||
Image.new("RGB", (40, 30), "white").save(output / f"{label}.jpg")
|
||||
Image.new("RGB", (40, 60), "red").save(output / "Concat_annotated.jpg")
|
||||
atomic_write_json(output / "score.json", {"Ex 1": "1", "Ex 2": "1"})
|
||||
answer = root / "answer.pdf"
|
||||
answer.touch()
|
||||
data = {
|
||||
"01": {
|
||||
label: {
|
||||
"result": {"score": 2, "feedback": []},
|
||||
"pdf_path": answer,
|
||||
"coordinates": (0, 0),
|
||||
}
|
||||
for label in ("Ex 1", "Ex 2")
|
||||
}
|
||||
}
|
||||
with (
|
||||
patch.object(
|
||||
reader.annotating,
|
||||
"make_base_image",
|
||||
return_value=(Image.new("RGB", (40, 20)), 0, 0),
|
||||
),
|
||||
patch.object(
|
||||
reader.annotating,
|
||||
"compose_label_image",
|
||||
return_value=(Image.new("RGB", (40, 20), "blue"), 0),
|
||||
),
|
||||
):
|
||||
for selected in ({"Ex 2"}, {"Ex 1"}):
|
||||
status, _ = reader.apply_actions_and_regenerate_grouped(
|
||||
workspace,
|
||||
data,
|
||||
"01",
|
||||
[],
|
||||
{},
|
||||
["Ex 1", "Ex 2"],
|
||||
annotation_dir="Anot",
|
||||
selected_labels=selected,
|
||||
)
|
||||
self.assertEqual(status, ExitCode.SUCCESS)
|
||||
if selected == {"Ex 2"}:
|
||||
with Image.open(output / "Ex 1.jpg") as untouched:
|
||||
self.assertGreater(untouched.getpixel((10, 10))[0], 240)
|
||||
with Image.open(output / "Concat.jpg") as concat:
|
||||
self.assertEqual(concat.size, (40, 40))
|
||||
self.assertGreater(concat.getpixel((10, 30))[2], 240)
|
||||
|
||||
def test_latest_group_coordinates_win_numerically(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
group = root / "Par label" / "Ex 1"
|
||||
group.mkdir(parents=True)
|
||||
for number, height in ((2, 100), (10, 40)):
|
||||
atomic_write_json(
|
||||
group / f"Group_{number}.json", [["01", 0, height, "", "Ex 1"]]
|
||||
)
|
||||
Image.new("RGB", (20, height)).save(group / f"Group_{number}.jpg")
|
||||
index, warnings = _coordinate_index(EvaluationWorkspace(root))
|
||||
self.assertFalse(warnings)
|
||||
self.assertEqual(index[("Ex 1", "01")].height, 40)
|
||||
|
||||
def test_unreadable_redo_is_rejected(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
atomic_write_json(root / "bnote.json", {"images": []})
|
||||
with (
|
||||
patch.object(
|
||||
reader, "detect_checks_and_notes", return_value=([], None)
|
||||
),
|
||||
self.assertRaises(ValueError),
|
||||
):
|
||||
reader._scan_annotation_directory(root, required=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user