use indexmap::IndexMap;
use serde::Deserialize;
use std::path::Path;
use crate::analyzer::{AnalyzerFieldState, CompressionOptions};
#[derive(Debug, Deserialize, Default)]
pub struct Schema {
pub version: String,
#[serde(default)]
pub metadata: Metadata,
#[serde(default)]
pub bit_order: BitOrder,
#[serde(default)]
pub conditional_offsets: Vec<ConditionalOffset>,
#[serde(default)]
pub analysis: AnalysisConfig,
pub root: Group,
}
#[derive(Clone, Debug, Deserialize, Default)]
pub struct Metadata {
#[serde(default)]
pub name: String,
#[serde(default)]
pub description: String,
}
#[derive(Debug, Deserialize, Default)]
pub struct AnalysisConfig {
#[serde(default)]
pub split_groups: Vec<SplitComparison>,
#[serde(default)]
pub compare_groups: Vec<CustomComparison>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct CompressionEstimationParams {
#[serde(default = "default_lz_match_multiplier")]
pub lz_match_multiplier: f64,
#[serde(default = "default_entropy_multiplier")]
pub entropy_multiplier: f64,
}
impl CompressionEstimationParams {
pub fn new(options: &CompressionOptions) -> Self {
Self {
lz_match_multiplier: options.lz_match_multiplier,
entropy_multiplier: options.entropy_multiplier,
}
}
}
#[derive(Debug, Deserialize)]
pub struct SplitComparison {
pub name: String,
pub group_1: Vec<String>,
pub group_2: Vec<String>,
#[serde(default)]
pub description: String,
#[serde(default)]
pub compression_estimation_group_1: Option<CompressionEstimationParams>,
#[serde(default)]
pub compression_estimation_group_2: Option<CompressionEstimationParams>,
}
#[derive(Debug, Deserialize)]
pub struct CustomComparison {
pub name: String,
pub baseline: Vec<GroupComponent>,
pub comparisons: IndexMap<String, Vec<GroupComponent>>,
#[serde(default)]
pub description: String,
}
pub(crate) fn default_lz_match_multiplier() -> f64 {
0.375
}
pub(crate) fn default_entropy_multiplier() -> f64 {
1.0
}
#[derive(Debug, Deserialize, Clone)]
#[serde(tag = "type")] pub enum GroupComponent {
#[serde(rename = "array")]
Array(GroupComponentArray),
#[serde(rename = "struct")]
Struct(GroupComponentStruct),
#[serde(rename = "padding")]
Padding(GroupComponentPadding),
#[serde(rename = "field")]
Field(GroupComponentField),
#[serde(rename = "skip")]
Skip(GroupComponentSkip),
}
#[derive(Debug, Deserialize, Clone)]
pub struct GroupComponentArray {
pub field: String,
#[serde(default)]
pub offset: u32,
#[serde(default)]
pub bits: u32,
#[serde(default = "default_lz_match_multiplier")]
pub lz_match_multiplier: f64,
#[serde(default = "default_entropy_multiplier")]
pub entropy_multiplier: f64,
}
impl Default for GroupComponentArray {
fn default() -> Self {
Self {
field: String::new(),
offset: 0,
bits: 0,
lz_match_multiplier: default_lz_match_multiplier(),
entropy_multiplier: default_entropy_multiplier(),
}
}
}
impl GroupComponentArray {
pub fn get_bits(&self, field: &AnalyzerFieldState) -> u32 {
if self.bits == 0 {
field.lenbits
} else {
self.bits
}
}
}
#[derive(Debug, Deserialize, Clone)]
pub struct GroupComponentStruct {
pub fields: Vec<GroupComponent>,
#[serde(default = "default_lz_match_multiplier")]
pub lz_match_multiplier: f64,
#[serde(default = "default_entropy_multiplier")]
pub entropy_multiplier: f64,
}
#[derive(Debug, Deserialize, Clone)]
pub struct GroupComponentPadding {
pub bits: u8,
#[serde(default)]
pub value: u8,
}
#[derive(Debug, Deserialize, Clone)]
pub struct GroupComponentSkip {
pub field: String,
pub bits: u32,
}
#[derive(Debug, Deserialize, Clone)]
pub struct GroupComponentField {
pub field: String,
#[serde(default)]
pub bits: u32,
}
impl GroupComponentField {
pub fn set_bits(&mut self, default: u32) {
if self.bits == 0 {
self.bits = default
}
}
}
#[derive(Debug, Deserialize)]
#[serde(untagged)]
#[non_exhaustive]
pub enum FieldDefinition {
Field(Field),
Group(Group),
}
#[derive(Debug)]
pub struct Field {
pub bits: u32,
pub description: String,
pub bit_order: BitOrder,
pub skip_if_not: Vec<Condition>,
pub skip_frequency_analysis: bool,
}
impl<'de> Deserialize<'de> for Field {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum FieldRepr {
Shorthand(u32),
Extended {
bits: u32,
#[serde(default)]
description: String,
#[serde(default)]
#[serde(rename = "bit_order")]
bit_order: BitOrder,
#[serde(default)]
skip_if_not: Vec<Condition>,
#[serde(default)]
skip_frequency_analysis: bool,
},
}
match FieldRepr::deserialize(deserializer)? {
FieldRepr::Shorthand(size) => Ok(Field {
bits: size,
description: String::new(),
bit_order: BitOrder::default(),
skip_if_not: Vec::new(),
skip_frequency_analysis: false,
}),
FieldRepr::Extended {
bits,
description,
bit_order,
skip_if_not,
skip_frequency_analysis,
} => Ok(Field {
bits,
description,
bit_order,
skip_if_not,
skip_frequency_analysis,
}),
}
}
}
#[derive(Debug, Default)]
pub struct Group {
_type: String,
pub description: String,
pub fields: IndexMap<String, FieldDefinition>,
pub bits: u32,
pub bit_order: BitOrder,
pub skip_if_not: Vec<Condition>,
pub skip_frequency_analysis: bool,
}
impl<'de> Deserialize<'de> for Group {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct GroupRepr {
#[serde(rename = "type")]
_type: String,
#[serde(default)]
description: String,
#[serde(default)]
bit_order: BitOrder,
#[serde(default)]
fields: IndexMap<String, FieldDefinition>,
#[serde(default)]
skip_if_not: Vec<Condition>,
#[serde(default)]
skip_frequency_analysis: bool,
}
let group = GroupRepr::deserialize(deserializer)?;
if group._type != "group" {
return Err(serde::de::Error::custom(format!(
"Invalid group type: {} (must be 'group')",
group._type
)));
}
let bits = group
.fields
.values()
.map(|fd| match fd {
FieldDefinition::Field(f) => f.bits,
FieldDefinition::Group(g) => g.bits,
})
.sum();
let mut group = Group {
_type: group._type,
description: group.description,
fields: group.fields,
bits,
bit_order: group.bit_order,
skip_if_not: group.skip_if_not,
skip_frequency_analysis: group.skip_frequency_analysis,
};
let bit_order = group.bit_order;
propagate_bit_order(&mut group, bit_order);
Ok(group)
}
}
impl Group {
fn collect_field_paths(&self, paths: &mut Vec<String>, parent_path: &str) {
for (name, item) in &self.fields {
match item {
FieldDefinition::Field(_) => {
let full_path = if parent_path.is_empty() {
name
} else {
&format!("{}.{}", parent_path, name)
};
paths.push(full_path.clone());
}
FieldDefinition::Group(g) => {
let new_parent = if parent_path.is_empty() {
name
} else {
&format!("{}.{}", parent_path, name)
};
paths.push(new_parent.clone());
g.collect_field_paths(paths, new_parent);
}
}
}
}
}
#[derive(Debug, Deserialize, Default, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum BitOrder {
#[default]
Default,
Msb,
Lsb,
}
impl BitOrder {
pub fn get_with_default_resolve(self) -> BitOrder {
if self == BitOrder::Default {
BitOrder::Msb
} else {
self
}
}
}
fn propagate_bit_order(group: &mut Group, parent_bit_order: BitOrder) {
for (_, field_def) in group.fields.iter_mut() {
match field_def {
FieldDefinition::Field(field) => {
if field.bit_order == BitOrder::Default {
field.bit_order = parent_bit_order;
}
}
FieldDefinition::Group(child_group) => {
if child_group.bit_order == BitOrder::Default {
child_group.bit_order = parent_bit_order;
}
propagate_bit_order(child_group, child_group.bit_order);
}
}
}
}
#[derive(Debug, PartialEq, Clone, serde::Deserialize)]
pub struct Condition {
pub byte_offset: u64,
pub bit_offset: u8,
pub bits: u8,
pub value: u64,
#[serde(default)]
pub bit_order: BitOrder,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ConditionalOffset {
pub offset: u64,
pub conditions: Vec<Condition>,
}
#[derive(thiserror::Error, Debug)]
pub enum SchemaError {
#[error("Invalid schema version (expected 1.0)")]
InvalidVersion,
#[error("YAML parsing error: {0}")]
YamlError(#[from] serde_yaml::Error),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid group type: {0} (must be 'group')")]
InvalidGroupType(String),
}
impl Schema {
pub fn from_yaml(content: &str) -> Result<Self, SchemaError> {
let schema: Schema = serde_yaml::from_str(content)?;
if schema.version != "1.0" {
return Err(SchemaError::InvalidVersion);
}
Ok(schema)
}
pub fn load_from_file(path: &Path) -> Result<Self, SchemaError> {
let content = std::fs::read_to_string(path)?;
Self::from_yaml(&content)
}
pub fn ordered_field_and_group_paths(&self) -> Vec<String> {
let mut paths = Vec::new();
self.root.collect_field_paths(&mut paths, "");
paths
}
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_schema {
($yaml:expr, $test:expr) => {{
let schema = Schema::from_yaml($yaml).expect("Failed to parse schema");
$test(schema);
}};
}
mod version_tests {
use super::*;
#[test]
fn supports_version_10() {
let yaml = r#"
version: '1.0'
metadata: { name: Test }
root: { type: group, fields: {} }
bit_order: msb
"#;
test_schema!(yaml, |schema: Schema| {
assert_eq!(schema.version, "1.0");
assert_eq!(schema.bit_order, BitOrder::Msb);
});
}
#[test]
fn rejects_unsupported_version() {
let yaml = r#"
version: '2.0'
metadata: { name: Test }
root: { type: group, fields: {} }
"#;
assert!(Schema::from_yaml(yaml).is_err());
}
}
mod metadata_tests {
use super::*;
#[test]
fn parses_full_metadata() {
let yaml = r#"
version: '1.0'
metadata:
name: BC7 Mode4
description: Test description
root: { type: group, fields: {} }
"#;
test_schema!(yaml, |schema: Schema| {
assert_eq!(schema.metadata.name, "BC7 Mode4");
assert_eq!(schema.metadata.description, "Test description");
});
}
#[test]
fn handles_empty_metadata() {
let yaml = r#"
version: '1.0'
root: { type: group, fields: {} }
"#;
test_schema!(yaml, |schema: Schema| {
assert_eq!(schema.metadata.name, "");
assert_eq!(schema.metadata.description, "");
});
}
}
mod fields_tests {
use super::*;
#[test]
fn supports_shorthand_field() {
let yaml = r#"
version: '1.0'
metadata: { name: Test }
root:
type: group
fields:
mode: 2
partition: 4
"#;
test_schema!(yaml, |schema: Schema| {
let mode = match schema.root.fields.get("mode") {
Some(FieldDefinition::Field(f)) => f,
_ => panic!("Expected field"),
};
assert_eq!(mode.bits, 2);
let partition = match schema.root.fields.get("partition") {
Some(FieldDefinition::Field(f)) => f,
_ => panic!("Expected field"),
};
assert_eq!(partition.bits, 4);
});
}
#[test]
fn supports_extended_field() {
let yaml = r#"
version: '1.0'
metadata: { name: Test }
root:
type: group
fields:
mode:
type: field
bits: 3
description: Mode selector
bit_order: lsb
bit_order: msb
"#;
test_schema!(yaml, |schema: Schema| {
let field = match schema.root.fields.get("mode") {
Some(FieldDefinition::Field(f)) => f,
_ => panic!("Expected field"),
};
assert_eq!(field.bits, 3);
assert_eq!(field.description, "Mode selector");
assert_eq!(field.bit_order, BitOrder::Lsb);
assert_eq!(schema.bit_order, BitOrder::Msb);
});
}
#[test]
fn supports_nested_groups() {
let yaml = r#"
version: '1.0'
metadata: { name: Test }
root:
type: group
fields:
header:
type: group
fields:
mode: 2
partition: 4
colors:
type: group
fields:
r:
type: group
fields:
R0: 5
R1: 5
bit_order: msb
"#;
test_schema!(yaml, |schema: Schema| {
let header = match schema.root.fields.get("header") {
Some(FieldDefinition::Group(g)) => g,
_ => panic!("Expected group"),
};
assert_eq!(header.fields.len(), 2);
let colors = match schema.root.fields.get("colors") {
Some(FieldDefinition::Group(g)) => g,
_ => panic!("Expected group"),
};
let r = match colors.fields.get("r") {
Some(FieldDefinition::Group(g)) => g,
_ => panic!("Expected group"),
};
assert_eq!(r.fields.len(), 2);
assert_eq!(schema.bit_order, BitOrder::Msb);
});
}
#[test]
fn calculates_group_bits_from_children() {
let yaml = r#"
version: '1.0'
root:
type: group
fields:
a: 4
b: 8
subgroup:
type: group
fields:
c: 2
d: 2
"#;
test_schema!(yaml, |schema: Schema| {
assert_eq!(schema.root.bits, 16);
match schema.root.fields.get("subgroup") {
Some(FieldDefinition::Group(g)) => assert_eq!(g.bits, 4),
_ => panic!("Expected subgroup"),
}
});
}
}
mod bit_order_tests {
use super::*;
#[test]
fn inherits_bit_order_from_parent() {
let yaml = r#"
version: '1.0'
root:
type: group
bit_order: lsb
fields:
a: 4
b: 8
subgroup:
type: group
fields:
c: 2
d: 2
bit_order: msb
"#;
test_schema!(yaml, |schema: Schema| {
match schema.root.fields.get("a") {
Some(FieldDefinition::Field(f)) => assert_eq!(f.bit_order, BitOrder::Lsb),
_ => panic!("Expected field"),
}
match schema.root.fields.get("b") {
Some(FieldDefinition::Field(f)) => assert_eq!(f.bit_order, BitOrder::Lsb),
_ => panic!("Expected field"),
}
match schema.root.fields.get("subgroup") {
Some(FieldDefinition::Group(g)) => {
assert_eq!(g.bit_order, BitOrder::Lsb);
match g.fields.get("c") {
Some(FieldDefinition::Field(f)) => {
assert_eq!(f.bit_order, BitOrder::Lsb)
}
_ => panic!("Expected field"),
}
match g.fields.get("d") {
Some(FieldDefinition::Field(f)) => {
assert_eq!(f.bit_order, BitOrder::Lsb)
}
_ => panic!("Expected field"),
}
}
_ => panic!("Expected subgroup"),
}
});
}
#[test]
fn preserves_explicit_bit_order_in_children() {
let yaml = r#"
version: '1.0'
root:
type: group
bit_order: lsb
fields:
a: 4
b:
type: field
bits: 8
bit_order: msb
subgroup:
type: group
bit_order: msb
fields:
c: 2
d: 2
bit_order: msb
"#;
test_schema!(yaml, |schema: Schema| {
match schema.root.fields.get("a") {
Some(FieldDefinition::Field(f)) => assert_eq!(f.bit_order, BitOrder::Lsb),
_ => panic!("Expected field"),
}
match schema.root.fields.get("b") {
Some(FieldDefinition::Field(f)) => assert_eq!(f.bit_order, BitOrder::Msb),
_ => panic!("Expected field"),
}
match schema.root.fields.get("subgroup") {
Some(FieldDefinition::Group(g)) => {
assert_eq!(g.bit_order, BitOrder::Msb);
match g.fields.get("c") {
Some(FieldDefinition::Field(f)) => {
assert_eq!(f.bit_order, BitOrder::Msb)
}
_ => panic!("Expected field"),
}
match g.fields.get("d") {
Some(FieldDefinition::Field(f)) => {
assert_eq!(f.bit_order, BitOrder::Msb)
}
_ => panic!("Expected field"),
}
}
_ => panic!("Expected subgroup"),
}
});
}
#[test]
fn uses_default_bit_order_when_not_specified() {
let yaml = r#"
version: '1.0'
root:
type: group
fields:
a: 4
b: 8
"#;
test_schema!(yaml, |schema: Schema| {
match schema.root.fields.get("a") {
Some(FieldDefinition::Field(f)) => assert_eq!(f.bit_order, BitOrder::Default),
_ => panic!("Expected field"),
}
match schema.root.fields.get("b") {
Some(FieldDefinition::Field(f)) => assert_eq!(f.bit_order, BitOrder::Default),
_ => panic!("Expected field"),
}
});
}
}
mod edge_cases {
use super::*;
#[test]
fn accepts_minimal_valid_schema() {
let yaml = r#"
version: '1.0'
root: { type: group, fields: {} }
"#;
test_schema!(yaml, |schema: Schema| {
assert_eq!(schema.version, "1.0");
assert!(schema.root.fields.is_empty());
});
}
#[test]
fn handles_empty_analysis() {
let yaml = r#"
version: '1.0'
metadata: { name: Test }
analysis: {}
root: { type: group, fields: {} }
"#;
test_schema!(yaml, |schema: Schema| {
assert!(schema.analysis.split_groups.is_empty());
});
}
}
mod conditional_offset_tests {
use super::*;
#[test]
fn parses_basic_conditional_offset() {
let yaml = r#"
version: '1.0'
metadata:
name: Test Schema
conditional_offsets:
- offset: 0x94
conditions:
- byte_offset: 0x00
bit_offset: 0
bits: 32
value: 0x44445320 # DDS magic
- byte_offset: 0x54
bit_offset: 0
bits: 32
value: 0x44583130
root:
type: group
fields: {}
"#;
let schema: Schema = serde_yaml::from_str(yaml).unwrap();
assert_eq!(schema.conditional_offsets.len(), 1);
let offset = &schema.conditional_offsets[0];
assert_eq!(offset.offset, 0x94);
assert_eq!(offset.conditions.len(), 2);
let cond1 = &offset.conditions[0];
assert_eq!(cond1.byte_offset, 0x00);
assert_eq!(cond1.bit_offset, 0);
assert_eq!(cond1.bits, 32);
assert_eq!(cond1.value, 0x44445320);
}
#[test]
fn handles_missing_optional_fields() {
let yaml = r#"
version: '1.0'
metadata:
name: Minimal Schema
root:
type: group
fields: {}
"#;
let schema: Schema = serde_yaml::from_str(yaml).unwrap();
assert!(schema.conditional_offsets.is_empty());
}
#[test]
fn supports_skip_if_not_conditions() {
let yaml = r#"
version: '1.0'
metadata:
name: Minimal Schema
root:
type: group
fields:
header:
type: group
skip_if_not:
- byte_offset: 0x00
bit_offset: 0
bits: 32
value: 0x44445320
fields:
magic:
type: field
bits: 32
skip_if_not:
- byte_offset: 0x54
bit_offset: 0
bits: 32
value: 0x44583130
bit_order: msb
"#;
let schema = Schema::from_yaml(yaml).unwrap();
let header_group = match &schema.root.fields["header"] {
FieldDefinition::Field(_field) => panic!("Expected group, got field"),
FieldDefinition::Group(group) => group,
};
let magic_field = match &header_group.fields["magic"] {
FieldDefinition::Field(field) => field,
FieldDefinition::Group(_group) => panic!("Expected field, got group"),
};
assert_eq!(header_group.skip_if_not.len(), 1);
assert_eq!(header_group.skip_if_not[0].byte_offset, 0x00);
assert_eq!(header_group.skip_if_not[0].value, 0x44445320);
assert_eq!(magic_field.skip_if_not.len(), 1);
assert_eq!(magic_field.skip_if_not[0].byte_offset, 0x54);
assert_eq!(magic_field.skip_if_not[0].value, 0x44583130);
assert_eq!(schema.bit_order, BitOrder::Msb);
}
}
mod split_compare_tests {
use super::*;
#[test]
fn parses_basic_comparison() {
let yaml = r#"
version: '1.0'
analysis:
split_groups:
- name: color_layouts
group_1: [colors]
group_2: [color_r, color_g, color_b]
description: Compare interleaved vs planar layouts
compression_estimation_group_1:
lz_match_multiplier: 0.5
entropy_multiplier: 1.2
compression_estimation_group_2:
lz_match_multiplier: 0.7
entropy_multiplier: 1.5
root:
type: group
fields: {}
"#;
let schema = Schema::from_yaml(yaml).unwrap();
let comparisons = &schema.analysis.split_groups;
assert_eq!(comparisons.len(), 1);
assert_eq!(comparisons[0].name, "color_layouts");
assert_eq!(comparisons[0].group_1, vec!["colors"]);
assert_eq!(
comparisons[0].group_2,
vec!["color_r", "color_g", "color_b"]
);
assert_eq!(
comparisons[0].description,
"Compare interleaved vs planar layouts"
);
assert!(comparisons[0].compression_estimation_group_1.is_some());
assert!(comparisons[0].compression_estimation_group_2.is_some());
let params1 = comparisons[0]
.compression_estimation_group_1
.as_ref()
.unwrap();
assert_eq!(params1.lz_match_multiplier, 0.5);
assert_eq!(params1.entropy_multiplier, 1.2);
let params2 = comparisons[0]
.compression_estimation_group_2
.as_ref()
.unwrap();
assert_eq!(params2.lz_match_multiplier, 0.7);
assert_eq!(params2.entropy_multiplier, 1.5);
}
#[test]
fn handles_minimal_comparison() {
let yaml = r#"
version: '1.0'
analysis:
split_groups:
- name: basic
group_1: [a]
group_2: [b]
root:
type: group
fields: {}
"#;
let schema = Schema::from_yaml(yaml).unwrap();
let comparisons = &schema.analysis.split_groups;
assert_eq!(comparisons.len(), 1);
assert_eq!(comparisons[0].name, "basic");
assert!(comparisons[0].description.is_empty());
assert!(comparisons[0].compression_estimation_group_1.is_none());
assert!(comparisons[0].compression_estimation_group_2.is_none());
}
}
mod group_compare_tests {
use crate::schema::{GroupComponent, Schema};
#[test]
fn parses_custom_comparison() {
let yaml = r#"
version: '1.0'
analysis:
compare_groups:
- name: convert_7_to_8_bit
description: "Adjust 7-bit color channel to 8-bit by appending a padding bit."
lz_match_multiplier: 0.45
entropy_multiplier: 1.1
baseline: # R, R, R
- type: array
field: color7
bits: 7
lz_match_multiplier: 0.5
entropy_multiplier: 1.2
comparisons:
padded_8bit:
- type: struct # R+0, R+0, R+0
lz_match_multiplier: 0.6
entropy_multiplier: 1.3
fields:
- { type: field, field: color7, bits: 7 }
- { type: padding, bits: 1, value: 0 }
- { type: skip, field: color7, bits: 0 }
root:
type: group
fields: {}
"#;
let schema = Schema::from_yaml(yaml).unwrap();
let comparisons = &schema.analysis.compare_groups;
assert_eq!(comparisons.len(), 1);
assert_eq!(comparisons[0].name, "convert_7_to_8_bit");
let baseline = &comparisons[0].baseline;
assert_eq!(baseline.len(), 1);
match baseline.first().unwrap() {
GroupComponent::Array(array) => {
assert_eq!(array.field, "color7");
assert_eq!(array.bits, 7);
assert_eq!(array.lz_match_multiplier, 0.5);
assert_eq!(array.entropy_multiplier, 1.2);
}
_ => unreachable!("Expected an array type"),
}
let comps = &comparisons[0].comparisons;
assert_eq!(comps.len(), 1);
assert!(comps.contains_key("padded_8bit"));
let padded = &comps["padded_8bit"];
assert_eq!(padded.len(), 1);
match padded.first().unwrap() {
GroupComponent::Struct(group) => {
assert_eq!(group.lz_match_multiplier, 0.6);
assert_eq!(group.entropy_multiplier, 1.3);
assert_eq!(group.fields.len(), 3);
match &group.fields[0] {
GroupComponent::Field(field) => {
assert_eq!(field.field, "color7");
assert_eq!(field.bits, 7);
}
_ => unreachable!("Expected a field type"),
}
match &group.fields[1] {
GroupComponent::Padding(padding) => {
assert_eq!(padding.bits, 1);
assert_eq!(padding.value, 0);
}
_ => unreachable!("Expected a padding type"),
}
match &group.fields[2] {
GroupComponent::Skip(skip) => {
assert_eq!(skip.bits, 0);
}
_ => unreachable!("Expected a skip type"),
}
}
_ => unreachable!("Expected a struct type"),
}
}
#[test]
fn rejects_invalid_custom_comparison() {
let yaml = r#"
version: '1.0'
root:
type: group
fields: {}
analysis:
compare_groups:
- name: missing_fields
group_1: [field_a]
"#;
let result = Schema::from_yaml(yaml);
assert!(result.is_err());
}
#[test]
fn preserves_comparison_order() {
let yaml = r#"
version: '1.0'
analysis:
compare_groups:
- name: bit_expansion
description: "Test multiple comparison order preservation"
baseline:
- { type: array, field: original }
comparisons:
comparison_c:
- { type: padding, bits: 1 }
comparison_a:
- { type: padding, bits: 2 }
comparison_b:
- { type: padding, bits: 3 }
root:
type: group
fields: {}
"#;
let schema = Schema::from_yaml(yaml).unwrap();
let comparison = &schema.analysis.compare_groups[0];
let keys: Vec<&str> = comparison.comparisons.keys().map(|s| s.as_str()).collect();
assert_eq!(keys, vec!["comparison_c", "comparison_a", "comparison_b"]);
assert_eq!(comparison.name, "bit_expansion");
assert_eq!(
comparison.description,
"Test multiple comparison order preservation"
);
assert_eq!(comparison.comparisons.len(), 3);
}
#[test]
fn handles_minimal_custom_comparison() {
let yaml = r#"
version: '1.0'
analysis:
compare_groups:
- name: minimal_test
baseline:
- { type: array, field: test_field, bits: 8 }
comparisons:
simple:
- { type: array, field: test_field, bits: 8 }
root:
type: group
fields: {}
"#;
let schema = Schema::from_yaml(yaml).unwrap();
let comparisons = &schema.analysis.compare_groups;
assert_eq!(comparisons.len(), 1);
assert_eq!(comparisons[0].name, "minimal_test");
assert!(comparisons[0].description.is_empty());
}
}
}