#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]
use zerodds_ast::{Ast, TypeKind};
use zerodds_xml_compose::{
ComposeOptions, KeyOnUnionDiscriminator, Severity, compose_all_sources, compose_source,
compose_source_with,
};
fn type_named<'a>(ast: &'a Ast, name: &str) -> &'a zerodds_ast::TypeNode {
ast.types()
.find(|(_, node)| node.name == name)
.map(|(_, node)| node)
.unwrap_or_else(|| panic!("no type named `{name}`"))
}
#[test]
fn a_type_name_that_resolves_to_nothing_is_reported() {
let composed = compose_source(
"x.xml",
r#"<dds><types><module name="M">
<struct name="S" extensibility="final">
<member name="a" type="nonBasic" nonBasicTypeName="Absent"/>
</struct>
</module></types></dds>"#,
);
assert!(composed.failed(), "an absent type name must be an error");
assert!(
composed
.diagnostics
.iter()
.any(|d| d.message.contains("resolve")),
"{:#?}",
composed.diagnostics
);
}
const KEYED_DISCRIMINATOR: &str = r#"<dds><types><module name="M">
<union name="U"><discriminator type="uint32" key="true"/>
<case><caseDiscriminator value="1"/><member name="x" type="int16"/></case>
</union>
</module></types></dds>"#;
#[test]
fn a_key_on_a_union_discriminator_is_carried_by_default() {
let composed = compose_source("x.xml", KEYED_DISCRIMINATOR);
assert!(!composed.failed(), "{:#?}", composed.diagnostics);
}
#[test]
fn a_key_on_a_union_discriminator_is_refused_under_the_refusing_setting_and_says_why() {
let composed = compose_source_with(
"x.xml",
KEYED_DISCRIMINATOR,
ComposeOptions {
key_on_union_discriminator: KeyOnUnionDiscriminator::Refused,
..ComposeOptions::default()
},
);
assert!(composed.failed());
let found = composed
.diagnostics
.iter()
.find(|d| d.spec_reference.contains("Table 21"))
.unwrap_or_else(|| panic!("{:#?}", composed.diagnostics));
assert_eq!(found.severity, Severity::Error);
assert!(found.message.contains("union discriminator"), "{found}");
}
#[test]
fn both_bitmask_spellings_reach_the_same_bits() {
let flags = compose_source(
"flags.xml",
r#"<dds><types><module name="M">
<bitmask name="B" bitBound="16">
<flag name="F1" value="0"/><flag name="F2" value="3"/>
</bitmask>
</module></types></dds>"#,
);
let values = compose_source(
"values.xml",
r#"<dds><types><module name="M">
<bitmask name="B" bitBound="16">
<bit_value name="F1" position="0"/><bit_value name="F2" position="3"/>
</bitmask>
</module></types></dds>"#,
);
assert!(!flags.failed(), "{:#?}", flags.diagnostics);
assert!(!values.failed(), "{:#?}", values.diagnostics);
for composed in [&flags, &values] {
let (id, _) = composed
.ast
.types()
.find(|(_, node)| node.name == "B")
.expect("the bitmask");
let bits = composed.ast.bits_of(id);
assert_eq!(bits.len(), 2);
assert_eq!(composed.ast.effective_bit_position(bits[1]), Some(3));
assert_eq!(composed.ast.effective_bit_bound(id), 16);
}
}
#[test]
fn a_length_unlimited_bound_is_an_unbounded_collection() {
for written in ["-1", "0"] {
let composed = compose_source_with(
"x.xml",
&format!(
r#"<dds><types><module name="M">
<struct name="S" extensibility="final">
<member name="a" type="int32" sequenceMaxLength="{written}"/>
</struct>
</module></types></dds>"#
),
ComposeOptions {
zero_bound_is_unbounded: true,
..ComposeOptions::default()
},
);
assert!(!composed.failed(), "{:#?}", composed.diagnostics);
let node = type_named(&composed.ast, "S");
let TypeKind::Structure { members, .. } = &node.kind else {
panic!("not a struct")
};
let member = composed.ast.member(members[0]).expect("the member");
let element = composed
.ast
.type_(member.type_.resolved().expect("resolved"))
.expect("the sequence");
match &element.kind {
TypeKind::Sequence { bound, .. } => assert_eq!(
*bound,
zerodds_ast::Bound::Unbounded,
"`{written}` is LENGTH_UNLIMITED"
),
other => panic!("expected a sequence, got {}", other.as_str()),
}
}
}
#[test]
fn two_documents_reopen_one_module() {
let composed = compose_all_sources(
&[
(
String::from("a.xml"),
String::from(
r#"<dds><types><module name="Test">
<struct name="First" extensibility="final">
<member name="a" type="int32"/>
</struct>
</module></types></dds>"#,
),
),
(
String::from("b.xml"),
String::from(
r#"<dds><types><module name="Test">
<struct name="Second" extensibility="final">
<member name="b" type="nonBasic" nonBasicTypeName="First"/>
</struct>
</module></types></dds>"#,
),
),
],
ComposeOptions::default(),
);
assert!(!composed.failed(), "{:#?}", composed.diagnostics);
let modules: Vec<&str> = composed
.ast
.modules()
.map(|(_, node)| node.name.as_str())
.filter(|name| *name == "Test")
.collect();
assert_eq!(modules.len(), 1, "one module node, not two");
let b = type_named(&composed.ast, "Second");
let TypeKind::Structure { members, .. } = &b.kind else {
panic!("not a struct")
};
let member = composed.ast.member(members[0]).expect("the member");
assert!(
member.type_.is_resolved(),
"a name written in one document resolves to a type declared in another"
);
}
#[test]
fn a_declaration_with_no_model_node_is_named_rather_than_dropped() {
let composed = compose_source(
"x.xml",
r#"<dds><types><module name="M">
<widget name="Bits"/>
</module></types></dds>"#,
);
assert!(composed.failed());
assert!(
composed
.diagnostics
.iter()
.any(|d| d.message.contains("<widget>")),
"{:#?}",
composed.diagnostics
);
}
#[test]
fn the_default_extensibility_only_reaches_types_the_document_left_open() {
use zerodds_ast::Extensibility;
let source = r#"<dds><types><module name="M">
<struct name="Said" extensibility="final"><member name="a" type="int32"/></struct>
<struct name="Silent"><member name="a" type="int32"/></struct>
</module></types></dds>"#;
let composed = zerodds_xml_compose::compose_source_with(
"x.xml",
source,
ComposeOptions {
default_extensibility: Some(Extensibility::Mutable),
..ComposeOptions::default()
},
);
assert!(!composed.failed(), "{:#?}", composed.diagnostics);
let of = |name: &str| {
let (id, _) = composed
.ast
.types()
.find(|(_, node)| node.name == name)
.expect("the type");
composed.ast.effective_extensibility(id)
};
assert_eq!(of("Said"), Extensibility::Final, "the document said final");
assert_eq!(of("Silent"), Extensibility::Mutable, "the option filled it");
let bare = compose_source("x.xml", source);
let (id, _) = bare
.ast
.types()
.find(|(_, node)| node.name == "Silent")
.expect("the type");
assert_eq!(
bare.ast.effective_extensibility(id),
Extensibility::Appendable,
"XTypes 1.3 §7.3.1.2.1.8"
);
}
#[test]
fn a_malformed_document_is_an_error_and_not_an_empty_model() {
let composed = compose_source("x.xml", "<dds><types><module name=\"M\">");
assert!(composed.failed(), "{:#?}", composed.diagnostics);
}
#[test]
fn a_bitmask_discriminated_union_keeps_its_cases() {
let composed = compose_source(
"x.xml",
r#"<dds><types><module name="M">
<bitmask name="B_16" bitBound="16">
<flag name="F1" value="0"/><flag name="F2" value="1"/>
</bitmask>
<union name="union_bitmask16" extensibility="appendable">
<discriminator type="nonBasic" nonBasicTypeName="B_16"/>
<case><caseDiscriminator value="2"/><member name="x1" type="int16"/></case>
<case><caseDiscriminator value="1"/><member name="x2" type="int32"/></case>
</union>
</module></types></dds>"#,
);
assert!(!composed.failed(), "{:#?}", composed.diagnostics);
let (id, _) = composed
.ast
.types()
.find(|(_, node)| node.name == "union_bitmask16")
.expect("the union");
assert_eq!(
composed.ast.cases_of(id).len(),
2,
"both cases are in the model"
);
let TypeKind::Union { discriminator, .. } = &composed.ast.type_(id).expect("the union").kind
else {
panic!("not a union")
};
let disc = composed
.ast
.type_(discriminator.resolved().expect("resolved"))
.expect("the discriminator");
assert!(matches!(disc.kind, TypeKind::Bitmask { .. }));
let identifier = zerodds_types::from_ast::type_identifier(
&composed.ast,
id,
zerodds_types::from_ast::Form::Complete,
);
assert!(identifier.is_ok(), "{identifier:?}");
}
#[test]
fn a_bound_of_zero_is_refused_by_default() {
let source = r#"<dds><types><module name="M">
<struct name="S"><member name="a" type="int32" sequenceMaxLength="0"/>
<member name="text" type="string" stringMaxLength="0"/></struct>
</module></types></dds>"#;
let composed = compose_source("x.xml", source);
assert!(composed.failed());
assert_eq!(
composed
.diagnostics
.iter()
.filter(|d| d.message.contains("bound of zero"))
.count(),
2,
"{:#?}",
composed.diagnostics
);
let lenient = compose_source_with(
"x.xml",
source,
ComposeOptions {
zero_bound_is_unbounded: true,
..ComposeOptions::default()
},
);
assert!(!lenient.failed(), "{:#?}", lenient.diagnostics);
}