use std::collections::BTreeMap;
use animsmith_core::check::{Check, CheckCtx};
use animsmith_core::config::{CheckSettings, SeveritySetting};
use animsmith_core::glam::Mat4;
use animsmith_core::measure::{
AssetMeasurements, ClipMeasurements, FrameGridMeasurement, ImageMeasurements,
LinearTransformClassification, LoopEndpointMode, MeasurementAvailability,
MeshDefinitionMeasurements, SkeletonNodeLocalRestMeasurements,
SkeletonNodeLocalRestUnavailableReason, SkeletonRestWorldMatrixUnavailableReason,
SkinDerivedMatrixUnavailableReason, measure_linear_transform,
};
use animsmith_core::{
Bone, CheckEvaluation, CheckOutput, CheckSelection, Config, CoverageGap, CoverageGapCode,
Document, EvaluationScope, EvaluationScopeCode, Finding, ImageContainerFormat,
ImageUnavailableReason, InputIdentity, LintEnvelope, LintFileReport, MEASUREMENTS_SCHEMA_ID,
MEASUREMENTS_SCHEMA_VERSION, MaterialResourceCoverage, MaterialTextureSlot, MeasureEnvelope,
MeasureFileReport, MeasurementContract, MeasurementContractError, MeasurementFileError,
MeasurementReportError, MeasurementReportFile, MeasurementReportInput, MetricGrids,
OUTPUT_SCHEMA_ID, OUTPUT_SCHEMA_VERSION, OUTPUT_V10_MAX_CHECKS_PER_FILE, OUTPUT_V10_MAX_FILES,
OutputContractError, Property, ResolvedRoles, RigInfo, RigInfoError, Role, Severity,
SourceInverseBindAccessorStatus, SourceSkeletonCoverage, ToolInfo, ToolSource, Transform,
evaluate_checks, sha256_hex,
};
fn tool() -> ToolInfo {
ToolInfo::animsmith(ToolSource::new(None, None))
}
fn rig() -> RigInfo {
let doc = Document::default();
RigInfo::from_resolved(&doc, &ResolvedRoles::default())
.expect("empty roles match an empty document")
}
fn measurements() -> MeasurementContract {
MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default())
.expect("empty measurements are valid")
}
fn input(bytes: &[u8]) -> InputIdentity {
InputIdentity::from_bytes(bytes)
}
#[test]
fn sha256_hex_matches_published_vectors() {
assert_eq!(
sha256_hex(b""),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
);
assert_eq!(
sha256_hex(b"abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn input_identity_serializes_sha256_and_byte_count() {
let identity = input(b"abc");
assert_eq!(
identity.sha256(),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
assert_eq!(identity.bytes(), 3);
assert_eq!(
serde_json::to_value(identity).expect("identity serializes"),
serde_json::json!({
"sha256": "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"bytes": 3,
})
);
}
#[test]
fn command_specific_file_types_serialize_only_their_valid_shape() {
let measure = MeasureEnvelope::new(
tool(),
vec![MeasureFileReport::new(
"measure.glb",
input(b"measure input"),
rig(),
measurements(),
)],
)
.expect("bounded measure envelope");
let lint = LintEnvelope::new(
tool(),
vec![
LintFileReport::new(
"lint.glb",
input(b"lint input"),
rig(),
None,
Vec::new(),
measurements(),
)
.expect("bounded lint file"),
],
)
.expect("bounded lint envelope");
let measure = serde_json::to_value(measure).expect("measure envelope serializes");
let lint = serde_json::to_value(lint).expect("lint envelope serializes");
assert!(measure["files"][0].get("checks").is_none());
assert_eq!(lint["files"][0]["checks"], serde_json::json!([]));
assert_eq!(measure["files"][0]["input"]["bytes"], 13);
let input: MeasurementReportInput =
serde_json::from_value(measure).expect("current measure envelope deserializes");
assert!(
input
.into_files()
.expect("current measure envelope is accepted")
.first()
.expect("measure envelope has one file")
.measurements()
.clips()
.is_empty()
);
}
#[test]
fn output_v10_file_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
let file = MeasureFileReport::new("limit.glb", input(b"limit"), rig(), measurements());
let at_limit_files = vec![file; OUTPUT_V10_MAX_FILES];
let at_limit =
MeasureEnvelope::new(tool(), at_limit_files).expect("exact output-v10 file limit is valid");
let at_limit_wire = serde_json::to_value(&at_limit).expect("bounded envelope serializes");
let at_limit_read: MeasurementReportInput =
serde_json::from_value(at_limit_wire.clone()).expect("bounded envelope reads");
assert_eq!(at_limit_read.file_count(), Some(OUTPUT_V10_MAX_FILES));
assert_eq!(
at_limit_read
.into_files()
.expect("exact output-v10 file limit validates")
.len(),
OUTPUT_V10_MAX_FILES
);
let extra = MeasureFileReport::new("extra.glb", input(b"extra"), rig(), measurements());
assert_eq!(
MeasureEnvelope::new(tool(), vec![extra; OUTPUT_V10_MAX_FILES + 1])
.expect_err("N+1 output files must fail on write"),
OutputContractError::TooManyFiles {
found: OUTPUT_V10_MAX_FILES + 1,
limit: OUTPUT_V10_MAX_FILES,
}
);
let mut above_limit_wire = at_limit_wire;
let first = above_limit_wire["files"][0].clone();
above_limit_wire["files"]
.as_array_mut()
.expect("files array")
.push(first);
let above_limit_read: MeasurementReportInput =
serde_json::from_value(above_limit_wire).expect("N+1 shape remains readable");
assert_eq!(
above_limit_read
.into_files()
.expect_err("N+1 output files must fail on read"),
MeasurementReportError::TooManyFiles {
found: OUTPUT_V10_MAX_FILES + 1,
limit: OUTPUT_V10_MAX_FILES,
}
);
}
#[test]
fn output_v10_check_bound_accepts_n_and_rejects_n_plus_one_on_write_and_read() {
let check = CheckEvaluation::evaluated(
"limit",
CheckOutput::from_coverage(Vec::new(), Vec::new(), Vec::new()),
)
.expect("empty active check is valid");
let at_limit_file = LintFileReport::new(
"limit.glb",
input(b"limit"),
rig(),
None,
vec![check.clone(); OUTPUT_V10_MAX_CHECKS_PER_FILE],
measurements(),
)
.expect("exact output-v10 check limit is valid");
let at_limit = LintEnvelope::new(tool(), vec![at_limit_file])
.expect("exact output-v10 check limit forms an envelope");
let at_limit_wire = serde_json::to_value(&at_limit).expect("bounded envelope serializes");
let at_limit_read: MeasurementReportInput =
serde_json::from_value(at_limit_wire.clone()).expect("bounded envelope reads");
at_limit_read
.into_files()
.expect("exact output-v10 check limit validates");
assert_eq!(
LintFileReport::new(
"limit.glb",
input(b"limit"),
rig(),
None,
vec![check; OUTPUT_V10_MAX_CHECKS_PER_FILE + 1],
measurements(),
)
.expect_err("N+1 output checks must fail on write"),
OutputContractError::TooManyChecks {
found: OUTPUT_V10_MAX_CHECKS_PER_FILE + 1,
limit: OUTPUT_V10_MAX_CHECKS_PER_FILE,
}
);
let mut above_limit_wire = at_limit_wire;
let first = above_limit_wire["files"][0]["checks"][0].clone();
above_limit_wire["files"][0]["checks"]
.as_array_mut()
.expect("checks array")
.push(first);
let above_limit_read: MeasurementReportInput =
serde_json::from_value(above_limit_wire).expect("N+1 shape remains readable");
assert_eq!(
above_limit_read
.into_files()
.expect_err("N+1 output checks must fail on read"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::TooManyChecks {
found: OUTPUT_V10_MAX_CHECKS_PER_FILE + 1,
limit: OUTPUT_V10_MAX_CHECKS_PER_FILE,
},
}
);
}
struct DisabledCheck;
impl Check for DisabledCheck {
fn id(&self) -> &'static str {
"disabled"
}
fn evaluate(&self, _ctx: &CheckCtx<'_>) -> CheckOutput {
panic!("disabled checks must not evaluate")
}
}
fn disabled_evaluation() -> CheckEvaluation {
let doc = Document::default();
let roles = ResolvedRoles::default();
let config = Config {
checks: BTreeMap::from([(
"disabled".to_owned(),
CheckSettings {
severity: Some(SeveritySetting::Off),
..CheckSettings::default()
},
)]),
..Config::default()
};
let grids = MetricGrids::new(&doc);
let ctx = CheckCtx::new(&grids, &roles, &config);
evaluate_checks(&ctx, &[Box::new(DisabledCheck)], CheckSelection::All)
.expect("disabled check record is valid")
.remove(0)
}
#[test]
fn lint_summary_aggregates_every_axis_and_finding_bucket_across_files() {
let partial = CheckEvaluation::evaluated(
"partial",
CheckOutput::from_coverage(
vec![
Finding::new("partial", Severity::Error, "error"),
Finding::new("partial", Severity::Note, "note one"),
],
vec![EvaluationScope::new(EvaluationScopeCode::custom(
"test:completed",
))],
vec![CoverageGap::new(
CoverageGapCode::custom("test:missing"),
"missing evidence",
)],
),
)
.expect("partial record is valid");
let complete = CheckEvaluation::evaluated(
"complete",
CheckOutput::from_coverage(
vec![
Finding::new("complete", Severity::Warning, "warning"),
Finding::new("complete", Severity::Note, "note two"),
],
Vec::new(),
Vec::new(),
),
)
.expect("complete record is valid");
let report = LintEnvelope::new(
tool(),
vec![
LintFileReport::new(
"first.glb",
input(b"first input"),
rig(),
None,
vec![partial, disabled_evaluation()],
measurements(),
)
.expect("bounded first lint file"),
LintFileReport::new(
"second.glb",
input(b"second input"),
rig(),
None,
vec![complete],
measurements(),
)
.expect("bounded second lint file"),
],
)
.expect("bounded lint envelope");
let report = serde_json::to_value(report).expect("lint envelope serializes");
assert_eq!(
report["summary"],
serde_json::json!({
"files": 2,
"findings": { "error": 1, "warning": 1, "note": 2 },
"prediction_facets": {
"available": 0,
"required_prediction_unavailable": 0
},
"checks": {
"total": 3,
"selection": { "selected": 3, "unselected": 0 },
"configuration": { "enabled": 2, "disabled": 1 },
"applicability": { "applicable": 3, "not_applicable": 0 },
"evaluation": { "complete": 1, "partial": 1, "not_evaluated": 1 },
"gaps": 1,
},
})
);
}
fn current_measure_report() -> serde_json::Value {
serde_json::json!({
"schema_version": OUTPUT_SCHEMA_VERSION,
"schema": OUTPUT_SCHEMA_ID,
"tool": {},
"command": "measure",
"files": [{
"path": "clip.glb",
"input": {
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"bytes": 0,
},
"rig": {},
"measurements": {
"schema_version": MEASUREMENTS_SCHEMA_VERSION,
"schema": MEASUREMENTS_SCHEMA_ID,
"clips": {},
"material_resource_coverage": "unavailable",
"material_definitions": [],
"textures": [],
"images": [],
"skeleton_source_coverage": "unavailable",
"skeleton_nodes": [],
"skins": [],
"mesh_definitions": [],
"node_instances": [],
"scenes": [],
},
}],
})
}
fn measurement_report_error(value: serde_json::Value) -> MeasurementReportError {
let input: MeasurementReportInput =
serde_json::from_value(value).expect("test case remains structurally deserializable");
input
.into_files()
.expect_err("malformed report must be rejected")
}
#[test]
fn measurement_report_input_rejects_every_invalid_contract_branch() {
let base = current_measure_report();
let without = |pointer: &str| {
let mut value = base.clone();
let (parent, key) = pointer.rsplit_once('/').expect("JSON pointer has a key");
let object = if parent.is_empty() {
&mut value
} else {
value.pointer_mut(parent).expect("fixture path exists")
};
object
.as_object_mut()
.expect("path ends at an object")
.remove(key);
value
};
let mut future_output = base.clone();
future_output["schema_version"] = serde_json::json!(OUTPUT_SCHEMA_VERSION + 1);
let mut v2_output = base.clone();
v2_output["schema_version"] = serde_json::json!(2);
v2_output["schema"] = serde_json::json!("urn:animsmith:schema:output:2");
let mut v4_output = base.clone();
v4_output["schema_version"] = serde_json::json!(4);
v4_output["schema"] = serde_json::json!("urn:animsmith:schema:output:4");
let mut wrong_output_identity = base.clone();
wrong_output_identity["schema"] = serde_json::json!("urn:other:output");
let mut unsupported_command = base.clone();
unsupported_command["command"] = serde_json::json!("inspect");
let mut future_measurements = base.clone();
future_measurements["files"][0]["measurements"]["schema_version"] =
serde_json::json!(MEASUREMENTS_SCHEMA_VERSION + 1);
let mut wrong_measurement_identity = base.clone();
wrong_measurement_identity["files"][0]["measurements"]["schema"] =
serde_json::json!("urn:other:measurements");
let mut invalid_input_sha256 = base.clone();
invalid_input_sha256["files"][0]["input"]["sha256"] = serde_json::json!("ABCDEF");
let input_without_files: MeasurementReportInput = serde_json::from_value(without("/files"))
.expect("report without files remains structurally deserializable");
assert_eq!(input_without_files.file_count(), None);
let cases = vec![
(
"missing output version",
without("/schema_version"),
MeasurementReportError::MissingOutputVersion,
"report envelope has no `schema_version`".to_owned(),
),
(
"unsupported output version",
future_output,
MeasurementReportError::UnsupportedOutputVersion {
found: OUTPUT_SCHEMA_VERSION + 1,
},
format!(
"has schema_version {}; this build reads schema_version {OUTPUT_SCHEMA_VERSION}",
OUTPUT_SCHEMA_VERSION + 1
),
),
(
"retired v2 output",
v2_output,
MeasurementReportError::UnsupportedOutputVersion { found: 2 },
format!(
"has schema_version 2; this build reads schema_version {OUTPUT_SCHEMA_VERSION}"
),
),
(
"retired v4 output",
v4_output,
MeasurementReportError::UnsupportedOutputVersion { found: 4 },
format!(
"has schema_version 4; this build reads schema_version {OUTPUT_SCHEMA_VERSION}"
),
),
(
"wrong output identity",
wrong_output_identity,
MeasurementReportError::WrongOutputIdentity,
format!("report envelope does not identify output contract {OUTPUT_SCHEMA_ID}"),
),
(
"missing command",
without("/command"),
MeasurementReportError::MissingCommand,
"report envelope has no `command`".to_owned(),
),
(
"unsupported command",
unsupported_command,
MeasurementReportError::UnsupportedCommand {
command: "inspect".into(),
},
"report command \"inspect\" does not carry measurement file records".to_owned(),
),
(
"missing files",
without("/files"),
MeasurementReportError::MissingFiles,
"report envelope has no `files` array".to_owned(),
),
(
"missing path",
without("/files/0/path"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingPath,
},
"files[0] has no `path`".to_owned(),
),
(
"missing input",
without("/files/0/input"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingInput,
},
"files[0] has no `input`".to_owned(),
),
(
"missing input sha256",
without("/files/0/input/sha256"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingSha256,
},
"files[0] input has no `sha256`".to_owned(),
),
(
"missing input bytes",
without("/files/0/input/bytes"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingBytes,
},
"files[0] input has no `bytes`".to_owned(),
),
(
"invalid input sha256",
invalid_input_sha256,
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::InvalidSha256,
},
"files[0] input `sha256` must be 64 lowercase hexadecimal characters".to_owned(),
),
(
"missing measurements",
without("/files/0/measurements"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingMeasurements,
},
"files[0] has no measurements".to_owned(),
),
(
"missing measurement version",
without("/files/0/measurements/schema_version"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingMeasurementVersion,
},
"files[0] has no versioned measurement contract".to_owned(),
),
(
"unsupported measurement version",
future_measurements,
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::UnsupportedMeasurementVersion {
found: MEASUREMENTS_SCHEMA_VERSION + 1,
},
},
format!(
"files[0] has measurement schema_version {}; this build reads measurement schema_version {MEASUREMENTS_SCHEMA_VERSION}",
MEASUREMENTS_SCHEMA_VERSION + 1
),
),
(
"wrong measurement identity",
wrong_measurement_identity,
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::WrongMeasurementIdentity,
},
format!("files[0] does not identify measurement contract {MEASUREMENTS_SCHEMA_ID}"),
),
(
"missing clips",
without("/files/0/measurements/clips"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingClips,
},
"files[0] measurement contract has no `clips` map".to_owned(),
),
(
"missing skeleton source coverage",
without("/files/0/measurements/skeleton_source_coverage"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingSkeletonSourceCoverage,
},
"files[0] measurement contract has no `skeleton_source_coverage`".to_owned(),
),
(
"missing skeleton nodes",
without("/files/0/measurements/skeleton_nodes"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingSkeletonNodes,
},
"files[0] measurement contract has no `skeleton_nodes` array".to_owned(),
),
(
"missing skins",
without("/files/0/measurements/skins"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingSkins,
},
"files[0] measurement contract has no `skins` array".to_owned(),
),
(
"missing mesh definitions",
without("/files/0/measurements/mesh_definitions"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingMeshDefinitions,
},
"files[0] measurement contract has no `mesh_definitions` array".to_owned(),
),
(
"missing node instances",
without("/files/0/measurements/node_instances"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingNodeInstances,
},
"files[0] measurement contract has no `node_instances` array".to_owned(),
),
(
"missing scenes",
without("/files/0/measurements/scenes"),
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::MissingScenes,
},
"files[0] measurement contract has no `scenes` array".to_owned(),
),
];
for (name, value, expected, expected_display) in cases {
let error = measurement_report_error(value);
assert_eq!(error, expected, "{name}");
let expected_file_index = match &expected {
MeasurementReportError::File { .. } => Some(0),
_ => None,
};
assert_eq!(error.file_index(), expected_file_index, "{name}");
assert_eq!(error.to_string(), expected_display, "{name}");
}
}
#[test]
fn measurement_report_input_rejects_finite_value_that_overflows_mesh_f32() {
let mut report = current_measure_report();
report["files"][0]["measurements"]["mesh_definitions"] = serde_json::json!([{
"mesh_index": 0,
"name": "overflow",
"vertex_count": 1,
"geometry_aabb": {
"min": [1e39, 0.0, 0.0],
"max": [1e39, 0.0, 0.0],
},
"max_joints_per_vertex": 0,
"additional_influence_sets": [],
}]);
let error = measurement_report_error(report);
assert_eq!(
error,
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::InvalidMeasurements {
source: MeasurementContractError::NonFiniteValue {
path: "mesh_definitions[0].geometry_aabb.min[0]".into(),
},
},
}
);
assert_eq!(error.file_index(), Some(0));
assert_eq!(
error.to_string(),
"files[0] has invalid measurements: measurement value mesh_definitions[0].geometry_aabb.min[0] must be finite"
);
}
#[test]
fn measurement_report_input_rejects_inconsistent_static_node_and_scene_evidence() {
let mut base = current_measure_report();
base["files"][0]["measurements"] = serde_json::json!({
"schema_version": MEASUREMENTS_SCHEMA_VERSION,
"schema": MEASUREMENTS_SCHEMA_ID,
"clips": {},
"material_resource_coverage": "unavailable",
"material_definitions": [],
"textures": [],
"images": [],
"skeleton_source_coverage": "unavailable",
"skeleton_nodes": [],
"skins": [],
"mesh_definitions": [{
"mesh_index": 0,
"name": "mesh",
"vertex_count": 3,
"geometry_aabb": {
"min": [0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0]
},
"max_joints_per_vertex": 0,
"additional_influence_sets": []
}],
"node_instances": [{
"node_index": 2,
"node_name": "node",
"mesh_index": 0,
"static_node_world_aabb": {
"min": [0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0]
}
}],
"scenes": [{
"scene_index": 4,
"instance_count": 1,
"static_scene_world_aabb": {
"min": [0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0]
},
"excluded_instance_count": 0
}],
"default_scene_index": 4
});
let mut duplicate_node = base.clone();
duplicate_node["files"][0]["measurements"]["node_instances"] = serde_json::json!([
base["files"][0]["measurements"]["node_instances"][0].clone(),
base["files"][0]["measurements"]["node_instances"][0].clone(),
]);
let mut dangling_mesh = base.clone();
dangling_mesh["files"][0]["measurements"]["node_instances"][0]["mesh_index"] =
serde_json::json!(99);
let mut unavailable_node_with_aabb = base.clone();
unavailable_node_with_aabb["files"][0]["measurements"]["node_instances"][0]["static_node_world_aabb_unavailable_reason"] =
serde_json::json!("non_finite_transform");
let mut inconsistent_scene_count = base.clone();
inconsistent_scene_count["files"][0]["measurements"]["scenes"][0]["excluded_instance_count"] =
serde_json::json!(2);
let mut inverted_node_aabb = base.clone();
inverted_node_aabb["files"][0]["measurements"]["node_instances"][0]["static_node_world_aabb"]
["min"][0] = serde_json::json!(2.0);
let mut dangling_default_scene = base;
dangling_default_scene["files"][0]["measurements"]["default_scene_index"] =
serde_json::json!(99);
let cases = [
(
"duplicate node identity",
duplicate_node,
"node_instances[1].node_index",
"node_index must be unique",
),
(
"dangling node mesh reference",
dangling_mesh,
"node_instances[0].mesh_index",
"mesh_index must reference a mesh definition",
),
(
"unavailable node with an AABB",
unavailable_node_with_aabb,
"node_instances[0]",
"an available static node AABB cannot have an unavailable reason",
),
(
"scene excludes more instances than it contains",
inconsistent_scene_count,
"scenes[0].excluded_instance_count",
"excluded_instance_count cannot exceed instance_count",
),
(
"node AABB has inverted corners",
inverted_node_aabb,
"node_instances[0].static_node_world_aabb.min[0]",
"AABB minimum cannot exceed maximum",
),
(
"dangling default scene reference",
dangling_default_scene,
"default_scene_index",
"default_scene_index must reference a declared scene",
),
];
for (name, report, path, reason) in cases {
let error = measurement_report_error(report);
assert_eq!(error.file_index(), Some(0), "{name}");
assert_eq!(
error,
MeasurementReportError::File {
file_index: 0,
source: MeasurementFileError::InvalidMeasurements {
source: MeasurementContractError::InvalidStructure {
path: path.into(),
reason: reason.into(),
},
},
},
"{name}",
);
}
}
#[test]
fn measurement_report_input_recovers_every_file_without_cardinality_policy() {
let mut report = current_measure_report();
report["files"] = serde_json::json!([
{
"path": ".\\Walk Assets\\..\\WALK.GLB",
"input": {
"sha256": "1111111111111111111111111111111111111111111111111111111111111111",
"bytes": 1,
},
"rig": {},
"measurements": {
"schema_version": MEASUREMENTS_SCHEMA_VERSION,
"schema": MEASUREMENTS_SCHEMA_ID,
"clips": { "walk": valid_clip_measurements() },
"material_resource_coverage": "unavailable",
"material_definitions": [],
"textures": [],
"images": [],
"skeleton_source_coverage": "unavailable",
"skeleton_nodes": [],
"skins": [],
"mesh_definitions": [],
"node_instances": [],
"scenes": [],
},
},
{
"path": "run.glb",
"input": {
"sha256": "2222222222222222222222222222222222222222222222222222222222222222",
"bytes": 2,
},
"rig": {},
"measurements": {
"schema_version": MEASUREMENTS_SCHEMA_VERSION,
"schema": MEASUREMENTS_SCHEMA_ID,
"clips": { "run": valid_clip_measurements() },
"material_resource_coverage": "unavailable",
"material_definitions": [],
"textures": [],
"images": [],
"skeleton_source_coverage": "unavailable",
"skeleton_nodes": [],
"skins": [],
"mesh_definitions": [valid_mesh_measurements()],
"node_instances": [],
"scenes": [],
},
},
{
"path": "middle.glb",
"input": {
"sha256": "3333333333333333333333333333333333333333333333333333333333333333",
"bytes": 3,
},
"rig": {},
"measurements": {
"schema_version": MEASUREMENTS_SCHEMA_VERSION,
"schema": MEASUREMENTS_SCHEMA_ID,
"clips": { "idle": valid_clip_measurements() },
"material_resource_coverage": "unavailable",
"material_definitions": [],
"textures": [],
"images": [],
"skeleton_source_coverage": "unavailable",
"skeleton_nodes": [],
"skins": [],
"mesh_definitions": [],
"node_instances": [],
"scenes": [],
},
},
]);
let expected_measurements = report["files"]
.as_array()
.expect("file records")
.iter()
.map(|file| file["measurements"].clone())
.collect::<Vec<_>>();
let input: MeasurementReportInput =
serde_json::from_value(report).expect("multi-file report deserializes");
assert_eq!(input.file_count(), Some(3));
let files: Vec<MeasurementReportFile> = input
.into_files()
.expect("multi-file report is consumer-neutral");
assert_eq!(files.len(), 3);
assert_eq!(
files
.iter()
.map(MeasurementReportFile::path)
.collect::<Vec<_>>(),
[".\\Walk Assets\\..\\WALK.GLB", "run.glb", "middle.glb"]
);
assert_eq!(
files
.iter()
.map(|file| (file.input().sha256(), file.input().bytes()))
.collect::<Vec<_>>(),
vec![
(
"1111111111111111111111111111111111111111111111111111111111111111",
1
),
(
"2222222222222222222222222222222222222222222222222222222222222222",
2
),
(
"3333333333333333333333333333333333333333333333333333333333333333",
3
),
]
);
assert_eq!(
files
.iter()
.map(|file| {
serde_json::to_value(file.measurements())
.expect("recovered measurement contract serializes")
})
.collect::<Vec<_>>(),
expected_measurements
);
type MeasurementParts = (BTreeMap<String, ClipMeasurements>, AssetMeasurements);
let into_parts: fn(MeasurementContract) -> MeasurementParts = MeasurementContract::into_parts;
let (run_clips, run_assets) = into_parts(
files
.into_iter()
.nth(1)
.expect("second recovered record")
.into_measurements(),
);
assert_eq!(
serde_json::to_value(run_clips).expect("recovered clips serialize"),
expected_measurements[1]["clips"]
);
assert_eq!(
serde_json::to_value(run_assets.mesh_definitions)
.expect("recovered mesh definitions serialize"),
expected_measurements[1]["mesh_definitions"]
);
let mut empty_report = current_measure_report();
empty_report["files"] = serde_json::json!([]);
let input: MeasurementReportInput =
serde_json::from_value(empty_report).expect("empty report deserializes");
assert_eq!(input.file_count(), Some(0));
assert!(
input
.into_files()
.expect("empty report has no core cardinality error")
.is_empty()
);
}
#[test]
fn measurement_report_input_identifies_invalid_file_without_cli_remediation() {
let base = current_measure_report();
let mut two_files = base.clone();
two_files["files"] = serde_json::json!([base["files"][0].clone(), base["files"][0].clone(),]);
let without = |pointer: &str| {
let mut report = two_files.clone();
let (parent, key) = pointer.rsplit_once('/').expect("JSON pointer has a key");
report
.pointer_mut(parent)
.expect("fixture path exists")
.as_object_mut()
.expect("path ends at an object")
.remove(key);
report
};
let mut future_measurements = two_files.clone();
future_measurements["files"][1]["measurements"]["schema_version"] =
serde_json::json!(MEASUREMENTS_SCHEMA_VERSION + 1);
let mut wrong_measurement_identity = two_files.clone();
wrong_measurement_identity["files"][1]["measurements"]["schema"] =
serde_json::json!("urn:other:measurements");
let cases = vec![
(
without("/files/1/path"),
MeasurementReportError::File {
file_index: 1,
source: MeasurementFileError::MissingPath,
},
"files[1] has no `path`".to_owned(),
),
(
without("/files/1/measurements"),
MeasurementReportError::File {
file_index: 1,
source: MeasurementFileError::MissingMeasurements,
},
"files[1] has no measurements".to_owned(),
),
(
without("/files/1/measurements/schema_version"),
MeasurementReportError::File {
file_index: 1,
source: MeasurementFileError::MissingMeasurementVersion,
},
"files[1] has no versioned measurement contract".to_owned(),
),
(
future_measurements,
MeasurementReportError::File {
file_index: 1,
source: MeasurementFileError::UnsupportedMeasurementVersion {
found: MEASUREMENTS_SCHEMA_VERSION + 1,
},
},
format!(
"files[1] has measurement schema_version {}; this build reads measurement schema_version {MEASUREMENTS_SCHEMA_VERSION}",
MEASUREMENTS_SCHEMA_VERSION + 1
),
),
(
wrong_measurement_identity,
MeasurementReportError::File {
file_index: 1,
source: MeasurementFileError::WrongMeasurementIdentity,
},
format!("files[1] does not identify measurement contract {MEASUREMENTS_SCHEMA_ID}"),
),
(
without("/files/1/measurements/clips"),
MeasurementReportError::File {
file_index: 1,
source: MeasurementFileError::MissingClips,
},
"files[1] measurement contract has no `clips` map".to_owned(),
),
];
for (report, expected, expected_display) in cases {
let error = measurement_report_error(report);
assert_eq!(error, expected);
assert_eq!(error.file_index(), Some(1));
assert_eq!(error.to_string(), expected_display);
}
}
#[test]
fn tool_source_drops_revision_text_outside_the_v7_schema() {
for invalid in ["f".repeat(39), "z".repeat(40), "f".repeat(41)] {
let source = ToolSource::new(Some(invalid), Some(true));
let json =
serde_json::to_value(ToolInfo::animsmith(source)).expect("tool identity serializes");
assert!(json["source"]["revision"].is_null());
assert_eq!(json["source"]["dirty"], true);
}
let revision = "0123456789abcdef0123456789abcdef01234567";
let source = ToolSource::new(Some(revision.into()), Some(false));
let json = serde_json::to_value(ToolInfo::animsmith(source)).expect("tool identity serializes");
assert_eq!(json["source"]["revision"], revision);
assert_eq!(json["source"]["dirty"], false);
}
#[test]
fn tool_info_uses_the_animsmith_core_package_version() {
let json = serde_json::to_value(ToolInfo::animsmith(ToolSource::new(None, None)))
.expect("tool identity serializes");
assert_eq!(json["name"], "animsmith");
assert_eq!(json["version"], env!("CARGO_PKG_VERSION"));
}
fn valid_clip_measurements() -> ClipMeasurements {
serde_json::from_value(serde_json::json!({
"duration_s": 1.0,
"frame_count": 2,
"animated_bones": ["hips"],
"bone_channels": [{
"bone_index": 0,
"bone_name": "hips",
"properties": ["translation", "rotation"]
}],
"bone_rotation_range_deg": { "hips": 10.0 },
"loop_continuity": { "bones": [{
"bone_index": 0,
"bone_name": "hips",
"position_delta_m": 0.01,
"rotation_delta_deg": 0.5,
"seam_velocity_delta_mps": 0.02,
"seam_angular_velocity_delta_degps": 2.0
}] },
"loop_continuity_availability": "measured",
"loop_endpoint_mode_availability": "not_applicable",
"frame_grid_availability": "not_applicable",
"loop_seam_ratio": 0.1,
"loop_seam_ratio_availability": "measured",
"gait": { "phase": 0.25, "phase_availability": "measured", "lr_amplitude_m": 0.2 },
"gait_availability": "measured",
"root_trajectory": {
"bone_index": 0,
"bone_name": "hips",
"source_role": "hips_fallback",
"translation": {
"horizontal_displacement_x_m": 0.0,
"horizontal_displacement_z_m": 1.0,
"horizontal_travel_m": 1.0,
"vertical_displacement_m": 0.0,
"vertical_min_displacement_m": 0.0,
"vertical_max_displacement_m": 0.0
},
"translation_availability": "measured",
"yaw": {
"heading_axis": "positive_z",
"net_yaw_deg": 0.0,
"unwrapped_yaw_deg": 0.0,
"yaw_travel_deg": 0.0
},
"yaw_availability": "measured"
},
"root_trajectory_availability": "measured",
"speed_mps": 1.0,
"speed_mps_availability": "measured",
}))
.expect("valid clip measurement fixture")
}
#[test]
fn measurement_contract_enforces_canonical_bone_channel_coverage() {
let invalid = |mutate: &dyn Fn(&mut ClipMeasurements), path: &str, reason: &str| {
let mut clip = valid_clip_measurements();
mutate(&mut clip);
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), clip)]),
AssetMeasurements::default(),
)
.expect_err("non-canonical bone channel coverage"),
MeasurementContractError::InvalidStructure {
path: path.into(),
reason: reason.into(),
}
);
};
invalid(
&|clip| clip.bone_channels.push(clip.bone_channels[0].clone()),
"clips[\"walk\"].bone_channels[1].bone_index",
"bone channel entries must use strictly increasing unique bone indices",
);
invalid(
&|clip| {
let lower_index = clip.bone_channels[0].clone();
let mut higher_index = lower_index.clone();
higher_index.bone_index = 1;
clip.bone_channels = vec![higher_index, lower_index];
},
"clips[\"walk\"].bone_channels[1].bone_index",
"bone channel entries must use strictly increasing unique bone indices",
);
invalid(
&|clip| clip.bone_channels[0].properties.clear(),
"clips[\"walk\"].bone_channels[0].properties",
"bone channel coverage must contain at least one property",
);
invalid(
&|clip| {
clip.bone_channels[0].properties = vec![Property::Rotation, Property::Translation];
},
"clips[\"walk\"].bone_channels[0].properties",
"channel properties must be unique and ordered translation, rotation, scale",
);
invalid(
&|clip| {
clip.bone_channels[0].properties = vec![Property::Translation, Property::Translation];
},
"clips[\"walk\"].bone_channels[0].properties",
"channel properties must be unique and ordered translation, rotation, scale",
);
invalid(
&|clip| clip.animated_bones = vec!["pelvis".into()],
"clips[\"walk\"].animated_bones",
"animated_bones must equal the sorted unique bone names in bone_channels",
);
invalid(
&|clip| {
clip.bone_rotation_range_deg.insert("pelvis".into(), 45.0);
},
"clips[\"walk\"].bone_rotation_range_deg[\"pelvis\"]",
"rotation-range bones must be present in animated_bones",
);
let mut duplicate_name = valid_clip_measurements();
let mut second_bone = duplicate_name.bone_channels[0].clone();
second_bone.bone_index = 1;
second_bone.properties = vec![Property::Scale];
duplicate_name.bone_channels.push(second_bone);
MeasurementContract::new(
BTreeMap::from([("walk".into(), duplicate_name)]),
AssetMeasurements::default(),
)
.expect("duplicate display names remain valid when bone indices differ");
}
fn frame_grid(fps: &str, frame_intervals: u32) -> FrameGridMeasurement {
toml::from_str(&format!(
"fps = {fps}\nframe_intervals = {frame_intervals}\n"
))
.expect("frame-grid fixture deserializes")
}
fn valid_mesh_measurements() -> MeshDefinitionMeasurements {
serde_json::from_value(serde_json::json!({
"mesh_index": 0,
"name": "mesh",
"vertex_count": 3,
"geometry_aabb": { "min": [0.0, 0.0, 0.0], "max": [1.0, 1.0, 1.0] },
"geometry_centroid": [0.5, 0.5, 0.5],
"max_joints_per_vertex": 4,
"weight_sum_min": 0.9,
"weight_sum_max": 1.1,
"additional_influence_sets": [],
}))
.expect("valid mesh measurement fixture")
}
fn additional_influence_set(
set_index: u32,
joints_present: bool,
weights_present: bool,
) -> animsmith_core::measure::AdditionalInfluenceSetMeasurements {
serde_json::from_value(serde_json::json!({
"set_index": set_index,
"joints_present": joints_present,
"weights_present": weights_present,
"joints_without_weights_present": joints_present && !weights_present,
"weights_without_joints_present": weights_present && !joints_present,
}))
.expect("valid additional influence-set fixture")
}
fn valid_asset_measurements() -> AssetMeasurements {
serde_json::from_value(serde_json::json!({
"material_resource_coverage": "unavailable",
"material_definitions": [],
"textures": [],
"images": [],
"mesh_definitions": [valid_mesh_measurements()],
"node_instances": [{
"node_index": 2,
"node_name": "node",
"mesh_index": 0,
"static_node_world_aabb": {
"min": [0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0]
}
}],
"scenes": [{
"scene_index": 4,
"instance_count": 1,
"static_scene_world_aabb": {
"min": [0.0, 0.0, 0.0],
"max": [1.0, 1.0, 1.0]
},
"excluded_instance_count": 0
}],
"default_scene_index": 4
}))
.expect("valid static asset measurement fixture")
}
fn complete_resource_assets() -> AssetMeasurements {
serde_json::from_value(serde_json::json!({
"material_resource_coverage": "complete",
"material_definitions": [
{ "material_index": 0, "name": "body", "texture_bindings": [
{ "slot": "base_color", "texture_index": 0 },
{ "slot": "normal", "texture_index": 1 },
{ "slot": "metallic_roughness", "texture_index": 0 },
{ "slot": "occlusion", "texture_index": 1 },
{ "slot": "emissive", "texture_index": 0 }
] },
{ "material_index": 1, "texture_bindings": [
{ "slot": "occlusion", "texture_index": 0 }
] }
],
"textures": [
{ "texture_index": 0, "name": "shared", "image_index": 0 },
{ "texture_index": 1, "image_index": 1 }
],
"images": [
{ "image_index": 0, "name": "image-0", "source_kind": "embedded", "declared_mime_type": "image/png", "detected_container": "png", "width": 2, "height": 1, "channel_count": 4, "decoded_color_type": "rgba8" },
{ "image_index": 1, "source_kind": "external", "unavailable_reason": "resource_limit" }
],
"mesh_definitions": [], "node_instances": [], "scenes": []
}))
.expect("complete material resource fixture")
}
fn complete_skeleton_assets() -> AssetMeasurements {
serde_json::from_value(serde_json::json!({
"material_resource_coverage": "unavailable",
"material_definitions": [], "textures": [], "images": [],
"skeleton_source_coverage": "complete",
"skeleton_nodes": [{
"node_index": 0,
"scene_root_indices": [],
"local_rest": {
"kind": "trs",
"translation_parent_space_m": [0.0, 0.0, 0.0],
"rotation_xyzw": [0.0, 0.0, 0.0, 1.0],
"scale": [1.0, 1.0, 1.0]
},
"rest_world_matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
"rest_world_translation_m": [0.0, 0.0, 0.0],
"rest_world_linear": {
"classification": "unit_orthonormal",
"axis_lengths": [1.0, 1.0, 1.0],
"determinant": 1.0,
"orientation": "positive",
"uniform_scale": 1.0
}
}],
"skins": [{
"skin_index": 0,
"joints": [{
"joint_index": 0, "node_index": 0,
"joint_bind_to_mesh": {
"source_inverse_bind_matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
"inversion_quality": { "reciprocal_condition_number_inf": 1.0 },
"matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
"linear": { "classification": "unit_orthonormal", "axis_lengths": [1.0, 1.0, 1.0], "determinant": 1.0, "orientation": "positive", "uniform_scale": 1.0 }
},
"mesh_bind_world": {
"source_inverse_bind_matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
"matrix": [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0],
"linear": { "classification": "unit_orthonormal", "axis_lengths": [1.0, 1.0, 1.0], "determinant": 1.0, "orientation": "positive", "uniform_scale": 1.0 }
}
}],
"joint_bind_linear_summary": { "classification": "consistent_uniform", "joint_count": 1, "available_joint_count": 1, "unavailable_joint_count": 0, "consistent_uniform_scale": 1.0 },
"inverse_bind_accessor": {
"status": "available", "declared_count": 1,
"matrices": [[1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]]
},
"attachments": [{ "node_index": 0 }]
}],
"mesh_definitions": [], "node_instances": [], "scenes": []
}))
.expect("complete skeleton fixture")
}
#[test]
fn measurement_contract_rejects_inconsistent_skeleton_source_evidence() {
let invalid = |mutate: &dyn Fn(&mut AssetMeasurements), path: &str, reason: &str| {
let mut assets = complete_skeleton_assets();
mutate(&mut assets);
assert_eq!(
MeasurementContract::new(BTreeMap::new(), assets)
.expect_err("invalid skeleton evidence"),
MeasurementContractError::InvalidStructure {
path: path.into(),
reason: reason.into(),
}
);
};
invalid(
&|assets| {
assets.skeleton_nodes[0].rest_world_translation_m = Some([1.0, 0.0, 0.0]);
},
"skeleton_nodes[0].rest_world_translation_m",
"rest_world_translation_m must equal the rest-world matrix translation column",
);
invalid(
&|assets| {
assets.skeleton_nodes[0].rest_world_linear.classification =
LinearTransformClassification::UniformScaled;
},
"skeleton_nodes[0].rest_world_linear",
"rest_world_linear must be derived from rest_world_matrix",
);
invalid(
&|assets| {
assets.skins[0].joints[0]
.joint_bind_to_mesh
.linear
.as_mut()
.expect("available linear facts")
.determinant = Some(2.0);
},
"skins[0].joints[0].joint_bind_to_mesh.linear",
"linear facts must be derived from the available matrix",
);
invalid(
&|assets| {
assets.skins[0]
.joint_bind_linear_summary
.available_joint_count = 0;
},
"skins[0].joint_bind_linear_summary",
"joint-bind linear summary must match the skin joint observations",
);
invalid(
&|assets| {
let duplicate = assets.skins[0].attachments[0].clone();
assets.skins[0].attachments.push(duplicate);
},
"skins[0].attachments[1].node_index",
"attachment node_index values must be strictly increasing and unique",
);
invalid(
&|assets| {
assets.skins[0].inverse_bind_accessor.status =
SourceInverseBindAccessorStatus::Unreadable;
assets.skins[0].inverse_bind_accessor.declared_count = None;
assets.skins[0].inverse_bind_accessor.matrices.clear();
},
"skins[0].inverse_bind_accessor",
"an unreadable inverse-bind declaration retains its count but cannot serialize matrices",
);
invalid(
&|assets| {
assets.skins[0].joints[0].joint_bind_to_mesh.matrix = None;
assets.skins[0].joints[0].joint_bind_to_mesh.linear = None;
assets.skins[0].joints[0]
.joint_bind_to_mesh
.unavailable_reason =
Some(SkinDerivedMatrixUnavailableReason::InverseBindAccessorAbsent);
},
"skins[0].joints[0].joint_bind_to_mesh.unavailable_reason",
"a usable inverse-bind matrix cannot be reported as accessor-unavailable",
);
invalid(
&|assets| {
assets.skins[0].joints[0]
.joint_bind_to_mesh
.source_inverse_bind_matrix
.as_mut()
.expect("source matrix")[12] = 1.0;
},
"skins[0].joints[0].joint_bind_to_mesh.source_inverse_bind_matrix",
"source_inverse_bind_matrix must equal the retained declaration slot exactly",
);
invalid(
&|assets| {
assets.skins[0].joints[0]
.joint_bind_to_mesh
.inversion_quality
.as_mut()
.expect("inversion quality")
.reciprocal_condition_number_inf = 0.5;
},
"skins[0].joints[0].joint_bind_to_mesh.inversion_quality",
"inversion quality must be derived from the source linear 3x3",
);
invalid(
&|assets| {
assets.skins[0].joints[0]
.joint_bind_to_mesh
.matrix
.as_mut()
.expect("derived inverse")[12] = 1.0;
},
"skins[0].joints[0].joint_bind_to_mesh",
"a trustworthy source inverse-bind matrix requires its exact inverse",
);
invalid(
&|assets| {
assets.skins[0].joints[0]
.mesh_bind_world
.matrix
.as_mut()
.expect("derived mesh bind")[12] = 1.0;
},
"skins[0].joints[0].mesh_bind_world",
"mesh_bind_world must equal joint_rest_world times the source inverse bind",
);
invalid(
&|assets| {
assets.skins[0].joints[0].mesh_bind_world.unavailable_reason =
Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable);
},
"skins[0].joints[0].mesh_bind_world",
"an available derived matrix cannot have an unavailable reason",
);
invalid(
&|assets| {
assets.skeleton_nodes[0].local_rest = SkeletonNodeLocalRestMeasurements::Unavailable {
reason: SkeletonNodeLocalRestUnavailableReason::NonFiniteTransform,
};
},
"skeleton_nodes[0]",
"an unavailable local_rest requires a non_finite_local_rest rest-world result",
);
invalid(
&|assets| {
assets.skeleton_nodes[0].rest_world_matrix = None;
assets.skeleton_nodes[0].rest_world_translation_m = None;
assets.skeleton_nodes[0].rest_world_linear =
measure_linear_transform(Mat4::from_cols_array(&[f32::NAN; 16]));
assets.skeleton_nodes[0].rest_world_matrix_unavailable_reason =
Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteLocalRest);
},
"skeleton_nodes[0]",
"rest-world availability must agree with local rest and parent rest-world evidence",
);
invalid(
&|assets| assets.skeleton_nodes[0].parent_node_index = Some(1),
"skeleton_nodes[0].parent_node_index",
"parent_node_index must reference a skeleton node",
);
invalid(
&|assets| {
let mut child = assets.skeleton_nodes[0].clone();
child.node_index = 1;
child.parent_node_index = Some(0);
assets.skeleton_nodes[0].parent_node_index = Some(1);
assets.skeleton_nodes.push(child);
},
"skeleton_nodes[0].parent_node_index",
"source node parent graph must be acyclic",
);
invalid(
&|assets| {
let mut child = assets.skeleton_nodes[0].clone();
child.node_index = 1;
child.parent_node_index = Some(0);
child.rest_world_matrix = None;
child.rest_world_translation_m = None;
child.rest_world_linear =
measure_linear_transform(Mat4::from_cols_array(&[f32::NAN; 16]));
child.rest_world_matrix_unavailable_reason =
Some(SkeletonRestWorldMatrixUnavailableReason::ParentRestWorldUnavailable);
assets.skeleton_nodes.push(child);
},
"skeleton_nodes[1]",
"rest-world availability must agree with local rest and parent rest-world evidence",
);
invalid(
&|assets| {
assets.skins[0].joints[0].joint_bind_to_mesh.matrix = None;
assets.skins[0].joints[0].joint_bind_to_mesh.linear = None;
assets.skins[0].joints[0]
.joint_bind_to_mesh
.unavailable_reason =
Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable);
},
"skins[0].joints[0].joint_bind_to_mesh.unavailable_reason",
"joint_bind_to_mesh cannot use a joint-rest-world unavailable reason",
);
invalid(
&|assets| {
assets.skins[0].joints[0].mesh_bind_world.matrix = None;
assets.skins[0].joints[0].mesh_bind_world.linear = None;
assets.skins[0].joints[0].mesh_bind_world.unavailable_reason =
Some(SkinDerivedMatrixUnavailableReason::JointRestWorldUnavailable);
},
"skins[0].joints[0].mesh_bind_world.unavailable_reason",
"an available joint rest-world matrix cannot be reported as unavailable",
);
invalid(
&|assets| {
assets.skeleton_nodes[0].local_rest = SkeletonNodeLocalRestMeasurements::Unavailable {
reason: SkeletonNodeLocalRestUnavailableReason::NonFiniteTransform,
};
assets.skeleton_nodes[0].rest_world_matrix = None;
assets.skeleton_nodes[0].rest_world_translation_m = None;
assets.skeleton_nodes[0].rest_world_linear =
measure_linear_transform(Mat4::from_cols_array(&[f32::NAN; 16]));
assets.skeleton_nodes[0].rest_world_matrix_unavailable_reason =
Some(SkeletonRestWorldMatrixUnavailableReason::NonFiniteLocalRest);
assets.skins[0].joints[0].mesh_bind_world.matrix = None;
assets.skins[0].joints[0].mesh_bind_world.linear = None;
assets.skins[0].joints[0].mesh_bind_world.unavailable_reason =
Some(SkinDerivedMatrixUnavailableReason::NonFiniteDerivedMatrix);
},
"skins[0].joints[0].mesh_bind_world.unavailable_reason",
"a non-finite mesh-bind-world result requires an available joint rest-world matrix",
);
invalid(
&|assets| {
assets.skins[0].joints[0].mesh_bind_world.matrix = None;
assets.skins[0].joints[0].mesh_bind_world.linear = None;
assets.skins[0].joints[0].mesh_bind_world.unavailable_reason =
Some(SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonInvertible);
},
"skins[0].joints[0].mesh_bind_world.unavailable_reason",
"mesh_bind_world does not require an invertible inverse-bind matrix",
);
invalid(
&|assets| {
assets.skeleton_source_coverage = SourceSkeletonCoverage::Unavailable;
},
"skeleton_source_coverage",
"unavailable skeleton source coverage requires empty skeleton_nodes and skins arrays",
);
}
#[test]
fn measurement_contract_accepts_deep_parent_chains_and_singular_raw_inverse_binds() {
let mut assets = complete_skeleton_assets();
let root = assets.skeleton_nodes[0].clone();
assets.skeleton_nodes = (0..20_000)
.map(|node_index| {
let mut node = root.clone();
node.node_index = node_index;
node.parent_node_index = node_index.checked_sub(1);
node
})
.collect();
MeasurementContract::new(BTreeMap::new(), assets)
.expect("a deep acyclic source hierarchy is valid without recursive traversal");
let mut assets = complete_skeleton_assets();
let singular = Mat4::from_scale(glam::Vec3::new(1.0, 1.0, 0.0)).to_cols_array();
assets.skins[0].inverse_bind_accessor.matrices[0] = singular;
assets.skins[0].joints[0]
.joint_bind_to_mesh
.source_inverse_bind_matrix = Some(singular);
assets.skins[0].joints[0]
.joint_bind_to_mesh
.inversion_quality = Some(
serde_json::from_value(serde_json::json!({
"reciprocal_condition_number_inf": 0.0
}))
.expect("singular quality fixture"),
);
assets.skins[0].joints[0].joint_bind_to_mesh.matrix = None;
assets.skins[0].joints[0].joint_bind_to_mesh.linear = None;
assets.skins[0].joints[0]
.joint_bind_to_mesh
.unavailable_reason =
Some(SkinDerivedMatrixUnavailableReason::InverseBindMatrixNonInvertible);
assets.skins[0].joints[0].mesh_bind_world.matrix = Some(singular);
assets.skins[0].joints[0]
.mesh_bind_world
.source_inverse_bind_matrix = Some(singular);
assets.skins[0].joints[0].mesh_bind_world.linear =
Some(measure_linear_transform(Mat4::from_cols_array(&singular)));
assets.skins[0].joint_bind_linear_summary = serde_json::from_value(serde_json::json!({
"classification": "unavailable",
"joint_count": 1,
"available_joint_count": 0,
"unavailable_joint_count": 1
}))
.expect("unavailable skin summary fixture");
MeasurementContract::new(BTreeMap::new(), assets)
.expect("a singular raw inverse-bind remains usable for mesh-bind-world multiplication");
}
#[test]
fn measurement_contract_accepts_complete_empty_and_shared_material_resources() {
let mut empty_complete = AssetMeasurements::default();
empty_complete.material_resource_coverage = MaterialResourceCoverage::Complete;
MeasurementContract::new(BTreeMap::new(), empty_complete)
.expect("a complete source with no material resources is valid");
MeasurementContract::new(BTreeMap::new(), AssetMeasurements::default())
.expect("unavailable coverage with empty arrays is valid");
let contract = MeasurementContract::new(BTreeMap::new(), complete_resource_assets())
.expect("shared texture/image references are valid");
let json = serde_json::to_value(contract).expect("resource measurements serialize");
assert_eq!(
json["material_definitions"][0]["texture_bindings"],
serde_json::json!([
{ "slot": "base_color", "texture_index": 0 },
{ "slot": "normal", "texture_index": 1 },
{ "slot": "metallic_roughness", "texture_index": 0 },
{ "slot": "occlusion", "texture_index": 1 },
{ "slot": "emissive", "texture_index": 0 }
]),
"the public contract serializes all core slots in fixed semantic order"
);
assert_eq!(json["images"][0]["decoded_color_type"], "rgba8");
assert!(json["images"][0].get("unavailable_reason").is_none());
assert_eq!(json["images"][1]["unavailable_reason"], "resource_limit");
assert!(json["images"][1].get("width").is_none());
}
#[test]
fn measurement_contract_rejects_invalid_material_resource_tables() {
let invalid_resource = |mutate: &dyn Fn(&mut AssetMeasurements), path: &str, reason: &str| {
let mut assets = complete_resource_assets();
mutate(&mut assets);
assert_eq!(
MeasurementContract::new(BTreeMap::new(), assets).expect_err("invalid resource table"),
MeasurementContractError::InvalidStructure {
path: path.into(),
reason: reason.into()
}
);
};
invalid_resource(
&|assets| assets.material_resource_coverage = MaterialResourceCoverage::Unavailable,
"material_resource_coverage",
"unavailable resource coverage requires empty material, texture, and image arrays",
);
invalid_resource(
&|assets| assets.material_definitions[1].material_index = 3,
"material_definitions[1].material_index",
"material_index must be contiguous and match source order",
);
invalid_resource(
&|assets| assets.textures[1].texture_index = 3,
"textures[1].texture_index",
"texture_index must be contiguous and match source order",
);
invalid_resource(
&|assets| assets.images[1].image_index = 3,
"images[1].image_index",
"image_index must be contiguous and match source order",
);
invalid_resource(
&|assets| assets.material_definitions[0].texture_bindings.swap(0, 1),
"material_definitions[0].texture_bindings[1].slot",
"texture bindings must be strictly ordered by slot and unique",
);
invalid_resource(
&|assets| assets.material_definitions[0].texture_bindings.swap(3, 4),
"material_definitions[0].texture_bindings[4].slot",
"texture bindings must be strictly ordered by slot and unique",
);
invalid_resource(
&|assets| {
assets.material_definitions[0].texture_bindings[1].slot = MaterialTextureSlot::BaseColor
},
"material_definitions[0].texture_bindings[1].slot",
"texture bindings must be strictly ordered by slot and unique",
);
invalid_resource(
&|assets| assets.material_definitions[0].texture_bindings[0].texture_index = 9,
"material_definitions[0].texture_bindings[0].texture_index",
"texture_index must reference a source texture",
);
invalid_resource(
&|assets| assets.textures[0].image_index = 9,
"textures[0].image_index",
"image_index must reference a source image",
);
}
#[test]
fn measurement_contract_rejects_invalid_image_measurement_relationships() {
let invalid_image = |mutate: &dyn Fn(&mut ImageMeasurements), path: &str, reason: &str| {
let mut assets = complete_resource_assets();
mutate(&mut assets.images[0]);
assert_eq!(
MeasurementContract::new(BTreeMap::new(), assets).expect_err("invalid image evidence"),
MeasurementContractError::InvalidStructure {
path: path.into(),
reason: reason.into()
}
);
};
invalid_image(
&|image| image.detected_container = None,
"images[0].detected_container",
"available image metadata requires a detected_container",
);
invalid_image(
&|image| image.channel_count = Some(3),
"images[0].channel_count",
"channel_count must match decoded_color_type",
);
invalid_image(
&|image| image.width = None,
"images[0]",
"available image metadata must include width, height, channel_count, and decoded_color_type",
);
invalid_image(
&|image| image.unavailable_reason = Some(ImageUnavailableReason::DecodeFailed),
"images[0]",
"available image metadata cannot have an unavailable_reason",
);
let unavailable = |reason, detected_container, expected_path: &str, expected_reason: &str| {
let mut assets = complete_resource_assets();
assets.images[0] = serde_json::from_value(serde_json::json!({
"image_index": 0,
"source_kind": "data_uri",
"detected_container": detected_container,
"unavailable_reason": reason,
}))
.expect("unavailable image fixture");
assert_eq!(
MeasurementContract::new(BTreeMap::new(), assets)
.expect_err("invalid unavailable image"),
MeasurementContractError::InvalidStructure {
path: expected_path.into(),
reason: expected_reason.into()
}
);
};
unavailable(
ImageUnavailableReason::DecodeFailed,
None,
"images[0].detected_container",
"decode_failed requires a detected_container",
);
for reason in [
ImageUnavailableReason::SourceUnavailable,
ImageUnavailableReason::InvalidDataUri,
ImageUnavailableReason::UnsupportedContainer,
] {
unavailable(
reason,
Some(ImageContainerFormat::Png),
"images[0].detected_container",
"this unavailable_reason cannot have a detected_container",
);
}
}
fn assert_invalid_assets(
mutate: impl FnOnce(&mut AssetMeasurements),
expected_path: &str,
expected_reason: &str,
) {
let mut assets = valid_asset_measurements();
mutate(&mut assets);
assert_eq!(
MeasurementContract::new(BTreeMap::new(), assets)
.expect_err("structurally inconsistent evidence must be rejected"),
MeasurementContractError::InvalidStructure {
path: expected_path.into(),
reason: expected_reason.into(),
}
);
}
fn assert_invalid_clip(mutate: impl FnOnce(&mut ClipMeasurements), expected_path: &str) {
let mut clip = valid_clip_measurements();
mutate(&mut clip);
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), clip)]),
AssetMeasurements::default(),
)
.expect_err("non-finite clip evidence must be rejected"),
MeasurementContractError::NonFiniteValue {
path: expected_path.into(),
}
);
}
fn assert_invalid_mesh(mutate: impl FnOnce(&mut MeshDefinitionMeasurements), expected_path: &str) {
let mut mesh = valid_mesh_measurements();
mutate(&mut mesh);
let mut assets = AssetMeasurements::default();
assets.mesh_definitions.push(mesh);
assert_eq!(
MeasurementContract::new(BTreeMap::new(), assets)
.expect_err("non-finite mesh evidence must be rejected"),
MeasurementContractError::NonFiniteValue {
path: expected_path.into(),
}
);
}
#[test]
fn measurement_contract_rejects_every_non_finite_numeric_branch() {
assert_invalid_clip(
|clip| clip.duration_s = f64::NAN,
"clips[\"walk\"].duration_s",
);
assert_invalid_clip(
|clip| {
clip.bone_rotation_range_deg
.insert("hips".into(), f64::INFINITY);
},
"clips[\"walk\"].bone_rotation_range_deg[\"hips\"]",
);
assert_invalid_clip(
|clip| {
clip.loop_continuity.as_mut().unwrap().bones[0].position_delta_m = f64::NAN;
},
"clips[\"walk\"].loop_continuity.bones[0].position_delta_m",
);
assert_invalid_clip(
|clip| {
clip.loop_continuity.as_mut().unwrap().bones[0].rotation_delta_deg = f64::INFINITY;
},
"clips[\"walk\"].loop_continuity.bones[0].rotation_delta_deg",
);
assert_invalid_clip(
|clip| {
clip.loop_continuity.as_mut().unwrap().bones[0].seam_velocity_delta_mps =
f64::NEG_INFINITY;
},
"clips[\"walk\"].loop_continuity.bones[0].seam_velocity_delta_mps",
);
assert_invalid_clip(
|clip| {
clip.loop_continuity.as_mut().unwrap().bones[0].seam_angular_velocity_delta_degps =
f64::NAN;
},
"clips[\"walk\"].loop_continuity.bones[0].seam_angular_velocity_delta_degps",
);
assert_invalid_clip(
|clip| clip.loop_seam_ratio = Some(f64::NEG_INFINITY),
"clips[\"walk\"].loop_seam_ratio",
);
assert_invalid_clip(
|clip| clip.gait.as_mut().expect("fixture gait").phase = Some(f64::NAN),
"clips[\"walk\"].gait.phase",
);
assert_invalid_clip(
|clip| clip.gait.as_mut().expect("fixture gait").lr_amplitude_m = f64::INFINITY,
"clips[\"walk\"].gait.lr_amplitude_m",
);
type RootNumericCase = (fn(&mut ClipMeasurements), &'static str);
let root_numeric_cases: [RootNumericCase; 9] = [
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.horizontal_displacement_x_m = f64::NAN
},
"clips[\"walk\"].root_trajectory.translation.horizontal_displacement_x_m",
),
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.horizontal_displacement_z_m = f64::INFINITY
},
"clips[\"walk\"].root_trajectory.translation.horizontal_displacement_z_m",
),
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.horizontal_travel_m = f64::NEG_INFINITY
},
"clips[\"walk\"].root_trajectory.translation.horizontal_travel_m",
),
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.vertical_displacement_m = f64::NAN
},
"clips[\"walk\"].root_trajectory.translation.vertical_displacement_m",
),
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.vertical_min_displacement_m = f64::NEG_INFINITY
},
"clips[\"walk\"].root_trajectory.translation.vertical_min_displacement_m",
),
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.vertical_max_displacement_m = f64::INFINITY
},
"clips[\"walk\"].root_trajectory.translation.vertical_max_displacement_m",
),
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.yaw
.as_mut()
.unwrap()
.net_yaw_deg = f64::NAN
},
"clips[\"walk\"].root_trajectory.yaw.net_yaw_deg",
),
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.yaw
.as_mut()
.unwrap()
.unwrapped_yaw_deg = f64::INFINITY
},
"clips[\"walk\"].root_trajectory.yaw.unwrapped_yaw_deg",
),
(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.yaw
.as_mut()
.unwrap()
.yaw_travel_deg = f64::NEG_INFINITY
},
"clips[\"walk\"].root_trajectory.yaw.yaw_travel_deg",
),
];
for (mutate, path) in root_numeric_cases {
assert_invalid_clip(mutate, path);
}
assert_invalid_clip(
|clip| clip.speed_mps = Some(f64::NAN),
"clips[\"walk\"].speed_mps",
);
assert_invalid_clip(
|clip| {
clip.frame_grid = Some(frame_grid("nan", 30));
clip.frame_grid_availability = MeasurementAvailability::Measured;
},
"clips[\"walk\"].frame_grid.fps",
);
assert_invalid_mesh(
|mesh| {
mesh.geometry_aabb.as_mut().expect("fixture aabb").min[1] = f32::NAN;
},
"mesh_definitions[0].geometry_aabb.min[1]",
);
assert_invalid_mesh(
|mesh| {
mesh.geometry_aabb.as_mut().expect("fixture aabb").max[2] = f32::INFINITY;
},
"mesh_definitions[0].geometry_aabb.max[2]",
);
assert_invalid_mesh(
|mesh| mesh.geometry_centroid.as_mut().expect("fixture centroid")[1] = f32::NAN,
"mesh_definitions[0].geometry_centroid[1]",
);
assert_invalid_mesh(
|mesh| mesh.geometry_centroid.as_mut().expect("fixture centroid")[2] = f32::INFINITY,
"mesh_definitions[0].geometry_centroid[2]",
);
assert_invalid_mesh(
|mesh| mesh.weight_sum_min = Some(f64::NEG_INFINITY),
"mesh_definitions[0].weight_sum_min",
);
assert_invalid_mesh(
|mesh| mesh.weight_sum_max = Some(f64::NAN),
"mesh_definitions[0].weight_sum_max",
);
}
#[test]
fn measurement_contract_rejects_invalid_frame_grid_structure() {
for (grid, path, reason) in [
(
frame_grid("0.0", 30),
"clips[\"walk\"].frame_grid.fps",
"declared frame-grid FPS must be positive",
),
(
frame_grid("30.0", 0),
"clips[\"walk\"].frame_grid.frame_intervals",
"declared frame-grid evidence must contain at least one interval",
),
] {
let mut clip = valid_clip_measurements();
clip.frame_grid = Some(grid);
clip.frame_grid_availability = MeasurementAvailability::Measured;
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), clip)]),
AssetMeasurements::default(),
)
.expect_err("invalid frame-grid structure must fail"),
MeasurementContractError::InvalidStructure {
path: path.into(),
reason: reason.into(),
}
);
}
}
#[test]
fn measurement_contract_rejects_availability_status_value_mismatches() {
let invalid = |mutate: fn(&mut ClipMeasurements), path: &str| {
let mut clip = valid_clip_measurements();
mutate(&mut clip);
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), clip)]),
AssetMeasurements::default(),
)
.expect_err("availability/value mismatch must fail"),
MeasurementContractError::InvalidStructure {
path: path.into(),
reason: "value presence must match availability status".into(),
}
);
};
invalid(
|clip| clip.loop_continuity_availability = MeasurementAvailability::NotApplicable,
"clips[\"walk\"].loop_continuity",
);
invalid(
|clip| clip.frame_grid_availability = MeasurementAvailability::Measured,
"clips[\"walk\"].frame_grid",
);
invalid(
|clip| clip.loop_seam_ratio_availability = MeasurementAvailability::NotApplicable,
"clips[\"walk\"].loop_seam_ratio",
);
invalid(
|clip| clip.gait_availability = MeasurementAvailability::Unavailable,
"clips[\"walk\"].gait",
);
invalid(
|clip| clip.root_trajectory_availability = MeasurementAvailability::Unavailable,
"clips[\"walk\"].root_trajectory",
);
invalid(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation_availability = MeasurementAvailability::Unavailable
},
"clips[\"walk\"].root_trajectory.translation",
);
invalid(
|clip| {
clip.root_trajectory.as_mut().unwrap().yaw_availability =
MeasurementAvailability::Unavailable
},
"clips[\"walk\"].root_trajectory.yaw",
);
invalid(
|clip| clip.speed_mps_availability = MeasurementAvailability::NotApplicable,
"clips[\"walk\"].speed_mps",
);
invalid(
|clip| {
clip.gait.as_mut().expect("fixture gait").phase_availability =
MeasurementAvailability::Unavailable
},
"clips[\"walk\"].gait.phase",
);
invalid(
|clip| clip.loop_endpoint_mode = Some(LoopEndpointMode::UniqueCycle),
"clips[\"walk\"].loop_endpoint_mode",
);
invalid(
|clip| clip.speed_mps_availability = MeasurementAvailability::Unavailable,
"clips[\"walk\"].speed_mps",
);
invalid(
|clip| {
clip.gait.as_mut().expect("fixture gait").phase_availability =
MeasurementAvailability::NotApplicable
},
"clips[\"walk\"].gait.phase",
);
}
#[test]
fn measurement_contract_roundtrips_absent_nested_gait_phase_states() {
for (availability, amplitude) in [
(MeasurementAvailability::NotApplicable, 0.0),
(MeasurementAvailability::Unavailable, 0.2),
] {
let mut clip = valid_clip_measurements();
let gait = clip.gait.as_mut().expect("fixture gait");
gait.phase = None;
gait.phase_availability = availability;
gait.lr_amplitude_m = amplitude;
let measurements = MeasurementContract::new(
BTreeMap::from([("walk".into(), clip)]),
AssetMeasurements::default(),
)
.expect("absent nested phase status is structurally valid");
let envelope = MeasureEnvelope::new(
tool(),
vec![MeasureFileReport::new(
"walk.glb",
input(b"gait phase"),
rig(),
measurements,
)],
)
.expect("measurement envelope is valid");
let wire = serde_json::to_value(envelope).expect("measurement envelope serializes");
assert!(
wire["files"][0]["measurements"]["clips"]["walk"]["gait"]
.get("phase")
.is_none()
);
let input: MeasurementReportInput =
serde_json::from_value(wire).expect("measurement envelope deserializes");
let recovered = input
.into_files()
.expect("measurement envelope validates")
.into_iter()
.next()
.expect("one measurement file")
.into_measurements();
let recovered_gait = recovered.clips()["walk"]
.gait
.as_ref()
.expect("gait present");
assert_eq!(recovered_gait.phase, None);
assert_eq!(recovered_gait.phase_availability, availability);
assert_eq!(recovered_gait.lr_amplitude_m, amplitude);
}
}
#[test]
fn measurement_contract_rejects_invalid_root_trajectory_structure() {
let invalid = |mutate: fn(&mut ClipMeasurements), path: &str, reason: &str| {
let mut clip = valid_clip_measurements();
mutate(&mut clip);
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), clip)]),
AssetMeasurements::default(),
)
.expect_err("invalid root-trajectory structure must fail"),
MeasurementContractError::InvalidStructure {
path: path.into(),
reason: reason.into(),
}
);
};
invalid(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.horizontal_travel_m = -0.1
},
"clips[\"walk\"].root_trajectory.translation.horizontal_travel_m",
"sampled horizontal travel must be non-negative",
);
invalid(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.horizontal_travel_m = 0.5
},
"clips[\"walk\"].root_trajectory.translation.horizontal_travel_m",
"sampled horizontal travel must contain endpoint displacement",
);
invalid(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.vertical_min_displacement_m = 0.1
},
"clips[\"walk\"].root_trajectory.translation",
"vertical extrema must include zero and the endpoint displacement",
);
invalid(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.translation
.as_mut()
.unwrap()
.vertical_displacement_m = 1.0
},
"clips[\"walk\"].root_trajectory.translation",
"vertical extrema must include zero and the endpoint displacement",
);
invalid(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.yaw
.as_mut()
.unwrap()
.net_yaw_deg = 181.0
},
"clips[\"walk\"].root_trajectory.yaw.net_yaw_deg",
"net yaw must be in the inclusive range [-180, 180]",
);
invalid(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.yaw
.as_mut()
.unwrap()
.net_yaw_deg = 1.0
},
"clips[\"walk\"].root_trajectory.yaw.net_yaw_deg",
"net yaw must be the canonical endpoint-equivalent unwrapped yaw",
);
invalid(
|clip| {
clip.root_trajectory
.as_mut()
.unwrap()
.yaw
.as_mut()
.unwrap()
.yaw_travel_deg = -0.1
},
"clips[\"walk\"].root_trajectory.yaw.yaw_travel_deg",
"sampled yaw travel must be non-negative",
);
invalid(
|clip| {
let yaw = clip.root_trajectory.as_mut().unwrap().yaw.as_mut().unwrap();
yaw.net_yaw_deg = 90.0;
yaw.unwrapped_yaw_deg = 90.0;
yaw.yaw_travel_deg = 89.0;
},
"clips[\"walk\"].root_trajectory.yaw.yaw_travel_deg",
"sampled yaw travel must contain signed unwrapped yaw",
);
invalid(
|clip| {
let trajectory = clip.root_trajectory.as_mut().unwrap();
trajectory.translation = None;
trajectory.translation_availability = MeasurementAvailability::NotApplicable;
},
"clips[\"walk\"].root_trajectory.translation_availability",
"translation remains applicable when a root-trajectory bone is selected",
);
invalid(
|clip| {
let trajectory = clip.root_trajectory.as_mut().unwrap();
trajectory.yaw = None;
trajectory.yaw_availability = MeasurementAvailability::NotApplicable;
},
"clips[\"walk\"].root_trajectory.yaw_availability",
"yaw remains applicable when a root-trajectory bone is selected",
);
}
#[test]
fn measurement_contract_rejects_invalid_loop_continuity_structure() {
let invalid = |mutate: fn(&mut ClipMeasurements), path: &str, reason: &str| {
let mut clip = valid_clip_measurements();
mutate(&mut clip);
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), clip)]),
AssetMeasurements::default(),
)
.expect_err("invalid loop-continuity structure must fail"),
MeasurementContractError::InvalidStructure {
path: path.into(),
reason: reason.into(),
}
);
};
invalid(
|clip| clip.loop_continuity.as_mut().unwrap().bones[0].bone_index = 1,
"clips[\"walk\"].loop_continuity.bones[0].bone_index",
"expected skeleton-order index 0, found 1",
);
invalid(
|clip| clip.loop_continuity.as_mut().unwrap().bones.clear(),
"clips[\"walk\"].loop_continuity.bones",
"present loop-continuity evidence must contain at least one bone",
);
invalid(
|clip| clip.loop_continuity.as_mut().unwrap().bones[0].position_delta_m = -0.01,
"clips[\"walk\"].loop_continuity.bones[0].position_delta_m",
"loop-continuity deltas must be non-negative",
);
let mut two_bone_json = serde_json::to_value(valid_clip_measurements()).expect("serializes");
two_bone_json["loop_continuity"]["bones"]
.as_array_mut()
.expect("bones array")
.push(serde_json::json!({
"bone_index": 1,
"bone_name": "foot",
"position_delta_m": 0.0,
"rotation_delta_deg": 0.0,
"seam_velocity_delta_mps": 0.0,
"seam_angular_velocity_delta_degps": 0.0
}));
let mut non_contiguous: ClipMeasurements =
serde_json::from_value(two_bone_json.clone()).expect("two-bone fixture");
non_contiguous.loop_continuity.as_mut().unwrap().bones[1].bone_index = 3;
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), non_contiguous)]),
AssetMeasurements::default(),
)
.expect_err("later rows must also be contiguous"),
MeasurementContractError::InvalidStructure {
path: "clips[\"walk\"].loop_continuity.bones[1].bone_index".into(),
reason: "expected skeleton-order index 1, found 3".into(),
}
);
let mut non_finite: ClipMeasurements =
serde_json::from_value(two_bone_json.clone()).expect("two-bone fixture");
non_finite.loop_continuity.as_mut().unwrap().bones[1].rotation_delta_deg = f64::NAN;
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), non_finite)]),
AssetMeasurements::default(),
)
.expect_err("later-row values must also be finite"),
MeasurementContractError::NonFiniteValue {
path: "clips[\"walk\"].loop_continuity.bones[1].rotation_delta_deg".into(),
}
);
let mut negative: ClipMeasurements =
serde_json::from_value(two_bone_json.clone()).expect("two-bone fixture");
negative.loop_continuity.as_mut().unwrap().bones[1].seam_velocity_delta_mps = -0.01;
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), negative)]),
AssetMeasurements::default(),
)
.expect_err("later-row values must also be nonnegative"),
MeasurementContractError::InvalidStructure {
path: "clips[\"walk\"].loop_continuity.bones[1].seam_velocity_delta_mps".into(),
reason: "loop-continuity deltas must be non-negative".into(),
}
);
let mut angular_negative: ClipMeasurements =
serde_json::from_value(two_bone_json).expect("two-bone fixture");
angular_negative.loop_continuity.as_mut().unwrap().bones[1].seam_angular_velocity_delta_degps =
-0.01;
assert_eq!(
MeasurementContract::new(
BTreeMap::from([("walk".into(), angular_negative)]),
AssetMeasurements::default(),
)
.expect_err("angular seam deltas must also be nonnegative"),
MeasurementContractError::InvalidStructure {
path: "clips[\"walk\"].loop_continuity.bones[1].seam_angular_velocity_delta_degps"
.into(),
reason: "loop-continuity deltas must be non-negative".into(),
}
);
}
#[test]
fn measurement_contract_rejects_inconsistent_static_asset_relationships() {
assert_invalid_assets(
|assets| {
assets.mesh_definitions[0]
.geometry_aabb
.as_mut()
.expect("fixture geometry AABB")
.min[0] = 2.0;
},
"mesh_definitions[0].geometry_aabb.min[0]",
"AABB minimum cannot exceed maximum",
);
assert_invalid_assets(
|assets| {
assets.node_instances[0]
.static_node_world_aabb
.as_mut()
.expect("fixture node AABB")
.min[1] = 2.0;
},
"node_instances[0].static_node_world_aabb.min[1]",
"AABB minimum cannot exceed maximum",
);
assert_invalid_assets(
|assets| {
assets.scenes[0]
.static_scene_world_aabb
.as_mut()
.expect("fixture scene AABB")
.min[2] = 2.0;
},
"scenes[0].static_scene_world_aabb.min[2]",
"AABB minimum cannot exceed maximum",
);
assert_invalid_assets(
|assets| {
let duplicate = assets.mesh_definitions[0].clone();
assets.mesh_definitions.push(duplicate);
},
"mesh_definitions[1].mesh_index",
"mesh_index must be unique",
);
assert_invalid_assets(
|assets| assets.node_instances[0].mesh_index = 99,
"node_instances[0].mesh_index",
"mesh_index must reference a mesh definition",
);
assert_invalid_assets(
|assets| assets.node_instances[0].static_node_world_aabb = None,
"node_instances[0]",
"a missing static node AABB requires an unavailable reason",
);
assert_invalid_assets(
|assets| {
assets.node_instances[0].static_node_world_aabb_unavailable_reason =
Some(animsmith_core::measure::StaticNodeAabbUnavailableReason::NonFiniteTransform);
},
"node_instances[0]",
"an available static node AABB cannot have an unavailable reason",
);
assert_invalid_assets(
|assets| assets.scenes[0].excluded_instance_count = 2,
"scenes[0].excluded_instance_count",
"excluded_instance_count cannot exceed instance_count",
);
assert_invalid_assets(
|assets| assets.scenes[0].static_scene_world_aabb = None,
"scenes[0].static_scene_world_aabb",
"a scene with available instances requires an AABB",
);
assert_invalid_assets(
|assets| assets.default_scene_index = Some(99),
"default_scene_index",
"default_scene_index must reference a declared scene",
);
}
#[test]
fn measurement_contract_rejects_invalid_additional_influence_set_ordering() {
assert_invalid_assets(
|assets| {
assets.mesh_definitions[0].additional_influence_sets =
vec![additional_influence_set(0, true, false)];
},
"mesh_definitions[0].additional_influence_sets[0].set_index",
"set_index must be at least 1",
);
assert_invalid_assets(
|assets| {
assets.mesh_definitions[0].additional_influence_sets = vec![
additional_influence_set(1, true, false),
additional_influence_set(0, false, true),
];
},
"mesh_definitions[0].additional_influence_sets[1].set_index",
"set_index must be at least 1",
);
assert_invalid_assets(
|assets| {
assets.mesh_definitions[0].additional_influence_sets = vec![
additional_influence_set(2, true, true),
additional_influence_set(1, false, true),
];
},
"mesh_definitions[0].additional_influence_sets[1].set_index",
"set_index values must be strictly increasing and unique",
);
assert_invalid_assets(
|assets| {
assets.mesh_definitions[0].additional_influence_sets = vec![
additional_influence_set(1, true, true),
additional_influence_set(3, true, false),
additional_influence_set(2, false, true),
];
},
"mesh_definitions[0].additional_influence_sets[2].set_index",
"set_index values must be strictly increasing and unique",
);
assert_invalid_assets(
|assets| {
assets.mesh_definitions[0].additional_influence_sets = vec![
additional_influence_set(1, true, true),
additional_influence_set(1, false, true),
];
},
"mesh_definitions[0].additional_influence_sets[1].set_index",
"set_index values must be strictly increasing and unique",
);
assert_invalid_assets(
|assets| {
assets.mesh_definitions[0].additional_influence_sets =
vec![additional_influence_set(1, false, false)];
},
"mesh_definitions[0].additional_influence_sets[0]",
"an additional influence set must declare joints, weights, or both",
);
assert_invalid_assets(
|assets| {
let set = &mut assets.mesh_definitions[0].additional_influence_sets;
*set = vec![additional_influence_set(1, true, false)];
set[0].joints_without_weights_present = false;
},
"mesh_definitions[0].additional_influence_sets[0].joints_without_weights_present",
"joints_without_weights_present is required when weights_present is false",
);
assert_invalid_assets(
|assets| {
let set = &mut assets.mesh_definitions[0].additional_influence_sets;
*set = vec![additional_influence_set(1, false, true)];
set[0].weights_without_joints_present = false;
},
"mesh_definitions[0].additional_influence_sets[0].weights_without_joints_present",
"weights_without_joints_present is required when joints_present is false",
);
assert_invalid_assets(
|assets| {
let set = &mut assets.mesh_definitions[0].additional_influence_sets;
*set = vec![additional_influence_set(1, false, true)];
set[0].joints_without_weights_present = true;
},
"mesh_definitions[0].additional_influence_sets[0].joints_without_weights_present",
"joints_without_weights_present requires joints_present",
);
assert_invalid_assets(
|assets| {
let set = &mut assets.mesh_definitions[0].additional_influence_sets;
*set = vec![additional_influence_set(1, true, false)];
set[0].weights_without_joints_present = true;
},
"mesh_definitions[0].additional_influence_sets[0].weights_without_joints_present",
"weights_without_joints_present requires weights_present",
);
let mut assets = valid_asset_measurements();
let mut complementary = additional_influence_set(1, true, true);
complementary.joints_without_weights_present = true;
complementary.weights_without_joints_present = true;
assets.mesh_definitions[0].additional_influence_sets = vec![complementary];
MeasurementContract::new(BTreeMap::new(), assets)
.expect("complementary mismatches on different primitives remain valid evidence");
}
#[test]
fn rig_info_rejects_roles_resolved_from_another_skeleton() {
let mut source = Document::default();
source.skeleton.bones = vec![
Bone {
name: "root".into(),
parent: None,
rest: Transform::IDENTITY,
inverse_bind: None,
},
Bone {
name: "foot".into(),
parent: Some(0),
rest: Transform::IDENTITY,
inverse_bind: None,
},
];
let roles = ResolvedRoles::from_names(&source.skeleton, [(Role::LeftFoot, "foot".to_owned())]);
let other = Document::default();
assert_eq!(
RigInfo::from_resolved(&other, &roles),
Err(RigInfoError::InvalidBoneId {
role: "left_foot",
bone: 1,
bone_count: 0,
})
);
let mut same_size = Document::default();
same_size.skeleton.bones = vec![
Bone {
name: "root".into(),
parent: None,
rest: Transform::IDENTITY,
inverse_bind: None,
},
Bone {
name: "hand".into(),
parent: Some(0),
rest: Transform::IDENTITY,
inverse_bind: None,
},
];
assert_eq!(
RigInfo::from_resolved(&same_size, &roles),
Err(RigInfoError::BoneNameMismatch {
role: "left_foot",
bone: 1,
expected: "foot".into(),
found: "hand".into(),
})
);
}