use quadlet_lens::model::{
DocumentSetError, NamedQuadletDocument, QuadletDocument, QuadletDocumentSet, QuadletUnitType, ReferenceResolution,
UnitFileName, UnitReferenceKind,
};
use quadlet_lens::source::SourceId;
const APP: &str = include_str!("../fixtures/typed-model/document-set-resolution/app.container");
const PROXY: &str = include_str!("../fixtures/typed-model/document-set-resolution/proxy.container");
const POD: &str = include_str!("../fixtures/typed-model/document-set-resolution/application.pod");
const NETWORK: &str = include_str!("../fixtures/typed-model/document-set-resolution/frontend.network");
const VOLUME: &str = include_str!("../fixtures/typed-model/document-set-resolution/cache.volume");
#[test]
fn document_set_resolves_container_pod_network_and_volume_dependencies() -> Result<(), String> {
let set = QuadletDocumentSet::new([
named("app.container", QuadletUnitType::Container, 51, APP)?,
named("proxy.container", QuadletUnitType::Container, 52, PROXY)?,
named("application.pod", QuadletUnitType::Pod, 53, POD)?,
named("frontend.network", QuadletUnitType::Network, 54, NETWORK)?,
named("cache.volume", QuadletUnitType::Volume, 55, VOLUME)?,
])
.map_err(|error| error.to_string())?;
assert!(set.is_valid(), "{:#?}", set.diagnostics());
assert!(set.graph().is_complete());
assert_eq!(set.graph().references().len(), 5);
assert_eq!(set.graph().edges().len(), 5);
assert_eq!(
set.document("application.pod")
.map(|document| document.document().unit_type()),
Some(QuadletUnitType::Pod)
);
let edges: Vec<_> = set
.graph()
.edges()
.iter()
.map(|edge| {
(
set.documents()[edge.source_document()].name().as_str(),
set.documents()[edge.target_document()].name().as_str(),
edge.kind(),
)
})
.collect();
assert_eq!(
edges,
[
("app.container", "cache.volume", UnitReferenceKind::Volume),
("app.container", "application.pod", UnitReferenceKind::Pod),
("proxy.container", "frontend.network", UnitReferenceKind::Network),
("application.pod", "frontend.network", UnitReferenceKind::Network),
("application.pod", "cache.volume", UnitReferenceKind::Volume),
]
);
Ok(())
}
#[test]
fn document_set_resolves_a_container_image_to_an_exact_build_unit() -> Result<(), String> {
let set = QuadletDocumentSet::new([
named(
"application.container",
QuadletUnitType::Container,
181,
"[Container]\nImage=application.build\n",
)?,
named(
"application.build",
QuadletUnitType::Build,
182,
"[Build]\nImageTag=localhost/application:latest\nTarget=build-stage\nSetWorkingDirectory=unit\n",
)?,
])
.map_err(|error| error.to_string())?;
assert!(set.is_valid(), "{:#?}", set.diagnostics());
assert_eq!(set.graph().references().len(), 1);
assert_eq!(set.graph().edges().len(), 1);
assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Build);
assert_eq!(
set.graph().references()[0].resolution(),
ReferenceResolution::Resolved { document_index: 1 }
);
assert_eq!(set.graph().edges()[0].target_document(), 1);
assert_eq!(
set.document("application.build")
.map(|document| document.document().unit_type()),
Some(QuadletUnitType::Build)
);
Ok(())
}
#[test]
fn document_set_resolves_a_volume_image_to_an_exact_build_unit() -> Result<(), String> {
let set = QuadletDocumentSet::new([
named(
"cache.volume",
QuadletUnitType::Volume,
183,
"[Volume]\nImage=application.build\n",
)?,
named(
"application.build",
QuadletUnitType::Build,
184,
"[Build]\nImageTag=localhost/application:latest\nTarget=build-stage\nSetWorkingDirectory=unit\n",
)?,
])
.map_err(|error| error.to_string())?;
assert!(set.is_valid(), "{:#?}", set.diagnostics());
assert_eq!(set.graph().references().len(), 1);
assert_eq!(set.graph().edges().len(), 1);
assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Build);
assert_eq!(set.graph().references()[0].target_name(), "application.build");
Ok(())
}
#[test]
fn document_set_resolves_exact_container_and_volume_image_references_to_an_image_unit() -> Result<(), String> {
let set = QuadletDocumentSet::new([
named(
"application.container",
QuadletUnitType::Container,
185,
"[Container]\nImage=application.image\n",
)?,
named(
"cache.volume",
QuadletUnitType::Volume,
186,
"[Volume]\nImage=application.image\n",
)?,
named(
"application.image",
QuadletUnitType::Image,
187,
"[Image]\nImage=example.invalid/application:latest\n",
)?,
])
.map_err(|error| error.to_string())?;
assert!(set.is_valid(), "{:#?}", set.diagnostics());
assert_eq!(set.graph().references().len(), 2);
assert_eq!(set.graph().edges().len(), 2);
assert!(set.graph().references().iter().all(|reference| {
reference.kind() == UnitReferenceKind::Image
&& reference.target_name() == "application.image"
&& reference.resolution() == ReferenceResolution::Resolved { document_index: 2 }
}));
assert_eq!(
set.document("application.image")
.map(|document| document.document().unit_type()),
Some(QuadletUnitType::Image)
);
Ok(())
}
#[test]
fn image_tag_does_not_create_or_mutate_document_set_edges() -> Result<(), String> {
let set = QuadletDocumentSet::new([
named(
"application.container",
QuadletUnitType::Container,
449,
"[Container]\nImage=application.image\n",
)?,
named(
"application.image",
QuadletUnitType::Image,
450,
"[Image]\nImage=example.invalid/application:latest\nImageTag=other.image\nServiceName=other.service\nAllTags=true\nArch=arm64\nAuthFile=/placeholder/quadlet-lens-auth.json\nCertDir=/placeholder/quadlet-lens-certs\nContainersConfModule=one.conf\nContainersConfModule=two.conf\n",
)?,
])
.map_err(|error| error.to_string())?;
assert!(set.is_valid(), "{:#?}", set.diagnostics());
assert_eq!(set.graph().references().len(), 1);
assert_eq!(set.graph().edges().len(), 1);
assert_eq!(set.graph().references()[0].target_name(), "application.image");
assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Image);
Ok(())
}
#[test]
fn document_set_resolves_build_volume_source_prefix_without_mutating_identities() -> Result<(), String> {
let set = QuadletDocumentSet::new([
named(
"application.build",
QuadletUnitType::Build,
220,
"[Build]\nImageTag=localhost/application\nVolume=cache.volume:/var/cache:Z\n",
)?,
named("cache.volume", QuadletUnitType::Volume, 221, "[Volume]\n")?,
])
.map_err(|error| error.to_string())?;
assert!(set.is_valid(), "{:#?}", set.diagnostics());
assert_eq!(set.graph().references().len(), 1);
assert_eq!(set.graph().references()[0].target_name(), "cache.volume");
assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Volume);
assert_eq!(set.graph().edges().len(), 1);
assert_eq!(
set.documents()[set.graph().edges()[0].target_document()]
.name()
.as_str(),
"cache.volume"
);
Ok(())
}
#[test]
fn document_set_resolves_an_exact_build_network_reference() -> Result<(), String> {
let set = QuadletDocumentSet::new([
named(
"application.build",
QuadletUnitType::Build,
183,
"[Build]\nNetwork=frontend.network\nNetwork=frontend.network:ip=192.0.2.10\nNetwork=frontend.container\n",
)?,
named("frontend.network", QuadletUnitType::Network, 184, "[Network]\n")?,
])
.map_err(|error| error.to_string())?;
assert!(set.is_valid(), "{:#?}", set.diagnostics());
assert_eq!(set.graph().references().len(), 1);
assert_eq!(set.graph().edges().len(), 1);
assert_eq!(set.graph().references()[0].kind(), UnitReferenceKind::Network);
assert_eq!(set.graph().references()[0].target_name(), "frontend.network");
assert_eq!(
set.graph().references()[0].resolution(),
ReferenceResolution::Resolved { document_index: 1 }
);
assert_eq!(set.graph().edges()[0].target_document(), 1);
Ok(())
}
#[test]
fn document_set_reports_missing_ambiguous_and_duplicate_identities() -> Result<(), String> {
let missing = QuadletDocumentSet::new([named(
"missing.container",
QuadletUnitType::Container,
56,
"[Container]\nImage=example.invalid/app\nNetwork=absent.network\n",
)?])
.map_err(|error| error.to_string())?;
assert!(!missing.is_valid());
assert_eq!(diagnostic_codes(&missing), ["QLG0001"]);
assert_eq!(
missing.graph().references()[0].resolution(),
ReferenceResolution::Missing
);
let ambiguous = QuadletDocumentSet::new([
named(
"ambiguous.container",
QuadletUnitType::Container,
57,
"[Container]\nImage=example.invalid/app\nNetwork=duplicate.network\n",
)?,
named("duplicate.network", QuadletUnitType::Network, 58, "[Network]\n")?,
named("duplicate.network", QuadletUnitType::Network, 59, "[Network]\n")?,
])
.map_err(|error| error.to_string())?;
assert!(!ambiguous.is_valid());
assert_eq!(diagnostic_codes(&ambiguous), ["QLG0003", "QLG0002"]);
assert_eq!(
ambiguous.graph().references()[0].resolution(),
ReferenceResolution::Ambiguous { candidates: 2 }
);
assert!(ambiguous.graph().edges().is_empty());
assert!(ambiguous.document("duplicate.network").is_none());
let duplicate_source = QuadletDocumentSet::new([
named("one.network", QuadletUnitType::Network, 60, "[Network]\n")?,
named("two.network", QuadletUnitType::Network, 60, "[Network]\n")?,
]);
assert!(matches!(duplicate_source, Err(DocumentSetError::DuplicateSourceId(id)) if id == SourceId::new(60)));
Ok(())
}
#[test]
fn unit_file_names_are_basenames_with_matching_supported_suffixes() -> Result<(), String> {
for invalid in [
"",
"application",
".container",
"application.",
"nested/app.container",
"nested\\app.container",
] {
assert!(
matches!(
UnitFileName::new(invalid),
Err(DocumentSetError::InvalidUnitFileName(_))
),
"unexpectedly accepted {invalid:?}"
);
}
assert!(matches!(
UnitFileName::new("application.service"),
Err(DocumentSetError::UnsupportedUnitFileExtension(_))
));
let image = UnitFileName::new("application.image").map_err(|error| error.to_string())?;
assert_eq!(image.as_str(), "application.image");
assert_eq!(image.to_string(), "application.image");
assert_eq!(image.unit_type(), QuadletUnitType::Image);
assert_eq!(
UnitFileName::new("application.build")
.map_err(|error| error.to_string())?
.unit_type(),
QuadletUnitType::Build
);
let network = named_document(QuadletUnitType::Network, 61, "[Network]\n")?;
let mismatch = NamedQuadletDocument::new("wrong.volume", network);
assert!(matches!(mismatch, Err(DocumentSetError::UnitTypeMismatch { .. })));
assert_eq!(
mismatch.err().map(|error| error.to_string()),
Some("Quadlet filename `wrong.volume` implies Volume, but the document is Network".to_owned())
);
Ok(())
}
#[test]
fn document_set_accessors_preserve_names_sources_and_reference_spans() -> Result<(), String> {
let source = "[Container]\nImage=example.invalid/app\nNetwork=frontend.network\n";
let set = QuadletDocumentSet::new([
named("app.container", QuadletUnitType::Container, 62, source)?,
named("frontend.network", QuadletUnitType::Network, 63, "[Network]\n")?,
])
.map_err(|error| error.to_string())?;
let reference = &set.graph().references()[0];
let edge = set.graph().edges()[0];
let expected_start = source
.find("frontend.network")
.ok_or_else(|| "test source must contain its reference".to_owned())?;
assert_eq!(reference.source_document(), 0);
assert_eq!(reference.span().source_id(), SourceId::new(62));
assert_eq!(
(reference.span().start(), reference.span().end()),
(expected_start, source.len() - 1)
);
assert_eq!(edge.source_document(), 0);
assert_eq!(edge.target_document(), 1);
assert_eq!(edge.kind(), UnitReferenceKind::Network);
assert_eq!(edge.span(), reference.span());
let decomposed = named("standalone.network", QuadletUnitType::Network, 64, "[Network]\n")?;
let (name, document) = decomposed.into_parts();
assert_eq!(name.as_str(), "standalone.network");
assert_eq!(document.source_id(), SourceId::new(64));
Ok(())
}
fn named(name: &str, unit_type: QuadletUnitType, source_id: u32, source: &str) -> Result<NamedQuadletDocument, String> {
let document = named_document(unit_type, source_id, source)?;
NamedQuadletDocument::new(name, document).map_err(|error| error.to_string())
}
fn named_document(unit_type: QuadletUnitType, source_id: u32, source: &str) -> Result<QuadletDocument, String> {
let result =
QuadletDocument::parse(unit_type, SourceId::new(source_id), source).map_err(|error| error.to_string())?;
if !result.is_valid() {
return Err(format!(
"test source has diagnostics: {:#?}",
result.model_diagnostics()
));
}
let (_, document, _) = result.into_parts();
Ok(document)
}
fn diagnostic_codes(set: &QuadletDocumentSet) -> Vec<&str> {
set.diagnostics()
.iter()
.map(|diagnostic| diagnostic.code().as_str())
.collect()
}