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
+206
View File
@@ -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()
+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()
+280
View File
@@ -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()