Files
Copies/tests/test_finalisation_web.py
T

552 lines
29 KiB
Python

from __future__ import annotations
from io import BytesIO, StringIO
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from unittest.mock import patch
from fastapi.testclient import TestClient
from PIL import Image
from copienator import CliError, EvaluationWorkspace, atomic_write_json, read_json
from copienator.annotation_modifications import APPLIED, DRAFT, stage_baseline
from copienator.commands import modify_annotations, giving_names
from copienator.evaluation_lock import EvaluationLock
from copienator.filesystem import staged_files
from copienator.finalisation import Finalisation
from copienator.finalisation_server import create_app
from copienator.return_identity import rename_return_copy
from copienator.score_editing import read_scores, set_score
def seed(root, mode='Bnot'):
(root / 'Copies' / 'Copie01').mkdir(parents=True, exist_ok=True)
(root / 'labels').write_text('Ex 1\nEx 2\nAbsent\n')
atomic_write_json(root / 'Copies' / 'Copie01.json', {'name': 'Unknown'})
scores = {'Ex 1': '3', 'Ex 2': '4', 'Absent': ''}
info = {label: {'present': label != 'Absent', 'not_empty': label != 'Absent', 'touched': False, 'score': score}
for label, score in scores.items()}
images = {'Ex 1': Image.new('RGB', (100, 120), 'blue'), 'Ex 2': Image.new('RGB', (100, 80), 'green')}
output = root / mode / 'Copie01'
with staged_files(output) as staging:
atomic_write_json(staging / 'score.json', scores)
atomic_write_json(staging / 'info.json', info)
for label, image in images.items():
image.save(staging / f'{label}.jpg')
images['Ex 1'].save(staging / 'Concat.jpg')
stage_baseline(root, mode, output, staging, images, {'Ex 1': 20, 'Ex 2': 10}, info, [], scores)
class WebTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
seed(self.root)
self.service = Finalisation(self.root, 'Bnot', 'worker-token')
self.client = TestClient(create_app(self.service, 'browser-token'))
self.addCleanup(self.client.close)
self.client.get('/?token=browser-token')
def save(self, kind, value, copy='01', label='Ex 1'):
response = self.client.post('/api/change', json={'kind': kind, 'value': value, 'copy': copy,
'label': label, 'revision': self.service.revision()})
self.assertEqual(response.status_code, 200, response.text)
return response.json()
def test_names_follow_gui_repository_even_when_package_is_elsewhere(self):
repository = self.root / 'application-folder'
repository.mkdir()
(repository / 'names').write_text('Alice\nBob\n', encoding='utf-8')
service = Finalisation(self.root, 'Bnot', 'token', repository=repository)
self.assertEqual(service.state()['expectedNames'], ['Alice', 'Bob'])
(self.root / 'names').write_text('Claire\n', encoding='utf-8')
self.assertEqual(service.state()['expectedNames'], ['Claire'])
def test_expected_names_use_local_roster_and_refresh_after_name_save(self):
(self.root / 'names').write_text('Alice\nBob\nAlice\n\n', encoding='utf-8')
state = self.client.get('/api/state').json()
self.assertEqual(state['expectedNames'], ['Alice', 'Bob'])
state = self.save('name', 'Alice')
self.assertEqual(state['copies'][0]['name'], 'Alice')
self.assertEqual(state['expectedNames'], ['Alice', 'Bob'])
(self.root / 'names').write_text('', encoding='utf-8')
self.assertEqual(self.service.state()['expectedNames'], [])
def test_session_origin_and_paths(self):
outsider = TestClient(create_app(self.service, 'browser-token'))
self.assertEqual(outsider.get('/api/state').status_code, 403)
self.assertEqual(self.client.get('/api/state', headers={'Host': 'evil.example'}).status_code, 403)
self.assertEqual(self.client.post('/api/stop', headers={'Origin':'https://evil.example'}).status_code, 403)
self.assertEqual(self.client.get('/api/image', params={'copy':'../', 'label':'Ex 1'}).status_code, 409)
self.assertEqual(self.client.get('/api/image', params={'copy':'01', 'label':'../labels'}).status_code, 409)
self.assertEqual(self.client.get('/api/document', params={'copy':'01', 'kind':'question','label':'Absent'}).status_code, 404)
self.assertEqual(self.client.get('/').status_code, 200)
def test_browser_and_existing_editor_share_scores_and_remarks(self):
for mode in ('Bnot', 'BGnot'):
with self.subTest(mode=mode):
seed(self.root, mode)
self.service = Finalisation(self.root, mode, 'worker-token')
self.client = TestClient(create_app(self.service, 'browser-token'))
self.client.get('/?token=browser-token')
self.save('score', '2,5')
score = self.root / mode / 'Copie01/score.json'
self.assertEqual(read_scores(score)['Ex 1'], '2.5')
set_score(score, 'Ex 1', '1')
self.assertEqual(self.service.state()['copies'][0]['answers'][0]['score'], '1')
self.save('remark', 'Une remarque $x^2$.')
self.save('general', ['Première', 'Deuxième'])
self.save('general', ['Deuxième', 'Première'])
self.assertEqual(self.service.state()['copies'][0]['general'], ['Deuxième','Première'])
self.save('remark', '')
self.assertEqual(self.service.state()['copies'][0]['answers'][0]['remark'], '')
def test_stale_tab_and_busy_writes_rejected(self):
old = self.service.revision()
self.save('score', '2')
response = self.client.post('/api/change', json={'kind':'score','value':'1','copy':'01','label':'Ex 1','revision':old})
self.assertEqual(response.status_code, 409)
self.service.job['running'] = True
self.assertEqual(self.client.post('/api/stop').status_code, 409)
response = self.client.post('/api/change', json={'kind':'score','value':'1','copy':'01','label':'Ex 1','revision':self.service.revision()})
self.assertEqual(response.status_code, 409)
self.service.job['running'] = False
def test_draft_preview_apply_reapply_and_undo(self):
text = '### Source: Bnot\nCopie01 Ex 1 c{40}1> Ex 2|\n'
before = (self.root / 'Bnot/Copie01/Ex 1.jpg').read_bytes()
self.save('draft', text)
self.assertEqual((self.root / 'Bnot/Copie01/Ex 1.jpg').read_bytes(), before)
self.assertFalse((self.root / 'Bnot' / APPLIED).exists())
result = self.client.post('/api/preview', json={'copy':'01','source':'Ex 1','target':'Ex 2','text':text})
self.assertEqual(result.status_code, 200, result.text)
import base64
rendered = Image.open(BytesIO(base64.b64decode(result.json()['Ex 1'].split(',')[1])))
self.assertEqual(rendered.height, 40) # cut uses 100px body, not 120px image
modify_annotations.run(EvaluationWorkspace(self.root), annotation_dir='Bnot')
applied = (self.root / 'Bnot/Copie01/Ex 2.jpg').read_bytes()
modify_annotations.run(EvaluationWorkspace(self.root), annotation_dir='Bnot')
self.assertEqual((self.root / 'Bnot/Copie01/Ex 2.jpg').read_bytes(), applied)
self.save('draft', '### Source: Bnot\n')
self.assertEqual((self.root / 'Bnot' / APPLIED).read_text(), text)
modify_annotations.run(EvaluationWorkspace(self.root), annotation_dir='Bnot')
self.assertEqual(Image.open(self.root / 'Bnot/Copie01/Ex 1.jpg').height, 120)
def test_invalid_draft_preserves_previous_file(self):
self.save('draft', '### Source: Bnot\n')
for text in ('### Source: BGnot\n', '### Source: Bnot\nCopie01 Ex 1 -> Ex 2|\nCopie01 Ex 2 -> Absent|\n'):
response = self.client.post('/api/change', json={'kind':'draft','value':text,'revision':self.service.revision()})
self.assertEqual(response.status_code, 409)
self.assertEqual((self.root / DRAFT).read_text(), '### Source: Bnot\n')
@patch('copienator.commands.giving_names.configuration.RETURN_ANSWERS_ENABLED', False)
def test_names_and_fallback_scores_survive_both_workflows(self):
workspace = EvaluationWorkspace(self.root)
self.save('name', 'Alice')
self.assertEqual(giving_names.prepare_named_returns(workspace, 'Bnot'), 0)
folder = rename_return_copy(self.root / 'A Rendre/Alice (01)', 'Alice corrigée')
self.assertEqual(self.service.state()['copies'][0]['name'], 'Alice corrigée')
self.assertEqual(giving_names.prepare_named_returns(workspace, 'Bnot'), 0)
self.assertEqual(list((self.root / 'A Rendre').iterdir()), [folder])
score = folder / 'score.json'
score.unlink()
atomic_write_json(score, {'Ex 1':'3','Ex 2':'4','Absent':''})
set_score(score, 'Ex 1', '1')
self.assertEqual(read_scores(self.root / 'Bnot/Copie01/score.json')['Ex 1'], '1')
self.save('score', '2')
self.assertEqual(read_scores(score)['Ex 1'], '2')
self.save('name', 'Alice finale')
self.assertTrue((self.root / 'A Rendre/Alice finale (01)/Alice finale.jpg').is_file())
def test_worker_order_flags_and_failure(self):
calls = []
class Process:
stdout = StringIO('done\n')
def __init__(self, command, **kwargs):
calls.append(command)
def __enter__(self): return self
def __exit__(self, *args): pass
def wait(self): return 0
with patch('copienator.finalisation.subprocess.Popen', Process):
self.service._regenerate(True)
self.assertEqual([c[3] for c in calls], ['read-annotations','modify-annotations','giving-names'])
self.assertTrue(any('return-answers-question' in v for v in calls[0]))
self.assertEqual(self.service.job['status'], 'success')
calls.clear()
with patch.object(Process, 'wait', return_value=4), patch('copienator.finalisation.subprocess.Popen', Process):
self.service._regenerate(True)
self.assertEqual(len(calls), 1)
self.assertEqual(self.service.job['status'], 'failed')
def test_names_only_skip_rendering_before_first_cache_and_after_restart(self):
from copienator.review_snapshot import stage
output = self.root / 'Bnot/Copie01'
data = {'Ex 1': {'pdf_path': self.root/'Copies/Copie01/Ex 1.pdf',
'result': {'score': 3, 'feedback': []}, 'coordinates': [0, 0]}}
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='standard', bnote={'images': []})
self.save('name', 'Alice')
self.assertFalse((output / '.reviewed-render-state.json').exists())
reopened = Finalisation(self.root, 'Bnot', 'token')
calls = []
class Process:
def __init__(self, command, **kwargs):
calls.append(command[3])
self.stdout = StringIO('done\n')
def __enter__(self): return self
def __exit__(self, *args): pass
def wait(self): return 0
with patch('copienator.finalisation.subprocess.Popen', Process):
reopened._regenerate(False)
self.assertEqual(calls, ['giving-names'])
self.save('name', 'Bob')
# A score edited outside the session must invalidate the names-only shortcut.
set_score(output/'score.json', 'Ex 1', '1')
calls.clear()
with patch('copienator.finalisation.subprocess.Popen', Process):
self.service._regenerate(False)
self.assertEqual(calls, ['regenerate-reviewed', 'giving-names'])
def test_exported_final_score_is_bound_to_copy_id_after_rename(self):
self.assertIsNone(self.client.get('/api/state').json()['copies'][0]['finalScore'])
atomic_write_json(self.root/'.copienator/final_scores.json',
{'version':1,'copies':{'1':{'name':'Old name','score':'12.3'}}})
self.assertEqual(self.client.get('/api/state').json()['copies'][0]['finalScore'], '12.3')
self.save('name', 'New name')
self.assertEqual(self.client.get('/api/state').json()['copies'][0]['finalScore'], '12.3')
def test_os_lock_blocks_cli_and_releases(self):
with EvaluationLock(self.root):
with self.assertRaises(CliError):
with EvaluationLock(self.root):
pass
result = subprocess.run([sys.executable,'-m','copienator','giving-names',str(self.root),'Bnot'], capture_output=True, text=True)
self.assertNotEqual(result.returncode, 0)
self.assertIn('utilisée', result.stderr)
with EvaluationLock(self.root):
pass
class SnapshotTests(unittest.TestCase):
def test_grouped_layout_without_handwriting_replaces_previous_ink(self):
from copienator.review_snapshot import stage, load
from copienator.annotation_layout import legacy_layout
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, 'BGnot')
output = root / 'BGnot/Copie01'
data = {label: {'pdf_path': root / f'{label}.pdf', 'result': {'score': 3}}
for label in ('Ex 1', 'Ex 2')}
ink = Image.new('RGBA', (100, 120), 'red')
notes = {label: {'img': ink, 'old_header_h': 20, 'layout': legacy_layout()}
for label in data}
with staged_files(output) as staging:
stage(output, staging, data, notes, engine='grouped')
notes['Ex 1']['img'] = None
for selected in ({'Ex 1'}, None):
with staged_files(output) as staging:
stage(output, staging, data, notes, engine='grouped', selected=selected)
saved, _, restored = load(root, output)
self.assertIsNone(saved['notes']['Ex 1']['file'])
self.assertEqual(restored['Ex 1'], notes['Ex 1'])
self.assertEqual(restored['Ex 2']['img'].getpixel((0, 0)), (255, 0, 0, 255))
def test_selective_redo_merges_raw_inputs_and_preserves_handwriting(self):
from copienator.review_snapshot import stage, load
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
output = root / 'Bnot/Copie01'
data = {l: {'pdf_path': root / 'Copies/Copie01' / f'{l}.pdf',
'result': {'score': 3, 'feedback': []}, 'coordinates': [0, 0]}
for l in ('Ex 1', 'Ex 2')}
ink = Image.new('RGBA', (100, 120), (255, 0, 0, 255))
bnote = {'images': [{'label':'Ex 1', 'header_height':20}, {'label':'Ex 2','header_height':10}]}
with staged_files(output) as staging:
stage(output, staging, data, {'Ex 1': {'img': ink}}, engine='standard', bnote=bnote)
data['Ex 2']['result']['score'] = 1
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='grouped', selected={'Ex 2'}, force_filtered={'Ex 1'})
saved, restored, notes = load(root, output)
self.assertEqual(restored['Ex 1']['result']['score'], 3)
self.assertEqual(restored['Ex 2']['result']['score'], 1)
self.assertEqual(notes['Ex 1']['img'].getpixel((0,0)), (255,0,0,255))
self.assertEqual(notes['Ex 1']['old_header_h'], 20)
self.assertIn('Ex 1', saved['force_filtered'])
self.assertEqual(restored['Ex 1']['pdf_path'], root / 'Copies/Copie01/Ex 1.pdf')
def test_cached_regeneration_does_not_accumulate_custom_remarks(self):
from copienator.review_snapshot import stage, load
from copienator.commands import regenerate_reviewed
from copienator.commands import annotating
from copienator.custom_remarks import set_remark
for mode in ('Bnot', 'BGnot', 'Anot'):
with self.subTest(mode=mode), tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, mode)
output = root / mode / 'Copie01'
pdf = root / 'Copies/Copie01/Ex 1.pdf'
pdf.write_bytes(b'placeholder')
data = {'Ex 1': {'pdf_path': pdf, 'result': {'score': 3, 'feedback': []}, 'coordinates': [0,0]}}
notes = {'Ex 1': {'img': None, 'old_header_h': 20}} if mode == 'BGnot' else {}
with staged_files(output) as staging:
stage(output, staging, data, notes, engine='standard' if mode == 'Bnot' else 'grouped',
bnote={'images':[{'label':'Ex 1','header_height':20}]})
set_remark(root, '01', 'Ex 1', 'Une seule remarque')
with patch.object(annotating, 'make_base_image', return_value=(Image.new('RGB',(100,100),'white'),0,0)), \
patch.object(annotating, 'compose_label_image', return_value=(Image.new('RGB',(100,120),'white'),20)) as compose, \
patch('copienator.commands.reading_grouped_annotations.get_extra_pdfs_as_images', return_value=[]):
for _ in range(2):
self.assertEqual(regenerate_reviewed.run(EvaluationWorkspace(root), mode), 0)
feedback = compose.call_args.args[2]['feedback']
self.assertEqual(len(feedback), 1)
self.assertEqual(feedback[0]['text'], 'Une seule remarque')
_, raw, _ = load(root, output)
self.assertEqual(raw['Ex 1']['result']['feedback'], [])
@unittest.skipUnless(os.environ.get('DISPLAY'), 'Tk requires a display')
class WebGuiTests(unittest.TestCase):
def test_web_button_requires_read_outputs_and_remains_enabled_when_stale(self):
from copienator_gui.app import CopienatorApp
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
(root / 'Copies').mkdir()
(root / 'labels').write_text('Ex 1\n')
app = CopienatorApp(Path.cwd(), False, root)
self.addCleanup(app.destroy)
def state():
app._update_controls()
return str(app.finalisation_web_button.cget('state'))
self.assertEqual(state(), 'disabled')
output = root / 'BGnot/Copie01'
output.mkdir(parents=True)
atomic_write_json(output/'bnote.json', {'images': []})
self.assertEqual(state(), 'disabled')
seed(root, 'BGnot')
self.assertEqual(state(), 'normal')
app.state_store.update_step('read_annotations', status='stale')
self.assertEqual(state(), 'normal')
app.active_step_id = 'read_annotations'
self.assertEqual(state(), 'disabled')
app.active_step_id = None
self.assertEqual(state(), 'normal')
app.state_store.evaluation = None
self.assertEqual(state(), 'disabled')
def test_anot_generation_enables_button_and_selects_anot(self):
from copienator_gui.app import CopienatorApp
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, 'Anot')
app = CopienatorApp(Path.cwd(), False, root)
self.addCleanup(app.destroy)
app._update_controls()
self.assertEqual(str(app.finalisation_web_button.cget('state')), 'normal')
app._open_finalisation_web()
dialog = app.grab_current()
def descendants(widget):
for child in widget.winfo_children():
yield child
yield from descendants(child)
combo = next(w for w in descendants(dialog) if w.winfo_class() == 'TCombobox')
self.assertEqual(combo.get(), 'Anot')
self.assertEqual(tuple(combo.cget('values')), ('Anot',))
dialog.grab_release()
dialog.destroy()
def test_modal_lock_and_return_to_existing_workflow(self):
from copienator_gui.app import CopienatorApp
from copienator.finalisation_server import WebSession
import tkinter as tk
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
app = CopienatorApp(Path.cwd(), False, root)
self.addCleanup(app.destroy)
service = Finalisation(root, 'Bnot', 'token')
class FakeSession:
url = 'http://127.0.0.1:12345/'
alive = True
def request_stop(self): self.alive = False
session = FakeSession()
session.service = service
def descendants(widget):
for child in widget.winfo_children():
yield child
yield from descendants(child)
def button(text):
return next(w for w in descendants(app) if w.winfo_class() == 'TButton' and w.cget('text') == text)
with patch('copienator.finalisation_server.WebSession', return_value=session), patch('webbrowser.open'):
app._open_finalisation_web()
app.update()
self.assertIsInstance(app.grab_current(), tk.Toplevel)
button('Ouvrir').invoke()
self.assertIs(app.web_session, session)
self.assertEqual(str(button('Ouvrir').cget('state')), 'disabled')
service.save({'kind':'score','copy':'01','label':'Ex 1','value':'1','revision':service.revision()})
with patch.object(app.runner, 'start') as start:
app._run_current_step()
start.assert_not_called()
button('Arrêter le serveur et reprendre').invoke()
app.after(450, app.quit)
app.mainloop()
self.assertIsNone(app.web_session)
self.assertIsNone(app.grab_current())
self.assertEqual(app.state_store.step('read_annotations')['status'], 'stale')
class IncrementalReviewedTests(unittest.TestCase):
def test_only_changed_copies_render_and_missing_outputs_are_retried(self):
from copienator.commands import regenerate_reviewed, annotating
from copienator.review_snapshot import stage
from copienator.custom_remarks import set_remark
import shutil
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, 'BGnot')
pdf = root / 'Copies/Copie01/Ex 1.pdf'
pdf.write_bytes(b'answer')
data = {'Ex 1': {'pdf_path':pdf, 'result':{'score':3,'feedback':[]},'coordinates':[0,0]}}
output = root / 'BGnot/Copie01'
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='grouped')
shutil.copytree(output, root / 'BGnot/Copie02')
workspace = EvaluationWorkspace(root)
with patch.object(annotating, 'make_base_image', return_value=(Image.new('RGB',(100,100),'white'),0,0)), \
patch.object(annotating, 'compose_label_image', return_value=(Image.new('RGB',(100,120),'white'),20)) as compose, \
patch('copienator.commands.reading_grouped_annotations.get_extra_pdfs_as_images', return_value=[]):
def run(count):
compose.reset_mock()
self.assertEqual(regenerate_reviewed.run(workspace,'BGnot',incremental=True), 0)
self.assertEqual(compose.call_count,count)
run(2)
run(0)
set_score(output/'score.json','Ex 1','2')
run(1)
run(0)
set_remark(root,'01','Ex 1','Justifier.')
run(1)
set_remark(root,'01','Ex 1','')
run(1)
(output/'Ex 1.jpg').unlink()
run(1)
(root/'Text2').mkdir()
(root/'Text2/Ex 1.pdf').write_bytes(b'changed question')
run(2)
run(0)
self.assertEqual(regenerate_reviewed.run(workspace,'BGnot'),0)
self.assertEqual(compose.call_count,2)
def test_incremental_transfers_apply_changes_and_undo_without_repeating(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
workspace = EvaluationWorkspace(root)
from copienator.annotation_modifications import rebuild
with patch.object(modify_annotations, 'rebuild', wraps=rebuild) as render:
def run(text, count):
(root / DRAFT).write_text('### Source: Bnot\n' + text)
render.reset_mock()
self.assertEqual(modify_annotations.run(workspace, annotation_dir='Bnot', incremental=True), 0)
self.assertEqual(render.call_count, count)
run('Copie01 Ex 1 c{40}1> Ex 2|\n', 1)
run('Copie01 Ex 1 c{40}1> Ex 2|\n', 0)
run('Copie01 Ex 1 c{50}1> Ex 2|\n', 1)
run('', 1)
run('', 0)
self.assertEqual(Image.open(root/'Bnot/Copie01/Ex 1.jpg').height,120)
@patch('copienator.commands.giving_names.configuration.RETURN_ANSWERS_ENABLED', False)
def test_return_publication_skips_unchanged_and_repairs_missing_files(self):
import shutil
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
shutil.copytree(root/'Bnot/Copie01',root/'Bnot/Copie02')
atomic_write_json(root/'Copies/Copie02.json',{'name':'Bob'})
atomic_write_json(root/'Copies/Copie01.json',{'name':'Alice'})
workspace = EvaluationWorkspace(root)
from copienator.return_answers import publish_answer_returns
with patch.object(giving_names,'publish_answer_returns',wraps=publish_answer_returns) as publish:
def run(count):
publish.reset_mock()
self.assertEqual(giving_names.prepare_named_returns(workspace,'Bnot',incremental=True),0)
self.assertEqual(publish.call_count,count)
run(2)
run(0)
set_score(root/'Bnot/Copie01/score.json','Ex 1','1')
run(1)
run(0)
(root/'A Rendre/Bob (02)/info.json').unlink()
run(1)
self.assertTrue((root/'A Rendre/Bob (02)/info.json').is_file())
class CrossProcessCacheTests(unittest.TestCase):
def test_reviewed_cache_survives_worker_restart_and_selects_only_two_score_edits(self):
from copienator.review_snapshot import stage
import shutil
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root, 'BGnot')
output = root/'BGnot/Copie01'
data = {'Ex 1': {'pdf_path':root/'Copies/Copie01/Ex 1.pdf',
'result':{'score':3,'feedback':[]},'coordinates':[0,0]}}
with staged_files(output) as staging:
stage(output, staging, data, {}, engine='grouped')
for cid in ('02','03','04'):
shutil.copytree(output,root/'BGnot'/f'Copie{cid}')
script = '''
import json, sys
from pathlib import Path
from copienator import EvaluationWorkspace, configuration
from copienator.reviewed_cache import ReviewedReadCache
from string import Template
configuration.LATEX_ANOT_BEFORE = Template(sys.argv[3])
root=Path(sys.argv[1])
directories=sorted((root/'BGnot').glob('Copie*'))
cache=ReviewedReadCache(EvaluationWorkspace(root),'BGnot',directories)
if sys.argv[2]=='remember':
cache.remember([d.name[5:] for d in directories])
print(json.dumps([d.name for d in directories if not cache.unchanged(d.name[5:])]))
'''
def run(action='check', template='Stable ${width_in} template'):
return __import__('json').loads(subprocess.check_output(
[sys.executable,'-c',script,str(root),action,template],text=True))
self.assertEqual(run('remember'), [])
self.assertEqual(run(), [])
set_score(root/'BGnot/Copie01/score.json','Ex 1','1')
set_score(root/'BGnot/Copie03/score.json','Ex 1','2')
self.assertEqual(run(), ['Copie01','Copie03'])
self.assertEqual(run('remember'), [])
self.assertEqual(run(), [])
self.assertEqual(run(template='Changed template'), ['Copie01','Copie02','Copie03','Copie04'])
def test_legacy_reader_cache_template_is_stable_across_workers(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
seed(root)
atomic_write_json(root/'Bnot/Copie01/review_snapshot.json', {})
script = '''
import json, sys
from pathlib import Path
from string import Template
from copienator import EvaluationWorkspace, configuration
from copienator.annotation_read_cache import AnnotationReadCache
configuration.LATEX_ANOT_BEFORE=Template('Stable ${width_in} template')
root=Path(sys.argv[1])
data={'01':{'Ex 1':{'pdf_path':root/'Copies/Copie01/Ex 1.pdf'}}}
cache=AnnotationReadCache(EvaluationWorkspace(root),'Bnot',data)
if sys.argv[2]=='remember': cache.remember(['01'])
print(json.dumps(cache.unchanged('01')))
'''
for action in ('remember','check'):
result=subprocess.check_output([sys.executable,'-c',script,str(root),action],text=True)
self.assertEqual(result.strip(),'true')