Miscs
This commit is contained in:
@@ -2,6 +2,8 @@ import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from tkinter import ttk
|
||||
from unittest.mock import patch
|
||||
|
||||
from copienator_gui.app import CopienatorApp
|
||||
from copienator_gui.workflow import command_display
|
||||
@@ -40,6 +42,16 @@ class GuiConvenienceTests(unittest.TestCase):
|
||||
self.assertEqual(self.app.state_store.step("inputs")["status"], "ready")
|
||||
self.assertIn("enonce.pdf", self.app.description_label.cget("text"))
|
||||
|
||||
def test_compact_evaluation_input_and_open_folder_button(self):
|
||||
self.assertEqual(int(self.app.evaluation_entry.cget("width")), 42)
|
||||
self.assertEqual(
|
||||
int(self.app.open_evaluation_button.grid_info()["column"]),
|
||||
int(self.app.evaluation_entry.grid_info()["column"]) + 1,
|
||||
)
|
||||
with patch("copienator_gui.app.open_path") as opened:
|
||||
self.app.open_evaluation_button.invoke()
|
||||
opened.assert_called_once_with(self.evaluation)
|
||||
|
||||
def test_redo_targets_selected_copy_and_copies_runnable_command(self):
|
||||
for folder in ("Copies", "Copies Originales"):
|
||||
(self.evaluation / folder).mkdir()
|
||||
@@ -48,7 +60,7 @@ class GuiConvenienceTests(unittest.TestCase):
|
||||
self.app.tree.selection_set("page_splitter")
|
||||
self.app.update()
|
||||
self.app.copy_var.set("Copie02.pdf")
|
||||
self.app._redo_selected_pages()
|
||||
self.app._target_selected_copy()
|
||||
command = self.app._make_command()
|
||||
target = Path(command[-1])
|
||||
self.assertEqual(target, self.evaluation / "Copies" / "Copie02.pdf")
|
||||
@@ -59,6 +71,36 @@ class GuiConvenienceTests(unittest.TestCase):
|
||||
self.app._target_all_pages()
|
||||
self.assertEqual(self.app.arg_vars["target"].get(), self.app._evaluation_arg())
|
||||
|
||||
def test_label_detection_can_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("labels")
|
||||
self.app.update()
|
||||
self.assertEqual(self.app.copy_var.get(), "Copie01.pdf")
|
||||
self.app.copy_var.set("Copie02.pdf")
|
||||
self.app._target_selected_copy()
|
||||
|
||||
command = self.app._make_command()
|
||||
self.assertEqual(Path(command[-1]), copies / "Copie02.pdf")
|
||||
|
||||
def button_texts(widget):
|
||||
return [
|
||||
text
|
||||
for child in widget.winfo_children()
|
||||
for text in (
|
||||
[child.cget("text")]
|
||||
if isinstance(child, ttk.Button)
|
||||
else button_texts(child)
|
||||
)
|
||||
]
|
||||
|
||||
controls = button_texts(self.app.form)
|
||||
self.assertIn("Cibler la copie sélectionnée", controls)
|
||||
self.assertIn("Cibler tout le dossier", controls)
|
||||
|
||||
def test_console_selection_survives_output_and_is_read_only(self):
|
||||
self.app._append_console("Première ligne\nDeuxième ligne\n")
|
||||
self.app.console.tag_add("sel", "1.0", "1.end")
|
||||
@@ -73,3 +115,25 @@ class GuiConvenienceTests(unittest.TestCase):
|
||||
self.app._select_console_all()
|
||||
self.app._copy_console_selection()
|
||||
self.assertEqual(self.app.clipboard_get(), expected)
|
||||
|
||||
def test_validate_rename_preserves_files_and_downstream_status(self):
|
||||
pdf = self.evaluation / "Copie01.pdf"
|
||||
pdf.write_bytes(b"unchanged PDF")
|
||||
self.app.state_store.update_step("rename", status="stale")
|
||||
self.app.state_store.update_step("page_splitter", status="success")
|
||||
self.app.tree.selection_set("rename")
|
||||
self.app.update()
|
||||
with patch.object(self.app.runner, "start") as start:
|
||||
self.app.validate_rename_button.invoke()
|
||||
start.assert_not_called()
|
||||
self.assertEqual(self.app.state_store.step("rename")["status"], "success")
|
||||
self.assertEqual(self.app.state_store.step("page_splitter")["status"], "success")
|
||||
self.assertEqual(pdf.read_bytes(), b"unchanged PDF")
|
||||
self.assertEqual(list(self.evaluation.glob("*.pdf")), [pdf])
|
||||
self.app.state_store.update_step("rename", status="stale")
|
||||
self.app.active_step_id = "page_splitter"
|
||||
self.app._update_controls()
|
||||
self.assertIn("disabled", self.app.validate_rename_button.state())
|
||||
self.app._validate_rename_step()
|
||||
self.assertEqual(self.app.state_store.step("rename")["status"], "stale")
|
||||
self.app.active_step_id = None
|
||||
|
||||
+90
-17
@@ -10,7 +10,7 @@ import tempfile
|
||||
import time
|
||||
import unittest
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from contextlib import redirect_stderr
|
||||
from contextlib import redirect_stderr, redirect_stdout
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
@@ -714,6 +714,8 @@ class StandardCliTests(unittest.TestCase):
|
||||
copy_pdf.parent.mkdir(parents=True)
|
||||
copy_pdf.write_bytes(b"pdf")
|
||||
with patch.object(module, "ImageReviewer") as reviewer:
|
||||
reviewer.return_value.completed = True
|
||||
reviewer.return_value.had_errors = False
|
||||
self.assertEqual(module.main([str(copy_pdf), "--fullpage"]), 0)
|
||||
files, output_dir = reviewer.call_args.args[:2]
|
||||
self.assertEqual(files, [copy_pdf])
|
||||
@@ -1011,6 +1013,11 @@ class StandardCliTests(unittest.TestCase):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
evaluation = Path(directory) / "Exam"
|
||||
(evaluation / "Copies").mkdir(parents=True)
|
||||
text_dir = evaluation / "Text"
|
||||
text_dir.mkdir()
|
||||
(text_dir / "Ex 1").write_text("Short match", encoding="utf-8")
|
||||
closest_text = text_dir / "Ex 1 : 1)"
|
||||
closest_text.write_text("Relevant statement text", encoding="utf-8")
|
||||
image = evaluation / "Cutleft" / "Copie01_01.jpg"
|
||||
image.parent.mkdir()
|
||||
image.write_bytes(b"image")
|
||||
@@ -1019,39 +1026,105 @@ class StandardCliTests(unittest.TestCase):
|
||||
Mock(
|
||||
text=(
|
||||
'{"name":"Student","list":'
|
||||
'[{"box_2d":[1,2,3,4],"label":"Wrong"}]}'
|
||||
'[{"box_2d":[1,2,3,4],"label":"Ex 1 : l)"}]}'
|
||||
)
|
||||
),
|
||||
Mock(
|
||||
text=(
|
||||
'{"name":"Student","list":'
|
||||
'[{"box_2d":[1,2,3,4],"label":"Ex 1"}]}'
|
||||
'[{"box_2d":[1,2,3,4],"label":"Ex 1 : 1)"}]}'
|
||||
)
|
||||
),
|
||||
]
|
||||
sleeps = []
|
||||
module.process_copy_group(
|
||||
EvaluationWorkspace(evaluation),
|
||||
"Copie01",
|
||||
[image],
|
||||
client=client,
|
||||
labels_text="Ex 1\n",
|
||||
names_text="Student\n",
|
||||
valid_labels={"Ex 1"},
|
||||
valid_names={"Student", "Unknown", "Continued"},
|
||||
overwrite=True,
|
||||
sleep=sleeps.append,
|
||||
target_interval=0,
|
||||
)
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
module.process_copy_group(
|
||||
EvaluationWorkspace(evaluation),
|
||||
"Copie01",
|
||||
[image],
|
||||
client=client,
|
||||
labels_text="Ex 1 : 1)\n",
|
||||
names_text="Student\n",
|
||||
valid_labels={"Ex 1 : 1)"},
|
||||
valid_names={"Student", "Unknown", "Continued"},
|
||||
overwrite=True,
|
||||
sleep=sleeps.append,
|
||||
target_interval=0,
|
||||
)
|
||||
self.assertEqual(client.models.generate_content.call_count, 2)
|
||||
self.assertIn(10, sleeps)
|
||||
retry_contents = client.models.generate_content.call_args_list[1].kwargs[
|
||||
"contents"
|
||||
]
|
||||
retry_prompt = retry_contents[0].parts[1].text
|
||||
self.assertIn('"Ex 1 : l)"', retry_prompt)
|
||||
self.assertIn("CRITICAL RETRY CONSTRAINT: NEVER return", retry_prompt)
|
||||
self.assertIn("Relevant statement text", retry_prompt)
|
||||
self.assertIn("`Ex 1 : 1)`", retry_prompt)
|
||||
self.assertIn(
|
||||
"Retry context for Copie01_01.jpg: Text/Ex 1 : 1)",
|
||||
output.getvalue(),
|
||||
)
|
||||
self.assertEqual(
|
||||
read_json(evaluation / "Copies" / "Copie01_01.json")["list"][0][
|
||||
"label"
|
||||
],
|
||||
"Ex 1",
|
||||
"Ex 1 : 1)",
|
||||
)
|
||||
|
||||
def test_label_detection_marks_a_thrice_repeated_unknown_label(self) -> None:
|
||||
module = self.modules["gemini_for_labels"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
evaluation = Path(directory) / "Exam"
|
||||
(evaluation / "Copies").mkdir(parents=True)
|
||||
(evaluation / "Text").mkdir()
|
||||
(evaluation / "Text" / "Ex 10").write_text(
|
||||
"Question 1 text", encoding="utf-8"
|
||||
)
|
||||
image = evaluation / "Cutleft" / "Copie42_03.jpg"
|
||||
image.parent.mkdir()
|
||||
image.write_bytes(b"image")
|
||||
repeated = Mock(
|
||||
text=(
|
||||
'{"name":"Continued","list":'
|
||||
'[{"box_2d":[1,2,3,4],"label":"Ex 10 : 1)a)"}]}'
|
||||
)
|
||||
)
|
||||
client = Mock()
|
||||
client.models.generate_content.side_effect = [
|
||||
repeated,
|
||||
repeated,
|
||||
repeated,
|
||||
]
|
||||
|
||||
output = io.StringIO()
|
||||
with redirect_stdout(output):
|
||||
generated = module.process_copy_group(
|
||||
EvaluationWorkspace(evaluation),
|
||||
"Copie42",
|
||||
[image],
|
||||
client=client,
|
||||
labels_text="Ex 10 : 1)\nEx 10 : 2)\nEx 10 : 3)\n",
|
||||
names_text="Student\n",
|
||||
valid_labels={"Ex 10 : 1)", "Ex 10 : 2)", "Ex 10 : 3)"},
|
||||
valid_names={"Student", "Unknown", "Continued"},
|
||||
overwrite=True,
|
||||
sleep=lambda _seconds: None,
|
||||
target_interval=0,
|
||||
)
|
||||
|
||||
self.assertEqual(generated, 1)
|
||||
self.assertEqual(client.models.generate_content.call_count, 3)
|
||||
seeds = [
|
||||
call.kwargs["config"].seed
|
||||
for call in client.models.generate_content.call_args_list
|
||||
]
|
||||
self.assertEqual(seeds, [0, 0, 1])
|
||||
result = read_json(evaluation / "Copies" / "Copie42_03.json")
|
||||
self.assertEqual(result["list"][0]["label"], "??Ex 10 : 1)a)")
|
||||
self.assertIn("keeping them with a ?? prefix", output.getvalue())
|
||||
|
||||
def test_correction_overwrite_keeps_previous_state_until_a_commit(self) -> None:
|
||||
module = self.modules["correction"]
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import os
|
||||
import tempfile
|
||||
import threading
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from copienator import CliError, EvaluationWorkspace, ExitCode
|
||||
from copienator.copy_errors import copy_errors, clear_copy_error, mark_copy_error, marked_copy_paths
|
||||
from copienator.commands import cutleft, page_splitter
|
||||
from copienator_gui.app import CopienatorApp
|
||||
|
||||
|
||||
class MarkedCopiesTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.workspace = EvaluationWorkspace(Path(self.temp.name))
|
||||
self.workspace.copies_dir.mkdir()
|
||||
self.workspace.original_copies_dir.mkdir()
|
||||
self.files = [self.workspace.copies_dir / name for name in ("Copie01.pdf", "Copie02.pdf")]
|
||||
for path in self.files:
|
||||
path.write_bytes(b"processed")
|
||||
(self.workspace.original_copies_dir / path.name).write_bytes(b"original")
|
||||
|
||||
def reviewer(self):
|
||||
review = cutleft.ImageReviewer.__new__(cutleft.ImageReviewer)
|
||||
review.workspace = self.workspace
|
||||
review.files = self.files
|
||||
review.output_dir = self.workspace.cutleft_dir
|
||||
review.index = 0
|
||||
review.is_processing = False
|
||||
review.had_errors = False
|
||||
review.completed = False
|
||||
review.current_shift = 50
|
||||
review.default_max_per_file = 5
|
||||
review.current_max_per_file = 1
|
||||
review.root = Mock()
|
||||
review.stop_prefetch = threading.Event()
|
||||
review.load_current_image = Mock()
|
||||
review.update_display = Mock()
|
||||
image = Image.new("RGB", (8, 8), "white")
|
||||
review.current_result = (image, [image], {"total_pages": 1, "columns_per_file": [1]})
|
||||
return review
|
||||
|
||||
def test_marks_survive_reload_and_resolve_originals_only_for_splitting(self):
|
||||
mark_copy_error(self.workspace, self.files[1], "Wrong page order")
|
||||
reloaded = EvaluationWorkspace(self.workspace.root)
|
||||
self.assertEqual(marked_copy_paths(reloaded), [self.files[1]])
|
||||
original = self.workspace.original_copies_dir / self.files[1].name
|
||||
self.assertEqual(marked_copy_paths(reloaded, originals=True), [original])
|
||||
self.files[1].unlink()
|
||||
with self.assertRaises(CliError):
|
||||
marked_copy_paths(reloaded)
|
||||
self.assertEqual(marked_copy_paths(reloaded, originals=True), [original])
|
||||
clear_copy_error(reloaded, original)
|
||||
self.assertEqual(copy_errors(self.workspace), {})
|
||||
|
||||
def test_skip_flags_and_advances_without_replacing_existing_crop(self):
|
||||
review = self.reviewer()
|
||||
review.output_dir.mkdir()
|
||||
previous = review.output_dir / "Copie01_01.jpg"
|
||||
previous.write_bytes(b"previous crop")
|
||||
review.handle_processing_result(review.current_result, self.files[0])
|
||||
review.on_skip()
|
||||
self.assertEqual(previous.read_bytes(), b"previous crop")
|
||||
self.assertIn("Copie01.pdf", copy_errors(self.workspace))
|
||||
self.assertEqual(review.index, 1)
|
||||
self.assertEqual(review.current_max_per_file, 5)
|
||||
self.assertTrue(review.had_errors)
|
||||
review.load_current_image.assert_called_once_with()
|
||||
|
||||
def test_accept_saves_and_clears_only_current_flag(self):
|
||||
for path in self.files:
|
||||
mark_copy_error(self.workspace, path, "Review needed")
|
||||
review = self.reviewer()
|
||||
review.on_next(None)
|
||||
self.assertTrue((review.output_dir / "Copie01_01.jpg").is_file())
|
||||
self.assertEqual(set(copy_errors(self.workspace)), {"Copie02.pdf"})
|
||||
self.assertEqual(review.index, 1)
|
||||
|
||||
def test_failed_save_and_window_close_preserve_flag(self):
|
||||
mark_copy_error(self.workspace, self.files[0], "Review needed")
|
||||
review = self.reviewer()
|
||||
with patch.object(cutleft, "save_results", side_effect=OSError("disk full")), patch.object(
|
||||
cutleft.messagebox, "showerror"
|
||||
):
|
||||
review.on_next(None)
|
||||
self.assertEqual(review.index, 0)
|
||||
self.assertIn("Copie01.pdf", copy_errors(self.workspace))
|
||||
review.on_close()
|
||||
self.assertFalse(review.completed)
|
||||
self.assertTrue(review.stop_prefetch.is_set())
|
||||
self.assertIn("Copie01.pdf", copy_errors(self.workspace))
|
||||
|
||||
def test_processing_blocks_skip_and_failed_conversion_is_flagged(self):
|
||||
review = self.reviewer()
|
||||
review.is_processing = True
|
||||
review.on_skip()
|
||||
self.assertEqual(review.index, 0)
|
||||
self.assertEqual(copy_errors(self.workspace), {})
|
||||
review.manual_queue = Queue()
|
||||
review.manual_queue.put(None)
|
||||
review.load_current_image.side_effect = lambda: setattr(review, "is_processing", True)
|
||||
review.check_manual_queue(self.files[0])
|
||||
self.assertIn("Copie01.pdf", copy_errors(self.workspace))
|
||||
self.assertEqual(review.index, 1)
|
||||
self.assertTrue(review.is_processing) # Still loading the next copy.
|
||||
|
||||
def test_cli_splitting_preserves_flags_and_cropping_reports_partial_or_interrupted(self):
|
||||
mark_copy_error(self.workspace, self.files[1], "Wrong order")
|
||||
with patch.object(page_splitter.tk, "Tk"), patch.object(page_splitter, "PDFPreviewer") as preview:
|
||||
preview.return_value.failed = False
|
||||
self.assertEqual(page_splitter.main([str(self.workspace.root), "--marked"]), 0)
|
||||
self.assertEqual(preview.call_args.args[2], [self.workspace.original_copies_dir / "Copie02.pdf"])
|
||||
self.assertIn("Copie02.pdf", copy_errors(self.workspace))
|
||||
with patch.object(cutleft, "ImageReviewer") as reviewer:
|
||||
for completed, errors, expected in ((True, True, ExitCode.PARTIAL),
|
||||
(False, False, ExitCode.INTERRUPTED),
|
||||
(True, False, ExitCode.SUCCESS)):
|
||||
reviewer.return_value.completed = completed
|
||||
reviewer.return_value.had_errors = errors
|
||||
self.assertEqual(cutleft.main([str(self.workspace.root), "--marked"]), expected)
|
||||
self.assertEqual(reviewer.call_args.args[0], [self.files[1]])
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get("DISPLAY"), "Tk requires a display")
|
||||
class MarkedCopiesGuiTests(unittest.TestCase):
|
||||
setUp = MarkedCopiesTests.setUp
|
||||
|
||||
def test_keyboard_skip_then_accept_and_retry_clears_flag(self):
|
||||
real_tk = cutleft.tk.Tk
|
||||
|
||||
def review_with_keys(files, keys):
|
||||
sent = []
|
||||
|
||||
def make_root():
|
||||
root = real_tk()
|
||||
|
||||
def send_when_ready():
|
||||
info = root.winfo_children()[-1].cget("text")
|
||||
index = len(sent)
|
||||
if index < len(keys) and info.startswith(f"[{index + 1}/{len(files)}]"):
|
||||
root.focus_force()
|
||||
sent.append(keys[index])
|
||||
root.event_generate(keys[index])
|
||||
if len(sent) < len(keys):
|
||||
root.after(10, send_when_ready)
|
||||
|
||||
root.after(20, send_when_ready)
|
||||
root.after(3000, root.destroy) # Bound a failed keyboard test.
|
||||
return root
|
||||
|
||||
with patch.object(cutleft.tk, "Tk", side_effect=make_root), patch.object(
|
||||
cutleft, "get_pdf_pages", return_value=[Image.new("RGB", (600, 300), "white")]
|
||||
), patch.object(cutleft, "OUTPUT_SIZE", (400, 200)):
|
||||
reviewer = cutleft.ImageReviewer(files, self.workspace.cutleft_dir)
|
||||
self.assertEqual(sent, keys)
|
||||
self.assertTrue(reviewer.completed)
|
||||
return reviewer
|
||||
|
||||
first = review_with_keys(self.files, ["<KeyPress-s>", "<Return>"])
|
||||
self.assertTrue(first.had_errors)
|
||||
self.assertEqual(set(copy_errors(self.workspace)), {"Copie01.pdf"})
|
||||
self.assertFalse((self.workspace.cutleft_dir / "Copie01_01.jpg").exists())
|
||||
self.assertTrue((self.workspace.cutleft_dir / "Copie02_01.jpg").exists())
|
||||
second = review_with_keys(marked_copy_paths(self.workspace), ["<Return>"])
|
||||
self.assertFalse(second.had_errors)
|
||||
self.assertEqual(copy_errors(self.workspace), {})
|
||||
self.assertTrue((self.workspace.cutleft_dir / "Copie01_01.jpg").exists())
|
||||
|
||||
def test_both_buttons_run_marked_and_single_copy_target_clears_filter(self):
|
||||
mark_copy_error(self.workspace, self.files[1], "Review needed")
|
||||
app = CopienatorApp(Path.cwd(), False, self.workspace.root)
|
||||
try:
|
||||
app.update()
|
||||
for ident, command in (("page_splitter", "page-split"), ("cutleft", "crop-labels")):
|
||||
app.tree.selection_set(ident)
|
||||
app.update()
|
||||
self.assertIn("(1)", app.marked_copies_button.cget("text"))
|
||||
with patch.object(app, "_run_current_step") as run:
|
||||
app.marked_copies_button.invoke()
|
||||
run.assert_called_once_with()
|
||||
self.assertIn(command, app._make_command())
|
||||
self.assertIn("--marked", app._make_command())
|
||||
self.assertEqual(app.arg_vars["target"].get(), app._evaluation_arg())
|
||||
app.copy_var.set("Copie01.pdf")
|
||||
app._target_selected_copy()
|
||||
self.assertNotIn("--marked", app._make_command())
|
||||
self.assertIn(str(self.files[0]), app._make_command())
|
||||
finally:
|
||||
for callback in app.tk.splitlist(app.tk.call("after", "info")):
|
||||
app.after_cancel(callback)
|
||||
app.destroy()
|
||||
@@ -118,6 +118,7 @@ class SelectiveGeminiTests(unittest.TestCase):
|
||||
self.assertIn("Barème Gemini", (self.root / "Persp" / label).read_text())
|
||||
for call in self.client.models.generate_content.call_args_list:
|
||||
self.assertEqual(call.kwargs["contents"][0].parts[0].text, gemini.PROMPT_4)
|
||||
self.assertTrue(call.kwargs["config"].automatic_function_calling.disable)
|
||||
|
||||
def test_incomplete_or_failed_rubrics_preserve_entire_persp(self):
|
||||
before = self.snapshot()
|
||||
|
||||
Reference in New Issue
Block a user