better faulty encoding detection

This commit is contained in:
sebastien committed 2026-09-29 08:03:47 +02:00
1 parent ec8e79bb1a
commit 2a4c5f4f7c
12 files changed
+299 -25

No files matched your search

+17 -3
View File
@@ -99,15 +99,29 @@ class GuiConvenienceTests(unittest.TestCase):
self.assertEqual(self.app.state_store.step("statement_persp")["status"], expected)
self.assertEqual(self.app.state_store.step("statement_groups")["status"], "ready")
def test_compact_evaluation_input_and_open_folder_button(self):
def test_evaluation_buttons_are_ordered_and_open_folder_and_statement(self):
self.assertEqual(int(self.app.evaluation_entry.cget("width")), 42)
self.assertEqual(
int(self.app.open_evaluation_button.grid_info()["column"]),
int(self.app.browse_evaluation_button.grid_info()["column"]),
int(self.app.evaluation_entry.grid_info()["column"]) + 1,
)
self.assertEqual(
int(self.app.open_evaluation_button.grid_info()["column"]),
int(self.app.browse_evaluation_button.grid_info()["column"]) + 1,
)
self.assertEqual(
int(self.app.open_statement_button.grid_info()["column"]),
int(self.app.open_evaluation_button.grid_info()["column"]) + 1,
)
statement = self.evaluation / "enonce.pdf"
statement.touch()
with patch("copienator_gui.app.open_path") as opened:
self.app.open_evaluation_button.invoke()
opened.assert_called_once_with(self.evaluation)
self.app.open_statement_button.invoke()
self.assertEqual(
opened.call_args_list,
[unittest.mock.call(self.evaluation), unittest.mock.call(statement)],
)
def test_manual_resolution_panel_follows_command_target(self):
manual = self.evaluation / "manual_resolutions.txt"
+61
View File
@@ -21,6 +21,7 @@ from pypdf import PdfReader, PdfWriter
from copienator import (
CliError,
EvaluationWorkspace,
ExitCode,
WorkspaceNotFoundError,
WorkspaceValidationError,
atomic_update_json,
@@ -1058,6 +1059,26 @@ class StandardCliTests(unittest.TestCase):
previous_image, previous_json, previous_meta
)
def test_plotting_accepts_lowercase_and_uppercase_previous_shortcuts(self) -> None:
module = self.modules["plotting"]
root = Mock()
root.winfo_screenwidth.return_value = 1200
root.winfo_screenheight.return_value = 800
label = Mock()
with patch.object(module.tk, "Label", return_value=label), patch.object(
module.ImageViewer, "poll_queue"
):
viewer = module.ImageViewer(
root,
SimpleNamespace(root=Path("/evaluation")),
set(),
queue.Queue(),
)
root.bind.assert_any_call("p", viewer.on_previous)
root.bind.assert_any_call("P", viewer.on_previous)
def test_plotting_validates_plain_and_directional_labels(self) -> None:
module = self.modules["plotting"]
self.assertEqual(
@@ -1317,6 +1338,16 @@ class StandardCliTests(unittest.TestCase):
self.assertEqual(result["list"][0]["label"], "??Ex 10 : 1)a)")
self.assertIn("keeping them with a ?? prefix", output.getvalue())
def test_correction_response_preserves_ambiguous_latex_commands(self) -> None:
module = self.modules["correction"]
parsed = module.parse_correction_response(
r'[{"result":{"feedback":[{"text":"$\forall x, f \big(x\big) \to x$"}]}}]'
)
self.assertEqual(
parsed[0]["result"]["feedback"][0]["text"],
r"$\forall x, f \big(x\big) \to x$",
)
def test_correction_overwrite_keeps_previous_state_until_a_commit(self) -> None:
module = self.modules["correction"]
with tempfile.TemporaryDirectory() as directory:
@@ -1774,6 +1805,23 @@ class StandardCliTests(unittest.TestCase):
r"Formulas $\frac{1}{2}$, $\theta \equiv 0 [2\pi]$, "
r"$e \times x$, and [\negthinspace[0,n]",
)
decoded_math_commands = (
"$"
+ "\x0corall x, f "
+ "\x08ig(x\x08ig) "
+ "\to "
+ "\text{false}$"
)
self.assertEqual(
module.clean_string(decoded_math_commands, {}),
r"$\forall x, f \big(x\big) \to \text{false}$",
)
self.assertEqual(
module.unresolved_math_control_paths(
{"feedback": ["Plain\fprose", "$unknown\fform$"]}
),
["$['feedback'][1]"],
)
self.assertEqual(
module.clean_string("x ∈ A and B ⊂ C", {}),
r"x \ensuremath{\in} A and B \ensuremath{\subset} C",
@@ -1800,6 +1848,19 @@ class StandardCliTests(unittest.TestCase):
self.assertEqual((evaluation / "correction.json").read_bytes(), cleaned)
self.assertEqual(backup.read_bytes(), original)
with tempfile.TemporaryDirectory() as directory:
evaluation = Path(directory) / "Exam"
evaluation.mkdir()
atomic_write_json(
evaluation / "correction.json",
{"text": "$unknown\fcommand$"},
)
output = io.StringIO()
with redirect_stdout(output):
result = module.run(EvaluationWorkspace(evaluation))
self.assertEqual(result, ExitCode.PARTIAL)
self.assertIn("$['text']", output.getvalue())
def test_manual_resolution_with_no_actions_is_safe(self) -> None:
module = self.modules["resolve_manual"]
with tempfile.TemporaryDirectory() as directory:
+2
View File
@@ -104,6 +104,8 @@ class LabelTimeoutTests(unittest.TestCase):
patch.object(labels, "wait", side_effect=wait), redirect_stdout(output):
result = labels.run(self.workspace, [], client=self.client, sleep=lambda _: None)
self.assertEqual(result, ExitCode.SUCCESS)
self.assertIn("Label detection progress: 0/1 copy groups completed.", output.getvalue())
self.assertIn("Label detection progress: 1/1 copy groups completed.", output.getvalue())
self.assertIn("1 copies outstanding", output.getvalue())
self.assertIn("Copie01_01.jpg, attempt 1/3", output.getvalue())
self.assertIn("elapsed for image", output.getvalue())
+49 -1
View File
@@ -1,7 +1,11 @@
import json
import unittest
from copienator.latex_json import repair_latex_json, normalize_literal_newlines
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
@@ -44,6 +48,50 @@ class LatexJsonTests(unittest.TestCase):
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}'))
+43 -3
View File
@@ -1,8 +1,16 @@
import subprocess
import os
import tkinter as tk
import unittest
from unittest.mock import patch
from copienator_gui.syncthing import SERVICE, control_service
from copienator_gui.syncthing import (
SERVICE,
SYSTEMCTL,
SyncthingPanel,
control_service,
service_status,
)
class SyncthingTests(unittest.TestCase):
@@ -16,7 +24,7 @@ class SyncthingTests(unittest.TestCase):
with patch("copienator_gui.syncthing.service_status", side_effect=["inactive", "active"]), \
patch("copienator_gui.syncthing.subprocess.run", return_value=subprocess.CompletedProcess([], 0)) as run:
self.assertEqual(control_service("ensure"), "active")
self.assertEqual(run.call_args.args[0], ["systemctl", "--no-ask-password", "start", SERVICE])
self.assertEqual(run.call_args.args[0], [*SYSTEMCTL, "start", SERVICE])
def test_failed_start_reports_error(self):
with patch("copienator_gui.syncthing.service_status", return_value="inactive"), \
@@ -28,4 +36,36 @@ class SyncthingTests(unittest.TestCase):
with patch("copienator_gui.syncthing.service_status", return_value="inactive"), \
patch("copienator_gui.syncthing.subprocess.run", return_value=subprocess.CompletedProcess([], 0)) as run:
self.assertEqual(control_service("stop"), "inactive")
self.assertEqual(run.call_args.args[0], ["systemctl", "--no-ask-password", "stop", SERVICE])
self.assertEqual(run.call_args.args[0], [*SYSTEMCTL, "stop", SERVICE])
def test_status_uses_the_non_privileged_user_service(self):
with patch(
"copienator_gui.syncthing.subprocess.run",
return_value=subprocess.CompletedProcess([], 0, stdout="active\n"),
) as run:
self.assertEqual(service_status(), "active")
self.assertEqual(
run.call_args.args[0],
[*SYSTEMCTL, "show", SERVICE, "--property=ActiveState", "--value"],
)
@unittest.skipUnless(os.environ.get("DISPLAY"), "Tk requires a display")
class SyncthingPanelTests(unittest.TestCase):
def test_failed_automatic_start_warns_and_continues_export(self):
root = tk.Tk()
self.addCleanup(root.destroy)
with patch("copienator_gui.syncthing.threading.Thread.start"):
panel = SyncthingPanel(root)
panel.pack()
continued = []
panel.pending_export = lambda: continued.append(True)
panel.busy = True
panel.events.put(("ensure", None, "Access denied"))
with patch("copienator_gui.syncthing.messagebox.showwarning") as warning:
panel.poll()
warning.assert_called_once()
self.assertIn("L’export va continuer", warning.call_args.args[1])
self.assertEqual(continued, [True])