Standardisation des scripts

This commit is contained in:
2026-08-20 13:01:26 +02:00
parent 2f1cd00e32
commit 352d36e38c
10 changed files with 590 additions and 144 deletions
+154
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import importlib.util
import io
import os
import queue
import sys
@@ -7,6 +9,7 @@ import tempfile
import time
import unittest
from concurrent.futures import ThreadPoolExecutor
from contextlib import redirect_stderr
from pathlib import Path
from unittest.mock import patch
@@ -20,6 +23,7 @@ from copienator import (
atomic_write_json,
read_json,
)
from copienator_gui.app import process_status
from copienator_gui.diagnostics import collect_diagnostics
from copienator_gui.runner import ProcessRunner
from copienator_gui.state import StateStore
@@ -36,6 +40,15 @@ from platform_utils import (
REPOSITORY = Path(__file__).resolve().parents[1]
def load_script_module(filename: str, module_name: str):
spec = importlib.util.spec_from_file_location(module_name, REPOSITORY / filename)
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)
return module
class WorkspaceTests(unittest.TestCase):
def test_canonical_paths_and_no_constructor_side_effects(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@@ -73,6 +86,10 @@ class WorkspaceTests(unittest.TestCase):
workspace.require("enonce.pdf", "labels")
self.assertEqual(context.exception.missing, ["enonce.pdf", "labels"])
(root / "Copies").write_text("not a directory", encoding="utf-8")
with self.assertRaises(WorkspaceValidationError):
workspace.require_directories("Copies")
def test_names_file_prefers_evaluation_then_repository(self) -> None:
with tempfile.TemporaryDirectory() as directory:
repository = Path(directory)
@@ -134,6 +151,143 @@ class AtomicJsonTests(unittest.TestCase):
self.assertEqual(persisted["steps"]["labels"]["status"], "success")
class StandardCliTests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
cls.modules = {
"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"),
}
def test_missing_evaluation_has_standard_exit_code(self) -> None:
with tempfile.TemporaryDirectory() as directory:
missing = str(Path(directory) / "missing")
invocations = {
"export": [missing],
"import": [missing],
"giving_names": [missing, "BGnot"],
"grouping": [missing],
}
for name, arguments in invocations.items():
with self.subTest(script=name), redirect_stderr(io.StringIO()):
self.assertEqual(self.modules[name].main(arguments), 3)
def test_invalid_arguments_use_argparse_exit_code(self) -> None:
with redirect_stderr(io.StringIO()), self.assertRaises(SystemExit) as context:
self.modules["grouping"].main([])
self.assertEqual(context.exception.code, 2)
def test_unexpected_processing_error_has_failure_exit_code(self) -> None:
module = self.modules["grouping"]
with tempfile.TemporaryDirectory() as directory:
with patch.object(
module, "run", side_effect=RuntimeError("broken")
), redirect_stderr(io.StringIO()) as errors:
self.assertEqual(module.main([directory]), 1)
self.assertIn("broken", errors.getvalue())
def test_prerequisites_are_checked_before_creating_outputs(self) -> None:
module = self.modules["export"]
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
evaluation = base / "Exam"
evaluation.mkdir()
export_dir = base / "Export"
with patch.object(module, "EXPORT_DIR", export_dir), redirect_stderr(
io.StringIO()
):
self.assertEqual(module.main([str(evaluation)]), 3)
self.assertFalse(export_dir.exists())
def test_gui_understands_standard_process_exit_codes(self) -> None:
self.assertEqual(process_status(0), "success")
self.assertEqual(process_status(1), "failed")
self.assertEqual(process_status(3), "failed")
self.assertEqual(process_status(4), "partial")
self.assertEqual(process_status(130), "interrupted")
self.assertEqual(process_status(0, interrupted=True), "interrupted")
def test_gui_commands_are_accepted_by_script_parsers(self) -> None:
steps = {step.id: step for step in build_workflow(True)}
evaluation = "Evaluation with spaces"
cases = {
"export": ("export", "default", {"target": evaluation, "refaire": True}),
"import": ("import", "default", {"target": evaluation, "refaire": True}),
"giving_names": (
"giving_names",
"default",
{"target": evaluation, "annotation_dir": "BGnot"},
),
"grouping": ("grouping", "default", {"target": evaluation}),
}
for module_name, (step_id, variant_id, values) in cases.items():
step = steps[step_id]
variant = next(item for item in step.variants if item.id == variant_id)
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)
def test_export_main_copies_outputs(self) -> None:
module = self.modules["export"]
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
evaluation = base / "Exam"
source = evaluation / "BGnot" / "Ex 1"
source.mkdir(parents=True)
(source / "Concat.pdf").write_bytes(b"annotated")
with patch.object(module, "EXPORT_DIR", base / "Export"):
self.assertEqual(module.main([str(evaluation)]), 0)
exported = base / "Export" / "Exam" / "Ex 1.pdf"
self.assertEqual(exported.read_bytes(), b"annotated")
def test_import_main_copies_handwritten_annotations(self) -> None:
module = self.modules["import"]
with tempfile.TemporaryDirectory() as directory:
base = Path(directory)
evaluation = base / "Exam"
target = evaluation / "BGnot" / "Ex 1"
target.mkdir(parents=True)
import_dir = base / "Import"
import_dir.mkdir()
(import_dir / "Ex 1.pdf").write_bytes(b"handwritten")
with patch.object(module, "IMPORT_DIR", import_dir):
self.assertEqual(module.main([str(evaluation)]), 0)
self.assertEqual(
(target / "Concat_annotated.pdf").read_bytes(), b"handwritten"
)
def test_giving_names_main_builds_return_directory(self) -> None:
module = self.modules["giving_names"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
copies = evaluation / "Copies"
annotations = evaluation / "BGnot" / "Copie01"
copies.mkdir(parents=True)
annotations.mkdir(parents=True)
atomic_write_json(copies / "Copie01.json", {"name": "Élève Test"})
atomic_write_json(annotations / "score.json", {"total": 10})
(annotations / "Concat.jpg").write_bytes(b"image")
(evaluation / "names").write_text("Élève Test\n", encoding="utf-8")
self.assertEqual(module.main([str(evaluation), "BGnot"]), 0)
destination = evaluation / "A Rendre" / "Élève Test (01)"
self.assertEqual((destination / "Élève Test.jpg").read_bytes(), b"image")
self.assertEqual(read_json(destination / "score.json"), {"total": 10})
def test_grouping_main_accepts_empty_copies_directory(self) -> None:
module = self.modules["grouping"]
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
(evaluation / "Copies").mkdir(parents=True)
self.assertEqual(module.main([str(evaluation)]), 0)
self.assertTrue((evaluation / "Par label").is_dir())
class WorkflowTests(unittest.TestCase):
def setUp(self) -> None:
self.steps = {step.id: step for step in build_workflow(True)}