Bnot fixes ; Missing énoncés fixes.
This commit is contained in:
1 parent
f39868207d
commit
55d5cf16c9
18 files changed
+486
-44
No files matched your search
@@ -0,0 +1,150 @@
|
||||
import io
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from copienator import EvaluationWorkspace, ExitCode
|
||||
from copienator import prompting
|
||||
from copienator.commands import correction, enonce_info, gemini_for_enonce
|
||||
from copienator.utils import get_exam_file_content, warn_missing_exam_material
|
||||
|
||||
|
||||
class ExamMaterialTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
temporary = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(temporary.cleanup)
|
||||
self.root = Path(temporary.name)
|
||||
|
||||
def write(self, mode, label, text):
|
||||
path = self.root / mode / label
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text)
|
||||
return path
|
||||
|
||||
def test_prompt_uses_section_statement_and_question_solution_for_nested_label(self):
|
||||
self.write('Text', 'Ex 3', 'Entire exercise')
|
||||
self.write('Text', 'Ex 3 : I)', 'Definitions and all questions in part I')
|
||||
self.write('Sol', 'Ex 3', 'Entire solution')
|
||||
self.write('Sol', 'Ex 3 : I)3)', 'Solution to 3)a) and 3)b)')
|
||||
self.write('Persp', 'Ex 3', 'Exercise marking guidance')
|
||||
for label in ('Ex 3 : I)3)a)', 'Ex 3 : I)3)b)'):
|
||||
prompt = prompting.make_prompt(self.root, label)
|
||||
self.assertIn('Definitions and all questions in part I', prompt)
|
||||
self.assertIn('Solution to 3)a) and 3)b)', prompt)
|
||||
self.assertIn('Exercise marking guidance', prompt)
|
||||
self.assertNotIn('Entire exercise', prompt)
|
||||
self.assertNotIn('Entire solution', prompt)
|
||||
|
||||
def test_exact_then_grouped_then_most_specific_parent_precedence(self):
|
||||
label = 'Ex 3 : I)3)b)'
|
||||
self.write('Sol', 'Ex 3', 'exercise')
|
||||
self.write('Sol', 'Ex 3 : I)', 'section')
|
||||
self.write('Sol', 'Ex 3 : I)3)', 'question')
|
||||
group = self.write('Sol', 'Ex 3 : I)3)[a),b)]', 'grouped answers')
|
||||
self.assertEqual(get_exam_file_content(self.root, 'Sol', label), 'grouped answers')
|
||||
exact = self.write('Sol', label, 'exact answer')
|
||||
self.assertEqual(get_exam_file_content(self.root, 'Sol', label), 'exact answer')
|
||||
exact.unlink()
|
||||
group.unlink()
|
||||
self.assertEqual(get_exam_file_content(self.root, 'Sol', label), 'question')
|
||||
|
||||
def test_grouped_parent_can_cover_deeper_questions(self):
|
||||
self.write('Text', 'Ex 3 : [I),II)]', 'two sections')
|
||||
self.assertEqual(get_exam_file_content(self.root, 'Text', 'Ex 3 : I)3)b)'), 'two sections')
|
||||
self.assertIsNone(get_exam_file_content(self.root, 'Text', 'Ex 3 : III)1)'))
|
||||
|
||||
def test_parent_lookup_respects_boundaries_and_ignores_rendered_files(self):
|
||||
self.write('Text', 'Ex 1', 'wrong exercise')
|
||||
self.write('Text', 'Ex 10 : I)1)', 'wrong question')
|
||||
self.write('Text', 'Ex 10 : I)10)a).tex', 'rendered latex')
|
||||
self.write('Text', 'Ex 10 : I)10)a).pdf', 'rendered pdf')
|
||||
self.assertIsNone(get_exam_file_content(self.root, 'Text', 'Ex 10 : I)10)a)'))
|
||||
self.write('Text', 'Ex 10 : I)', 'right section')
|
||||
self.assertEqual(get_exam_file_content(self.root, 'Text', 'Ex 10 : I)10)a)'), 'right section')
|
||||
|
||||
def test_slash_sanitization_and_dot_hierarchies(self):
|
||||
self.write('Text', 'Sheet_A', 'sheet context')
|
||||
self.assertEqual(get_exam_file_content(self.root, 'Text', 'Sheet/A : 1)a)'), 'sheet context')
|
||||
self.write('Sol', '1.a', 'solution')
|
||||
self.assertEqual(get_exam_file_content(self.root, 'Sol', '1.a.i'), 'solution')
|
||||
self.assertIsNone(get_exam_file_content(self.root, 'Sol', '1.ab.i'))
|
||||
|
||||
def test_missing_material_does_not_block_prompt_or_regrading_request(self):
|
||||
for mode in ('Text', 'Sol'):
|
||||
self.write(mode, 'Ex 1', ' \n ')
|
||||
self.assertIsInstance(prompting.make_prompt(self.root, 'Ex 1 : 1)'), str)
|
||||
with patch.object(prompting, 'get_single_image_bytes', return_value=b'answer') as render:
|
||||
prompting.request_for_confirmed_label(self.root, 'answer.pdf', 'Ex 1 : 1)', '01')
|
||||
render.assert_called_once()
|
||||
|
||||
def test_coverage_warns_for_every_missing_label_and_accepts_parent_material(self):
|
||||
self.write('', 'labels', 'Ex 1 : 1)\nEx 2 : 1)\nEx 3 : 1)\n')
|
||||
self.write('Text', 'Ex 1', 'all questions')
|
||||
self.write('Sol', 'Ex 1', 'all solutions')
|
||||
self.write('Text', 'Ex 2', 'questions')
|
||||
self.write('Sol', 'Ex 2 : 1)', ' \n ')
|
||||
output = io.StringIO()
|
||||
with patch('sys.stdout', output):
|
||||
warn_missing_exam_material(EvaluationWorkspace(self.root))
|
||||
report = output.getvalue()
|
||||
self.assertIn('1/3 labels', report)
|
||||
self.assertIn('Ex 2 : 1) : corrigé absent ou vide', report)
|
||||
self.assertIn('Ex 3 : 1) : énoncé absent ou vide', report)
|
||||
self.assertIn('Vous pouvez continuer', report)
|
||||
|
||||
def test_correction_can_continue_when_material_is_missing(self):
|
||||
workspace = EvaluationWorkspace(self.root)
|
||||
(self.root / 'Copies').mkdir()
|
||||
(self.root / 'Par label').mkdir()
|
||||
(self.root / 'labels').write_text('Ex 1 : 1)\n')
|
||||
args = correction.build_parser().parse_args([str(self.root)])
|
||||
with (
|
||||
patch.object(correction, 'discover_tasks', return_value=([('group.jpg', 'Ex 1 : 1)')], [])),
|
||||
patch.object(correction, 'configure_runtime') as configure,
|
||||
patch.object(correction, 'run_configured', return_value=ExitCode.SUCCESS) as run,
|
||||
):
|
||||
self.assertEqual(correction.run(workspace, [self.root], args, api_client=Mock()), ExitCode.SUCCESS)
|
||||
configure.assert_called_once()
|
||||
run.assert_called_once()
|
||||
|
||||
def test_personal_statement_checks_material_without_changing_success(self):
|
||||
self.write('', 'enonce.tex', '%%SHEETINFO : {"id": 42}\n')
|
||||
output = io.StringIO()
|
||||
def fetch(url):
|
||||
return io.BytesIO(b'Question### ###Rubric' if '/emacs/' in url else b'fragment')
|
||||
with (
|
||||
patch.object(enonce_info.urllib.request, 'urlopen', side_effect=fetch),
|
||||
patch.object(enonce_info, 'compile_to_pdf'),
|
||||
patch('sys.stdout', output),
|
||||
):
|
||||
status = enonce_info.process_directory(EvaluationWorkspace(self.root))
|
||||
self.assertEqual(status, ExitCode.SUCCESS)
|
||||
self.assertIn('Ex 1 : corrigé absent ou vide', output.getvalue())
|
||||
self.assertIn('0/1 labels', output.getvalue())
|
||||
|
||||
def test_gemini_statement_checks_material_without_changing_success(self):
|
||||
extraction = gemini_for_enonce.GroupedExamExtraction(groups=[[
|
||||
gemini_for_enonce.QuestionItem(label='Ex 1', question_content='Question', solution_content=''),
|
||||
]])
|
||||
output = io.StringIO()
|
||||
with (
|
||||
patch.object(gemini_for_enonce, 'generate_rubrics', return_value={'Ex 1': 'Criteria'}),
|
||||
patch.object(gemini_for_enonce, 'compile_existing_pdfs', return_value=ExitCode.SUCCESS),
|
||||
patch('sys.stdout', output),
|
||||
):
|
||||
status = gemini_for_enonce.write_exam_outputs(EvaluationWorkspace(self.root), extraction, Mock())
|
||||
self.assertEqual(status, ExitCode.SUCCESS)
|
||||
self.assertIn('Ex 1 : corrigé absent ou vide', output.getvalue())
|
||||
self.assertIn('0/1 labels', output.getvalue())
|
||||
|
||||
def test_compile_only_rechecks_existing_material(self):
|
||||
self.write('', 'labels', 'Ex 1\n')
|
||||
output = io.StringIO()
|
||||
with (
|
||||
patch.object(gemini_for_enonce, 'compile_existing_pdfs', return_value=ExitCode.SUCCESS),
|
||||
patch('sys.stdout', output),
|
||||
):
|
||||
status = gemini_for_enonce.main([str(self.root), '--compile-only'])
|
||||
self.assertEqual(status, ExitCode.SUCCESS)
|
||||
self.assertIn('Ex 1 : énoncé absent ou vide', output.getvalue())
|
||||
Reference in new issue
Block a user