Files
Copies/tests/test_latex_json.py

98 lines
4.4 KiB
Python

import json
import unittest
from copienator.latex_json import (
normalize_literal_newlines,
repair_decoded_latex_controls,
repair_latex_json,
)
from copienator.commands.gemini_for_enonce import ExamQuestions, ExamSolutions, ExamContext, GroupRubrics
class LatexJsonTests(unittest.TestCase):
def test_literal_newlines_preserve_tex_commands_and_row_breaks(self):
content = r"Paragraph\n\n Next $\neq \notin \newline$ \\ next \\\n row"
expected = "Paragraph\n\n Next " + r"$\neq \notin \newline$ \\ next \\" + "\n row"
self.assertEqual(normalize_literal_newlines(content), expected)
self.assertEqual(normalize_literal_newlines(expected), expected)
raw = json.dumps({'solutions': [{'label': '1)', 'solution_content': content}]})
parsed = ExamSolutions.model_validate_json(repair_latex_json(raw))
self.assertEqual(parsed.solutions[0].solution_content, expected)
def test_mixed_correct_and_broken_escapes_in_all_extraction_fields(self):
cases = [
(ExamQuestions, 'questions', 'question_content', {'label': '1)'}),
(ExamSolutions, 'solutions', 'solution_content', {'label': '1)'}),
(ExamContext, 'contexts', 'context_content',
{'target_question_label': '1)', 'last_question_label': '2)'}),
(GroupRubrics, 'rubrics', 'rubric_content', {'label': '1)'})]
raw_content = r'$\circ \rightarrow \frac{1}{2} \not= \theta \begin{x} \unknown \underline{x}$\nSuite : $\\sqrt{x}$'
expected = r'$\circ \rightarrow \frac{1}{2} \not= \theta \begin{x} \unknown \underline{x}$' + '\n' + r'Suite : $\sqrt{x}$'
for model, collection, field, labels in cases:
with self.subTest(field=field):
raw = json.dumps({collection: [{**labels, field: 'PLACEHOLDER'}]}).replace('PLACEHOLDER', raw_content)
repaired = repair_latex_json(raw)
parsed = model.model_validate_json(repaired)
self.assertEqual(getattr(getattr(parsed, collection)[0], field), expected)
self.assertEqual(repair_latex_json(repaired), repaired)
def test_valid_json_is_unchanged(self):
content = 'Énoncé "cité"\nNouvelle ligne\ntexte\nnext\ni \ne \nu \tindent\r\n' + r'$\not= \frac{1}{2} \\ \begin{array}$'
raw = json.dumps({'question_content': content, 'label': r'\not'})
self.assertEqual(repair_latex_json(raw), raw)
self.assertEqual(repair_latex_json(r'{"rubric_content":"\u00e9 \/ \"ok\""}'),
r'{"rubric_content":"\u00e9 \/ \"ok\""}')
def test_only_content_values_are_repaired(self):
raw = r'{"label":"\not", "other":"\frac", "rubric_content":"\frac"}'
self.assertEqual(repair_latex_json(raw),
r'{"label":"\not", "other":"\frac", "rubric_content":"\\frac"}')
def test_feedback_text_can_be_repaired_before_json_decoding(self):
raw = (
r'{"text":"$\forall x, f \big(x\big) \neq x \to \text{false}$",'
r'"other":"\forall"}'
)
repaired = repair_latex_json(
raw,
additional_content_fields={"text"},
)
parsed = json.loads(repaired)
self.assertEqual(
parsed["text"],
r"$\forall x, f \big(x\big) \neq x \to \text{false}$",
)
self.assertEqual(parsed["other"], "\x0corall")
valid_prose = json.dumps({"text": "First line\ntext outside math"})
self.assertEqual(
repair_latex_json(
valid_prose,
additional_content_fields={"text"},
),
valid_prose,
)
def test_decoded_control_repair_is_limited_to_known_commands_in_math(self):
corrupted = (
"$"
+ "\x0corall x, f "
+ "\x08ig(x\x08ig) "
+ "\neq x "
+ "\to "
+ "\text{false}$"
)
self.assertEqual(
repair_decoded_latex_controls(corrupted),
r"$\forall x, f \big(x\big) \neq x \to \text{false}$",
)
ordinary_whitespace = "First paragraph\nnext line\tindented"
self.assertEqual(
repair_decoded_latex_controls(ordinary_whitespace),
ordinary_whitespace,
)
def test_does_not_repair_structural_errors(self):
with self.assertRaises(json.JSONDecodeError):
json.loads(repair_latex_json(r'{"question_content":"\frac", broken}'))