Copy removal
This commit is contained in:
1 parent
c36642e8de
commit
213576f83d
8 files changed
+464
-26
No files matched your search
@@ -0,0 +1,140 @@
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from PIL import Image
|
||||
|
||||
from copienator.copy_removal import available_copies, removal_plan, remove_copy
|
||||
|
||||
|
||||
class CopyRemovalTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = Path(self.temp.name)
|
||||
for name in ('Copies/Copie24.pdf', 'Copies/Copie25.pdf', 'Copies Originales/Copie24.pdf',
|
||||
'Cutleft/Copie24_01.jpg', 'Copies/Copie24/Ex 1.pdf',
|
||||
'BGnot/Copie24/Concat.pdf', 'A Rendre/Antoine (24)/score.json',
|
||||
'.copienator/runs/old/Copies/Copie24.pdf',
|
||||
'BackUp/previous/Copie24.pdf'):
|
||||
self.write(name, b'original bytes')
|
||||
|
||||
def write(self, name, value):
|
||||
p = self.root / name
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
p.write_bytes(value)
|
||||
return p
|
||||
|
||||
def test_removes_all_copy_artifacts_and_preserves_neighbors_and_backups(self):
|
||||
self.write('Copies/Copie240.pdf', b'other copy')
|
||||
self.write('Copies/Copie24_01.json', b'{"name": "Antoine"}')
|
||||
self.write('.copienator/copy_errors.json', b'{"Copie24.pdf":{"plotting":"bad"}, "Copie25.pdf":{"plotting":"keep"}}')
|
||||
self.write('correction.json', b'{"Ex 1":[[{"id":"24","result":{"score":3}}, {"id":"25","result":{"score":4}}]]}')
|
||||
self.write('refaire.json', b'[["Copie24", []], ["Copie25", []]]')
|
||||
self.write('general_remarks.json', b'{"24":"remove", "25":"keep"}')
|
||||
self.write('batch_requests_flash.jsonl', b'old payload')
|
||||
self.write('correction_pending_responses.json', b'{"group": "cached raw response"}')
|
||||
self.write('manual_resolutions.txt', b'Copie24 Ex 1 -> Ex 2\nCopie25 Ex 1 -> Ex 2\n')
|
||||
before = {p.relative_to(self.root): p.read_bytes() for p in self.root.rglob('*') if p.is_file()}
|
||||
plan = removal_plan(self.root, 'Copie24')
|
||||
backup = remove_copy(self.root, 'Copie24')
|
||||
self.assertEqual(available_copies(self.root), ['Copie25', 'Copie240'])
|
||||
self.assertFalse((self.root/'correction_pending_responses.json').exists())
|
||||
self.assertEqual((self.root/'manual_resolutions.txt').read_text(), 'Copie25 Ex 1 -> Ex 2\n')
|
||||
for p in plan.removed:
|
||||
self.assertFalse(p.exists())
|
||||
for relative in [p.relative_to(self.root) for p in (*plan.removed, *plan.updated) if p.relative_to(self.root) in before]:
|
||||
self.assertEqual((backup / 'files' / relative).read_bytes(), before[relative])
|
||||
self.assertEqual((self.root / 'Copies/Copie25.pdf').read_bytes(), b'original bytes')
|
||||
self.assertEqual((self.root / 'BackUp/previous/Copie24.pdf').read_bytes(), b'original bytes')
|
||||
self.assertEqual(json.loads((self.root / 'correction.json').read_text())['Ex 1'][0], [{'id':'25','result':{'score':4}}])
|
||||
self.assertEqual(json.loads((self.root / 'refaire.json').read_text()), [['Copie25', []]])
|
||||
self.assertEqual(json.loads((self.root / 'general_remarks.json').read_text()), {'25':'keep'})
|
||||
self.assertEqual(json.loads((backup / 'manifest.json').read_text())['status'], 'complete')
|
||||
|
||||
def test_group_removal_preserves_other_students_coordinates(self):
|
||||
path = self.write('Par label/Ex 1/Group_1.json', b'[["24", 50, 100, 1, "Ex 1"], ["25", 160, 210, 1, "Ex 1"]]')
|
||||
Image.new('RGB', (100, 220), 'blue').save(path.with_suffix('.jpg'))
|
||||
remove_copy(self.root, 'Copie24')
|
||||
self.assertEqual(json.loads(path.read_text()), [['25',160,210,1,'Ex 1']])
|
||||
with Image.open(path.with_suffix('.jpg')) as image:
|
||||
self.assertGreater(image.getpixel((20,60))[0], 240)
|
||||
self.assertLess(image.getpixel((20,180))[0], 30)
|
||||
|
||||
def test_failure_restores_already_removed_files(self):
|
||||
from copienator.copy_removal import atomic_write_bytes
|
||||
self.write('refaire.json', b'[["Copie24", []]]')
|
||||
original = (self.root/'Copies/Copie24.pdf').read_bytes()
|
||||
def failing_write(path, data):
|
||||
if path == self.root/'refaire.json':
|
||||
raise OSError('simulated full disk')
|
||||
return atomic_write_bytes(path, data)
|
||||
with patch('copienator.copy_removal.atomic_write_bytes', side_effect=failing_write):
|
||||
with self.assertRaises(OSError):
|
||||
remove_copy(self.root, 'Copie24')
|
||||
self.assertEqual((self.root/'Copies/Copie24.pdf').read_bytes(), original)
|
||||
self.assertEqual((self.root/'refaire.json').read_bytes(), b'[["Copie24", []]]')
|
||||
|
||||
def test_cleanup_keeps_removal_backups(self):
|
||||
from copienator import EvaluationWorkspace
|
||||
from copienator.commands.clean import build_cleanup_plan
|
||||
self.write('correction.json', b'{}')
|
||||
self.write('A Rendre/Antoine (24)/Antoine.jpg', b'returned image')
|
||||
plan = build_cleanup_plan(EvaluationWorkspace(self.root))
|
||||
saved = self.root/'BackUp/previous/Copie24.pdf'
|
||||
self.assertIn(saved, plan.kept_files)
|
||||
self.assertNotIn(saved, plan.deleted_files)
|
||||
self.assertNotIn(saved.parent, plan.deleted_directories)
|
||||
|
||||
def test_invalid_selection_and_external_symlink(self):
|
||||
for invalid in ('../Copie24', 'Copie99', ''):
|
||||
with self.assertRaises(ValueError):
|
||||
removal_plan(self.root, invalid)
|
||||
with tempfile.TemporaryDirectory() as external:
|
||||
p = Path(external)/'Copie24.pdf'
|
||||
p.write_bytes(b'outside')
|
||||
(self.root/'external').symlink_to(external, target_is_directory=True)
|
||||
remove_copy(self.root, 'Copie24')
|
||||
self.assertEqual(p.read_bytes(), b'outside')
|
||||
|
||||
|
||||
@unittest.skipUnless(os.environ.get('DISPLAY'), 'Tk requires a display')
|
||||
class CopyRemovalGuiTests(unittest.TestCase):
|
||||
def test_first_visit_skips_without_invalidating_then_revisit_can_remove(self):
|
||||
from copienator_gui.app import CopienatorApp
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
evaluation = root/'Eval'
|
||||
evaluation.mkdir()
|
||||
(evaluation/'Copie24.pdf').write_bytes(b'copy')
|
||||
app = CopienatorApp(root, False, evaluation)
|
||||
app.update()
|
||||
try:
|
||||
steps = [s.id for s in app.steps]
|
||||
self.assertEqual(steps[steps.index('rename')+1], 'remove_copy')
|
||||
app.state_store.update_step('page_splitter', status='success')
|
||||
step = app.step_by_id['remove_copy']
|
||||
app._move_selection_from('rename', 1)
|
||||
app.update()
|
||||
self.assertEqual(app.current_step.id, 'page_splitter')
|
||||
self.assertEqual(app.state_store.step('remove_copy')['status'], 'skipped')
|
||||
self.assertEqual(app.state_store.step('page_splitter')['status'], 'success')
|
||||
app.current_step = step
|
||||
app._render_step()
|
||||
app._handle_first_visit(step)
|
||||
app.removal_copy_var.set('Copie24')
|
||||
with patch('copienator_gui.app.messagebox.askyesno', return_value=False):
|
||||
app._run_current_step()
|
||||
self.assertTrue((evaluation/'Copie24.pdf').exists())
|
||||
with patch('copienator_gui.app.messagebox.askyesno', return_value=True):
|
||||
app._run_current_step()
|
||||
self.assertFalse((evaluation/'Copie24.pdf').exists())
|
||||
self.assertEqual(app.state_store.step('page_splitter')['status'], 'stale')
|
||||
self.assertEqual(len(list((evaluation/'BackUp').glob('*/manifest.json'))), 1)
|
||||
finally:
|
||||
for callback in app.tk.splitlist(app.tk.call('after', 'info')):
|
||||
app.after_cancel(callback)
|
||||
app.destroy()
|
||||
Reference in new issue
Block a user