améliorations diverses
This commit is contained in:
@@ -85,6 +85,53 @@ class GuiConvenienceTests(unittest.TestCase):
|
||||
self.app.manual_panel.editor_button.invoke()
|
||||
opened.assert_called_once_with(other_manual)
|
||||
|
||||
def test_successful_manual_resolution_returns_to_refaire_correction(self):
|
||||
manual = self.evaluation / "manual_resolutions.txt"
|
||||
manual.write_text("Copie01 A -> B|\n", encoding="utf-8")
|
||||
self.app.state_store.update_step("manual_resolution", visited=True)
|
||||
self.app.tree.selection_set("manual_resolution")
|
||||
self.app.update()
|
||||
|
||||
self.app.active_step_id = "manual_resolution"
|
||||
self.app._finish_process(0, False)
|
||||
self.app.update()
|
||||
|
||||
self.assertEqual(self.app.current_step.id, "correction")
|
||||
self.assertEqual(self.app.variant_var.get(), "refaire")
|
||||
self.assertEqual(
|
||||
self.app.state_store.step("correction")["variant"], "refaire"
|
||||
)
|
||||
self.assertNotIn("overwrite", self.app.arg_vars)
|
||||
self.assertIn("--refaire", self.app._make_command())
|
||||
|
||||
def test_giving_names_waits_for_explicit_manual_completion(self):
|
||||
return_dir = self.evaluation / "A Rendre" / "Student (01)"
|
||||
return_dir.mkdir(parents=True)
|
||||
(return_dir / "Student.jpg").write_bytes(b"jpg")
|
||||
(return_dir / "Student.pdf").write_bytes(b"pdf")
|
||||
self.app.tree.selection_set("giving_names")
|
||||
self.app.update()
|
||||
|
||||
self.app.active_step_id = "giving_names"
|
||||
self.app._finish_process(0, False)
|
||||
self.app.update()
|
||||
|
||||
self.assertEqual(self.app.current_step.id, "giving_names")
|
||||
self.assertEqual(
|
||||
self.app.state_store.step("giving_names")["status"], "detected"
|
||||
)
|
||||
self.assertEqual(
|
||||
self.app.complete_giving_names_button.cget("text"),
|
||||
"Marquer terminée",
|
||||
)
|
||||
|
||||
self.app.complete_giving_names_button.invoke()
|
||||
self.app.update()
|
||||
self.assertEqual(
|
||||
self.app.state_store.step("giving_names")["status"], "success"
|
||||
)
|
||||
self.assertNotEqual(self.app.current_step.id, "giving_names")
|
||||
|
||||
def test_batch_status_waits_until_all_jobs_are_ready(self):
|
||||
self.app.state_store.update_step("correction", variant="batch")
|
||||
self.app.state_store.update_step("batch_status", visited=True)
|
||||
@@ -213,6 +260,28 @@ class GuiConvenienceTests(unittest.TestCase):
|
||||
self.assertIn("Cibler la copie sélectionnée", controls)
|
||||
self.assertIn("Cibler tout le dossier", controls)
|
||||
|
||||
def test_label_review_can_show_and_target_one_copy(self):
|
||||
copies = self.evaluation / "Copies"
|
||||
copies.mkdir()
|
||||
for name in ("Copie01.pdf", "Copie02.pdf"):
|
||||
(copies / name).touch()
|
||||
|
||||
self.app.tree.selection_set("plotting")
|
||||
self.app.update()
|
||||
self.app.copy_var.set("Copie02.pdf")
|
||||
|
||||
with patch("copienator_gui.app.open_path") as opened:
|
||||
self.app._open_selected_copy()
|
||||
opened.assert_called_once_with(copies / "Copie02.pdf")
|
||||
|
||||
self.app._target_selected_copy()
|
||||
command = self.app._make_command()
|
||||
self.assertEqual(Path(command[-1]), copies / "Copie02.pdf")
|
||||
labels = self._label_texts(self.app.form)
|
||||
self.assertTrue(
|
||||
any("Seule la copie ciblée sera vérifiée" in text for text in labels)
|
||||
)
|
||||
|
||||
def test_free_form_arguments_are_shown_only_when_documented(self):
|
||||
self.app.tree.selection_set("labels")
|
||||
self.app.update()
|
||||
|
||||
@@ -43,6 +43,11 @@ from copienator_gui.app import (
|
||||
plotting_shortcut_lines,
|
||||
process_status,
|
||||
)
|
||||
from copienator_gui.giving_names import (
|
||||
find_name_issues,
|
||||
original_copy_path,
|
||||
rename_return_copy,
|
||||
)
|
||||
from copienator_gui.diagnostics import collect_diagnostics
|
||||
from copienator_gui.runner import ProcessRunner
|
||||
from copienator_gui.state import StateStore
|
||||
@@ -911,6 +916,43 @@ class StandardCliTests(unittest.TestCase):
|
||||
)
|
||||
self.assertIsNone(viewer.accumulated_results)
|
||||
|
||||
def test_plotting_detects_labels_missing_from_two_thirds_of_copies(self) -> None:
|
||||
module = self.modules["plotting"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
copies = Path(directory)
|
||||
detected = {
|
||||
"Copie01_01.json": ["Ex 1", "Ex 3"],
|
||||
"Copie02_01.json": ["Ex 1", "Ex 3"],
|
||||
"Copie03_01.json": ["Ex 1", "Ex 2", "Ex 3"],
|
||||
}
|
||||
for filename, labels in detected.items():
|
||||
atomic_write_json(
|
||||
copies / filename,
|
||||
{"list": [{"label": label} for label in labels]},
|
||||
)
|
||||
|
||||
missing = module.frequently_missing_labels(
|
||||
copies,
|
||||
["Ex 1", "Ex 2", "Ex 3", "Ex 4"],
|
||||
)
|
||||
|
||||
self.assertEqual(missing, {"Ex 2", "Ex 4"})
|
||||
|
||||
def test_plotting_mutes_only_a_single_commonly_missing_label(self) -> None:
|
||||
module = self.modules["plotting"]
|
||||
labels = ["Ex 1", "Ex 2", "Ex 3", "Ex 4"]
|
||||
|
||||
muted, index = module.label_color("Ex 3", labels, 0, {"Ex 2"})
|
||||
ordinary, _ = module.label_color("Ex 3", labels, 0, set())
|
||||
two_missing, _ = module.label_color(
|
||||
"Ex 4", labels, 0, {"Ex 2", "Ex 3"}
|
||||
)
|
||||
|
||||
self.assertEqual(muted, module.COMMON_MISSING_LABEL_COLOR)
|
||||
self.assertEqual(index, 2)
|
||||
self.assertEqual(ordinary, module.MISSING_LABEL_COLOR)
|
||||
self.assertEqual(two_missing, module.MISSING_LABEL_COLOR)
|
||||
|
||||
def test_plotting_validation_preserves_manually_edited_order(self) -> None:
|
||||
module = self.modules["plotting"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -931,6 +973,7 @@ class StandardCliTests(unittest.TestCase):
|
||||
"part": 1,
|
||||
}
|
||||
viewer.valid_labels = {"Ex 1", "Ex 2"}
|
||||
viewer.active_copie_name = "Copie01"
|
||||
viewer.accumulated_results = {"name": "Student", "list": []}
|
||||
viewer.history = []
|
||||
viewer.current_pil_image = Mock()
|
||||
@@ -944,6 +987,49 @@ class StandardCliTests(unittest.TestCase):
|
||||
["Ex 2", "Ex 1"],
|
||||
)
|
||||
|
||||
def test_plotting_previous_restores_state_across_copies(self) -> None:
|
||||
module = self.modules["plotting"]
|
||||
viewer = module.ImageViewer.__new__(module.ImageViewer)
|
||||
previous_image = Mock()
|
||||
previous_json = Path("Copie01_02.json")
|
||||
previous_meta = {"copie": "Copie01"}
|
||||
previous_results = {
|
||||
"name": "Student 1",
|
||||
"list": [{"label": "Ex 1"}],
|
||||
}
|
||||
current_image = Mock()
|
||||
current_json = Path("Copie02_01.json")
|
||||
current_meta = {"copie": "Copie02"}
|
||||
viewer.is_viewing = True
|
||||
viewer.history = [
|
||||
(
|
||||
previous_image,
|
||||
previous_json,
|
||||
previous_meta,
|
||||
"Copie01",
|
||||
previous_results,
|
||||
)
|
||||
]
|
||||
viewer.forward_stack = []
|
||||
viewer.current_pil_image = current_image
|
||||
viewer.current_json_path = current_json
|
||||
viewer.current_meta = current_meta
|
||||
viewer.active_copie_name = "Copie02"
|
||||
viewer.accumulated_results = {"name": "Student 2", "list": []}
|
||||
viewer.display_image = Mock()
|
||||
|
||||
viewer.on_previous(None)
|
||||
|
||||
self.assertEqual(viewer.active_copie_name, "Copie01")
|
||||
self.assertEqual(viewer.accumulated_results, previous_results)
|
||||
self.assertEqual(
|
||||
viewer.forward_stack,
|
||||
[(current_image, current_json, current_meta)],
|
||||
)
|
||||
viewer.display_image.assert_called_once_with(
|
||||
previous_image, previous_json, previous_meta
|
||||
)
|
||||
|
||||
def test_plotting_validates_plain_and_directional_labels(self) -> None:
|
||||
module = self.modules["plotting"]
|
||||
self.assertEqual(
|
||||
@@ -1226,6 +1312,27 @@ class StandardCliTests(unittest.TestCase):
|
||||
self.assertEqual(module.results, {"Ex 1": []})
|
||||
self.assertEqual(module.tasks_to_process, [("new.jpg", "Ex 1")])
|
||||
|
||||
def test_correction_rejects_overwrite_with_refaire_without_writing(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
evaluation = Path(directory) / "Exam"
|
||||
evaluation.mkdir()
|
||||
correction = evaluation / "correction.json"
|
||||
original = {"Ex 1": [[{"id": "01", "result": {"feedback": []}}]]}
|
||||
atomic_write_json(correction, original)
|
||||
|
||||
errors = io.StringIO()
|
||||
with redirect_stderr(errors):
|
||||
status = module.main(
|
||||
[str(evaluation), "--refaire", "--overwrite"]
|
||||
)
|
||||
|
||||
self.assertEqual(status, module.ExitCode.INVALID_ARGUMENTS)
|
||||
self.assertIn(
|
||||
"--overwrite cannot be used with --refaire", errors.getvalue()
|
||||
)
|
||||
self.assertEqual(read_json(correction), original)
|
||||
|
||||
def test_correction_reserves_unique_group_indices_concurrently(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -1247,6 +1354,251 @@ class StandardCliTests(unittest.TestCase):
|
||||
self.assertEqual(sorted(indices), list(range(3, 11)))
|
||||
self.assertEqual(len(indices), len(set(indices)))
|
||||
|
||||
def test_live_correction_reports_progress_for_generated_groups(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
workspace = EvaluationWorkspace(Path(directory))
|
||||
args = module.build_parser().parse_args(
|
||||
[str(workspace.root), "--overwrite"]
|
||||
)
|
||||
initial_tasks = [("group-1.jpg", "Ex 1"), ("group-2.jpg", "Ex 2")]
|
||||
module.configure_runtime(
|
||||
workspace,
|
||||
initial_tasks,
|
||||
args,
|
||||
api_client=Mock(),
|
||||
)
|
||||
processed = []
|
||||
|
||||
def process(task, _precomputed=None):
|
||||
processed.append(task[0])
|
||||
if task[0] == "group-1.jpg":
|
||||
return [("group-3.jpg", "Ex 3", False)]
|
||||
return []
|
||||
|
||||
output = io.StringIO()
|
||||
with patch.object(
|
||||
module, "process_single_task", side_effect=process
|
||||
), patch.object(
|
||||
module, "resolve_delayed_moves", return_value=[]
|
||||
), redirect_stdout(output):
|
||||
self.assertEqual(module.run_configured(args), 0)
|
||||
|
||||
self.assertCountEqual(
|
||||
processed,
|
||||
["group-1.jpg", "group-2.jpg", "group-3.jpg"],
|
||||
)
|
||||
progress = [
|
||||
line
|
||||
for line in output.getvalue().splitlines()
|
||||
if line.startswith("[Progression correction]")
|
||||
]
|
||||
self.assertEqual(progress[0], "[Progression correction] Groupes traités : 0/2")
|
||||
self.assertEqual(progress[-1], "[Progression correction] Groupes traités : 3/3")
|
||||
|
||||
def test_live_correction_stops_scheduling_and_waits_for_active_task(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
self.addCleanup(module.stop_requested.clear)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
workspace = EvaluationWorkspace(Path(directory))
|
||||
args = module.build_parser().parse_args(
|
||||
[str(workspace.root), "--overwrite"]
|
||||
)
|
||||
module.configure_runtime(
|
||||
workspace,
|
||||
[("group-1.jpg", "Ex 1"), ("group-2.jpg", "Ex 2")],
|
||||
args,
|
||||
api_client=Mock(),
|
||||
)
|
||||
processed = []
|
||||
|
||||
def process(task, _precomputed=None):
|
||||
processed.append(task[0])
|
||||
module.request_graceful_stop()
|
||||
(workspace.root / "saved-result").write_text(task[0])
|
||||
return []
|
||||
|
||||
with patch.object(module, "NB_THREADS", 1), patch.object(
|
||||
module, "process_single_task", side_effect=process
|
||||
), patch.object(
|
||||
module, "resolve_delayed_moves", return_value=[]
|
||||
):
|
||||
status = module.run_configured(args)
|
||||
saved_result = (workspace.root / "saved-result").read_text()
|
||||
|
||||
self.assertEqual(status, module.ExitCode.INTERRUPTED)
|
||||
self.assertEqual(processed, ["group-1.jpg"])
|
||||
self.assertEqual(saved_result, "group-1.jpg")
|
||||
|
||||
def test_gemini_call_finishes_inflight_stream_but_does_not_retry(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
self.addCleanup(module.stop_requested.clear)
|
||||
module.stop_requested.clear()
|
||||
api_client = Mock()
|
||||
|
||||
def stream(**_kwargs):
|
||||
yield SimpleNamespace(text="first")
|
||||
module.stop_requested.set()
|
||||
yield SimpleNamespace(text="-second")
|
||||
|
||||
api_client.models.generate_content_stream.side_effect = stream
|
||||
with patch.object(module, "client", api_client):
|
||||
self.assertEqual(
|
||||
module.call_gemini_with_retries("model", [], Mock()),
|
||||
"first-second",
|
||||
)
|
||||
with self.assertRaises(module.CorrectionStopRequested):
|
||||
module.call_gemini_with_retries("model", [], Mock())
|
||||
self.assertEqual(api_client.models.generate_content_stream.call_count, 1)
|
||||
|
||||
def test_interrupted_live_correction_resumes_only_remaining_groups(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
self.addCleanup(module.stop_requested.clear)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
workspace = EvaluationWorkspace(Path(directory))
|
||||
workspace.copies_dir.mkdir()
|
||||
tasks = []
|
||||
for index, label in enumerate(("Ex 1", "Ex 2"), start=1):
|
||||
group_dir = workspace.groups_dir / label
|
||||
group_dir.mkdir(parents=True)
|
||||
image = group_dir / f"Group_{index}.jpg"
|
||||
image.write_bytes(b"image")
|
||||
atomic_write_json(
|
||||
image.with_suffix(".json"),
|
||||
[["01", 0, 400, 1.0, label]],
|
||||
)
|
||||
tasks.append((str(image), label))
|
||||
|
||||
response = json.dumps(
|
||||
[{"id": "01", "result": {"error": "", "feedback": []}}]
|
||||
)
|
||||
call_count = 0
|
||||
|
||||
def stream(**_kwargs):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
module.stop_requested.set()
|
||||
yield SimpleNamespace(text=response)
|
||||
|
||||
api_client = Mock()
|
||||
api_client.models.generate_content_stream.side_effect = stream
|
||||
args = module.build_parser().parse_args([str(workspace.root)])
|
||||
|
||||
with patch.object(module, "NB_THREADS", 1), patch.object(
|
||||
module.prompting, "generate_request", return_value=([], Mock())
|
||||
):
|
||||
module.configure_runtime(
|
||||
workspace, tasks, args, api_client=api_client
|
||||
)
|
||||
first_status = module.run_configured(args)
|
||||
first_progress = read_json(workspace.correction_progress_file)
|
||||
first_results = read_json(workspace.correction_file)
|
||||
|
||||
module.configure_runtime(
|
||||
workspace, tasks, args, api_client=api_client
|
||||
)
|
||||
pending_on_resume = list(module.tasks_to_process)
|
||||
second_status = module.run_configured(args)
|
||||
|
||||
final_progress = read_json(workspace.correction_progress_file)
|
||||
final_results = read_json(workspace.correction_file)
|
||||
|
||||
self.assertEqual(first_status, module.ExitCode.INTERRUPTED)
|
||||
self.assertEqual(first_progress, [list(tasks[0])])
|
||||
self.assertEqual(len(first_results["Ex 1"]), 1)
|
||||
self.assertEqual(first_results["Ex 2"], [])
|
||||
self.assertEqual(pending_on_resume, [tasks[1]])
|
||||
self.assertEqual(second_status, module.ExitCode.SUCCESS)
|
||||
self.assertEqual(final_progress, [list(tasks[0]), list(tasks[1])])
|
||||
self.assertEqual(len(final_results["Ex 1"]), 1)
|
||||
self.assertEqual(len(final_results["Ex 2"]), 1)
|
||||
self.assertEqual(call_count, 2)
|
||||
|
||||
def test_interrupted_label_errors_resume_auxiliary_requests(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
self.addCleanup(module.stop_requested.clear)
|
||||
|
||||
for error_type in ("wrong-label", "additional-answer"):
|
||||
with self.subTest(error_type=error_type), tempfile.TemporaryDirectory() as directory:
|
||||
workspace = EvaluationWorkspace(Path(directory))
|
||||
workspace.copies_dir.mkdir()
|
||||
group_dir = workspace.groups_dir / "Ex 1"
|
||||
group_dir.mkdir(parents=True)
|
||||
image = group_dir / "Group_1.jpg"
|
||||
image.write_bytes(b"image")
|
||||
atomic_write_json(
|
||||
image.with_suffix(".json"),
|
||||
[["01", 0, 400, 1.0, "Ex 1"]],
|
||||
)
|
||||
task = (str(image), "Ex 1")
|
||||
response = json.dumps(
|
||||
[
|
||||
{
|
||||
"id": "01",
|
||||
"result": {"error": error_type, "feedback": []},
|
||||
}
|
||||
]
|
||||
)
|
||||
primary_call_count = 0
|
||||
|
||||
def stream(**_kwargs):
|
||||
nonlocal primary_call_count
|
||||
primary_call_count += 1
|
||||
module.stop_requested.set()
|
||||
yield SimpleNamespace(text=response)
|
||||
|
||||
def resolve_error(_pid, _label, result, _pdf_path):
|
||||
self.assertEqual(result["error"], error_type)
|
||||
result["error"] = ""
|
||||
return []
|
||||
|
||||
api_client = Mock()
|
||||
api_client.models.generate_content_stream.side_effect = stream
|
||||
args = module.build_parser().parse_args(
|
||||
[str(workspace.root), "--overwrite"]
|
||||
)
|
||||
|
||||
with patch.object(module, "NB_THREADS", 1), patch.object(
|
||||
module.prompting, "generate_request", return_value=([], Mock())
|
||||
), patch.object(
|
||||
module, "handle_label_errors", side_effect=resolve_error
|
||||
) as handler:
|
||||
module.configure_runtime(
|
||||
workspace, [task], args, api_client=api_client
|
||||
)
|
||||
first_status = module.run_configured(args)
|
||||
saved_pending = read_json(
|
||||
workspace.correction_pending_responses_file
|
||||
)
|
||||
first_progress_exists = (
|
||||
workspace.correction_progress_file.exists()
|
||||
)
|
||||
first_correction_exists = workspace.correction_file.exists()
|
||||
|
||||
module.configure_runtime(
|
||||
workspace, [task], args, api_client=api_client
|
||||
)
|
||||
second_status = module.run_configured(args)
|
||||
|
||||
self.assertEqual(first_status, module.ExitCode.INTERRUPTED)
|
||||
self.assertFalse(first_progress_exists)
|
||||
self.assertFalse(first_correction_exists)
|
||||
self.assertEqual(
|
||||
json.loads(saved_pending[str(image)]), json.loads(response)
|
||||
)
|
||||
self.assertEqual(second_status, module.ExitCode.SUCCESS)
|
||||
self.assertEqual(primary_call_count, 1)
|
||||
handler.assert_called_once()
|
||||
self.assertEqual(
|
||||
read_json(workspace.correction_progress_file), [list(task)]
|
||||
)
|
||||
corrected = read_json(workspace.correction_file)
|
||||
self.assertEqual(corrected["Ex 1"][0][0]["result"]["error"], "")
|
||||
self.assertEqual(
|
||||
read_json(workspace.correction_pending_responses_file), {}
|
||||
)
|
||||
|
||||
def test_correction_reset_restores_old_and_deletes_new_files(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
@@ -1255,6 +1607,9 @@ class StandardCliTests(unittest.TestCase):
|
||||
copy_dir.mkdir(parents=True)
|
||||
atomic_write_json(evaluation / "correction.json", {"old": True})
|
||||
atomic_write_json(evaluation / "correction_progress.json", ["old"])
|
||||
atomic_write_json(
|
||||
evaluation / "correction_pending_responses.json", {"group": "response"}
|
||||
)
|
||||
(copy_dir / "Ex 1.pdf").write_bytes(b"current")
|
||||
(copy_dir / "Ex 1_old.pdf").write_bytes(b"original")
|
||||
(copy_dir / "Ex 2_new.pdf").write_bytes(b"generated")
|
||||
@@ -1262,6 +1617,9 @@ class StandardCliTests(unittest.TestCase):
|
||||
self.assertEqual(module.main([str(evaluation), "--reset"]), 0)
|
||||
self.assertFalse((evaluation / "correction.json").exists())
|
||||
self.assertFalse((evaluation / "correction_progress.json").exists())
|
||||
self.assertFalse(
|
||||
(evaluation / "correction_pending_responses.json").exists()
|
||||
)
|
||||
self.assertEqual((copy_dir / "Ex 1.pdf").read_bytes(), b"original")
|
||||
self.assertFalse((copy_dir / "Ex 1_old.pdf").exists())
|
||||
self.assertFalse((copy_dir / "Ex 2_new.pdf").exists())
|
||||
@@ -1798,6 +2156,13 @@ class WorkflowTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(skip_without_conflicts, {"manual_resolution"})
|
||||
|
||||
def test_manual_resolution_precedes_post_correction(self) -> None:
|
||||
ordered_ids = [step.id for step in build_workflow(False)]
|
||||
self.assertLess(
|
||||
ordered_ids.index("manual_resolution"),
|
||||
ordered_ids.index("post_correction"),
|
||||
)
|
||||
|
||||
def test_crop_auto_start_follows_always_crop_configuration(self) -> None:
|
||||
for enabled in (False, True):
|
||||
with self.subTest(enabled=enabled), patch(
|
||||
@@ -1823,6 +2188,16 @@ class WorkflowTests(unittest.TestCase):
|
||||
def test_review_persp_has_shorter_title(self) -> None:
|
||||
self.assertEqual(self.steps["review_persp"].title, "Relire les barèmes")
|
||||
|
||||
def test_cutleft_title_and_shortcuts_are_documented(self) -> None:
|
||||
step = self.steps["cutleft"]
|
||||
self.assertEqual(
|
||||
step.title,
|
||||
"Découper une partie à gauche pour détection des labels",
|
||||
)
|
||||
for shortcut in ("n", "N", "t", "l", "1", "Entrée", "s"):
|
||||
with self.subTest(shortcut=shortcut):
|
||||
self.assertIn(shortcut, step.description)
|
||||
|
||||
def test_export_has_shorter_title_and_annotation_argument(self) -> None:
|
||||
self.assertEqual(self.steps["export"].title, "Exporter")
|
||||
command = self.command(
|
||||
@@ -1857,6 +2232,47 @@ class WorkflowTests(unittest.TestCase):
|
||||
detected_annotation_directories(evaluation), ("Bnot", "Anot")
|
||||
)
|
||||
|
||||
def test_giving_names_utilities_find_and_rename_problematic_copies(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
evaluation = Path(directory)
|
||||
return_dir = evaluation / "A Rendre"
|
||||
original_dir = evaluation / "Copies Originales"
|
||||
return_dir.mkdir()
|
||||
original_dir.mkdir()
|
||||
(original_dir / "Copie01.pdf").write_bytes(b"original")
|
||||
folders = {
|
||||
"Unknown (01)": (b"unknown-jpg", b"unknown-pdf"),
|
||||
"Dupont (02)": (b"two-jpg", b"two-pdf"),
|
||||
"Dupont (03)": (b"three-jpg", b"three-pdf"),
|
||||
"Unique (04)": (b"four-jpg", b"four-pdf"),
|
||||
}
|
||||
for folder_name, (jpg, pdf) in folders.items():
|
||||
folder = return_dir / folder_name
|
||||
folder.mkdir()
|
||||
base_name = folder_name.rsplit(" (", 1)[0]
|
||||
(folder / f"{base_name}.jpg").write_bytes(jpg)
|
||||
(folder / f"{base_name}.pdf").write_bytes(pdf)
|
||||
(folder / "score.json").write_text("{}", encoding="utf-8")
|
||||
(folder / "answers").mkdir()
|
||||
|
||||
issues = find_name_issues(evaluation)
|
||||
self.assertEqual([issue.copy_id for issue in issues], ["01", "02", "03"])
|
||||
self.assertEqual(
|
||||
original_copy_path(evaluation, "01"),
|
||||
original_dir / "Copie01.pdf",
|
||||
)
|
||||
|
||||
renamed = rename_return_copy(return_dir / "Unknown (01)", "Alice")
|
||||
self.assertEqual(renamed.name, "Alice (01)")
|
||||
self.assertEqual((renamed / "Alice.jpg").read_bytes(), b"unknown-jpg")
|
||||
self.assertEqual((renamed / "Alice.pdf").read_bytes(), b"unknown-pdf")
|
||||
self.assertTrue((renamed / "score.json").is_file())
|
||||
self.assertTrue((renamed / "answers").is_dir())
|
||||
self.assertEqual(
|
||||
[issue.copy_id for issue in find_name_issues(evaluation)],
|
||||
["02", "03"],
|
||||
)
|
||||
|
||||
def test_export_and_import_defaults_follow_previous_runs(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
evaluation = Path(directory)
|
||||
@@ -1883,10 +2299,27 @@ class WorkflowTests(unittest.TestCase):
|
||||
|
||||
def test_plotting_shortcuts_describe_open_actions(self) -> None:
|
||||
rendered = "\n".join(plotting_shortcut_lines())
|
||||
self.assertIn("p : revenir à la précédente", rendered)
|
||||
self.assertIn("ouvrir l’énoncé", rendered)
|
||||
self.assertIn("ouvrir la copie traitée", rendered)
|
||||
self.assertIn("ouvrir la copie originale", rendered)
|
||||
|
||||
def test_gui_tracks_correction_progress_across_output_chunks(self) -> None:
|
||||
app = object.__new__(CopienatorApp)
|
||||
app.active_step_id = "correction"
|
||||
app._correction_progress_buffer = ""
|
||||
app.info_var = Mock()
|
||||
|
||||
app._track_correction_progress(
|
||||
"[Progression correction] Groupes trai"
|
||||
)
|
||||
app.info_var.set.assert_not_called()
|
||||
app._track_correction_progress("tés : 3/8\n")
|
||||
|
||||
app.info_var.set.assert_called_once_with(
|
||||
"Correction : 3 groupe(s) traité(s) sur 8 (38 %)."
|
||||
)
|
||||
|
||||
def test_proxy_is_opt_in_and_prefilled(self) -> None:
|
||||
base = {"HTTPS_PROXY": "http://system-proxy", "OTHER": "kept"}
|
||||
without_proxy = build_runner_environment(
|
||||
@@ -1922,6 +2355,21 @@ class WorkflowTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(command[4], "correct")
|
||||
|
||||
def test_refaire_correction_does_not_offer_overwrite(self) -> None:
|
||||
step = self.steps["correction"]
|
||||
overwrite = next(
|
||||
argument for argument in step.arguments if argument.name == "overwrite"
|
||||
)
|
||||
self.assertNotIn("refaire", overwrite.variants)
|
||||
|
||||
command = self.command(
|
||||
"correction",
|
||||
"refaire",
|
||||
{"target": self.evaluation, "overwrite": True},
|
||||
)
|
||||
self.assertIn("--refaire", command)
|
||||
self.assertNotIn("--overwrite", command)
|
||||
|
||||
def test_hybrid_correction_arguments(self) -> None:
|
||||
command = self.command(
|
||||
"correction", "hybrid", {"target": self.evaluation, "batch_from": "Ex 4"}
|
||||
|
||||
@@ -36,6 +36,7 @@ class MarkedCopiesTests(unittest.TestCase):
|
||||
review.had_errors = False
|
||||
review.completed = False
|
||||
review.current_shift = 50
|
||||
review.current_width_offset = 0
|
||||
review.default_max_per_file = 5
|
||||
review.current_max_per_file = 1
|
||||
review.root = Mock()
|
||||
@@ -70,6 +71,7 @@ class MarkedCopiesTests(unittest.TestCase):
|
||||
self.assertIn("Copie01.pdf", copy_errors(self.workspace))
|
||||
self.assertEqual(review.index, 1)
|
||||
self.assertEqual(review.current_max_per_file, 5)
|
||||
self.assertEqual(review.current_width_offset, 0)
|
||||
self.assertTrue(review.had_errors)
|
||||
review.load_current_image.assert_called_once_with()
|
||||
|
||||
@@ -96,6 +98,30 @@ class MarkedCopiesTests(unittest.TestCase):
|
||||
self.assertTrue(review.stop_prefetch.is_set())
|
||||
self.assertIn("Copie01.pdf", copy_errors(self.workspace))
|
||||
|
||||
def test_enlarge_reprocesses_with_a_wider_selection(self):
|
||||
review = self.reviewer()
|
||||
review.trigger_processing = Mock()
|
||||
|
||||
review.on_enlarge(cutleft.CROP_WIDTH_STEP)
|
||||
|
||||
self.assertEqual(review.current_width_offset, 50)
|
||||
review.trigger_processing.assert_called_once_with(self.files[0], 50)
|
||||
|
||||
def test_process_single_pdf_enlarges_crop_and_caps_it_at_page_edge(self):
|
||||
page = Image.new("RGB", (900, 300), "white")
|
||||
with patch.object(cutleft, "get_pdf_pages", return_value=[page]):
|
||||
regular = cutleft.process_single_pdf(self.files[0])
|
||||
enlarged = cutleft.process_single_pdf(self.files[0], width_offset=200)
|
||||
capped = cutleft.process_single_pdf(self.files[0], width_offset=1000)
|
||||
fullpage = cutleft.process_single_pdf(
|
||||
self.files[0], max_per_file=1, width_offset=200
|
||||
)
|
||||
|
||||
self.assertEqual(regular[1][0].size, (300, 300))
|
||||
self.assertEqual(enlarged[1][0].size, (500, 300))
|
||||
self.assertEqual(capped[1][0].size, (800, 300))
|
||||
self.assertEqual(fullpage[1][0].size, (900, 300))
|
||||
|
||||
def test_processing_blocks_skip_and_failed_conversion_is_flagged(self):
|
||||
review = self.reviewer()
|
||||
review.is_processing = True
|
||||
|
||||
Reference in New Issue
Block a user