animsmith-core 0.4.1

Engine-agnostic data model, sampling, measurements, and checks for the animsmith animation-clip linter
Documentation
use animsmith_core::config::{
    CheckSettings, RuntimeNodeSelectorResolution, RuntimeNodesConfig, glob_match,
};
use animsmith_core::{Config, ConfigValidationError};

fn direct_config(selectors: Option<&[&str]>) -> Config {
    Config {
        runtime_nodes: RuntimeNodesConfig {
            selectors: selectors.map(|selectors| {
                selectors
                    .iter()
                    .map(|selector| (*selector).to_owned())
                    .collect()
            }),
        },
        ..Config::default()
    }
}

#[test]
fn direct_shared_runtime_node_policy_is_normalized_and_resolved_deterministically() {
    let config = direct_config(Some(&["socket", "hand_*", "socket", "missing"]));
    config.validate().expect("shared policy is valid");
    let selectors = config
        .runtime_node_selectors()
        .expect("non-empty policy is an authority");

    assert_eq!(selectors.selectors(), ["socket", "hand_*", "missing"]);
    assert_eq!(
        selectors.resolve([("socket", 1_u8), ("hand_l", 2), ("hand_r", 3)]),
        vec![
            RuntimeNodeSelectorResolution::ExactlyOne {
                selector: "socket".into(),
                node: 1,
            },
            RuntimeNodeSelectorResolution::Ambiguous {
                selector: "hand_*".into(),
                nodes: vec![2, 3],
            },
            RuntimeNodeSelectorResolution::NoMatch {
                selector: "missing".into(),
            },
        ]
    );
}

#[test]
fn absent_or_empty_shared_and_legacy_selector_fields_declare_no_policy() {
    for config in [
        Config::default(),
        direct_config(Some(&[])),
        Config {
            checks: [(
                "rest-world-scale".into(),
                CheckSettings {
                    node_selectors: Some(Vec::new()),
                    ..CheckSettings::default()
                },
            )]
            .into(),
            ..Config::default()
        },
    ] {
        config.validate().expect("empty policy is valid");
        assert!(config.runtime_node_selectors().is_none());
    }
}

#[test]
fn toml_shared_runtime_node_policy_preserves_first_selector_occurrence() {
    let config: Config = toml::from_str(
        r#"
            [runtime_nodes]
            selectors = ["socket", "hand_*", "socket"]
        "#,
    )
    .expect("shared TOML parses");
    config.validate().expect("shared TOML is valid");
    assert_eq!(
        config
            .runtime_node_selectors()
            .expect("shared policy is an authority")
            .selectors(),
        ["socket", "hand_*"]
    );

    let empty: Config =
        toml::from_str("[runtime_nodes]\nselectors = []").expect("empty shared TOML parses");
    empty.validate().expect("empty shared TOML is valid");
    assert!(empty.runtime_node_selectors().is_none());
}

#[test]
fn legacy_selector_alias_is_used_only_when_shared_selector_field_is_absent() {
    let config: Config = toml::from_str(
        r#"
            [checks.rest-world-scale]
            node_selectors = ["socket", "socket"]
        "#,
    )
    .expect("legacy TOML parses");
    config.validate().expect("legacy alias is valid");
    assert_eq!(
        config
            .runtime_node_selectors()
            .expect("legacy policy is an authority")
            .selectors(),
        ["socket"]
    );
}

#[test]
fn toml_rejects_simultaneous_shared_and_legacy_selector_forms() {
    for source in [
        r#"
            [runtime_nodes]
            selectors = ["socket"]

            [checks.rest-world-scale]
            node_selectors = ["socket"]
        "#,
        r#"
            [runtime_nodes]
            selectors = []

            [checks.rest-world-scale]
            node_selectors = []
        "#,
    ] {
        let config: Config = toml::from_str(source).expect("both forms parse before validation");
        assert_eq!(
            config.validate(),
            Err(ConfigValidationError::ConflictingRuntimeNodeSelectors)
        );
    }
}

#[test]
fn direct_config_rejects_simultaneous_shared_and_legacy_selector_forms() {
    let mut config = direct_config(Some(&["socket"]));
    config.checks.insert(
        "rest-world-scale".into(),
        CheckSettings {
            node_selectors: Some(vec!["socket".into()]),
            ..CheckSettings::default()
        },
    );

    assert_eq!(
        config.validate(),
        Err(ConfigValidationError::ConflictingRuntimeNodeSelectors)
    );
}

#[test]
fn toml_runtime_nodes_rejects_unknown_fields() {
    let error = toml::from_str::<Config>(
        r#"
            [runtime_nodes]
            selector = ["socket"]
        "#,
    )
    .expect_err("runtime-node config must be closed");
    assert!(error.to_string().contains("selector"));
}

#[test]
fn glob_match_preserves_semantics_with_linear_hostile_input_work() {
    for (pattern, name, expected) in [
        ("", "", true),
        ("", "socket", false),
        ("*", "", true),
        ("*", "socket", true),
        ("socket", "socket", true),
        ("socket", "Socket", false),
        ("hand_*", "hand_左", true),
        ("*左", "右左", true),
        ("*左", "", false),
        ("*ab", "aab", true),
        ("*aba", "aaba", true),
        ("a*a", "a", false),
        ("*aba*aba*", "ababa", false),
        ("*aba*aba*", "abaaba", true),
        ("a**b***c", "abc", true),
        ("a**b***c", "a---b+++c", true),
        ("a**b***c", "a---b+++d", false),
    ] {
        assert_eq!(
            glob_match(pattern, name),
            expected,
            "{pattern:?} vs {name:?}"
        );
    }

    let deep_stars = "*".repeat(32_768);
    assert!(glob_match(&deep_stars, ""));

    let hostile_literal = "a".repeat(32_768);
    let hostile_pattern = format!("*{hostile_literal}b*");
    let hostile_name = "a".repeat(32_769);
    assert!(!glob_match(&hostile_pattern, &hostile_name));
}