Standardisation 3

This commit is contained in:
2026-08-20 14:35:04 +02:00
parent 63f690b353
commit 8b087bb3e4
9 changed files with 1187 additions and 683 deletions
+207 -1
View File
@@ -13,6 +13,7 @@ from contextlib import redirect_stderr
from pathlib import Path
from unittest.mock import patch
from PIL import Image
from pypdf import PdfReader, PdfWriter
from copienator import (
@@ -22,7 +23,10 @@ from copienator import (
atomic_update_json,
atomic_write_json,
read_json,
workspace_from_target,
)
from copienator.annotation_data import AnnotationLoadResult, load_annotation_data
from copienator.filesystem import staged_directory
from copienator_gui.app import process_status
from copienator_gui.diagnostics import collect_diagnostics
from copienator_gui.runner import ProcessRunner
@@ -156,10 +160,95 @@ class AtomicJsonTests(unittest.TestCase):
self.assertEqual(persisted["steps"]["labels"]["status"], "success")
class AnnotationDataTests(unittest.TestCase):
def test_loader_indexes_coordinates_without_mutating_correction(self) -> None:
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
copy_dir = evaluation / "Copies" / "Copie01"
group_dir = evaluation / "Par label" / "Ex 1"
copy_dir.mkdir(parents=True)
group_dir.mkdir(parents=True)
(copy_dir / "Ex 1_new.pdf").write_bytes(b"pdf")
correction = {
"Ex 1": [
[
{
"id": "01",
"result": {
"suffix": "_new",
"feedback": [{"text": "x", "box_2d": [100, 200, 300, 400]}],
},
}
]
]
}
atomic_write_json(evaluation / "correction.json", correction)
atomic_write_json(
group_dir / "Group_1.json",
[["01", 10, 90, 1.0, "Ex 1"]],
)
Image.new("RGB", (100, 200), "white").save(group_dir / "Group_1.jpg")
loaded = load_annotation_data(EvaluationWorkspace(evaluation))
item = loaded.data["01"]["Ex 1"]
self.assertEqual(item["pdf_path"], copy_dir / "Ex 1_new.pdf")
self.assertEqual(item["coordinates"], (10, 90))
self.assertEqual(item["result"]["feedback"][0]["box_2d"], [20, 20, 60, 40])
self.assertEqual(read_json(evaluation / "correction.json"), correction)
self.assertEqual(loaded.warnings, [])
def test_refaire_filter_adds_a_missing_correction_entry(self) -> None:
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
copy_dir = evaluation / "Copies" / "Copie01"
copy_dir.mkdir(parents=True)
(evaluation / "Par label").mkdir()
(copy_dir / "Ex 2.pdf").write_bytes(b"pdf")
atomic_write_json(evaluation / "correction.json", {})
loaded = load_annotation_data(
EvaluationWorkspace(evaluation),
refaire_list=[["Copie01", ["Ex 2"]]],
)
item = loaded.data["01"]["Ex 2"]
self.assertEqual(item["pdf_path"], copy_dir / "Ex 2.pdf")
self.assertEqual(item["result"]["error"], "non traité")
def test_staged_directory_preserves_then_replaces_destination(self) -> None:
with tempfile.TemporaryDirectory() as directory:
destination = Path(directory) / "output"
destination.mkdir()
(destination / "state.txt").write_text("old", encoding="utf-8")
with self.assertRaises(RuntimeError), staged_directory(
destination
) as staging:
(staging / "state.txt").write_text("broken", encoding="utf-8")
raise RuntimeError("rendering failed")
self.assertEqual(
(destination / "state.txt").read_text(encoding="utf-8"), "old"
)
with staged_directory(destination) as staging:
(staging / "state.txt").write_text("new", encoding="utf-8")
self.assertEqual(
(destination / "state.txt").read_text(encoding="utf-8"), "new"
)
leftovers = [path for path in destination.parent.iterdir() if path.name.startswith(".output.")]
self.assertEqual(leftovers, [])
class StandardCliTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.modules = {
"annotating": load_script_module("annotating.py", "annotating"),
"annotating_with_checks": load_script_module(
"annotating_with_checks.py", "annotating_with_checks"
),
"annotating_by_label": load_script_module(
"annotating_by_label.py", "annotating_by_label"
),
"copies_tools": load_script_module(
"copies_tools.py", "copienator_copies_tools_test"
),
@@ -192,6 +281,9 @@ class StandardCliTests(unittest.TestCase):
"resolve_manual": [missing],
"verify_groups": [missing],
"copies_tools": ["rotate", missing],
"annotating": [missing],
"annotating_with_checks": [missing],
"annotating_by_label": [missing],
}
for name, arguments in invocations.items():
with self.subTest(script=name), redirect_stderr(io.StringIO()):
@@ -259,6 +351,21 @@ class StandardCliTests(unittest.TestCase):
"default",
{"target": evaluation},
),
"annotating": (
"annotation",
"simple",
{"target": evaluation, "overwrite": True},
),
"annotating_with_checks": (
"annotation",
"checks",
{"target": evaluation, "overwrite": True, "refaire": True},
),
"annotating_by_label": (
"annotation",
"grouped",
{"target": evaluation, "overwrite": True},
),
}
for module_name, (step_id, variant_id, values) in cases.items():
step = steps[step_id]
@@ -266,7 +373,8 @@ class StandardCliTests(unittest.TestCase):
command = build_command(REPOSITORY, step, variant, values, evaluation)
with self.subTest(script=module_name):
parsed = self.modules[module_name].build_parser().parse_args(command[3:])
self.assertEqual(str(parsed.evaluation), evaluation)
parsed_path = getattr(parsed, "evaluation", None) or parsed.target
self.assertEqual(str(parsed_path), evaluation)
for step_id in ("rotate", "rename"):
step = steps[step_id]
@@ -458,6 +566,104 @@ class StandardCliTests(unittest.TestCase):
)
self.assertEqual(module.main([str(evaluation)]), 0)
def test_checked_annotation_discovers_workspace_from_copy_pdf(self) -> None:
module = self.modules["annotating_with_checks"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
copy_pdf = evaluation / "Copies" / "Copie40.pdf"
copy_pdf.parent.mkdir(parents=True)
copy_pdf.write_bytes(b"pdf")
args = module.build_parser().parse_args([str(copy_pdf)])
workspace, target = workspace_from_target(args)
self.assertEqual(workspace.root, evaluation)
self.assertEqual(target, copy_pdf)
self.assertEqual(module._copy_id_from_target(workspace, target), "40")
def test_checked_refaire_requires_refaire_file(self) -> None:
module = self.modules["annotating_with_checks"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
(evaluation / "Copies").mkdir(parents=True)
(evaluation / "Par label").mkdir()
(evaluation / "labels").write_text("Ex 1\n", encoding="utf-8")
atomic_write_json(evaluation / "correction.json", {})
with redirect_stderr(io.StringIO()):
self.assertEqual(module.main([str(evaluation), "--refaire"]), 3)
def test_checked_render_failure_preserves_previous_student_output(self) -> None:
module = self.modules["annotating_with_checks"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
copy_dir = evaluation / "Copies" / "Copie01"
copy_dir.mkdir(parents=True)
(evaluation / "Par label").mkdir()
(evaluation / "labels").write_text("Ex 1\n", encoding="utf-8")
atomic_write_json(evaluation / "correction.json", {})
answer = copy_dir / "Ex 1.pdf"
answer.write_bytes(b"pdf")
previous = evaluation / "Bnot" / "Copie01"
previous.mkdir(parents=True)
(previous / "sentinel.txt").write_text("old", encoding="utf-8")
loaded = AnnotationLoadResult(
{
"01": {
"Ex 1": {
"pdf_path": answer,
"result": {"feedback": [], "score": 1},
"coordinates": (0, 0),
}
}
},
[],
)
with patch.object(
module, "load_annotation_data", return_value=loaded
), patch.object(
self.modules["annotating"],
"make_base_image",
side_effect=RuntimeError("render failed"),
), redirect_stderr(io.StringIO()):
self.assertEqual(module.main([str(evaluation), "--overwrite"]), 1)
self.assertEqual(
(previous / "sentinel.txt").read_text(encoding="utf-8"), "old"
)
def test_grouped_overwrite_preserves_previous_output_when_incomplete(self) -> None:
module = self.modules["annotating_by_label"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
(evaluation / "Copies").mkdir(parents=True)
(evaluation / "Par label").mkdir()
(evaluation / "labels").write_text("Ex 1\n", encoding="utf-8")
(evaluation / "label_groups").write_text("Ex 1\n", encoding="utf-8")
atomic_write_json(evaluation / "correction.json", {})
previous = evaluation / "BGnot"
previous.mkdir()
(previous / "sentinel.txt").write_text("old", encoding="utf-8")
loaded = AnnotationLoadResult(
{"01": {"Ex 1": {"pdf_path": Path("missing")}}},
[],
)
with patch.object(
module, "load_annotation_data", return_value=loaded
), patch.object(module, "_generate_groups", return_value=(1, True)):
self.assertEqual(module.main([str(evaluation), "--overwrite"]), 4)
self.assertEqual(
(previous / "sentinel.txt").read_text(encoding="utf-8"), "old"
)
def test_grouped_batching_does_not_split_one_student(self) -> None:
module = self.modules["annotating_by_label"]
image = Image.new("RGB", (10, 60), "white")
rendered = [
("01", "Ex 1", image, 0, []),
("01", "Ex 2", image, 0, []),
("02", "Ex 1", image, 0, []),
]
with patch.object(module, "MAX_HEIGHT_PX", 100):
batches = module.split_batches(rendered)
self.assertEqual([len(batch) for batch in batches], [2, 1])
class WorkflowTests(unittest.TestCase):
def setUp(self) -> None: