import contextlib import io import tempfile import unittest from pathlib import Path import pymupdf from copienator.cli import CliError from copienator.commands import crop_exercise_bottoms from copienator.commands.clean import apply_cleanup, build_cleanup_plan from copienator.dispatcher import main from copienator.workspace import EvaluationWorkspace from copienator_gui.workflow import build_workflow class CropExerciseBottomsCommandTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.addCleanup(self.temp.cleanup) self.workspace = EvaluationWorkspace(Path(self.temp.name) / "Évaluation") self.workspace.copies_dir.mkdir(parents=True) self.copy_pdf = self.workspace.copies_dir / "Copie01.pdf" with pymupdf.open() as document: document.new_page(width=600, height=800) document.save(self.copy_pdf) self.answers = self.workspace.copies_dir / "Copie01" self.answers.mkdir() self.large = self.answers / "Ex 1.pdf" self.short = self.answers / "Ex 2.pdf" self._make_answer(self.large, 400, 100, footer=True) self._make_answer(self.short, 200, 100, footer=False) self.large_original = self.large.read_bytes() self.short_original = self.short.read_bytes() @staticmethod def _make_answer(path: Path, height: float, text_y: float, *, footer: bool) -> None: with pymupdf.open() as document: page = document.new_page(width=600, height=height) page.insert_text((80, text_y), "student answer", fontsize=16) if footer: page.insert_text((20, height - 3), "Ex 2", fontsize=10) document.save(path) def test_dispatcher_replaces_only_changed_answers_and_backs_them_up(self): with contextlib.redirect_stdout(io.StringIO()) as log: self.assertEqual( main(["crop-answer-bottoms", str(self.workspace.root), "--workers", "2"]), 0, ) self.assertIn("1 PDF remplacé", log.getvalue()) with pymupdf.open(self.large) as cropped: self.assertLess(cropped[0].rect.height, 250) self.assertEqual(self.short.read_bytes(), self.short_original) backups = list( self.workspace.runs_dir.glob( "crop-exercise-bottoms-*/Copies/Copie01/Ex 1.pdf" ) ) self.assertEqual(len(backups), 1) self.assertEqual(backups[0].read_bytes(), self.large_original) self.assertFalse( list( self.workspace.runs_dir.glob( "crop-exercise-bottoms-*/Copies/Copie01/Ex 2.pdf" ) ) ) self.assertTrue((backups[0].parents[2] / "report.json").is_file()) def test_detection_failure_does_not_publish_an_earlier_result(self): broken = self.answers / "Ex 3.pdf" broken.write_bytes(b"not a PDF") with contextlib.redirect_stdout(io.StringIO()), self.assertRaises(Exception): crop_exercise_bottoms.run( self.workspace, self.workspace.root, workers=1 ) self.assertEqual(self.large.read_bytes(), self.large_original) self.assertEqual(self.short.read_bytes(), self.short_original) self.assertEqual(broken.read_bytes(), b"not a PDF") self.assertFalse(self.workspace.runs_dir.exists()) def test_invalid_target_and_worker_count_are_rejected(self): with self.assertRaises(CliError): crop_exercise_bottoms.run(self.workspace, self.large, workers=1) with self.assertRaises(CliError): crop_exercise_bottoms.run(self.workspace, self.workspace.root, workers=0) def test_archiving_removes_the_retained_originals_and_report(self): with contextlib.redirect_stdout(io.StringIO()): crop_exercise_bottoms.run( self.workspace, self.workspace.root, workers=1 ) run_dirs = list( self.workspace.runs_dir.glob("crop-exercise-bottoms-*") ) self.assertEqual(len(run_dirs), 1) self.workspace.correction_file.write_text("{}") student = self.workspace.return_dir / "Student" student.mkdir(parents=True) (student / "answer.jpg").write_bytes(b"return image") (student / "score.json").write_text("{}") plan = build_cleanup_plan(self.workspace) self.assertTrue( any(path.name == "report.json" for path in plan.deleted_files) ) with contextlib.redirect_stdout(io.StringIO()): apply_cleanup(self.workspace, plan) self.assertFalse(self.workspace.runs_dir.exists()) def test_optional_step_follows_splitting_and_precedes_grouping(self): steps = build_workflow(False) index = next( i for i, step in enumerate(steps) if step.id == "crop_exercise_bottoms" ) self.assertEqual(steps[index - 1].id, "splitting") self.assertEqual(steps[index + 1].id, "grouping") self.assertTrue(steps[index].optional) self.assertTrue(steps[index].auto_start_first_visit) if __name__ == "__main__": unittest.main()