"""Evaluation files override shared settings without leaking across evaluations.""" import contextlib import os from pathlib import Path import subprocess import sys import tempfile import unittest from copienator import EvaluationWorkspace REPOSITORY = Path(__file__).resolve().parents[1] class LocalConfigurationTests(unittest.TestCase): def setUp(self): self.temporary = tempfile.TemporaryDirectory() self.addCleanup(self.temporary.cleanup) self.root = Path(self.temporary.name) self.evaluation = self.root / "Exam" self.evaluation.mkdir() (self.evaluation / "labels").write_text("Ex 1\n") self.global_config = self.root / "config.py" self.global_config.write_text("MODEL_FLASH_ID = 'global-model'\nRETURN_PDF_ENABLED = False\n") def run_python(self, source, *, explicit=True): env = dict(os.environ, PYTHONPATH=str(REPOSITORY)) env.pop("COPIENATOR_CONFIG", None) if explicit: env["COPIENATOR_CONFIG"] = str(self.global_config) result = subprocess.run([sys.executable, "-c", source], cwd=self.root, env=env, capture_output=True, text=True) self.assertEqual(result.returncode, 0, result.stdout + result.stderr) return result.stdout def test_partial_local_config_inherits_global_and_defaults(self): (self.evaluation / "config.py").write_text("MODEL_FLASH_ID = 'local-model'\n") for explicit in (False, True): with self.subTest(explicit=explicit): self.run_python(""" from copienator import configuration as c c.use_evaluation('Exam') assert c.MODEL_FLASH_ID == 'local-model' assert c.RETURN_PDF_ENABLED is False assert c.RETURN_JPEG_ENABLED is True assert c.CONFIG_PATH.name == 'config.py' assert c.LOCAL_CONFIG_PATH.parent.name == 'Exam' c.use_evaluation(None) assert c.MODEL_FLASH_ID == 'global-model' """, explicit=explicit) def test_switching_evaluations_removes_previous_overrides(self): (self.evaluation / "config.py").write_text("RETURN_JPEG_ENABLED = False\n") (self.root / "Other").mkdir() self.run_python(""" from copienator import configuration as c c.use_evaluation('Exam') assert c.RETURN_JPEG_ENABLED is False c.use_evaluation('Other') assert c.RETURN_JPEG_ENABLED is True assert c.RETURN_PDF_ENABLED is False assert c.LOCAL_CONFIG_PATH is None """) def test_command_loads_config_for_directory_and_nested_target(self): (self.evaluation / "config.py").write_text("MODEL_FLASH_ID = 'local-model'\n") self.run_python(""" from copienator import configuration as c from copienator.cli import execute, evaluation_parser, target_parser from copienator.commands import correction def check(args): assert correction.settings.MODEL_FLASH_ID == 'local-model' return 0 assert execute(evaluation_parser('test'), ['Exam'], check) == 0 c.use_evaluation(None) assert execute(target_parser('test'), ['Exam/labels'], check) == 0 """) def test_annotation_limits_and_gui_defaults_follow_active_config(self): (self.evaluation / "config.py").write_text( "ANNOTATION_MAX_HEIGHT_PX = 12345\nALWAYS_CROP = True\n") self.run_python(""" from copienator import configuration as c from copienator.commands import annotating_by_label, annotating_with_checks from copienator_gui.workflow import build_workflow c.use_evaluation('Exam') assert annotating_by_label.annotation_limits()[0] == 12345 assert annotating_with_checks.annotation_limits()[0] == 12345 steps = {step.id: step for step in build_workflow(False)} assert steps['crop_blank_margins'].auto_start_first_visit """) @unittest.skipUnless(os.environ.get("DISPLAY"), "GUI display unavailable") def test_gui_switches_evaluation_settings(self): (self.evaluation / "config.py").write_text("ALWAYS_CROP = True\n") (self.root / "Other").mkdir() self.run_python(""" from pathlib import Path from copienator import configuration as c from copienator_gui.app import CopienatorApp app = CopienatorApp(Path.cwd(), False, Path('Exam').resolve()) try: assert app.step_by_id['crop_blank_margins'].auto_start_first_visit app.evaluation_var.set(str(Path('Other').resolve())) app._load_evaluation() assert not app.step_by_id['crop_blank_margins'].auto_start_first_visit assert c.LOCAL_CONFIG_PATH is None finally: app.destroy() """) def test_explicit_command_option_overrides_local_config(self): (self.evaluation / "config.py").write_text("RETURN_ANSWERS_CONTEXT = True\n") self.run_python(""" from unittest.mock import patch from copienator.commands import reading_annotations as command from copienator import configuration as c def check(workspace, **options): assert c.RETURN_ANSWERS_CONTEXT is True assert options['return_answers_context'] is False return 0 with patch.object(command, 'run', check): assert command.main(['Exam', '--no-return-answers-context']) == 0 """) def test_invalid_local_config_reports_error_before_handler(self): (self.evaluation / "config.py").write_text("ANNOTATION_MAX_HEIGHT_PX = 0\n") self.run_python(""" from unittest.mock import Mock from copienator.cli import execute, evaluation_parser handler = Mock() assert execute(evaluation_parser('test'), ['Exam'], handler) == 1 handler.assert_not_called() """) def test_local_names_replace_global_including_an_empty_roster(self): (self.root / "names").write_text("Global\n") workspace = EvaluationWorkspace(self.evaluation, self.root) self.assertEqual(workspace.names_file(), self.root / "names") local = self.evaluation / "names" local.write_text("Local\n") self.assertEqual(workspace.names_file(), local) local.write_text("") self.assertEqual(workspace.names_file().read_text(), "") with contextlib.chdir(self.root): self.assertEqual(EvaluationWorkspace(self.evaluation).names_file(), local) local.unlink() self.assertEqual(EvaluationWorkspace(self.evaluation).names_file(), self.root / "names")