Standardisation 2

This commit is contained in:
2026-08-20 14:17:32 +02:00
parent 0a95afacdd
commit 63f690b353
8 changed files with 578 additions and 308 deletions
+172 -1
View File
@@ -45,7 +45,12 @@ def load_script_module(filename: str, module_name: str):
if spec is None or spec.loader is None:
raise RuntimeError(f"Could not load {filename}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
sys.modules[module_name] = module
try:
spec.loader.exec_module(module)
except Exception:
sys.modules.pop(module_name, None)
raise
return module
@@ -155,12 +160,24 @@ class StandardCliTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.modules = {
"copies_tools": load_script_module(
"copies_tools.py", "copienator_copies_tools_test"
),
"export": load_script_module("export.py", "copienator_export_test"),
"import": load_script_module("import.py", "copienator_import_test"),
"giving_names": load_script_module(
"giving_names.py", "copienator_giving_names_test"
),
"grouping": load_script_module("grouping.py", "copienator_grouping_test"),
"post_correction": load_script_module(
"post-correction.py", "copienator_post_correction_test"
),
"resolve_manual": load_script_module(
"resolve_manual.py", "copienator_resolve_manual_test"
),
"verify_groups": load_script_module(
"verify_groups.py", "copienator_verify_groups_test"
),
}
def test_missing_evaluation_has_standard_exit_code(self) -> None:
@@ -171,6 +188,10 @@ class StandardCliTests(unittest.TestCase):
"import": [missing],
"giving_names": [missing, "BGnot"],
"grouping": [missing],
"post_correction": [missing],
"resolve_manual": [missing],
"verify_groups": [missing],
"copies_tools": ["rotate", missing],
}
for name, arguments in invocations.items():
with self.subTest(script=name), redirect_stderr(io.StringIO()):
@@ -223,6 +244,21 @@ class StandardCliTests(unittest.TestCase):
{"target": evaluation, "annotation_dir": "BGnot"},
),
"grouping": ("grouping", "default", {"target": evaluation}),
"post_correction": (
"post_correction",
"default",
{"target": evaluation},
),
"resolve_manual": (
"manual_resolution",
"default",
{"target": evaluation},
),
"verify_groups": (
"verify_groups",
"default",
{"target": evaluation},
),
}
for module_name, (step_id, variant_id, values) in cases.items():
step = steps[step_id]
@@ -232,6 +268,23 @@ class StandardCliTests(unittest.TestCase):
parsed = self.modules[module_name].build_parser().parse_args(command[3:])
self.assertEqual(str(parsed.evaluation), evaluation)
for step_id in ("rotate", "rename"):
step = steps[step_id]
variant = step.variants[0]
command = build_command(
REPOSITORY,
step,
variant,
{"target": evaluation},
evaluation,
)
with self.subTest(script=f"copies_tools:{step_id}"):
parsed = self.modules["copies_tools"].build_parser().parse_args(
command[3:]
)
self.assertEqual(parsed.operation, step_id)
self.assertEqual(str(parsed.evaluation), evaluation)
def test_export_main_copies_outputs(self) -> None:
module = self.modules["export"]
with tempfile.TemporaryDirectory() as directory:
@@ -287,6 +340,124 @@ class StandardCliTests(unittest.TestCase):
self.assertEqual(module.main([str(evaluation)]), 0)
self.assertTrue((evaluation / "Par label").is_dir())
def test_post_correction_main_cleans_json_atomically(self) -> None:
module = self.modules["post_correction"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
evaluation.mkdir()
atomic_write_json(
evaluation / "correction.json",
{"text": "outside_name and $math_name$", "suffix": "_new"},
)
self.assertEqual(module.main([str(evaluation)]), 0)
self.assertEqual(
read_json(evaluation / "correction.json"),
{"text": r"outside\_name and $math_name$", "suffix": "_new"},
)
def test_manual_resolution_with_no_actions_is_safe(self) -> None:
module = self.modules["resolve_manual"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
(evaluation / "Copies").mkdir(parents=True)
atomic_write_json(evaluation / "correction.json", {})
manual = evaluation / "manual_resolutions.txt"
manual.write_text("### Nothing to do\n", encoding="utf-8")
self.assertEqual(module.main([str(evaluation)]), 0)
self.assertFalse(manual.exists())
self.assertEqual(read_json(evaluation / "correction.json"), {})
def test_malformed_manual_resolution_is_not_deleted(self) -> None:
module = self.modules["resolve_manual"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
(evaluation / "Copies").mkdir(parents=True)
correction = evaluation / "correction.json"
atomic_write_json(correction, {})
manual = evaluation / "manual_resolutions.txt"
manual.write_text("this is malformed\n", encoding="utf-8")
before = correction.read_bytes()
with redirect_stderr(io.StringIO()):
self.assertEqual(module.main([str(evaluation)]), 1)
self.assertTrue(manual.exists())
self.assertEqual(correction.read_bytes(), before)
def test_documented_manual_resolution_operators_are_parsed(self) -> None:
module = self.modules["resolve_manual"]
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "manual_resolutions.txt"
path.write_text(
"\n".join(
f"Copie01 Old label {operator} New label"
for operator in module.OPERATORS
),
encoding="utf-8",
)
instructions = module.parse_instructions(path)
self.assertEqual(
[instruction.operator for instruction in instructions],
list(module.OPERATORS),
)
def test_replace_manual_resolution_creates_refaire_state(self) -> None:
module = self.modules["resolve_manual"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
copy_dir = evaluation / "Copies" / "Copie01"
copy_dir.mkdir(parents=True)
(copy_dir / "Old.pdf").write_bytes(b"old answer")
(copy_dir / "New.pdf").write_bytes(b"replaced answer")
atomic_write_json(
evaluation / "correction.json",
{
"Old": [
[
{
"id": "01",
"result": {"error": "wrg-lbl:New?delayed"},
}
]
],
"New": [[{"id": "01", "result": {"error": ""}}]],
},
)
(evaluation / "manual_resolutions.txt").write_text(
"Copie01 Old -x New\n", encoding="utf-8"
)
self.assertEqual(module.main([str(evaluation)]), 0)
self.assertEqual((copy_dir / "New_old.pdf").read_bytes(), b"replaced answer")
self.assertEqual((copy_dir / "New_new.pdf").read_bytes(), b"old answer")
self.assertEqual(
read_json(evaluation / "refaire.json"),
[["Copie01", ["New"]]],
)
correction = read_json(evaluation / "correction.json")
self.assertEqual(
correction["Old"][0][0]["result"]["error"],
"wrg-lbl-moved-to:New",
)
self.assertEqual(correction["New"][0][0]["result"]["suffix"], "_new")
def test_verify_groups_reports_success_and_missing_answers(self) -> None:
module = self.modules["verify_groups"]
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.pdf").write_bytes(b"pdf")
self.assertEqual(module.main([str(evaluation)]), 1)
atomic_write_json(
group_dir / "Group_1.json",
[["01", 0, 100, 1.0, "Ex 1"]],
)
self.assertEqual(module.main([str(evaluation)]), 0)
class WorkflowTests(unittest.TestCase):
def setUp(self) -> None: