Files
Copies/tests/test_checked_documents.py

182 lines
9.2 KiB
Python

"""Large checked copies survive the tablet round trip as separate documents."""
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from PIL import Image, ImageDraw
from copienator import configuration
from copienator import CliError, EvaluationWorkspace, ExitCode, read_json
from copienator.annotation_data import AnnotationLoadResult
from copienator.annotation_documents import annotation_limits, copy_documents
from copienator.commands import annotating_with_checks as checks
from copienator.commands import export, import_annotations, reading_annotations as reader
from copienator.commands import reading_grouped_annotations as grouped_reader
class CheckedDocumentTests(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.workspace = EvaluationWorkspace(self.root / 'Exam')
for name in ('Copies', 'Par label'):
(self.workspace.root / name).mkdir(parents=True)
(self.workspace.root / 'labels').write_text('Ex 1\nEx 2\nEx 3\n')
(self.workspace.root / 'correction.json').write_text('{}')
self.heights = {'Ex 1': 60, 'Ex 2': 40, 'Ex 3': 60}
self.labels = {}
for label in self.heights:
pdf = self.workspace.root / 'Copies' / f'{label}.pdf'
pdf.touch()
self.labels[label] = {'pdf_path': pdf, 'result': {'score': 2, 'feedback': []},
'coordinates': (0, 0)}
self.output = self.workspace.root / 'Bnot' / 'Copie01'
def compose(self, base, label, result, coordinates, **kwargs):
image = Image.new('RGB', (100, self.heights[label]), 'white')
callback = kwargs.get('draw_callback')
if callback:
callback.__self__.checkboxes.append({
'type': 'score', 'label': label, 'value': 3,
'final_box': [10, 5, 30, 25],
})
ImageDraw.Draw(image).rectangle((10, 5, 30, 25), outline='black', width=2)
return image, 0
def generate(self, overwrite=False):
with (
patch.object(configuration, 'ANNOTATION_MAX_HEIGHT_PX', 100),
patch.object(checks.annotating, 'make_base_image', return_value=(None, 0, 0)),
patch.object(checks.annotating, 'compose_label_image', side_effect=self.compose),
):
return checks._render_student(self.workspace, '01', self.labels,
overwrite=overwrite, output_mode='checks')
def test_splits_on_labels_and_resets_geometry_at_each_part(self):
self.assertEqual(annotation_limits()[0], 33000)
self.assertEqual(self.generate(), 'success')
parts = copy_documents(self.output)
self.assertEqual([part.name for part in parts], ['Part1', 'Part2'])
self.assertEqual([read_json(p / 'bnote.json')['height'] for p in parts], [100, 60])
self.assertEqual([item['hmin'] for item in read_json(self.output / 'bnote.json')['images']],
[0, 60, 0])
self.assertEqual(read_json(parts[1] / 'checkboxes.json')[0]['global_box'], [10, 5, 30, 25])
self.assertTrue(checks._output_complete(self.output))
self.assertEqual(self.generate(), 'skipped')
(parts[1] / 'Reference.jpg').unlink()
self.assertFalse(checks._output_complete(self.output))
def test_overwrite_removes_stale_parts_and_keeps_small_copy_layout(self):
self.generate()
self.labels = {'Ex 1': self.labels['Ex 1']}
self.generate(overwrite=True)
self.assertEqual(copy_documents(self.output), [self.output])
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(configuration, 'ANNOTATION_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(configuration, 'ANNOTATION_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(configuration, 'ANNOTATION_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'
sentinel.write_text('previous')
self.heights['Ex 3'] = 101
with self.assertRaisesRegex(CliError, 'Copie01.*Ex 3'):
self.generate(overwrite=True)
self.assertEqual(sentinel.read_text(), 'previous')
self.heights['Ex 3'] = 60
original = checks._save_document
def fail_second(directory, *args):
if directory.name == 'Part2':
raise OSError('disk full')
original(directory, *args)
with patch.object(checks, '_save_document', side_effect=fail_second):
with self.assertRaisesRegex(OSError, 'disk full'):
self.generate(overwrite=True)
self.assertEqual(sentinel.read_text(), 'previous')
def returned_parts(self):
self.generate()
export_root = self.root / 'Export'
with patch.object(export.configuration, 'EXPORT_DIR', export_root):
self.assertEqual(export.run(self.workspace, annotation_dir='Bnot'), ExitCode.SUCCESS)
self.assertEqual(sorted(p.name for p in (export_root / 'Exam').iterdir()),
['Copie01 Part1.pdf', 'Copie01 Part2.pdf'])
import_root = self.root / 'Import'
import_root.mkdir()
for part in copy_documents(self.output):
with Image.open(part / 'Reference.jpg') as reference:
image = reference.convert('RGB')
draw = ImageDraw.Draw(image)
# Change Ex 3's score and add handwriting in its body.
if part.name == 'Part2':
draw.rectangle((14, 9, 26, 21), fill='black')
draw.rectangle((70, 40, 85, 50), fill='black')
image.save(import_root / f'Copie01 {part.name}.pdf', 'PDF', resolution=72)
self.assertEqual(import_annotations.sync_annotated(
self.workspace, annotation_dir_name='Bnot', import_dir=import_root), ExitCode.SUCCESS)
def test_export_import_and_reader_merge_parts_without_moving_notes(self):
self.returned_parts()
with (
patch.object(reader, 'load_annotation_data', return_value=AnnotationLoadResult({'01': self.labels}, [])),
patch.object(reader.annotating, 'make_base_image', return_value=(None, 0, 0)),
patch.object(reader.annotating, 'compose_label_image', side_effect=self.compose),
):
self.assertEqual(reader.run(self.workspace), ExitCode.SUCCESS)
self.assertEqual(read_json(self.output / 'score.json'), {'Ex 1': '2', 'Ex 2': '2', 'Ex 3': '3'})
with Image.open(self.output / 'Ex 3.jpg') as image:
self.assertLess(max(image.getpixel((77, 45))), 80)
with Image.open(self.output / 'Ex 1.jpg') as image:
self.assertGreater(min(image.getpixel((77, 45))), 230)
def test_missing_return_part_preserves_previous_regenerated_files(self):
self.returned_parts()
(self.output / 'Part2' / 'Concat_annotated.pdf').unlink()
(self.output / 'score.json').write_text('{"previous": true}')
with patch.object(reader, 'load_annotation_data', return_value=AnnotationLoadResult({'01': self.labels}, [])):
self.assertEqual(reader.run(self.workspace), ExitCode.PARTIAL)
self.assertEqual(read_json(self.output / 'score.json'), {'previous': True})
def test_redo_reader_accepts_split_copy_documents(self):
self.returned_parts()
# Redo uses the same document layout under BRnot.
self.output.parent.rename(self.workspace.root / 'BRnot')
actions, notes, incomplete = grouped_reader._scan_redo_annotations(
self.workspace.root / 'BRnot', {'01': set(self.labels)})
self.assertFalse(incomplete)
self.assertEqual(set(notes['01']), set(self.labels))
self.assertEqual(actions['01'][0]['label'], 'Ex 3')
(self.workspace.root / 'BRnot/Copie01/Part2/Concat_annotated.pdf').unlink()
_, _, incomplete = grouped_reader._scan_redo_annotations(
self.workspace.root / 'BRnot', {'01': set(self.labels)})
self.assertEqual(incomplete, {'01'})
if __name__ == '__main__':
unittest.main()