Files
Copies/tests/test_label_timeouts.py
T
2026-09-26 23:01:47 +02:00

110 lines
5.2 KiB
Python

import io
import tempfile
import unittest
from concurrent.futures import wait as real_wait
from contextlib import redirect_stdout
from pathlib import Path
from threading import Event
from unittest.mock import Mock, patch
from copienator import EvaluationWorkspace, ExitCode
from copienator.commands import gemini_for_labels as labels
class LabelTimeoutTests(unittest.TestCase):
def setUp(self):
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.workspace = EvaluationWorkspace(Path(directory.name))
(self.workspace.root / "labels").write_text("Ex 1\n")
(self.workspace.root / "names").write_text("Student\n")
self.workspace.copies_dir.mkdir()
(self.workspace.root / "Cutleft").mkdir()
self.images = []
for name in ("Copie01_01", "Copie01_02", "Copie02_01"):
image = self.workspace.root / "Cutleft" / f"{name}.jpg"
image.write_bytes(b"image")
self.images.append(image)
self.client = Mock()
self.response = Mock(text='{"name":"Student","list":[]}')
def group(self, images):
return labels.process_copy_group(
self.workspace, "Copie01", images, client=self.client,
labels_text="Ex 1\n", names_text="Student\n",
valid_labels={"Ex 1"}, valid_names={"Student"}, overwrite=False,
sleep=lambda _: None, target_interval=0,
)
def test_timeout_recovery_uses_bounded_requests_and_saves_result(self):
self.client.models.generate_content.side_effect = [TimeoutError(), self.response]
self.assertEqual(self.group(self.images[:1]), 1)
self.assertEqual(self.client.models.generate_content.call_count, 2)
for call in self.client.models.generate_content.call_args_list:
options = call.kwargs["config"].http_options
self.assertEqual(options.timeout, 120_000)
self.assertEqual(options.retry_options.attempts, 1)
self.assertTrue((self.workspace.copies_dir / "Copie01_01.json").exists())
def test_exhaustion_defers_dependent_pages_but_other_copies_finish_and_resume(self):
self.client.models.generate_content.side_effect = [
TimeoutError(), TimeoutError(), TimeoutError(), self.response,
]
output = io.StringIO()
with patch.object(labels, "selected_images", return_value=(self.images, [])), redirect_stdout(output):
result = labels.run(self.workspace, [], client=self.client,
sleep=lambda _: None, max_workers=1)
self.assertEqual(result, ExitCode.PARTIAL)
self.assertEqual(self.client.models.generate_content.call_count, 4)
self.assertFalse((self.workspace.copies_dir / "Copie01_01.json").exists())
self.assertFalse((self.workspace.copies_dir / "Copie01_02.json").exists())
self.assertTrue((self.workspace.copies_dir / "Copie02_01.json").exists())
self.assertIn("Incomplete images", output.getvalue())
self.assertIn("Copie01_02.jpg", output.getvalue())
self.client.models.generate_content.reset_mock(side_effect=True)
self.client.models.generate_content.return_value = self.response
with patch.object(labels, "selected_images", return_value=(self.images, [])):
result = labels.run(self.workspace, [], client=self.client, sleep=lambda _: None)
self.assertEqual(result, ExitCode.SUCCESS)
self.assertEqual(self.client.models.generate_content.call_count, 2)
def test_changing_invalid_labels_cannot_retry_forever(self):
self.client.models.generate_content.side_effect = [Mock(text=(
'{"name":"Student","list":[{"box_2d":[1,2,3,4],"label":"bad%d"}]}' % index
)) for index in range(3)]
with self.assertRaises(labels.IncompleteCopyError):
self.group(self.images[:1])
self.assertEqual(self.client.models.generate_content.call_count, 3)
def test_progress_reports_waiting_image_and_elapsed_time(self):
entered, release = Event(), Event()
self.addCleanup(release.set)
def request(**kwargs):
entered.set()
if not release.wait(2):
raise AssertionError("Test did not release request")
return self.response
first_wait = True
def wait(futures, timeout):
nonlocal first_wait
self.assertEqual(timeout, 30)
if first_wait:
first_wait = False
self.assertTrue(entered.wait(2))
return set(), set(futures)
release.set()
return real_wait(futures, timeout=2)
self.client.models.generate_content.side_effect = request
output = io.StringIO()
with patch.object(labels, "selected_images", return_value=(self.images[:1], [])), \
patch.object(labels, "wait", side_effect=wait), redirect_stdout(output):
result = labels.run(self.workspace, [], client=self.client, sleep=lambda _: None)
self.assertEqual(result, ExitCode.SUCCESS)
self.assertIn("1 copies outstanding", output.getvalue())
self.assertIn("Copie01_01.jpg, attempt 1/3", output.getvalue())
self.assertIn("elapsed for image", output.getvalue())