GuestOps/tests/test_gate_b_bundle.py

142 lines
6.8 KiB
Python

import contextlib
import hashlib
import importlib.util
import json
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("gate_b_bundle", ROOT / "deploy" / "gate_b_bundle.py")
bundle = importlib.util.module_from_spec(spec)
spec.loader.exec_module(bundle)
COMMIT = "a" * 40
API = {"reference": f"guestops-api:{COMMIT}", "id": "sha256:" + "b" * 64}
WORKER = {"reference": f"guestops-worker:{COMMIT}", "id": "sha256:" + "c" * 64}
ORIGIN = "https://sandbox-guestops.futuresens.co.uk"
class GateBBundleTests(unittest.TestCase):
def fixture(self, directory: str):
root = Path(directory)
archive = root / "GuestOps-0.2.1-aaaaaaaaaaaa.tar.gz"
source = root / "GuestOps-0.2.1-aaaaaaaaaaaa.source.json"
release = root / "release-record.json"
metrics = root / "host-metrics.json"
archive.write_bytes(b"approved source")
artifact = {
"name": archive.name,
"size": archive.stat().st_size,
"sha256": hashlib.sha256(archive.read_bytes()).hexdigest(),
}
source.write_text(json.dumps({"schemaVersion": 1, "version": "0.2.1", "commit": COMMIT, "artifact": artifact}), encoding="utf-8")
release.write_text(json.dumps({
"schemaVersion": 1, "version": "0.2.1", "commit": COMMIT,
"artifact": artifact, "images": {"api": API, "worker": WORKER},
}), encoding="utf-8")
release_sha = hashlib.sha256(release.read_bytes()).hexdigest()
metrics.write_bytes(b'{"cpu":40,"memory":35}')
values = {}
paths = {}
for name in bundle.RECORD_KEYS:
value = {"releaseCommit": COMMIT, "releaseRecordSha256": release_sha}
if name in {
"debian-host", "persistence", "backup-restore", "google-mailbox",
"automation", "identity-privacy", "inbox-usability", "incident-support",
"pilot-findings",
}:
value["environment"] = ORIGIN
if name in {"debian-host", "persistence", "backup-restore"}:
value["archiveSha256"] = artifact["sha256"]
value["images"] = {"api": API, "worker": WORKER}
values[name] = value
values["capacity"].update({
"schemaVersion": 1, "kind": "guestops-read-only-capacity",
"originHost": "sandbox-guestops.futuresens.co.uk",
"paths": ["/health/ready", "/api/hotel", "/api/conversations/page"],
"concurrency": 10, "requests": 100, "successes": 100, "failures": 0,
"errorRate": 0.0, "latencyMs": {"median": 100, "p95": 250, "maximum": 300},
})
pilot_path = root / "pilot-findings.json"
pilot_path.write_text(json.dumps(values["pilot-findings"]), encoding="utf-8")
paths["pilot-findings"] = pilot_path
capacity_path = root / "capacity.json"
capacity_path.write_text(json.dumps(values["capacity"]), encoding="utf-8")
paths["capacity"] = capacity_path
values["pilot-approval"].update({
"capacity": {
"reportSha256": hashlib.sha256(capacity_path.read_bytes()).hexdigest(),
"hostMetricsSha256": hashlib.sha256(metrics.read_bytes()).hexdigest(),
"observedConcurrency": 10, "observedP95Ms": 250, "observedErrorRate": 0.0,
},
"pilot": {"recordSha256": hashlib.sha256(pilot_path.read_bytes()).hexdigest()},
})
for name in bundle.RECORD_KEYS - paths.keys():
path = root / f"{name}.json"
path.write_text(json.dumps(values[name]), encoding="utf-8")
paths[name] = path
records = {name: (paths[name], values[name]) for name in bundle.RECORD_KEYS}
return archive, source, release, metrics, records
@contextlib.contextmanager
def mocked_individual_validators(self):
patches = [
patch.object(bundle.debian_acceptance, "validate_pair"),
patch.object(bundle.backup_restore_acceptance, "validate"),
patch.object(bundle.google_acceptance, "validate"),
patch.object(bundle.automation_acceptance, "validate"),
patch.object(bundle.identity_privacy_acceptance, "validate"),
patch.object(bundle.desktop_acceptance, "validate"),
patch.object(bundle.incident_exercise, "validate"),
patch.object(bundle.pilot_run, "validate"),
patch.object(bundle.pilot_approval, "validate"),
]
with contextlib.ExitStack() as stack:
for item in patches:
stack.enter_context(item)
yield
def test_complete_bundle_is_bound_to_one_release(self):
with tempfile.TemporaryDirectory() as directory, self.mocked_individual_validators():
archive, source, release, metrics, records = self.fixture(directory)
result = bundle.validate_bundle(archive, source, release, records, metrics)
self.assertTrue(result["validated"])
self.assertEqual(result["releaseCommit"], COMMIT)
self.assertEqual(set(result["records"]), bundle.RECORD_KEYS)
def test_different_environment_or_checksum_is_rejected(self):
with tempfile.TemporaryDirectory() as directory, self.mocked_individual_validators():
archive, source, release, metrics, records = self.fixture(directory)
records["google-mailbox"][1]["environment"] = "https://other.example.invalid"
with self.assertRaisesRegex(ValueError, "different environments"):
bundle.validate_bundle(archive, source, release, records, metrics)
with tempfile.TemporaryDirectory() as directory, self.mocked_individual_validators():
archive, source, release, metrics, records = self.fixture(directory)
records["pilot-approval"][1]["capacity"]["reportSha256"] = "0" * 64
with self.assertRaisesRegex(ValueError, "capacity report checksum"):
bundle.validate_bundle(archive, source, release, records, metrics)
def test_capacity_counts_and_latency_are_checked(self):
report = {
"schemaVersion": 1, "kind": "guestops-read-only-capacity",
"releaseCommit": COMMIT, "releaseRecordSha256": "d" * 64,
"paths": ["/health/ready", "/api/hotel", "/api/conversations/page"],
"concurrency": 10, "requests": 10, "successes": 9, "failures": 1,
"errorRate": 0.1, "latencyMs": {"median": 10, "p95": 20, "maximum": 30},
}
bundle.validate_capacity(report, COMMIT, "d" * 64)
report["failures"] = 2
with self.assertRaisesRegex(ValueError, "do not match requests"):
bundle.validate_capacity(report, COMMIT, "d" * 64)
if __name__ == "__main__":
unittest.main()