import tempfile import unittest from pathlib import Path from types import SimpleNamespace from unittest.mock import patch from PIL import Image from reportlab.pdfgen import canvas from copienator.commands.reading_grouped_annotations import ( context_content_key, get_extra_pdfs_as_images, save_paginated_pdf, ) class PdfSupplementTests(unittest.TestCase): def setUp(self): temporary = tempfile.TemporaryDirectory() self.addCleanup(temporary.cleanup) self.root = Path(temporary.name) (self.root / "Text2").mkdir() (self.root / "Sol2").mkdir() self.labels = ["Q1", "Q2", "Q3"] self.rendered = [] def render(path): self.rendered.append(Path(path).name) return Image.new("RGB", (100, 30), "white"), 30, 100 self.renderer = SimpleNamespace(make_base_image=render) def pdf(self, folder, name, text): path = self.root / folder / name document = canvas.Canvas(str(path)) document.drawString(20, 800, text) document.save() return path def extras(self, label, seen): return get_extra_pdfs_as_images( self.root, label, self.renderer, self.labels, seen_contexts=seen, ) def test_content_deduplication_across_ranges_and_copy_reset(self): self.pdf("Text2", "CTXT Q1 -> Q2.pdf", "Bob collects coupons: N >= 1") self.pdf("Text2", "CTXT Q2 -> Q3.pdf", "Bob collects coupons: N >= 1") self.pdf("Text2", "CTXT Q2 -> Q2.pdf", "Bob collects coupons: N >= 2") seen = set() self.assertEqual(len(self.extras("Q1", seen)), 1) self.assertEqual(len(self.extras("Q2", seen)), 1) self.assertEqual(len(self.extras("Q3", seen)), 0) self.assertEqual(len(seen), 2) self.assertEqual(len(self.extras("Q3", set())), 1) def test_first_retained_question_gets_context_and_semantic_borders(self): self.pdf("Text2", "CTXT Q1 -> Q3.pdf", "Context") self.pdf("Text2", "CTXT Q2 -> Q3.pdf", "Other context") self.pdf("Text2", "Q2.pdf", "Question") self.pdf("Sol2", "Q2.pdf", "Solution") # Q1 was filtered out; it never consumes the shared context. images = self.extras("Q2", set()) self.assertEqual([i.getpixel((0, 0)) for i in images], [(255, 255, 255), (255, 255, 255), (0, 0, 0), (0, 0, 255)]) self.assertTrue(all(i.getpixel((50, 15)) == (255, 255, 255) for i in images)) def test_missing_question_does_not_shift_solution_border(self): self.pdf("Text2", "CTXT Q1 -> Q3.pdf", "Context") self.pdf("Sol2", "Q2.pdf", "Solution") images = self.extras("Q2", set()) self.assertEqual([i.getpixel((0, 0)) for i in images], [(255, 255, 255), (0, 0, 255)]) def test_unrendered_context_is_not_consumed(self): self.pdf("Text2", "CTXT Q1 -> Q3.pdf", "Context") seen = set() with patch.object(self.renderer, "make_base_image", return_value=(None, 0, 0)): self.assertEqual(self.extras("Q1", seen), []) self.assertFalse(seen) self.assertEqual(len(self.extras("Q2", seen)), 1) def test_missing_solution_does_not_shift_border_to_student_answer(self): self.pdf("Text2", "Q2.pdf", "Question") images = self.extras("Q2", set()) answer = Image.new("RGB", (100, 200), "white") captured = [] def save(page, *args, **kwargs): captured.extend([page.copy(), *kwargs['append_images']]) with patch.object(Image.Image, "save", save): save_paginated_pdf([images + [answer], [answer]], self.root / "out.pdf") self.assertEqual(len(captured), 2) self.assertEqual(captured[0].getpixel((11, 7)), (0, 0, 0)) self.assertEqual(captured[0].getpixel((11, 37)), (255, 255, 255)) self.assertEqual(captured[1].getpixel((11, 7)), (255, 255, 255)) self.assertEqual(answer.getpixel((0, 0)), (255, 255, 255)) def test_empty_extracted_text_does_not_merge_distinct_contexts(self): paths = [] for name, color in [("a.pdf", "red"), ("b.pdf", "blue")]: path = self.root / name document = canvas.Canvas(str(path)) document.setFillColor(color) document.rect(20, 20, 100, 100, fill=1) document.save() paths.append(path) self.assertNotEqual(context_content_key(paths[0]), context_content_key(paths[1])) if __name__ == "__main__": unittest.main()