102 lines
7.5 KiB
Python
102 lines
7.5 KiB
Python
"""Filesystem lifecycle tests use an isolated miniature card workspace."""
|
|
from pathlib import Path
|
|
import hashlib, json, os, sys, tempfile, unittest
|
|
from unittest.mock import patch
|
|
from PIL import Image
|
|
sys.path.insert(0,str(Path(__file__).resolve().parents[1]))
|
|
import card_workspace as workspace
|
|
|
|
class CardWorkspaceTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.temp=tempfile.TemporaryDirectory();self.repo=Path(self.temp.name)
|
|
self.repo_patch=patch.object(workspace,'REPO',self.repo);self.repo_patch.start()
|
|
self.size_patch=patch.object(workspace,'RESOLUTIONS',{'low':(5,7),'med':(10,14),'high':(20,28)});self.size_patch.start()
|
|
self.revision=workspace.scaffold('BP-999','Test card','Common','v01')
|
|
def tearDown(self):
|
|
self.size_patch.stop();self.repo_patch.stop();self.temp.cleanup()
|
|
def assemble(self):
|
|
for label,size in workspace.RESOLUTIONS.items():
|
|
for printing in workspace.PRINTINGS:
|
|
directory=self.revision/label/printing
|
|
Image.new('RGBA',size,(40,60,80,255)).save(directory/'card.png')
|
|
Image.new('RGBA',size,(127,127,127,255)).save(directory/'finish-mask.png')
|
|
if printing in ('normal','borderless'):Image.new('RGBA',size,(0,0,0,255)).save(directory/'text-mask.png')
|
|
source=self.repo/'fitted-art.png';Image.new('RGB',(20,28),'blue').save(source)
|
|
link=self.revision/'source/art-master.png';link.symlink_to(os.path.relpath(source,link.parent))
|
|
editable=self.revision/'high/normal/card.svg'
|
|
editable.write_text(f'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><image xlink:href="{os.path.relpath(source,editable.parent)}"/></svg>')
|
|
alias=self.repo/'spikes/proof/card.png';alias.parent.mkdir(parents=True,exist_ok=True);alias.symlink_to(os.path.relpath(self.revision/'med/normal/card.png',alias.parent))
|
|
manifest=workspace.read(self.revision/'manifest.json');manifest['stage']='assembly-review';manifest['compatibilityAliases']=[{'alias':'spikes/proof/card.png','target':'med/normal/card.png'}];workspace.write(self.revision/'manifest.json',manifest)
|
|
workspace.refresh(self.revision)
|
|
return alias
|
|
def test_scaffold_tracks_art_review_without_derived_exports(self):
|
|
self.assertEqual(workspace.validate(self.revision)['stage'],'art-review')
|
|
self.assertEqual(workspace.resolve_revision('BP-999','v01'),self.revision)
|
|
self.assertEqual(len(list(self.revision.glob('*/normal'))),3)
|
|
def test_approval_preserves_exports_sources_and_aliases_then_freezes(self):
|
|
alias=self.assemble();before={str(p.relative_to(self.revision)):workspace.digest(p) for p in self.revision.rglob('*.png')}
|
|
target=workspace.approve('BP-999','v01','Test fixture approval only')
|
|
self.assertFalse(self.revision.exists())
|
|
self.assertEqual(target,self.repo/'artifacts/cards/BP-999-test-card')
|
|
self.assertEqual(workspace.validate(target)['stage'],'approved')
|
|
self.assertEqual(alias.resolve(),target/'med/normal/card.png')
|
|
for relative,digest in before.items():self.assertEqual(workspace.digest(target/relative),digest)
|
|
with self.assertRaisesRegex(ValueError,'frozen'):workspace.refresh(target)
|
|
def test_flat_acceptance_keeps_revision_identity_and_content(self):
|
|
self.assemble()
|
|
content=workspace.read(self.revision/'card.json');content['verse']='A retained verse';workspace.write(self.revision/'card.json',content);workspace.refresh(self.revision)
|
|
target=workspace.approve('BP-999','v01','Test fixture approval only')
|
|
self.assertEqual(workspace.resolve_revision('BP-999','v01'),target)
|
|
self.assertEqual(workspace.read(target/'card.json')['verse'],'A retained verse')
|
|
self.assertEqual(workspace.read(target/'manifest.json')['revision'],'v01')
|
|
with self.assertRaises(ValueError):workspace.resolve_revision('BP-999','v02')
|
|
with self.assertRaisesRegex(ValueError,'already exists'):workspace.scaffold('BP-999','Test card','Common','v01')
|
|
|
|
def test_new_acceptance_archives_previous_and_pins_linked_art(self):
|
|
alias=self.assemble()
|
|
accepted=workspace.approve('BP-999','v01','First fixture approval')
|
|
before=workspace.digest(accepted/'med/normal/card.png')
|
|
self.revision=workspace.scaffold('BP-999','Test card','Common','v02')
|
|
alias.unlink();self.assemble()
|
|
# This revision deliberately depends on the earlier accepted full-art export.
|
|
link=self.revision/'source/art-master.png';link.unlink();link.symlink_to(os.path.relpath(accepted/'high/boundless/card.png',link.parent))
|
|
content=workspace.read(self.revision/'card.json');content['art']=str((accepted/'high/boundless/card.png').relative_to(self.repo));workspace.write(self.revision/'card.json',content);workspace.refresh(self.revision)
|
|
target=workspace.approve('BP-999','v02','Second fixture approval')
|
|
archive=self.repo/'in-progress/cards/BP-999-test-card/history/accepted-v01'
|
|
self.assertEqual(workspace.resolve_revision('BP-999','v01'),archive)
|
|
self.assertEqual(workspace.resolve_revision('BP-999','v02'),target)
|
|
self.assertEqual(workspace.digest(archive/'med/normal/card.png'),before)
|
|
self.assertEqual((target/'source/art-master.png').resolve(),archive/'high/boundless/card.png')
|
|
self.assertEqual(workspace.validate(archive)['stage'],'approved')
|
|
self.assertEqual(workspace.validate(target)['stage'],'approved')
|
|
self.assertEqual(alias.resolve(),target/'med/normal/card.png')
|
|
self.assertEqual(workspace.update_index()[0]['selectedRevision'],'v02')
|
|
|
|
def test_changed_or_invalid_exports_block_promotion(self):
|
|
self.assemble();Image.new('RGBA',(9,14),(40,60,80,255)).save(self.revision/'med/normal/card.png')
|
|
with self.assertRaises(ValueError):workspace.approve('BP-999','v01','Test fixture approval only')
|
|
self.assertTrue(self.revision.exists());self.assertFalse((self.repo/'artifacts/cards/BP-999-test-card').exists())
|
|
def test_catalogue_identity_survives_folder_title_rename(self):
|
|
base=self.revision.parents[1]
|
|
renamed=base.with_name('BP-999-renamed-title')
|
|
base.rename(renamed)
|
|
profile=workspace.read(renamed/'card.json')
|
|
profile.update({'title':'Renamed title','folderName':renamed.name})
|
|
workspace.write(renamed/'card.json',profile)
|
|
# The old index is deliberately stale; resolution must recover from profiles.
|
|
self.assertEqual(workspace.resolve_revision('BP-999','v01'),renamed/'revisions/v01')
|
|
self.assertEqual(workspace.update_index()[0]['cardId'],'BP-999')
|
|
self.revision=renamed/'revisions/v01'
|
|
def test_catalogue_id_is_required_and_duplicate_profiles_are_rejected(self):
|
|
with self.assertRaises(ValueError):workspace.scaffold('test-card','Test card','Common','v02')
|
|
base=self.repo/'in-progress/cards/BP-999-duplicate'
|
|
base.mkdir()
|
|
profile=workspace.read(self.revision.parents[1]/'card.json');profile['folderName']=base.name
|
|
workspace.write(base/'card.json',profile)
|
|
with self.assertRaisesRegex(ValueError,'Duplicate'):workspace.update_index()
|
|
def test_art_review_cannot_be_promoted_and_duplicate_revision_is_rejected(self):
|
|
with self.assertRaisesRegex(ValueError,'assembly-review'):workspace.approve('BP-999','v01','Test fixture approval only')
|
|
with self.assertRaisesRegex(ValueError,'already exists'):workspace.scaffold('BP-999','Test card','Common','v01')
|
|
|
|
if __name__=='__main__':unittest.main()
|