533 lines
27 KiB
Python
533 lines
27 KiB
Python
import contextlib
|
|
import io
|
|
import itertools
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from PIL import Image
|
|
|
|
from copienator import EvaluationWorkspace, ExitCode, atomic_write_json, configuration, read_json
|
|
from copienator.annotation_data import AnnotationLoadResult
|
|
from copienator.commands import annotating, giving_names
|
|
from copienator.commands import reading_annotations as standard
|
|
from copienator.commands import reading_grouped_annotations as reader
|
|
from copienator.return_answers import (
|
|
publish_answer_returns,
|
|
save_return_answer_options,
|
|
)
|
|
|
|
|
|
class AnswerReturnTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp = tempfile.TemporaryDirectory()
|
|
self.addCleanup(self.temp.cleanup)
|
|
self.root = Path(self.temp.name)
|
|
self.source = self.root / "BGnot" / "Copie01"
|
|
self.source.mkdir(parents=True)
|
|
self.destination = self.root / "A Rendre" / "Student (01)"
|
|
self.destination.mkdir(parents=True)
|
|
(self.root / "labels").write_text("Ex 1\nEx 2\nEmpty\n")
|
|
atomic_write_json(self.source / "score.json", {"Ex 1": "4", "Ex 2": "2", "Empty": "0"})
|
|
atomic_write_json(self.source / "info.json", {
|
|
"Ex 1": {"present": True, "not_empty": True, "touched": False, "score": "4"},
|
|
"Ex 2": {"present": True, "not_empty": True, "touched": True, "score": "2"},
|
|
"Empty": {"present": True, "not_empty": False, "touched": False, "score": "0"},
|
|
})
|
|
for label in ("Ex 1", "Ex 2", "Empty"):
|
|
Image.new("RGB", (100, 30), "red").save(self.source / f"{label}.jpg")
|
|
for folder in ("Text2", "Sol2"):
|
|
(self.root / folder).mkdir()
|
|
for label in ("Ex 1", "Ex 2"):
|
|
(self.root / "Text2" / f"{label}.pdf").touch()
|
|
(self.root / "Sol2" / f"{label}.pdf").touch()
|
|
(self.root / "Text2" / "CTXT Ex 1 -> Ex 2.pdf").touch()
|
|
|
|
@staticmethod
|
|
def supplement(path):
|
|
path = Path(path)
|
|
color = "blue" if path.name.startswith("CTXT") else "green" if path.parent.name == "Text2" else "yellow"
|
|
return Image.new("RGB", (100, 20), color), 0, 0
|
|
|
|
def test_all_supplement_combinations_always_include_annotated_answer(self):
|
|
for context, question, solution in itertools.product((False, True), repeat=3):
|
|
with self.subTest(context=context, question=question, solution=solution), patch.multiple(
|
|
configuration, RETURN_ANSWERS_ENABLED=True,
|
|
RETURN_ANSWERS_CONTEXT=context, RETURN_ANSWERS_QUESTION=question,
|
|
RETURN_ANSWERS_SOLUTION=solution,
|
|
), patch.object(annotating, "make_base_image", side_effect=self.supplement):
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
files = sorted((self.destination / "answers").glob("*.jpg"))
|
|
self.assertEqual([p.name for p in files], ["Empty.jpg", "Ex 1.jpg", "Ex 2.jpg"])
|
|
with Image.open(self.destination / "answers" / "Ex 1.jpg") as image:
|
|
self.assertEqual(image.size, (100, 30 + 20 * sum((context, question, solution))))
|
|
colors = []
|
|
if context:
|
|
colors.append((0, 0, 255))
|
|
if question:
|
|
colors.append((0, 128, 0))
|
|
if solution:
|
|
colors.append((255, 255, 0))
|
|
colors.append((255, 0, 0))
|
|
for index, color in enumerate(colors):
|
|
pixel = image.getpixel((50, index * 20 + 10))
|
|
self.assertTrue(all(abs(a - b) < 10 for a, b in zip(pixel, color)))
|
|
self.assertEqual(read_json(self.destination / "info.json"), read_json(self.source / "info.json"))
|
|
|
|
def test_saved_reading_options_override_configuration_for_answers(self):
|
|
save_return_answer_options(
|
|
self.root,
|
|
context=True,
|
|
question=False,
|
|
solution=True,
|
|
)
|
|
with patch.multiple(
|
|
configuration,
|
|
RETURN_ANSWERS_ENABLED=True,
|
|
RETURN_ANSWERS_CONTEXT=False,
|
|
RETURN_ANSWERS_QUESTION=True,
|
|
RETURN_ANSWERS_SOLUTION=False,
|
|
), patch.object(
|
|
annotating, "make_base_image", side_effect=self.supplement
|
|
):
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
|
|
with Image.open(self.destination / "answers" / "Ex 1.jpg") as image:
|
|
self.assertEqual(image.size, (100, 70))
|
|
for y, color in (
|
|
(10, (0, 0, 255)),
|
|
(30, (255, 255, 0)),
|
|
(50, (255, 0, 0)),
|
|
):
|
|
self.assertTrue(
|
|
all(
|
|
abs(actual - expected) < 10
|
|
for actual, expected in zip(image.getpixel((50, y)), color)
|
|
)
|
|
)
|
|
|
|
def test_missing_supplement_is_optional_and_failure_preserves_old_answers(self):
|
|
with patch.multiple(configuration, RETURN_ANSWERS_ENABLED=True,
|
|
RETURN_ANSWERS_CONTEXT=False, RETURN_ANSWERS_QUESTION=True,
|
|
RETURN_ANSWERS_SOLUTION=False), patch.object(
|
|
annotating, "make_base_image", side_effect=self.supplement
|
|
):
|
|
(self.root / "Text2" / "Ex 1.pdf").unlink()
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
path = self.destination / "answers" / "Ex 1.jpg"
|
|
with Image.open(path) as image:
|
|
self.assertEqual(image.size, (100, 30))
|
|
original = path.read_bytes()
|
|
(self.source / "Ex 2.jpg").unlink()
|
|
with self.assertRaises(FileNotFoundError):
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
self.assertEqual(path.read_bytes(), original)
|
|
self.assertTrue((self.destination / "answers" / "Ex 2.jpg").exists())
|
|
|
|
def test_disabling_clears_individual_images_but_retains_info(self):
|
|
with patch.multiple(configuration, RETURN_ANSWERS_ENABLED=True,
|
|
RETURN_ANSWERS_CONTEXT=False, RETURN_ANSWERS_QUESTION=False,
|
|
RETURN_ANSWERS_SOLUTION=False):
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
with patch.object(configuration, "RETURN_ANSWERS_ENABLED", False):
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
self.assertEqual(list((self.destination / "answers").iterdir()), [])
|
|
self.assertTrue(read_json(self.destination / "info.json")["Ex 2"]["touched"])
|
|
|
|
def test_missing_info_requires_recompilation_instead_of_guessing(self):
|
|
(self.source / "info.json").unlink()
|
|
(self.source / "Concat_F.pdf").touch()
|
|
with self.assertRaisesRegex(ValueError, "recompile"):
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
|
|
def test_info_replaces_touched_and_tracks_manual_score_edits(self):
|
|
atomic_write_json(self.destination / "touched.json", {"obsolete": True})
|
|
atomic_write_json(self.source / "score.json", {"Ex 1": "3.5", "Ex 2": "2", "Empty": "0"})
|
|
with patch.object(configuration, "RETURN_ANSWERS_ENABLED", False):
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
self.assertFalse((self.destination / "touched.json").exists())
|
|
self.assertEqual(read_json(self.destination / "info.json")["Ex 1"], {
|
|
"present": True, "not_empty": True, "touched": False, "score": "3.5"
|
|
})
|
|
|
|
def test_invalid_info_preserves_previous_return(self):
|
|
atomic_write_json(self.destination / "info.json", {"previous": "keep"})
|
|
info = read_json(self.source / "info.json")
|
|
info["Empty"]["touched"] = True
|
|
atomic_write_json(self.source / "info.json", info)
|
|
with self.assertRaisesRegex(ValueError, "Invalid question information"):
|
|
publish_answer_returns(self.root, self.source, self.destination)
|
|
self.assertEqual(read_json(self.destination / "info.json"), {"previous": "keep"})
|
|
|
|
def test_publication_is_independent_of_full_jpeg_and_pdf_options(self):
|
|
workspace = EvaluationWorkspace(self.root)
|
|
workspace.copies_dir.mkdir()
|
|
atomic_write_json(workspace.copies_dir / "Copie01.json", {"name": "Student"})
|
|
(self.root / "names").write_text("Student\n")
|
|
with patch.multiple(configuration, RETURN_ANSWERS_ENABLED=True,
|
|
RETURN_ANSWERS_CONTEXT=False, RETURN_ANSWERS_QUESTION=False,
|
|
RETURN_ANSWERS_SOLUTION=False, RETURN_JPEG_ENABLED=False,
|
|
RETURN_PDF_ENABLED=False), contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(giving_names.run(workspace, annotation_dir="BGnot"), 0)
|
|
self.assertEqual({p.name for p in self.destination.iterdir()}, {"answers", "score.json", "info.json"})
|
|
|
|
def test_update_uses_copy_id_from_renamed_folder_and_touches_only_answers(self):
|
|
workspace = EvaluationWorkspace(self.root)
|
|
workspace.copies_dir.mkdir()
|
|
atomic_write_json(workspace.copies_dir / "Copie01.json", {"name": "Student"})
|
|
renamed = self.destination.with_name("Nom modifié manuellement (01)")
|
|
self.destination.rename(renamed)
|
|
(renamed / "Nom personnalisé.jpg").write_bytes(b"keep-jpeg")
|
|
(renamed / "Nom personnalisé.pdf").write_bytes(b"keep-pdf")
|
|
(renamed / "score.json").write_bytes(b"keep-score")
|
|
(renamed / "info.json").write_bytes(b"keep-info")
|
|
answers = renamed / "answers"
|
|
answers.mkdir()
|
|
(answers / "obsolete.jpg").write_bytes(b"obsolete")
|
|
preserved = {
|
|
path.name: path.read_bytes()
|
|
for path in renamed.iterdir()
|
|
if path.is_file()
|
|
}
|
|
|
|
with patch.multiple(
|
|
configuration,
|
|
RETURN_ANSWERS_ENABLED=True,
|
|
RETURN_ANSWERS_CONTEXT=False,
|
|
RETURN_ANSWERS_QUESTION=False,
|
|
RETURN_ANSWERS_SOLUTION=False,
|
|
), contextlib.redirect_stdout(io.StringIO()):
|
|
self.assertEqual(
|
|
giving_names.run(workspace, annotation_dir="BGnot", update=True),
|
|
0,
|
|
)
|
|
|
|
self.assertFalse((workspace.return_dir / "Student (01)").exists())
|
|
self.assertEqual(
|
|
{
|
|
path.name: path.read_bytes()
|
|
for path in renamed.iterdir()
|
|
if path.is_file()
|
|
},
|
|
preserved,
|
|
)
|
|
self.assertEqual(
|
|
sorted(path.name for path in answers.iterdir()),
|
|
["Empty.jpg", "Ex 1.jpg", "Ex 2.jpg"],
|
|
)
|
|
|
|
|
|
class RereadAnswerReturnTests(unittest.TestCase):
|
|
def check_reread(self, module, mode, *, existing_returns=True, fail_publication=False,
|
|
failed_copy=False):
|
|
temporary = tempfile.TemporaryDirectory()
|
|
self.addCleanup(temporary.cleanup)
|
|
root = Path(temporary.name)
|
|
workspace = EvaluationWorkspace(root)
|
|
for folder in ("Copies", "Par label", mode, "Text2"):
|
|
(root / folder).mkdir()
|
|
(root / "labels").write_text("Ex 1\n")
|
|
atomic_write_json(root / "correction.json", {})
|
|
question = root / "Text2" / "Ex 1.pdf"
|
|
question.touch()
|
|
data = {}
|
|
original_score = b'{\n "Ex 1": "3.5"\n}\n'
|
|
destinations = {}
|
|
for copy_id in ("01", "02"):
|
|
output = root / mode / f"Copie{copy_id}"
|
|
output.mkdir()
|
|
(output / "score.json").write_bytes(original_score)
|
|
atomic_write_json(output / "bnote.json", {"images": [{"label": "Ex 1"}]})
|
|
pdf = root / "Copies" / f"{copy_id}.pdf"
|
|
if copy_id == "01":
|
|
pdf.touch()
|
|
data[copy_id] = {"Ex 1": {
|
|
"result": {"score": 1, "feedback": []},
|
|
"pdf_path": pdf,
|
|
"coordinates": (0, 0),
|
|
}}
|
|
destination = workspace.return_dir / f"Renamed student ({copy_id})"
|
|
destinations[copy_id] = destination
|
|
if existing_returns:
|
|
(destination / "answers").mkdir(parents=True)
|
|
(destination / "answers" / "old.jpg").write_bytes(b"previous answers")
|
|
(destination / "custom.pdf").write_bytes(b"keep PDF")
|
|
(destination / "info.json").write_bytes(b"keep info")
|
|
|
|
if not failed_copy:
|
|
del data["02"]
|
|
|
|
def compose(base, label, result, *args, **kwargs):
|
|
# Make the rendered score observable in the published answer pixels.
|
|
color = "red" if str(result["score"]) == "3.5" else "black"
|
|
return Image.new("RGB", (100, 50), color), 10
|
|
|
|
def render(path):
|
|
if Path(path) == question and fail_publication:
|
|
raise ValueError("cannot render supplement")
|
|
return Image.new("RGB", (100, 20), "blue"), 0, 0
|
|
|
|
with (
|
|
patch.multiple(configuration, RETURN_ANSWERS_ENABLED=True),
|
|
patch.object(module, "load_annotation_data", return_value=AnnotationLoadResult(data, [])),
|
|
patch.object(standard, "detect_checks_and_notes", return_value=([], None)),
|
|
patch.object(annotating, "make_base_image", side_effect=render),
|
|
patch.object(annotating, "compose_label_image", side_effect=compose),
|
|
patch.object(reader, "get_extra_pdfs_as_images", return_value=[]),
|
|
patch.object(reader, "save_paginated_pdf"),
|
|
patch.object(reader, "save_checked_pdf"),
|
|
contextlib.redirect_stdout(io.StringIO()),
|
|
contextlib.redirect_stderr(io.StringIO()),
|
|
):
|
|
status = module.run(
|
|
workspace, update_score=True,
|
|
return_answers_context=False, return_answers_question=True,
|
|
return_answers_solution=False,
|
|
)
|
|
|
|
expected = ExitCode.PARTIAL if failed_copy or fail_publication else ExitCode.SUCCESS
|
|
self.assertEqual(status, expected)
|
|
self.assertEqual((root / mode / "Copie01" / "score.json").read_bytes(), original_score)
|
|
if not existing_returns:
|
|
self.assertFalse(workspace.return_dir.exists())
|
|
return
|
|
for destination in destinations.values():
|
|
self.assertEqual((destination / "custom.pdf").read_bytes(), b"keep PDF")
|
|
self.assertEqual((destination / "info.json").read_bytes(), b"keep info")
|
|
self.assertEqual((destinations["02"] / "answers" / "old.jpg").read_bytes(), b"previous answers")
|
|
answers = destinations["01"] / "answers"
|
|
if fail_publication:
|
|
self.assertEqual((answers / "old.jpg").read_bytes(), b"previous answers")
|
|
return
|
|
self.assertEqual([p.name for p in answers.iterdir()], ["Ex 1.jpg"])
|
|
with Image.open(answers / "Ex 1.jpg") as image:
|
|
self.assertEqual(image.size, (100, 70))
|
|
self.assertGreater(image.getpixel((50, 10))[2], 240)
|
|
self.assertGreater(image.getpixel((50, 45))[0], 240)
|
|
self.assertLess(image.getpixel((50, 45))[2], 10)
|
|
|
|
def test_reread_refreshes_answers_from_new_scores_and_supplements(self):
|
|
for module, mode in ((standard, "Bnot"), (reader, "BGnot")):
|
|
with self.subTest(mode=mode):
|
|
self.check_reread(module, mode)
|
|
|
|
def test_reread_does_not_create_named_returns(self):
|
|
for module, mode in ((standard, "Bnot"), (reader, "BGnot")):
|
|
with self.subTest(mode=mode):
|
|
self.check_reread(module, mode, existing_returns=False)
|
|
|
|
def test_failed_copy_does_not_republish_stale_annotations(self):
|
|
for module, mode in ((standard, "Bnot"), (reader, "BGnot")):
|
|
with self.subTest(mode=mode):
|
|
self.check_reread(module, mode, failed_copy=True)
|
|
|
|
def test_failed_publication_preserves_previous_answers(self):
|
|
for module, mode in ((standard, "Bnot"), (reader, "BGnot")):
|
|
with self.subTest(mode=mode):
|
|
self.check_reread(module, mode, fail_publication=True)
|
|
|
|
|
|
class CompiledMembershipTests(unittest.TestCase):
|
|
def test_update_score_preserves_file_and_manual_value_wins(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
workspace = EvaluationWorkspace(Path(directory))
|
|
output = workspace.root / "BGnot" / "Copie01"
|
|
output.mkdir(parents=True)
|
|
original_score = b'{\n "Ex 1": "3.5"\n}\n'
|
|
(output / "score.json").write_bytes(original_score)
|
|
answer = workspace.root / "answer.pdf"
|
|
answer.touch()
|
|
data = {
|
|
"01": {
|
|
"Ex 1": {
|
|
"result": {"score": 1, "feedback": []},
|
|
"pdf_path": answer,
|
|
"coordinates": (0, 0),
|
|
}
|
|
}
|
|
}
|
|
rendered_scores = []
|
|
|
|
def compose(base, label, result, *args, **kwargs):
|
|
rendered_scores.append(result["score"])
|
|
return Image.new("RGB", (100, 50), "white"), 0
|
|
|
|
with patch.object(
|
|
annotating, "make_base_image", return_value=(None, 0, 0)
|
|
), patch.object(
|
|
annotating, "compose_label_image", side_effect=compose
|
|
), patch.object(
|
|
reader, "get_extra_pdfs_as_images", return_value=[]
|
|
), patch.object(reader, "save_paginated_pdf"):
|
|
status, _ = reader.apply_actions_and_regenerate_grouped(
|
|
workspace,
|
|
data,
|
|
"01",
|
|
[{"label": "Ex 1", "type": "score", "value": "2"}],
|
|
{},
|
|
["Ex 1"],
|
|
update_score=True,
|
|
)
|
|
|
|
self.assertEqual(status, 0)
|
|
self.assertEqual(rendered_scores, ["3.5"])
|
|
self.assertEqual((output / "score.json").read_bytes(), original_score)
|
|
self.assertEqual(read_json(output / "info.json")["Ex 1"]["score"], "3.5")
|
|
|
|
def test_membership_matches_actual_groups_and_saves_every_nonempty_answer(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
workspace = EvaluationWorkspace(root)
|
|
output = root / "BGnot" / "Copie01"
|
|
output.mkdir(parents=True)
|
|
answer = root / "answer.pdf"
|
|
answer.touch()
|
|
results = {
|
|
"Perfect": {"score": 4, "feedback": []},
|
|
"Low": {"score": 2, "feedback": []},
|
|
"Feedback": {"score": 4, "feedback": [{"text": "keep"}]},
|
|
"Deleted": {"score": 4, "feedback": [{"text": "delete", "to_delete": True}]},
|
|
"Handwriting": {"score": 4, "feedback": []},
|
|
"Empty": {"score": 0, "error": "empty-answer"},
|
|
}
|
|
data = {"01": {label: {"result": result, "pdf_path": answer, "coordinates": (0, 0)}
|
|
for label, result in results.items()}}
|
|
all_labels = [*results, "Absent"]
|
|
rendered = []
|
|
|
|
def compose(base, label, *args, **kwargs):
|
|
rendered.append(label)
|
|
return Image.new("RGB", (100, 50), "white"), 0
|
|
|
|
notes = {"Handwriting": {"img": Image.new("RGBA", (100, 50), "red"), "old_header_h": 0}}
|
|
with patch.object(annotating, "make_base_image", return_value=(None, 0, 0)), patch.object(
|
|
annotating, "compose_label_image", side_effect=compose
|
|
), patch.object(reader, "get_extra_pdfs_as_images", return_value=[]), patch.object(
|
|
reader, "save_paginated_pdf"
|
|
) as save_pdf:
|
|
status, _ = reader.apply_actions_and_regenerate_grouped(workspace, data, "01", [], notes, all_labels)
|
|
self.assertEqual(status, 0)
|
|
info = read_json(output / "info.json")
|
|
touched = {label: entry["touched"] for label, entry in info.items()}
|
|
self.assertEqual({label for label, value in touched.items() if value}, {"Low", "Feedback", "Handwriting"})
|
|
self.assertEqual(len(save_pdf.call_args.args[0]), sum(touched.values()))
|
|
self.assertEqual({label for label, entry in info.items() if entry["present"] and entry["not_empty"]}, set(results) - {"Empty"})
|
|
self.assertEqual(info["Empty"], {"present": True, "not_empty": False, "touched": False, "score": "0"})
|
|
self.assertEqual(info["Absent"], {"present": False, "not_empty": False, "touched": False, "score": ""})
|
|
self.assertEqual(info["Perfect"], {"present": True, "not_empty": True, "touched": False, "score": "4"})
|
|
self.assertIn("Empty", rendered)
|
|
for label in rendered:
|
|
self.assertTrue((output / f"{label}.jpg").is_file())
|
|
# Selective redo keeps unselected saved answers in the PDF,
|
|
# including previously perfect answers; touched must follow it.
|
|
status, _ = reader.apply_actions_and_regenerate_grouped(
|
|
workspace, data, "01", [], {}, all_labels, selected_labels={"Low"}
|
|
)
|
|
self.assertEqual(status, 0)
|
|
self.assertTrue(read_json(output / "info.json")["Perfect"]["touched"])
|
|
self.assertFalse(read_json(output / "info.json")["Empty"]["not_empty"])
|
|
|
|
def test_real_empty_pdf_is_published_in_all_annotation_modes(self):
|
|
import fitz
|
|
from copienator.commands import reading_annotations as standard
|
|
for mode in ("Anot", "Bnot", "BGnot"):
|
|
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as directory:
|
|
root = Path(directory)
|
|
workspace = EvaluationWorkspace(root)
|
|
(root / "labels").write_text("Empty\nAbsent\n")
|
|
pdf = root / "answer.pdf"
|
|
with fitz.open() as document:
|
|
page = document.new_page(width=200, height=60)
|
|
page.insert_text((10, 30), "Copied question only")
|
|
document.save(pdf)
|
|
data = {"01": {"Empty": {"pdf_path": pdf, "coordinates": (0, 0),
|
|
"result": {"score": 0, "error": "empty-answer", "feedback": []}}}}
|
|
if mode == "Anot":
|
|
status = annotating.process_student("01", data["01"], root, ["Empty", "Absent"], True)
|
|
self.assertEqual(status, "success")
|
|
elif mode == "Bnot":
|
|
status = standard.apply_actions_and_regenerate(
|
|
workspace, data, "01", [], None, ["Empty", "Absent"])
|
|
self.assertEqual(status, 0)
|
|
else:
|
|
status, _ = reader.apply_actions_and_regenerate_grouped(
|
|
workspace, data, "01", [], {}, ["Empty", "Absent"])
|
|
self.assertEqual(status, 0)
|
|
output = root / mode / "Copie01"
|
|
destination = root / "return"
|
|
destination.mkdir()
|
|
with patch.multiple(configuration, RETURN_ANSWERS_ENABLED=True,
|
|
RETURN_ANSWERS_CONTEXT=False, RETURN_ANSWERS_QUESTION=False,
|
|
RETURN_ANSWERS_SOLUTION=False):
|
|
publish_answer_returns(root, output, destination)
|
|
self.assertEqual([p.name for p in (destination / "answers").iterdir()], ["Empty.jpg"])
|
|
with Image.open(destination / "answers" / "Empty.jpg") as image:
|
|
self.assertGreater(image.height, 0)
|
|
self.assertLess(image.convert("L").getextrema()[0], 100)
|
|
self.assertFalse(read_json(destination / "info.json")["Empty"]["not_empty"])
|
|
self.assertFalse((output / "Concat.jpg").exists())
|
|
self.assertFalse((output / "Concat_F.pdf").exists())
|
|
|
|
def test_bnot_recovers_empty_scans_missing_from_review_layout(self):
|
|
from copienator.commands import reading_annotations as standard
|
|
for only_empty in (False, True):
|
|
with self.subTest(only_empty=only_empty), tempfile.TemporaryDirectory() as directory:
|
|
workspace = EvaluationWorkspace(Path(directory))
|
|
output = workspace.root / "Bnot" / "Copie01"
|
|
output.mkdir(parents=True)
|
|
answer = workspace.root / "answer.pdf"
|
|
answer.touch()
|
|
labels = {"Empty": {"pdf_path": answer, "coordinates": (0, 0),
|
|
"result": {"score": 0, "error": "empty-answer"}}}
|
|
if not only_empty:
|
|
labels["Full"] = {"pdf_path": answer, "coordinates": (0, 0),
|
|
"result": {"score": 4}}
|
|
atomic_write_json(output / "bnote.json", {"images": [
|
|
{"label": "Full", "hmin": 0, "hmax": 50, "header_height": 0}]})
|
|
with patch.object(annotating, "make_base_image", return_value=(None, 0, 0)), patch.object(
|
|
annotating, "compose_label_image", return_value=(Image.new("RGB", (100, 50)), 0)
|
|
) as render:
|
|
status = standard.apply_actions_and_regenerate(
|
|
workspace, {"01": labels}, "01", [], None, [*labels, "Absent"])
|
|
self.assertEqual(status, 0)
|
|
self.assertTrue(all(call.kwargs["with_empty"] for call in render.call_args_list))
|
|
self.assertTrue((output / "Empty.jpg").is_file())
|
|
self.assertFalse((output / "Absent.jpg").exists())
|
|
self.assertFalse(read_json(output / "info.json")["Empty"]["not_empty"])
|
|
self.assertEqual((output / "Concat.jpg").exists(), not only_empty)
|
|
self.assertFalse((output / "Concat_F.jpg").exists())
|
|
|
|
def test_all_empty_removes_stale_concat_and_records_false(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
workspace = EvaluationWorkspace(Path(directory))
|
|
output = workspace.root / "BGnot" / "Copie01"
|
|
output.mkdir(parents=True)
|
|
for name in ("Concat.jpg", "Concat_F.pdf"):
|
|
(output / name).write_bytes(b"stale")
|
|
atomic_write_json(output / "touched.json", {"Empty": True})
|
|
atomic_write_json(output / "answer_labels.json", ["Empty"])
|
|
answer = workspace.root / "answer.pdf"
|
|
answer.touch()
|
|
with patch.object(annotating, "make_base_image", return_value=(Image.new("RGB", (100, 50)), 0, 0)), patch.object(
|
|
annotating, "compose_label_image", return_value=(Image.new("RGB", (100, 50)), 0)
|
|
):
|
|
status, _ = reader.apply_actions_and_regenerate_grouped(
|
|
workspace, {"01": {"Empty": {"result": {"score": 0, "error": "empty-answer"},
|
|
"pdf_path": answer, "coordinates": (0, 0)}}},
|
|
"01", [], {}, ["Empty"]
|
|
)
|
|
self.assertTrue((output / "Empty.jpg").is_file())
|
|
self.assertEqual(status, 0)
|
|
self.assertEqual(read_json(output / "info.json"), {
|
|
"Empty": {"present": True, "not_empty": False, "touched": False, "score": "0"}
|
|
})
|
|
self.assertFalse((output / "Concat.jpg").exists())
|
|
self.assertFalse((output / "Concat_F.pdf").exists())
|
|
self.assertFalse((output / "touched.json").exists())
|
|
self.assertFalse((output / "answer_labels.json").exists())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|