Standardisation 3
This commit is contained in:
+207
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user