acorn-schema 0.4.0

Portable ACORN schema, validation, and codecs
use super::{Device, Inventory, Reading, RequirementAssessment, RequirementState, Sample};
use crate::hardware::{Backend, GpuArchitecture, Memory, Resource, Vendor};
use crate::validation::Validate;
use acorn_core::time::{Milliseconds, UnitOfTime};
use serde_json::{from_str, to_string};

fn device(identifier: &str, index: Option<u32>) -> Device {
    Device {
        identifier: identifier.to_string(),
        index,
        name: format!("{identifier} name"),
        memory_total_mib: Some(81_920),
    }
}
fn gpu_resource(count: Option<u32>, memory: Option<Memory>) -> Resource {
    Resource::GPU {
        architecture: None,
        backend: None,
        compute_capability: None,
        count,
        memory,
        name: None,
        required: None,
        vendor: None,
    }
}
fn issues<T: Validate>(value: &T) -> Vec<(String, String)> {
    value
        .validate()
        .expect_err("value should be invalid")
        .into_issues()
        .into_iter()
        .map(|issue| (issue.path, issue.error.code))
        .collect()
}
fn reading(identifier: &str, utilization_percent: Option<u8>) -> Reading {
    Reading {
        identifier: identifier.to_string(),
        memory_used_mib: Some(1_024),
        utilization_percent,
    }
}

