Files
cadSet/SimpleCADAPI/tools/generate_scene_contract_fixtures.py

481 lines
17 KiB
Python

"""Regenerate immutable cross-language Scene 1.0 contract vectors."""
from __future__ import annotations
import argparse
import json
import sys
from copy import deepcopy
from dataclasses import asdict
from pathlib import Path
from simplecadapi.scene import (
BASE_LIMITS,
canonical_json_bytes,
validate_connector_binding,
validate_entity_asset,
validate_normalized_product,
validate_presentation,
validate_scene_manifest,
validate_scene_package,
with_scene_revision,
)
from scene_contract_fixtures.authored_cases import (
build_connector_binding_matrix_cases,
build_normalized_product_matrix_cases,
build_presentation_matrix_cases,
)
from scene_contract_fixtures.base_scene import build_scene_package
from scene_contract_fixtures.binary_cases import build_binary_cases
from scene_contract_fixtures.common import b64, content_hash, first_issue, report_case
from scene_contract_fixtures.entity_cases import build_entity_matrix_cases
from scene_contract_fixtures.numeric_cases import build_numeric_vectors
from scene_contract_fixtures.package_cases import build_package_matrix_cases
from scene_contract_fixtures.resource_cases import build_resource_cases
from scene_contract_fixtures.schema_field_matrix import build_schema_field_matrices
from scene_contract_fixtures.scene_corpus import (
build_revision_vectors,
build_scene_field_cases,
build_scene_shape_cases,
)
ROOT = Path(__file__).resolve().parents[1]
OUTPUT = ROOT / "test" / "fixtures" / "scene-contract" / "corpus.json"
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--check", action="store_true", help="fail if output is stale")
args = parser.parse_args()
scene, scene_bytes, entity, entity_bytes, blobs = build_scene_package()
blob_pool = {uri: b64(payload) for uri, payload in blobs.items()}
scene_shape_cases, scene_packages = build_scene_shape_cases(
scene, entity, blobs, blob_pool
)
scene_field_cases = build_scene_field_cases(scene)
geometry_uri = next(uri for uri in blobs if uri.startswith("geometry/"))
edge_uri = next(uri for uri in blobs if uri.startswith("edges/"))
entity_uri = next(uri for uri in blobs if uri.startswith("entities/"))
triangle_glb = blobs[geometry_uri]
line_glb = blobs[edge_uri]
canonical_zip, deflate_zip, glb_cases, zip_cases = build_binary_cases(
triangle_glb,
line_glb,
{"scene.json": scene_bytes, **blobs},
)
missing_nodes = deepcopy(scene)
del missing_nodes["nodes"]
missing_nodes_bytes = canonical_json_bytes(missing_nodes)
empty_nodes = with_scene_revision({**scene, "nodes": []})
empty_nodes_bytes = canonical_json_bytes(empty_nodes)
revision_mismatch = deepcopy(scene)
revision_mismatch["revision"] = "sha256:" + "0" * 64
revision_mismatch_bytes = canonical_json_bytes(revision_mismatch)
transform_invalid = deepcopy(scene)
transform_invalid["nodes"][0]["transform"]["x_axis"] = [2, 0, 0]
transform_invalid = with_scene_revision(transform_invalid)
transform_invalid_bytes = canonical_json_bytes(transform_invalid)
nonunit_entity = deepcopy(entity)
next(item for item in nonunit_entity["entities"] if item["kind"] == "edge")[
"geometry"
]["direction"] = [2, 0, 0]
nonunit_entity_bytes = canonical_json_bytes(nonunit_entity)
unordered_entity = deepcopy(entity)
shared_edge = next(
item
for item in unordered_entity["entities"]
if item["kind"] == "edge" and len(item["parent_entity_ids"]) == 2
)
shared_edge["parent_entity_ids"].reverse()
unordered_entity_bytes = canonical_json_bytes(unordered_entity)
presentation = {
"appearances": [
{
"alpha_mode": "opaque",
"base_color": [0.2, 0.3, 0.4, 1],
"double_sided": False,
"edge_color": [0.05, 0.05, 0.05, 1],
"metallic": 0.1,
"name": "fixture",
"roughness": 0.7,
}
],
"cameras": [
{
"far": 10000,
"name": "overview",
"near": 1,
"parent_node_id": None,
"projection": "perspective",
"transform": {
"origin": [2000, -2000, 2000],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
},
"vertical_fov_degrees": 45,
}
],
"node_overrides": [
{
"appearance_name": "fixture",
"node_id": "instance/root",
"visible": True,
}
],
"presentation_id": "fixture",
"schema_version": "1.0",
"source_scene_id": "fixture",
}
presentation_bytes = canonical_json_bytes(presentation)
missing_appearance = deepcopy(presentation)
missing_appearance["node_overrides"][0]["appearance_name"] = "missing"
missing_appearance_bytes = canonical_json_bytes(missing_appearance)
bad_camera = deepcopy(presentation)
bad_camera["cameras"][0]["far"] = 1
bad_camera_bytes = canonical_json_bytes(bad_camera)
connector_binding = {
"binding_id": "fixture",
"connector_id": "fixture_connector",
"name": None,
"owner_definition_id": "definition/root/part/fixture",
"schema_version": "1.0",
"selected_occurrence_node_id": "instance/root",
"source_model": {
"artifact_hash": "sha256:" + "1" * 64,
"graph_id": "fixture_graph",
"model_schema_version": "2.0",
},
"source_scene": {"revision": scene["revision"], "scene_id": "fixture"},
"target": {
"entity_asset_id": entity["geometry_asset_id"],
"entity_id": "entity/face/0",
"expected_source": {
"graph_id": "fixture_graph",
"kind": "model_output",
"node_id": "fixture_body",
"output_slot": 0,
},
"flip": True,
"kind": "topology_entity",
},
}
connector_binding_bytes = canonical_json_bytes(connector_binding)
wrong_binding_graph = deepcopy(connector_binding)
wrong_binding_graph["target"]["expected_source"]["graph_id"] = "other_graph"
wrong_binding_graph_bytes = canonical_json_bytes(wrong_binding_graph)
vertex_flip = deepcopy(connector_binding)
vertex_flip["target"]["entity_id"] = "entity/vertex/0"
vertex_flip_bytes = canonical_json_bytes(vertex_flip)
normalized_part = {
"body_source": {"kind": "manual", "source_id": "fixture_body"},
"connectors": [],
"kind": "part",
"material": None,
"metadata": {},
"name": None,
"part_id": "fixture_part",
}
normalized_part_bytes = canonical_json_bytes(normalized_part)
normalized_assembly = {
"assembly_id": "fixture_assembly",
"components": [
{
"component_id": "component_a",
"definition_ref": "definition/root/part/fixture",
"local_placement": {
"origin": [0, 0, 0],
"x_axis": [1, 0, 0],
"y_axis": [0, 1, 0],
"z_axis": [0, 0, 1],
},
"name": None,
}
],
"connectors": [],
"constraints": [],
"grounded_component_ids": ["component_a"],
"kind": "assembly",
"metadata": {},
"name": None,
}
normalized_assembly_bytes = canonical_json_bytes(normalized_assembly)
missing_grounded_component = deepcopy(normalized_assembly)
missing_grounded_component["grounded_component_ids"] = ["missing"]
missing_grounded_component_bytes = canonical_json_bytes(missing_grounded_component)
duplicate_components = deepcopy(normalized_assembly)
duplicate_components["components"].append(
deepcopy(duplicate_components["components"][0])
)
duplicate_components_bytes = canonical_json_bytes(duplicate_components)
bad_bounds = deepcopy(scene)
bad_bounds["geometry_assets"][0]["scene_local_bounds"]["max"][0] = 999
bad_bounds = with_scene_revision(bad_bounds)
bad_bounds_bytes = canonical_json_bytes(bad_bounds)
missing_blob = dict(blobs)
del missing_blob[edge_uri]
mutated_blobs = dict(blobs)
mutated_blobs[geometry_uri] = bytes([triangle_glb[0] ^ 1]) + triangle_glb[1:]
resource_cases = build_resource_cases(
scene,
normalized_part,
)
manifest_cases = [
report_case("valid_scene", scene_bytes, validate_scene_manifest(scene_bytes)),
report_case(
"missing_nodes",
missing_nodes_bytes,
validate_scene_manifest(missing_nodes_bytes),
),
report_case(
"empty_nodes",
empty_nodes_bytes,
validate_scene_manifest(empty_nodes_bytes),
),
report_case(
"revision_mismatch",
revision_mismatch_bytes,
validate_scene_manifest(revision_mismatch_bytes),
),
report_case(
"transform_invalid",
transform_invalid_bytes,
validate_scene_manifest(transform_invalid_bytes),
),
report_case(
"noncanonical_json",
scene_bytes + b"\n",
validate_scene_manifest(scene_bytes + b"\n"),
),
report_case(
"duplicate_json_key",
b'{"schema_version":"1.0","schema_version":"1.0"}',
validate_scene_manifest(
b'{"schema_version":"1.0","schema_version":"1.0"}'
),
),
report_case(
"bom",
b"\xef\xbb\xbf" + scene_bytes,
validate_scene_manifest(b"\xef\xbb\xbf" + scene_bytes),
),
]
entity_cases = [
report_case(
"valid_entities", entity_bytes, validate_entity_asset(entity_bytes)
),
report_case(
"nonunit_analytic_direction",
nonunit_entity_bytes,
validate_entity_asset(nonunit_entity_bytes),
),
report_case(
"unordered_adjacency",
unordered_entity_bytes,
validate_entity_asset(unordered_entity_bytes),
),
]
entity_cases.extend(build_entity_matrix_cases(entity))
presentation_cases = [
report_case(
"valid_presentation",
presentation_bytes,
validate_presentation(presentation_bytes),
),
report_case(
"missing_presentation_appearance",
missing_appearance_bytes,
validate_presentation(missing_appearance_bytes),
),
report_case(
"invalid_presentation_camera",
bad_camera_bytes,
validate_presentation(bad_camera_bytes),
),
]
presentation_cases.extend(build_presentation_matrix_cases(presentation))
connector_binding_cases = [
report_case(
"valid_connector_binding",
connector_binding_bytes,
validate_connector_binding(connector_binding_bytes),
),
report_case(
"connector_binding_graph_mismatch",
wrong_binding_graph_bytes,
validate_connector_binding(wrong_binding_graph_bytes),
),
report_case(
"connector_binding_vertex_flip",
vertex_flip_bytes,
validate_connector_binding(vertex_flip_bytes),
),
]
connector_binding_cases.extend(
build_connector_binding_matrix_cases(connector_binding)
)
normalized_product_cases = [
report_case(
"valid_normalized_part",
normalized_part_bytes,
validate_normalized_product(normalized_part_bytes),
),
report_case(
"valid_normalized_assembly",
normalized_assembly_bytes,
validate_normalized_product(normalized_assembly_bytes),
),
report_case(
"missing_grounded_component",
missing_grounded_component_bytes,
validate_normalized_product(missing_grounded_component_bytes),
),
report_case(
"duplicate_components",
duplicate_components_bytes,
validate_normalized_product(duplicate_components_bytes),
),
]
normalized_product_cases.extend(
build_normalized_product_matrix_cases(normalized_part, normalized_assembly)
)
package_cases = [
{
"blob_uris": sorted(blobs),
"expected": first_issue(validate_scene_package(scene_bytes, blobs)),
"manifest_base64": b64(scene_bytes),
"name": "valid_package",
"valid": True,
},
{
"blob_uris": sorted(blobs),
"expected": first_issue(validate_scene_package(bad_bounds_bytes, blobs)),
"manifest_base64": b64(bad_bounds_bytes),
"name": "glb_bounds_mismatch",
"valid": False,
},
{
"blob_uris": sorted(missing_blob),
"expected": first_issue(validate_scene_package(scene_bytes, missing_blob)),
"manifest_base64": b64(scene_bytes),
"name": "missing_blob",
"valid": False,
},
{
"blob_mutations": {geometry_uri: b64(mutated_blobs[geometry_uri])},
"blob_uris": sorted(mutated_blobs),
"expected": first_issue(validate_scene_package(scene_bytes, mutated_blobs)),
"manifest_base64": b64(scene_bytes),
"name": "blob_hash_mismatch",
"valid": False,
},
]
package_cases.extend(
build_package_matrix_cases(
scene, entity, blobs, scene_packages, blob_pool
)
)
jcs_input = {
"literals": [None, True, False],
"numbers": [333333333.33333329, 1e30, 4.50, 2e-3, 1e-27],
"string": "€$\u000f\nA'B\"\\\\\"/",
}
jcs_bytes = canonical_json_bytes(jcs_input)
corpus = {
"artifacts": {
"canonical_zip": {
"base64": b64(canonical_zip),
"sha256": content_hash(canonical_zip),
},
"deflate_zip": {
"base64": b64(deflate_zip),
"sha256": content_hash(deflate_zip),
},
"edge_glb": {
"base64": b64(line_glb),
"sha256": content_hash(line_glb),
"uri": edge_uri,
},
"entities": {
"base64": b64(entity_bytes),
"sha256": content_hash(entity_bytes),
"uri": entity_uri,
},
"geometry_glb": {
"base64": b64(triangle_glb),
"sha256": content_hash(triangle_glb),
"uri": geometry_uri,
},
"scene": {
"base64": b64(scene_bytes),
"revision": scene["revision"],
"sha256": content_hash(scene_bytes),
},
},
"blobs": dict(sorted(blob_pool.items())),
"entity_cases": entity_cases,
"connector_binding_cases": connector_binding_cases,
"format_version": "1.0",
"glb_cases": glb_cases,
"jcs_vectors": [
{
"canonical_base64": b64(jcs_bytes),
"input": jcs_input,
"name": "rfc8785_number_and_string_sample",
"sha256": content_hash(jcs_bytes),
}
],
"manifest_cases": manifest_cases,
"numeric_vectors": build_numeric_vectors(),
"normalized_product_cases": normalized_product_cases,
"package_cases": package_cases,
"presentation_cases": presentation_cases,
"resource_cases": resource_cases,
"resource_limits": asdict(BASE_LIMITS),
"revision_vectors": build_revision_vectors(scene_packages),
"scene_field_cases": scene_field_cases,
"scene_shape_cases": scene_shape_cases,
"schema_field_matrices": build_schema_field_matrices(
ROOT / "src/simplecadapi/scene/contracts/schemas"
),
"zip_cases": zip_cases,
}
for collection in (
manifest_cases,
entity_cases,
presentation_cases,
connector_binding_cases,
normalized_product_cases,
package_cases,
):
for case in collection:
assert case["valid"] == (case["expected"] is None), case["name"]
payload = (
json.dumps(corpus, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
).encode("utf-8")
relative_output = OUTPUT.relative_to(ROOT)
if args.check:
if not OUTPUT.exists() or OUTPUT.read_bytes() != payload:
print(f"stale generated fixture: {relative_output}", file=sys.stderr)
return 1
print(f"{relative_output} is current ({len(payload)} bytes)")
return 0
OUTPUT.write_bytes(payload)
print(f"wrote {relative_output} ({len(payload)} bytes)")
return 0
if __name__ == "__main__":
raise SystemExit(main())