Enforce configurable height and pixel limits for tablet documents

This commit is contained in:
sebastien committed 2026-10-26 15:09:05 +01:00
1 parent 8918d64452
commit a38c1ff4b3
10 files changed
+187 -57

No files matched your search

+20 -3
View File
@@ -8,7 +8,7 @@ import unittest
class AnnotationHeightConfigTests(unittest.TestCase):
def load_generators(self, setting):
def load_generators(self, setting, attribute="MAX_HEIGHT_PX"):
with tempfile.TemporaryDirectory() as directory:
config = Path(directory) / "config.py"
config.write_text("from default_config import *\n" + setting, encoding="utf-8")
@@ -16,7 +16,7 @@ class AnnotationHeightConfigTests(unittest.TestCase):
[sys.executable, "-c",
"from copienator.commands import annotating_by_label as grouped; "
"from copienator.commands import annotating_with_checks as checks; "
"print(grouped.MAX_HEIGHT_PX, checks.MAX_HEIGHT_PX)"],
f"print(grouped.{attribute}, checks.{attribute})"],
cwd=Path(__file__).resolve().parents[1],
env={**os.environ, "COPIENATOR_CONFIG": str(config)},
capture_output=True, text=True, check=False,
@@ -30,7 +30,7 @@ class AnnotationHeightConfigTests(unittest.TestCase):
def test_older_configuration_keeps_current_default(self):
result = self.load_generators("del ANNOTATION_MAX_HEIGHT_PX\n")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "32000 32000")
self.assertEqual(result.stdout.strip(), "33000 33000")
def test_invalid_heights_are_rejected(self):
for value in ("0", "-1", "1.5", "True", "'32000'"):
@@ -38,3 +38,20 @@ class AnnotationHeightConfigTests(unittest.TestCase):
result = self.load_generators(f"ANNOTATION_MAX_HEIGHT_PX = {value}\n")
self.assertNotEqual(result.returncode, 0)
self.assertIn("ANNOTATION_MAX_HEIGHT_PX must be a positive integer", result.stderr)
def test_custom_pixel_limit_reaches_both_generators(self):
result = self.load_generators("ANNOTATION_MAX_PIXELS = 50000000\n", "MAX_PIXELS")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "50000000 50000000")
def test_older_configuration_keeps_default_pixel_limit(self):
result = self.load_generators("del ANNOTATION_MAX_PIXELS\n", "MAX_PIXELS")
self.assertEqual(result.returncode, 0, result.stderr)
self.assertEqual(result.stdout.strip(), "67108864 67108864")
def test_invalid_pixel_limits_are_rejected(self):
for value in ("0", "-1", "1.5", "True", "'67108864'"):
with self.subTest(value=value):
result = self.load_generators(f"ANNOTATION_MAX_PIXELS = {value}\n")
self.assertNotEqual(result.returncode, 0)
self.assertIn("ANNOTATION_MAX_PIXELS must be a positive integer", result.stderr)
+25 -1
View File
@@ -54,7 +54,7 @@ class CheckedDocumentTests(unittest.TestCase):
overwrite=overwrite, output_mode='checks')
def test_splits_on_labels_and_resets_geometry_at_each_part(self):
self.assertEqual(MAX_HEIGHT_PX, 32000)
self.assertEqual(MAX_HEIGHT_PX, 33000)
self.assertEqual(self.generate(), 'success')
parts = copy_documents(self.output)
self.assertEqual([part.name for part in parts], ['Part1', 'Part2'])
@@ -75,6 +75,30 @@ class CheckedDocumentTests(unittest.TestCase):
self.assertTrue((self.output / 'Concat.pdf').is_file())
self.assertFalse((self.output / 'Part1').exists())
def test_pixel_limit_splits_documents_at_exact_boundary(self):
with patch.object(checks, 'MAX_PIXELS', 6000):
self.assertEqual(self.generate(), 'success')
parts = copy_documents(self.output)
self.assertEqual([read_json(p / 'bnote.json')['height'] for p in parts], [60, 40, 60])
def test_pixel_limit_accounts_for_width_growth_and_preserves_previous_copy(self):
original = self.compose
def wider(base, label, *args, **kwargs):
image, header = original(base, label, *args, **kwargs)
if label == 'Ex 2':
image = image.resize((200, image.height))
return image, header
with patch.object(self, 'compose', side_effect=wider), patch.object(checks, 'MAX_PIXELS', 12000):
self.assertEqual(self.generate(), 'success')
parts = copy_documents(self.output)
self.assertEqual([read_json(p / 'bnote.json')['width'] for p in parts], [100, 200, 100])
sentinel = self.output / 'sentinel'
sentinel.write_text('previous')
with patch.object(checks, 'MAX_PIXELS', 5999):
with self.assertRaisesRegex(CliError, 'Copie01.*Ex 1'):
self.generate(overwrite=True)
self.assertEqual(sentinel.read_text(), 'previous')
def test_single_oversized_answer_and_save_failure_preserve_previous_copy(self):
self.generate()
sentinel = self.output / 'sentinel'
+34 -17
View File
@@ -2330,7 +2330,7 @@ class StandardCliTests(unittest.TestCase):
all(sum(item[2].height for item in batch) <= 100 for batch in batches)
)
def test_grouped_batching_never_splits_one_student(self) -> None:
def test_grouped_batching_splits_oversized_student_between_answers(self) -> None:
module = self.modules["annotating_by_label"]
image = Image.new("RGB", (10, 60), "white")
rendered = [
@@ -2340,26 +2340,43 @@ class StandardCliTests(unittest.TestCase):
]
with patch.object(module, "MAX_HEIGHT_PX", 100):
batches = module.split_batches(rendered)
self.assertEqual(batches, [rendered[:2], rendered[2:]])
self.assertEqual(batches, [[rendered[0]], [rendered[1]], [rendered[2]]])
def test_grouped_oversized_answer_is_saved_as_its_own_group(self) -> None:
def test_grouped_oversized_answer_is_rejected(self) -> None:
module = self.modules["annotating_by_label"]
rendered = [("02", "Ex 1", Image.new("RGB", (10, 120)), 0, [])]
with tempfile.TemporaryDirectory() as directory, patch.object(module, "MAX_HEIGHT_PX", 100):
with self.assertRaisesRegex(CliError, "Copie02.*Ex 1"):
module.split_batches(rendered)
with self.assertRaises(CliError):
module.save_batch(rendered, "Ex 1", 1, Path(directory))
self.assertFalse((Path(directory) / "Ex 1 G1").exists())
def test_grouped_pixels_include_padding_to_the_widest_answer(self) -> None:
module = self.modules["annotating_by_label"]
rendered = [
("01", "Ex 1", Image.new("RGB", (10, 40)), 0, []),
("02", "Ex 1", Image.new("RGB", (10, 120)), 0, []),
("03", "Ex 1", Image.new("RGB", (10, 50)), 0, []),
("01", "Ex 1", Image.new("RGB", (10, 60)), 0, []),
("02", "Ex 1", Image.new("RGB", (30, 20)), 0, []),
("03", "Ex 1", Image.new("RGB", (10, 40)), 0, []),
]
with tempfile.TemporaryDirectory() as directory, patch.object(module, "MAX_HEIGHT_PX", 100):
with patch.object(module, "MAX_HEIGHT_PX", 100), patch.object(module, "MAX_PIXELS", 2000):
batches = module.split_batches(rendered)
self.assertEqual(batches, [[rendered[0], rendered[2]], [rendered[1]]])
for index, batch in enumerate(batches, start=1):
module.save_batch(batch, "Ex 1", index, Path(directory))
metadata = read_json(Path(directory) / "Ex 1 G2/bnote.json")
self.assertEqual(metadata["height"], 120)
self.assertEqual([item["id"] for item in metadata["images"]], ["02"])
self.assertTrue((Path(directory) / "Ex 1 G2/Concat.pdf").is_file())
with self.assertRaises(CliError):
module.save_batch(rendered[:2], "Ex 1", 3, Path(directory))
with tempfile.TemporaryDirectory() as directory:
for index, batch in enumerate(batches):
module.save_batch(batch, "Ex 1", index, Path(directory))
with self.assertRaises(CliError):
module.save_batch(rendered[:2], "Ex 1", 3, Path(directory))
def test_grouped_pixel_limit_splits_student_and_rejects_single_answer(self) -> None:
module = self.modules["annotating_by_label"]
image = Image.new("RGB", (30, 40))
rendered = [("01", label, image, 0, []) for label in ("Ex 1", "Ex 2")]
with patch.object(module, "MAX_HEIGHT_PX", 100), patch.object(module, "MAX_PIXELS", 1200):
self.assertEqual(module.split_batches(rendered), [[rendered[0]], [rendered[1]]])
with patch.object(module, "MAX_PIXELS", 1199):
with self.assertRaisesRegex(CliError, "single answer cannot be split"):
module.split_batches(rendered)
def test_grouped_batching_reuses_space_in_earlier_groups(self) -> None:
module = self.modules["annotating_by_label"]
@@ -2378,9 +2395,9 @@ class StandardCliTests(unittest.TestCase):
[100, 100])
self.assertEqual(batches[0][-2:], rendered[-2:])
def test_grouped_annotation_height_limit_is_32000_pixels(self) -> None:
def test_grouped_annotation_height_limit_is_33000_pixels(self) -> None:
module = self.modules["annotating_by_label"]
self.assertEqual(module.MAX_HEIGHT_PX, 32000)
self.assertEqual(module.MAX_HEIGHT_PX, 33000)
class WorkflowTests(unittest.TestCase):