#[test]
fn test_gpu_contract_rejects_unknown_and_legacy_fields() {
    assert!(from_str::<Device>(r#"{"identifier":"GPU-a","index":0,"name":"A","memory_total_mib":1,"extra":true}"#).is_err());
    assert!(from_str::<Device>(r#"{"uuid":"GPU-a","index":0,"name":"A","memory_total_mib":1}"#).is_err());
    assert!(from_str::<Reading>(r#"{"identifier":"GPU-a","memory_used_mib":1,"utilization_percent":2,"extra":true}"#).is_err());
    assert!(from_str::<Sample>(r#"{"timestamp":0,"readings":[],"extra":true}"#).is_err());
}
#[test]
fn test_gpu_contract_round_trips_exact_json() {
    let device_json = r#"{"identifier":"GPU-a","index":0,"name":"NVIDIA H100","memory_total_mib":81920}"#;
    let parsed_device = from_str::<Device>(device_json).expect("device should deserialize");
    assert_eq!(to_string(&parsed_device).expect("device should serialize"), device_json);
    let inventory_json = format!("[{device_json}]");
    let parsed_inventory = from_str::<Inventory>(&inventory_json).expect("inventory should deserialize");
    assert_eq!(to_string(&parsed_inventory).expect("inventory should serialize"), inventory_json);
    let reading_json = r#"{"identifier":"GPU-a","memory_used_mib":null,"utilization_percent":null}"#;
    let parsed_reading = from_str::<Reading>(reading_json).expect("reading should deserialize");
    assert_eq!(to_string(&parsed_reading).expect("reading should serialize"), reading_json);
    let sample_json = format!(r#"{{"timestamp":0,"readings":[{reading_json}]}}"#);
    let parsed_sample = from_str::<Sample>(&sample_json).expect("sample should deserialize");
    assert_eq!(parsed_sample.timestamp, Milliseconds::new(0));
    assert_eq!(to_string(&parsed_sample).expect("sample should serialize"), sample_json);
}
#[test]
fn test_gpu_inventory_constructor_and_accessors_preserve_order() {
    let devices = vec![device("GPU-b", Some(1)), device("GPU-a", Some(0))];
    let inventory = Inventory::new(devices.clone()).expect("inventory should be valid");
    assert_eq!(inventory.devices(), devices.as_slice());
    assert_eq!(inventory.into_devices(), devices);
}
#[test]
fn test_gpu_inventory_reports_duplicate_identifiers_and_present_indices_in_order() {
    let inventory = Inventory(vec![device("GPU-a", Some(2)), device("GPU-a", Some(2)), device("GPU-a", None)]);
    assert_eq!(
        issues(&inventory),
        vec![
            ("[1].identifier".to_string(), "unique".to_string()),
            ("[2].identifier".to_string(), "unique".to_string()),
            ("[1].index".to_string(), "unique".to_string()),
        ]
    );
}
#[test]
fn test_gpu_requirement_assessment_checks_count_and_memory_per_device() {
    let inventory = Inventory::new(vec![device("GPU-a", Some(0)), device("GPU-b", Some(1))]).expect("inventory should be valid");
    let assessment = gpu_resource(Some(2), Some(Memory::gb(80)))
        .assess_gpu_inventory(&inventory)
        .expect("GPU resource should be assessed");
    assert_eq!(assessment.count, RequirementState::Satisfied);
    assert_eq!(assessment.memory, RequirementState::Satisfied);
    assert_eq!(assessment.overall(), RequirementState::Satisfied);
    let shortfall = gpu_resource(Some(3), Some(Memory::gb(80)))
        .assess_gpu_inventory(&inventory)
        .expect("GPU resource should be assessed");
    assert_eq!(shortfall.count, RequirementState::Unsatisfied);
    assert_eq!(shortfall.memory, RequirementState::Unsatisfied);
}
#[test]
fn test_gpu_requirement_assessment_preserves_unknown_observations() {
    let mut unknown_memory = device("GPU-a", Some(0));
    unknown_memory.memory_total_mib = None;
    let inventory = Inventory::new(vec![unknown_memory]).expect("inventory should be valid");
    let resource = Resource::GPU {
        architecture: Some(GpuArchitecture::Ampere),
        backend: Some(Backend::Cuda),
        compute_capability: Some(8.0),
        count: None,
        memory: Some(Memory::gb(80)),
        name: Some("NVIDIA H100".to_string()),
        required: None,
        vendor: Some(Vendor::Nvidia),
    };
    let assessment = resource.assess_gpu_inventory(&inventory).expect("GPU resource should be assessed");
    assert_eq!(assessment.count, RequirementState::Satisfied);
    assert_eq!(assessment.memory, RequirementState::Unknown);
    assert_eq!(assessment.architecture, RequirementState::Unknown);
    assert_eq!(assessment.backend, RequirementState::Unknown);
    assert_eq!(assessment.compute_capability, RequirementState::Unknown);
    assert_eq!(assessment.name, RequirementState::Unknown);
    assert_eq!(assessment.vendor, RequirementState::Unknown);
    assert_eq!(assessment.overall(), RequirementState::Unknown);
}
#[test]
fn test_gpu_requirement_assessment_prioritizes_failure_and_skips_other_resources() {
    let assessment = RequirementAssessment {
        count: RequirementState::Unknown,
        memory: RequirementState::Unsatisfied,
        architecture: RequirementState::Satisfied,
        backend: RequirementState::Satisfied,
        compute_capability: RequirementState::Satisfied,
        name: RequirementState::Satisfied,
        vendor: RequirementState::Satisfied,
    };
    assert_eq!(assessment.overall(), RequirementState::Unsatisfied);
    let inventory = Inventory::new(vec![device("GPU-a", Some(0))]).expect("inventory should be valid");
    assert_eq!(Resource::Other("custom".to_string()).assess_gpu_inventory(&inventory), None);
}
#[test]
fn test_gpu_sample_reports_duplicate_identifiers_in_order() {
    let sample = Sample {
        timestamp: Milliseconds::new(1),
        readings: vec![reading("GPU-a", Some(1)), reading("GPU-a", None), reading("GPU-a", Some(100))],
    };
    assert_eq!(
        issues(&sample),
        vec![
            ("readings[1].identifier".to_string(), "unique".to_string()),
            ("readings[2].identifier".to_string(), "unique".to_string()),
        ]
    );
}
#[test]
fn test_gpu_validation_accepts_optional_values_boundaries_and_opaque_identifiers() {
    let inventory = Inventory::new(vec![device("GPU-a", None), device("gpu-a", None)]).expect("absent indices should repeat");
    assert_eq!(inventory.devices().first().map(|device| device.identifier.as_str()), Some("GPU-a"));
    let sample = Sample {
        timestamp: Milliseconds::new(0),
        readings: vec![
            Reading {
                identifier: " GPU-a ".to_string(),
                memory_used_mib: None,
                utilization_percent: Some(0),
            },
            Reading {
                identifier: "unlisted-device".to_string(),
                memory_used_mib: Some(u64::MAX),
                utilization_percent: Some(100),
            },
        ],
    };
    assert!(sample.validate().is_ok());
}
#[test]
fn test_gpu_validation_rejects_empty_collections() {
    assert_eq!(issues(&Inventory(Vec::new())), vec![(String::new(), "nonempty".to_string())]);
    assert_eq!(
        issues(&Sample {
            timestamp: Milliseconds::new(0),
            readings: Vec::new(),
        }),
        vec![("readings".to_string(), "nonempty".to_string())]
    );
    assert!(Inventory::new(Vec::new()).is_err());
}
#[test]
fn test_gpu_validation_reports_local_field_failures() {
    assert_eq!(
        issues(&Device {
            identifier: " \n".to_string(),
            index: None,
            name: "\t".to_string(),
            memory_total_mib: None,
        }),
        vec![
            ("identifier".to_string(), "nonempty".to_string()),
            ("name".to_string(), "nonempty".to_string()),
        ]
    );
    assert_eq!(
        issues(&reading("", Some(101))),
        vec![
            ("identifier".to_string(), "nonempty".to_string()),
            ("utilization_percent".to_string(), "range".to_string()),
        ]
    );
}