"""What the server guarantees, as opposed to what the client intends. The two interesting groups here are the ones that make the tier split a property of the system rather than a convention in ClojureScript: A KEY DESCRIBES ITS BYTES. The server recomputes every tier-2 key it is handed and refuses a mismatch, so nothing can store a block under a name that is not the hash of its own descriptor. A BLOCK CAN NAME ITS DETECTOR VERSION. Every block names an analysis and every analysis declares a detector and a version, both enforced here. That chain is what docs/architecture.md asks for: without it, a model upgrade that silently reuses old landmarks presents as "the tool got worse" with no event to attach it to. The rest is the load/save round trip, the conditional write, and the footage manifest that makes the frames the backend's to serve. """ import base64 import hashlib import json import shutil import struct import subprocess import tempfile import zlib from io import StringIO from pathlib import Path from unittest import skipUnless from unittest.mock import Mock, patch from django.core.files.uploadedfile import SimpleUploadedFile from django.core.management import call_command from django.test import TestCase, override_settings from clips import blobs, extraction from clips.models import Analysis, Block, Blob, Clip, Footage, Leaf, Project, Revision, Sound, Source BLOB_DIR = tempfile.mkdtemp(prefix="arthur-test-blobs-") def key_for(descriptor: str) -> str: return "sha256:" + hashlib.sha256(descriptor.encode("utf-8")).hexdigest() def analysis_descriptor(version="1.0.1"): # Canonical JSON, written the way arthur.domain.canon writes it: sorted keys, # no spaces, integral doubles with no point. return ('{"aspect":1,"detector":"mediapipe","frames":48,"fps":30,"scheme":1,' f'"version":"{version}"}}') def block_descriptor(analysis_key, role="geom", anchor_avg=2): return (f'{{"analysis":"{analysis_key}","features":["mouth"],' f'"layout":{{"frames":48,"scale":16384,"stride":16,"tracks":1,"type":"int16"}},' f'"observation":null,"params":{{"anchor-avg":{anchor_avg}}},' f'"role":"{role}","scheme":1,"tracks":["outer"]}}') def png(width=4, height=3): """The smallest valid PNG of a given size, written by hand. So that `blobs.png_size` and the ingest path are exercised without Pillow. The one thing the backend needs from a PNG is its IHDR, and this is a PNG with one. """ def chunk(kind, payload): return (struct.pack(">I", len(payload)) + kind + payload + struct.pack(">I", zlib.crc32(kind + payload) & 0xFFFFFFFF)) ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0) raw = b"".join(b"\x00" + b"\x40\x40\x40" * width for _ in range(height)) return (b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", ihdr) + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b"")) @override_settings(BLOB_ROOT=BLOB_DIR) class BlobStoreTests(TestCase): def test_the_same_bytes_are_stored_once(self): a, size = blobs.write(b"the same bytes") b, _ = blobs.write(b"the same bytes") self.assertEqual(a, b) self.assertEqual(size, 14) self.assertEqual(blobs.read(a), b"the same bytes") def test_a_path_that_is_not_a_hash_is_refused(self): # The blob route takes its digest from the URL, so this is the check that # stops `/blob/../../etc/passwd` being a path at all. with self.assertRaises(ValueError): blobs.path_for("../../etc/passwd") with self.assertRaises(ValueError): blobs.path_for("deadbeef") def test_a_png_reports_its_own_size(self): with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as fh: fh.write(png(17, 5)) self.assertEqual((17, 5), blobs.png_size(Path(fh.name))) def test_a_blob_is_served_immutable(self): digest, size = blobs.write(b"bytes on the wire") Blob.objects.create(digest=digest, size=size, media_type="application/octet-stream") response = self.client.get(f"/blob/{digest}") self.assertEqual(200, response.status_code) self.assertIn("immutable", response["Cache-Control"]) self.assertEqual(f'"{digest}"', response["ETag"]) self.assertEqual(b"bytes on the wire", b"".join(response.streaming_content)) def test_an_unknown_blob_is_a_404_and_not_a_traceback(self): self.assertEqual(404, self.client.get("/blob/" + "0" * 64).status_code) self.assertEqual(404, self.client.get("/blob/nonsense").status_code) def test_a_blob_serves_byte_ranges(self): # NOT AN OPTIMISATION. A