Standardisation des scripts
This commit is contained in:
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user