use super::*;
#[test]
fn extra_attributes_force_raw_and_preserve_everything() {
for version in [2, 3] {
let root = fixture(&format!("rd_extra_attr_v{version}.rds"));
assert!(
root.attributes().get("rootextra").is_some(),
"fixture root should carry the unknown rootextra attribute"
);
let doc = lower_r_object(&root).expect("root attributes never fail the lowering");
let raw_by_tag = |tag: &str| {
doc.nodes().iter().find_map(|node| match node {
RdNode::Raw(raw) if raw.tag() == Some(tag) => Some(raw),
_ => None,
})
};
let attribute_names = |raw: &crate::RawRdNode| {
raw.attributes()
.iter()
.map(|attribute| attribute.name().to_string())
.collect::<Vec<_>>()
};
let title = raw_by_tag(r"\title").expect(r"\title goes Raw (unknown attribute)");
let title_attrs = attribute_names(title);
assert!(
title_attrs.contains(&"mystery".to_string()),
"{title_attrs:?}"
);
assert!(
title_attrs.contains(&"srcref".to_string()),
"{title_attrs:?}"
);
let srcref = title
.attributes()
.iter()
.find(|attribute| attribute.name() == "srcref")
.expect("Raw title preserves srcref");
assert!(
matches!(srcref.value().value(), RawRdValue::Integer(_)),
"srcref should preserve its integer vector: {srcref:?}"
);
let srcfile = srcref
.value()
.attributes()
.iter()
.find(|attribute| attribute.name() == "srcfile")
.expect("srcref preserves its nested srcfile attribute");
assert!(
matches!(srcfile.value().value(), RawRdValue::Environment(_)),
"srcfile should preserve its environment handle: {srcfile:?}"
);
let name = raw_by_tag(r"\name").expect(r"\name goes Raw (class is root-only)");
assert!(attribute_names(name).contains(&"class".to_string()));
let alias = raw_by_tag(r"\alias").expect(r"\alias goes Raw (macros is root-only)");
assert!(attribute_names(alias).contains(&"macros".to_string()));
assert!(
doc.nodes().iter().any(|node| matches!(
node,
RdNode::Tagged(tagged) if tagged.tag() == &RdTag::Description
)),
r"\description (Rd_tag + srcref only) stays Tagged"
);
}
}
#[test]
fn valid_dynamic_flag_on_list_nodes_lowers_structurally() {
let details = dynamic_fixture_node(r"\description");
let details = with_value(&details, RValue::List(Vec::new()));
let dynamic_flag = details
.attributes()
.iter()
.find(|attribute| attribute.name().as_str() == "dynamicFlag")
.expect("dynamicFlag attribute");
let scalar_18 = Attribute::new(dynamic_flag.name().clone(), RObject::from_parts(RValue::Integer(vec![Some(18)]), Default::default()));
let context = NodeContext {
tag: Some(r"\details"),
value: details.value(),
attributes: details.attributes(),
};
assert_eq!(
classify_attribute(&context, &scalar_18),
AttributeDisposition::Discard
);
let document = lower_r_object(&RObject::from_parts(RValue::List(vec![details]), Default::default()))
.expect("lower document");
assert!(matches!(document.nodes(), [RdNode::Tagged(_)]));
let sexpr = dynamic_fixture_node(r"\Sexpr");
let document = lower_r_object(&RObject::from_parts(RValue::List(vec![sexpr]), Default::default()))
.expect("lower document");
assert!(matches!(document.nodes(), [RdNode::Tagged(_)]));
}
#[test]
fn dynamic_flag_with_srcref_is_discardable() {
let description = dynamic_fixture_node(r"\description");
assert!(description.attributes().get("srcref").is_some());
let document = lower_r_object(&RObject::from_parts(RValue::List(vec![description]), Default::default()))
.expect("lower document");
assert!(matches!(document.nodes(), [RdNode::Tagged(_)]));
}
fn class_fixture_attribute() -> Attribute {
let root = fixture("rd_extra_attr_v2.rds");
root.attributes()
.iter()
.find(|attribute| attribute.name().as_str() == "class")
.expect("class attribute")
.clone()
}
fn attribute_with_value(template: &Attribute, value: RValue) -> Attribute {
Attribute::new(
template.name().clone(),
RObject::from_parts(value, Default::default()),
)
}
fn fixture_attribute(name: &str) -> Attribute {
fn find(object: &RObject, name: &str) -> Option<Attribute> {
if let Some(attribute) = object
.attributes()
.iter()
.find(|attribute| attribute.name().as_str() == name)
{
return Some(attribute.clone());
}
match object.value() {
RValue::List(objects) => objects.iter().find_map(|object| find(object, name)),
_ => None,
}
}
[
"rd_minimal_v2.rds",
"rd_options_v2.rds",
"rd_extra_attr_v2.rds",
]
.into_iter()
.find_map(|fixture_name| find(&fixture(fixture_name), name))
.expect("fixture attribute")
}
fn invalid_string() -> RStr {
RStr::Value {
bytes: Arc::from(&b"\xff"[..]),
encoding: REncoding::Bytes,
native_encoding: None,
}
}
fn attribute(name: &str, value: RValue) -> Attribute {
attribute_with_value(&fixture_attribute(name), value)
}
fn with_nested_metadata_attributes(template: &RObject, metadata_names: &[&str]) -> RObject {
let nested_attributes = fixture("rd_options_v2.rds")
.attributes()
.get("srcref")
.expect("fixture srcref attribute")
.attributes()
.clone();
let attributes = template
.attributes()
.iter()
.map(|attribute| {
let mut attribute = attribute.clone();
if metadata_names
.iter()
.any(|name| *name == attribute.name().as_str())
{
let (value, _) = attribute.value().clone().into_parts();
attribute = Attribute::new(
attribute.name().clone(),
RObject::from_parts(value, nested_attributes.clone()),
);
}
attribute
})
.collect();
RObject::from_parts(template.value().clone(), Attributes::new(attributes))
}
fn assert_raw_metadata_has_nested_attributes(node: &RdNode, names: &[&str]) {
let RdNode::Raw(raw) = node else {
panic!("expected Raw node, got {node:?}");
};
for name in names {
let attribute = raw
.attributes()
.iter()
.find(|attribute| attribute.name() == *name)
.expect("metadata attribute preserved in Raw");
assert!(!attribute.value().attributes().is_empty());
}
}
#[test]
fn valid_rd_class_is_discardable_only_on_list_nodes() {
let class = class_fixture_attribute();
let root = fixture("rd_extra_attr_v2.rds");
let list_context = NodeContext {
tag: Some("LIST"),
value: &RValue::List(Vec::new()),
attributes: root.attributes(),
};
assert_eq!(
classify_attribute(&list_context, &class),
AttributeDisposition::Discard
);
let name_context = NodeContext {
tag: Some(r"\name"),
value: &RValue::List(Vec::new()),
attributes: root.attributes(),
};
assert_eq!(
classify_attribute(&name_context, &class),
AttributeDisposition::Reject
);
let non_list_context = NodeContext {
tag: Some("LIST"),
value: &RValue::Character(Vec::new()),
attributes: root.attributes(),
};
assert_eq!(
classify_attribute(&non_list_context, &class),
AttributeDisposition::Reject
);
}
#[test]
fn classed_list_can_coexist_with_discardable_metadata() {
let class = class_fixture_attribute();
let description = dynamic_fixture_node(r"\description");
let dynamic_flag = description
.attributes()
.iter()
.find(|attribute| attribute.name().as_str() == "dynamicFlag")
.expect("dynamicFlag attribute");
let srcref = description
.attributes()
.iter()
.find(|attribute| attribute.name().as_str() == "srcref")
.expect("srcref attribute");
let context = NodeContext {
tag: Some("LIST"),
value: &RValue::List(Vec::new()),
attributes: description.attributes(),
};
assert_eq!(
classify_attribute(&context, &class),
AttributeDisposition::Discard
);
assert_eq!(
classify_attribute(&context, dynamic_flag),
AttributeDisposition::Discard
);
assert_eq!(
classify_attribute(&context, srcref),
AttributeDisposition::Discard
);
}
#[test]
fn invalid_list_class_values_and_metadata_force_rejection() {
let class = class_fixture_attribute();
let root = fixture("rd_extra_attr_v2.rds");
let list_context = NodeContext {
tag: Some("LIST"),
value: &RValue::List(Vec::new()),
attributes: root.attributes(),
};
let invalid = [
attribute_with_value(&class, RValue::Character(Vec::new())),
attribute_with_value(
&class,
RValue::Character(vec![
match class.value().value() {
RValue::Character(values) => values[0].clone(),
_ => unreachable!("class is character"),
},
match class.value().value() {
RValue::Character(values) => values[0].clone(),
_ => unreachable!("class is character"),
},
]),
),
attribute_with_value(&class, RValue::Integer(vec![Some(1)])),
attribute_with_value(&class, RValue::Character(vec![RStr::Na])),
];
for attribute in invalid {
assert_eq!(
classify_attribute(&list_context, &attribute),
AttributeDisposition::Reject
);
}
let unknown = root
.attributes()
.iter()
.find(|attribute| attribute.name().as_str() == "rootextra")
.expect("unknown attribute")
.clone();
assert_eq!(
classify_attribute(&list_context, &unknown),
AttributeDisposition::Reject
);
}
#[test]
fn expanded_fragment_metadata_requires_the_validated_pair() {
let root = fixture("rd_extra_attr_v2.rds");
let class = class_fixture_attribute();
let context = NodeContext {
tag: Some(r"\ifelse"),
value: &RValue::List(Vec::new()),
attributes: root.attributes(),
};
assert_eq!(
classify_attribute(&context, &class),
AttributeDisposition::Reject
);
assert!(!valid_expanded_fragment_class(&context, &class));
let rdfile_values = [
RValue::Character(Vec::new()),
RValue::Character(vec![RStr::Na, RStr::Na]),
RValue::Integer(vec![Some(1)]),
];
for value in rdfile_values {
let candidate = attribute_with_value(&class, value);
assert!(!valid_expanded_fragment_rdfile(&context, &candidate));
}
let details_context = NodeContext {
tag: Some(r"\details"),
value: &RValue::List(Vec::new()),
attributes: root.attributes(),
};
assert!(!valid_expanded_fragment_class(&details_context, &class));
assert!(!valid_expanded_fragment_rdfile(&details_context, &class));
}