import contextlib import io import tempfile import unittest from pathlib import Path from unittest.mock import patch import pandas as pd from PIL import Image from copienator.commands import add_final_score from copienator import atomic_write_json, read_json class AddFinalScoreTests(unittest.TestCase): def test_statistics_average_all_score_files_and_round_up_without_float_drift(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) returns = root / 'A Rendre' atomic_write_json(returns / 'Alice/score.json', { 'Ex 1': '2.0', 'Ex 2': '0.1', 'Ex 3': '', 'Empty': '', 'Zero': 0, }) atomic_write_json(returns / 'Bob/score.json', { 'Ex 1': 2.01, 'Ex 2': '0.1', 'Ex 3': '3.5', 'Empty': None, }) output = root / 'output' output.mkdir() add_final_score.write_score_statistics(returns, output) self.assertEqual(read_json(output / 'stat.json'), { 'Ex 1': '2.1', 'Ex 2': '0.1', 'Ex 3': '3.5', 'Empty': '', 'Zero': '0.0', }) # Rebuilding uses current returns, without accumulating old averages. atomic_write_json(returns / 'Bob/score.json', {'Ex 1': 4}) add_final_score.write_score_statistics(returns, output) self.assertEqual(read_json(output / 'stat.json')['Ex 1'], '3.0') def test_merged_histogram_is_copied_only_when_selected(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) histogram = root / 'Staging/histogramme.pdf' histogram.parent.mkdir() histogram.write_bytes(b'histogram') merged = histogram.with_name('merged_histogramme.pdf') merged.write_bytes(b'merged') output = root / 'output' output.mkdir() with patch.object(add_final_score, 'HISTOGRAM_PATH', histogram): add_final_score.copy_histogram(output) self.assertFalse((output / merged.name).exists()) add_final_score.copy_histogram(output, copy_merged_histogramme=True) self.assertEqual((output / 'histogramme.pdf').read_bytes(), b'histogram') self.assertEqual((output / merged.name).read_bytes(), b'merged') def test_cli_forwards_merged_histogram_option(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) with patch.object(add_final_score, 'OUTPUT_DIR', root / 'output'), patch.object( add_final_score, 'process_images' ) as process: self.assertEqual(add_final_score.main([str(root), '--copy-merged-histogramme']), 0) process.assert_called_once_with(root, root / 'output' / root.name, copy_merged_histogramme=True) def test_creates_complete_student_directory(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) source = root / "A Rendre" / "Student (01)" answers = source / "answers" answers.mkdir(parents=True) Image.new("RGB", (300, 200), "white").save(source / "Student.jpg") (source / "Student.pdf").write_bytes(b"pdf") (source / "score.json").write_bytes(b'{"Ex 1": "4"}') (source / "info.json").write_bytes(b'{"Ex 1": {}}') (answers / "Ex 1.jpg").write_bytes(b"answer") output = root / "output" output.mkdir() (output / "Student.jpg").write_bytes(b"legacy jpeg") (output / "Student.pdf").write_bytes(b"legacy pdf") stale_answers = output / "Student" / "answers" stale_answers.mkdir(parents=True) (stale_answers / "001 - Ex 1.jpg").write_bytes(b"stale") scores = pd.DataFrame({0: ["Student"], 1: [12.39]}) histogram = root / "histogramme.pdf" histogram.write_bytes(b"histogram") with patch.object(add_final_score.pd, "read_excel", return_value=scores), \ patch.object(add_final_score, "HISTOGRAM_PATH", histogram), \ contextlib.redirect_stdout(io.StringIO()): add_final_score.process_images(root, output) student = output / "Student" self.assertEqual( {path.name for path in student.iterdir()}, {"Student.jpg", "Student.pdf", "score.json", "info.json", "answers"}, ) self.assertEqual((student / "Student.pdf").read_bytes(), b"pdf") self.assertEqual((student / "score.json").read_bytes(), b'{"Ex 1": "4"}') self.assertEqual((student / "info.json").read_bytes(), b'{"Ex 1": {}}') self.assertEqual( (student / "answers" / "Ex 1.jpg").read_bytes(), b"answer" ) self.assertFalse((output / "Student.jpg").exists()) self.assertFalse((output / "Student.pdf").exists()) self.assertFalse((student / "answers" / "001 - Ex 1.jpg").exists()) self.assertEqual((output / "histogramme.pdf").read_bytes(), b"histogram") self.assertEqual(read_json(output / "stat.json"), {"Ex 1": "4.0"}) def test_omits_answers_directory_when_it_was_not_generated(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) source = root / "A Rendre" / "Student (01)" source.mkdir(parents=True) Image.new("RGB", (300, 200), "white").save(source / "Student.jpg") (source / "Student.pdf").write_bytes(b"pdf") (source / "score.json").write_text("{}") (source / "info.json").write_text("{}") output = root / "output" scores = pd.DataFrame({0: ["Student"], 1: [10]}) with patch.object(add_final_score.pd, "read_excel", return_value=scores), \ contextlib.redirect_stdout(io.StringIO()): add_final_score.process_images(root, output) self.assertFalse((output / "Student" / "answers").exists()) def test_missing_histogram_warns_without_discarding_student_outputs(self): with tempfile.TemporaryDirectory() as temporary: root = Path(temporary) source = root / "A Rendre" / "Student (01)" source.mkdir(parents=True) Image.new("RGB", (300, 200), "white").save(source / "Student.jpg") scores = pd.DataFrame({0: ["Student"], 1: [10]}) output = root / "output" messages = io.StringIO() with patch.object( add_final_score.pd, "read_excel", return_value=scores ), patch.object( add_final_score, "HISTOGRAM_PATH", root / "missing.pdf" ), contextlib.redirect_stdout(messages): add_final_score.process_images(root, output) self.assertTrue((output / "Student" / "Student.jpg").is_file()) self.assertFalse((output / "histogramme.pdf").exists()) self.assertIn("Missing histogram", messages.getvalue()) if __name__ == "__main__": unittest.main()