Standardisation 4

This commit is contained in:
2026-08-20 14:45:53 +02:00
parent 8b087bb3e4
commit bcba5facc8
6 changed files with 838 additions and 732 deletions
+104 -1
View File
@@ -25,8 +25,9 @@ from copienator import (
read_json,
workspace_from_target,
)
from copienator.annotation_actions import apply_checkbox_actions, apply_score_overrides
from copienator.annotation_data import AnnotationLoadResult, load_annotation_data
from copienator.filesystem import staged_directory
from copienator.filesystem import staged_directory, staged_files
from copienator_gui.app import process_status
from copienator_gui.diagnostics import collect_diagnostics
from copienator_gui.runner import ProcessRunner
@@ -237,6 +238,52 @@ class AnnotationDataTests(unittest.TestCase):
leftovers = [path for path in destination.parent.iterdir() if path.name.startswith(".output.")]
self.assertEqual(leftovers, [])
def test_staged_files_preserve_inputs_and_roll_back_outputs(self) -> None:
with tempfile.TemporaryDirectory() as directory:
destination = Path(directory) / "Copie01"
destination.mkdir()
(destination / "bnote.json").write_text("input", encoding="utf-8")
(destination / "score.json").write_text("old", encoding="utf-8")
with self.assertRaises(RuntimeError), staged_files(destination) as staging:
(staging / "score.json").write_text("broken", encoding="utf-8")
(staging / "Concat.jpg").write_text("partial", encoding="utf-8")
raise RuntimeError("rendering failed")
self.assertEqual((destination / "score.json").read_text(), "old")
self.assertFalse((destination / "Concat.jpg").exists())
with staged_files(destination) as staging:
(staging / "score.json").write_text("new", encoding="utf-8")
(staging / "Concat.jpg").write_text("complete", encoding="utf-8")
self.assertEqual((destination / "score.json").read_text(), "new")
self.assertEqual((destination / "bnote.json").read_text(), "input")
def test_annotation_actions_are_shared_and_do_not_touch_json_sources(self) -> None:
with tempfile.TemporaryDirectory() as directory:
score_path = Path(directory) / "score.json"
atomic_write_json(score_path, {"Ex 1": "3"})
labels_data = {
"Ex 1": {
"result": {
"score": 1,
"feedback": [{"text": "global"}],
}
}
}
logs = []
dirty = apply_checkbox_actions(
labels_data,
[{"label": "Ex 1", "type": "del_global", "index": 0}],
logs.append,
)
dirty |= apply_score_overrides(labels_data, score_path, logs.append)
self.assertEqual(dirty, {"Ex 1"})
self.assertTrue(
labels_data["Ex 1"]["result"]["feedback"][0]["to_delete"]
)
self.assertEqual(labels_data["Ex 1"]["result"]["score"], "3")
self.assertEqual(read_json(score_path), {"Ex 1": "3"})
class StandardCliTests(unittest.TestCase):
@classmethod
@@ -249,6 +296,12 @@ class StandardCliTests(unittest.TestCase):
"annotating_by_label": load_script_module(
"annotating_by_label.py", "annotating_by_label"
),
"reading_annotations": load_script_module(
"reading_annotations.py", "reading_annotations"
),
"reading_grouped_annotations": load_script_module(
"reading_grouped_annotations.py", "reading_grouped_annotations"
),
"copies_tools": load_script_module(
"copies_tools.py", "copienator_copies_tools_test"
),
@@ -284,6 +337,8 @@ class StandardCliTests(unittest.TestCase):
"annotating": [missing],
"annotating_with_checks": [missing],
"annotating_by_label": [missing],
"reading_annotations": [missing],
"reading_grouped_annotations": [missing],
}
for name, arguments in invocations.items():
with self.subTest(script=name), redirect_stderr(io.StringIO()):
@@ -366,6 +421,16 @@ class StandardCliTests(unittest.TestCase):
"grouped",
{"target": evaluation, "overwrite": True},
),
"reading_annotations": (
"read_annotations",
"standard",
{"target": evaluation, "update_score": True},
),
"reading_grouped_annotations": (
"read_annotations",
"grouped",
{"target": evaluation, "update_score": True, "refaire": True},
),
}
for module_name, (step_id, variant_id, values) in cases.items():
step = steps[step_id]
@@ -590,6 +655,44 @@ class StandardCliTests(unittest.TestCase):
with redirect_stderr(io.StringIO()):
self.assertEqual(module.main([str(evaluation), "--refaire"]), 3)
def test_grouped_reader_refaire_requires_refaire_file(self) -> None:
module = self.modules["reading_grouped_annotations"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
(evaluation / "Copies").mkdir(parents=True)
(evaluation / "Par label").mkdir()
(evaluation / "BGnot").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_note_detection_accepts_a_missing_note_layer(self) -> None:
module = self.modules["reading_annotations"]
self.assertFalse(module.has_significant_notes(None))
def test_grouped_reader_reports_worker_failures(self) -> None:
module = self.modules["reading_grouped_annotations"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
(evaluation / "Copies").mkdir(parents=True)
(evaluation / "Par label").mkdir()
(evaluation / "BGnot" / "Ex 1").mkdir(parents=True)
(evaluation / "labels").write_text("Ex 1\n", encoding="utf-8")
atomic_write_json(evaluation / "correction.json", {})
loaded = AnnotationLoadResult({"01": {"Ex 1": {}}}, [])
with (
patch.object(module, "load_annotation_data", return_value=loaded),
patch.object(
module,
"_scan_annotation_directory",
side_effect=RuntimeError("worker failed"),
),
redirect_stderr(io.StringIO()) as errors,
):
self.assertEqual(module.main([str(evaluation)]), 1)
self.assertIn("worker failed", errors.getvalue())
def test_checked_render_failure_preserves_previous_student_output(self) -> None:
module = self.modules["annotating_with_checks"]
with tempfile.TemporaryDirectory() as directory: