This commit is contained in:
2026-09-08 16:41:04 +02:00
parent db4ed2ef31
commit bf05272797
14 changed files with 725 additions and 75 deletions
+197
View File
@@ -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()