import base64 import hashlib import json import math import re import struct import subprocess import sys from collections.abc import Mapping from copy import deepcopy from dataclasses import FrozenInstanceError, asdict, is_dataclass, replace from pathlib import Path from types import MappingProxyType from types import SimpleNamespace import pytest from jsonschema import Draft202012Validator from referencing import Registry, Resource from referencing.jsonschema import DRAFT202012 import simplecadapi.scene.archive as scene_archive from simplecadapi.scene import ( BASE_LIMITS, ConnectorBindingDocument, EntityDocument, NormalizedProductDocument, PresentationDocument, SCHEMA_FILES, SceneContractError, SceneDocument, SceneResourceLimits, canonical_json_bytes, canonical_zip_bytes, parse_canonical_json, parse_strict_json, preflight_aggregate_compression_ratio, preflight_archive_member_sizes, preflight_glb, preflight_glb_counts, preflight_input_archive_size, preflight_member_compression_ratio, preflight_zip_bytes, profile_cross, profile_f32_bits, profile_normalize, validate_entity_asset, validate_connector_binding, validate_normalized_product, validate_presentation, validate_scene_manifest, validate_scene_package, json_resource_issues, resource_count_issues, with_scene_revision, ) from simplecadapi.scene.validation import ( _compute_package_budget_totals, _issue, _package_budget_issues, _report, _validate_rule_registry, load_contract_artifact, ) from scene_contract_corpus_support import ( apply_scene_field_case, corpus_blobs, scene_shape_facts, ) IDENTITY = { "origin": [0, 0, 0], "x_axis": [1, 0, 0], "y_axis": [0, 1, 0], "z_axis": [0, 0, 1], } ASSET_TO_SCENE = [1000, 0, 0, 0, 0, 0, -1000, 0, 0, 1000, 0, 0, 0, 0, 0, 1] def _f32(value): result = struct.unpack("d", bytes.fromhex(value))[0] def _f64_hex(value): return struct.pack(">d", value).hex() def _first_report_issue(report): issue = report.first_error return None if issue is None else {"code": issue.code, "path": issue.path} def _replace_glb_json(glb, mutate): json_length = struct.unpack_from(" 1: occurrences = [ node for node in scene["nodes"] if node["definition_id"] == definition_id ] assert len(occurrences) == count assert len({node["definition_id"] for node in occurrences}) == 1 assert definitions[definition_id]["geometry_asset_id"] is not None def test_python_matches_shared_two_pass_revision_vectors(): for vector in _load_shared_corpus()["revision_vectors"]: draft_bytes = _decode_base64(vector["draft_base64"]) draft = parse_canonical_json(draft_bytes) scene_bytes = _decode_base64(vector["canonical_base64"]) scene = with_scene_revision(draft) assert canonical_json_bytes(draft) == draft_bytes, vector["name"] assert scene["revision"] == vector["revision"], vector["name"] assert canonical_json_bytes(scene) == scene_bytes, vector["name"] assert "sha256:" + hashlib.sha256(scene_bytes).hexdigest() == vector["sha256"] def test_python_matches_all_shared_glb_and_zip_cases(): corpus = _load_shared_corpus() for case in corpus["glb_cases"]: payload = _decode_base64(case["payload_base64"]) if case["valid"]: assert preflight_glb(payload, expected_kind=case["expected_kind"]).kind == case["kind"] else: with pytest.raises(ValueError, match="^" + re.escape(case["error"]) + "$"): preflight_glb(payload, expected_kind=case["expected_kind"]) for case in corpus["zip_cases"]: payload = _decode_base64(case["payload_base64"]) if case["valid"]: assert preflight_zip_bytes(payload).used_deflate is case["used_deflate"] else: with pytest.raises(ValueError, match="^" + re.escape(case["error"]) + "$"): preflight_zip_bytes(payload) def test_python_matches_shared_numeric_profile_vectors(): for vector in _load_shared_corpus()["numeric_vectors"]: try: if vector["operation"] == "f32": actual = f"{profile_f32_bits(_f64_from_hex(vector['input_bits'][0])):08x}" elif vector["operation"] == "cross": left, right = ( tuple(_f64_from_hex(component) for component in value) for value in vector["input_bits"] ) actual = [_f64_hex(component) for component in profile_cross(left, right)] elif vector["operation"] == "normalize": value = tuple( _f64_from_hex(component) for component in vector["input_bits"][0] ) actual = [ f"{profile_f32_bits(component):08x}" for component in profile_normalize(value) ] else: raise AssertionError(vector["operation"]) except ValueError as exc: assert not vector["valid"], vector["name"] assert str(exc) == vector["error"], vector["name"] else: assert vector["valid"], vector["name"] assert actual == vector["expected_bits"], vector["name"] def test_python_matches_shared_resource_boundaries(): corpus = _load_shared_corpus() assert asdict(BASE_LIMITS) == corpus["resource_limits"] valid_scene = parse_canonical_json( _decode_base64(corpus["artifacts"]["scene"]["base64"]) ) for case in corpus["resource_cases"]: operation = case["operation"] parameters = case["parameters"] limits = replace(BASE_LIMITS, **parameters.get("limits", {})) try: if operation == "input_archive_size": preflight_input_archive_size(parameters["size"], limits=limits) valid, error, first = True, None, None elif operation in {"archive_member_sizes", "archive_member_count"}: if operation == "archive_member_sizes": sizes = parameters["sizes"] else: sizes = {"scene.json": 0} sizes.update( { f"x/{index:05d}": 0 for index in range(parameters["count"] - 1) } ) preflight_archive_member_sizes(sizes, limits=limits) valid, error, first = True, None, None elif operation in { "aggregate_compression_ratio", "member_compression_ratio", }: callback = ( preflight_aggregate_compression_ratio if operation == "aggregate_compression_ratio" else preflight_member_compression_ratio ) callback( parameters["uncompressed_size"], parameters["compressed_size"], limits=limits, ) valid, error, first = True, None, None elif operation == "json_depth": value = {} current = value for _index in range(parameters["depth"]): current["x"] = {} current = current["x"] issues = json_resource_issues(value, limits=limits) valid = not issues error = None first = ( None if not issues else {"code": issues[0].code, "path": issues[0].path} ) elif operation == "json_domain": text = parameters["text"] kind = parameters["kind"] field = parameters.get("field") if kind == "value": value = {"value": text} elif kind == "object_key": value = {text: 0} elif kind == "metadata_key": value = {"metadata": {text: 0}} elif kind == "sdk_metadata_key": value = {"sdk_metadata": {text: 0}} elif kind == "identifier": value = {field or "node_id": text} elif kind == "identifier_array": value = {field or "component_path": [text]} elif kind == "uri": value = {"uri": text} else: raise AssertionError(kind) issues = json_resource_issues(value, limits=limits) valid = not issues error = None first = ( None if not issues else {"code": issues[0].code, "path": issues[0].path} ) elif operation == "resource_count": count = parameters["count"] kind = parameters["kind"] field = parameters.get("field") if kind == "collection": value = {field: [None] * count} elif kind == "hierarchy": value = { "nodes": [ {"source": {"component_path": ["x"] * count}} ] } elif kind == "forwarded": value = { "connectors": [ { "anchor_kind": "forwarded", "connector_snapshot_id": f"c{index}", "forwarded_from": { "source_connector_snapshot_id": f"c{index + 1}" }, } for index in range(count) ] } else: raise AssertionError(kind) issues = resource_count_issues( value, parameters["artifact"], limits=limits ) valid = not issues error = None first = ( None if not issues else {"code": issues[0].code, "path": issues[0].path} ) elif operation == "glb_counts": preflight_glb_counts( parameters["kind"], parameters["vertex_count"], parameters["index_count"], limits=limits, ) valid, error, first = True, None, None elif operation == "package_budget": contributions = parameters["contributions"] totals = _compute_package_budget_totals(**contributions) result = _report( _package_budget_issues(totals, limits=limits), artifact="package", ) valid, error, first = result.valid, None, _first_report_issue(result) elif operation == "scene_geometry_byte_length": value = deepcopy(valid_scene) byte_length = int(parameters["value"]) value["geometry_assets"][0]["byte_length"] = byte_length if byte_length <= 9_007_199_254_740_991: value = with_scene_revision(value) result = validate_scene_manifest(value) valid, error, first = result.valid, None, _first_report_issue(result) elif operation == "scene_compile_option": value = deepcopy(valid_scene) field, number = parameters["field"], parameters["value"] value["compile_options"][field] = number value["geometry_assets"][0]["tessellation"][field] = number if field == "linear_tolerance": value["edge_assets"][0]["tessellation"][field] = number result = validate_scene_manifest(with_scene_revision(value)) valid, error, first = result.valid, None, _first_report_issue(result) else: raise AssertionError(operation) except ValueError as exc: valid, error, first = False, str(exc), None assert valid == case["valid"], case["name"] if "error" in case: assert error == case["error"], case["name"] if case.get("expected") is not None: assert first == case["expected"], case["name"] def test_shared_exact_byte_artifacts_and_jcs_vectors(): corpus = _load_shared_corpus() for artifact in corpus["artifacts"].values(): payload = _decode_base64(artifact["base64"]) assert "sha256:" + hashlib.sha256(payload).hexdigest() == artifact["sha256"] for vector in corpus["jcs_vectors"]: payload = canonical_json_bytes(vector["input"]) assert payload == _decode_base64(vector["canonical_base64"]) assert "sha256:" + hashlib.sha256(payload).hexdigest() == vector["sha256"] archive = preflight_zip_bytes(_decode_base64(corpus["artifacts"]["canonical_zip"]["base64"])) assert canonical_zip_bytes(archive.members) == _decode_base64( corpus["artifacts"]["canonical_zip"]["base64"] ) def _apply_schema_mutations(base, mutations): value = deepcopy(base) for mutation in mutations: current = value for part in mutation["path"][:-1]: current = current[part] field = mutation["path"][-1] if mutation["operation"] == "delete": del current[field] else: current[field] = deepcopy(mutation["value"]) return value def test_python_replays_all_closed_schema_record_field_matrices(): corpus = _load_shared_corpus() schemas = { artifact: parse_strict_json(load_contract_artifact(path)) for artifact, path in SCHEMA_FILES.items() } for matrix in corpus["schema_field_matrices"]: root = schemas[matrix["artifact"]] pointer = matrix["schema_pointer"] schema = ( root if pointer == "#" else { "$schema": "https://json-schema.org/draft/2020-12/schema", "$ref": pointer, "$defs": root["$defs"], } ) validator = Draft202012Validator(schema) for case in matrix["cases"]: value = _apply_schema_mutations(matrix["base"], case["mutations"]) assert validator.is_valid(value) is case["valid"], ( matrix["artifact"], pointer, matrix["variant"], case["name"], ) def _valid_document_payloads(): corpus = _load_shared_corpus() return ( ("scene", SceneDocument, _decode_base64(corpus["artifacts"]["scene"]["base64"])), ("entities", EntityDocument, _decode_base64(corpus["artifacts"]["entities"]["base64"])), ( "presentation", PresentationDocument, _decode_base64(next(case for case in corpus["presentation_cases"] if case["valid"])["payload_base64"]), ), ( "connector_binding", ConnectorBindingDocument, _decode_base64(next(case for case in corpus["connector_binding_cases"] if case["valid"])["payload_base64"]), ), ( "normalized_product", NormalizedProductDocument, _decode_base64(next(case for case in corpus["normalized_product_cases"] if case["valid"])["payload_base64"]), ), ) def _assert_deeply_immutable(value): if isinstance(value, Mapping): assert isinstance(value, MappingProxyType) for child in value.values(): _assert_deeply_immutable(child) elif isinstance(value, tuple): for child in value: _assert_deeply_immutable(child) else: assert not isinstance(value, (dict, list)) def _nested_container(value): for child in value.values(): if isinstance(child, (Mapping, tuple, dict, list)): return child raise AssertionError("fixture has no nested container") def test_immutable_documents_construct_parse_validate_and_round_trip_canonically(): for name, document_type, payload in _valid_document_payloads(): mutable = parse_canonical_json(payload) constructed = document_type.from_value(mutable) parsed = document_type.parse(payload) assert is_dataclass(constructed), name assert constructed.canonical_bytes == payload, name assert parsed.canonical_bytes == payload, name assert constructed.canonical_hash == "sha256:" + hashlib.sha256(payload).hexdigest(), name assert constructed == parsed, name assert bytes(constructed) == payload, name assert document_type.from_value(constructed.value) == constructed, name assert document_type.from_value(constructed.to_mutable()) == constructed, name def test_immutable_documents_resist_nested_mutation_and_isolate_mutable_exports(): for name, document_type, payload in _valid_document_payloads(): source = parse_canonical_json(payload) document = document_type.from_value(source) expected_bytes = document.canonical_bytes _assert_deeply_immutable(document.value) with pytest.raises(TypeError): document.value["mutation"] = True frozen_nested = _nested_container(document.value) with pytest.raises((TypeError, AttributeError)): if isinstance(frozen_nested, Mapping): frozen_nested["mutation"] = True else: frozen_nested.append(None) with pytest.raises(FrozenInstanceError): document._canonical_bytes = b"mutated" source["mutation"] = True first_copy = document.to_mutable() second_copy = document.to_mutable() mutable_nested = _nested_container(first_copy) if isinstance(mutable_nested, dict): mutable_nested["mutation"] = True else: mutable_nested.append(None) first_copy["mutation"] = True assert "mutation" not in document.value, name assert "mutation" not in second_copy, name assert first_copy != second_copy, name assert document.canonical_bytes == expected_bytes, name def test_immutable_document_constructors_reject_invalid_values_and_stale_revisions(): for _name, document_type, _payload in _valid_document_payloads(): with pytest.raises(SceneContractError): document_type.from_value({}) with pytest.raises(ValueError, match="canonical"): SceneDocument.parse(b'{ "schema_version": "1.0" }') corpus = _load_shared_corpus() scene = parse_canonical_json(_decode_base64(corpus["artifacts"]["scene"]["base64"])) scene["revision"] = "sha256:" + "0" * 64 with pytest.raises(SceneContractError) as error: SceneDocument.from_value(scene) assert error.value.report.first_error.code == "revision_mismatch"