use headwater_resolve::{assembly, flatten, package};
use std::path::{Path, PathBuf};
use std::process::Command;
fn repository() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../../..")
.canonicalize()
.expect("the repository root resolves")
}
struct Root(PathBuf);
impl Root {
fn scratch(label: &str) -> Root {
let at = std::env::temp_dir().join(format!(
"headwater-cli-publish-{}-{label}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("the scratch tree is made");
Root(at)
}
fn colliding(label: &str) -> Root {
let root = Root::scratch(label);
copy(
&repository().join("taxonomy-source/headwater-standard"),
&root.0.join(".headwater/packages/headwater-standard"),
);
copy(
&repository().join("docs/taxonomies"),
&root.0.join("docs/taxonomies"),
);
std::fs::write(
root.0
.join(".headwater/packages/headwater-standard/bundles"),
"a regular file where the artifact needs a directory\n",
)
.expect("the colliding file is written");
root
}
fn copied(label: &str) -> Root {
let root = Root::scratch(label);
copy(
&repository().join("taxonomy-source/headwater-standard"),
&root.0.join(".headwater/packages/headwater-standard"),
);
assert!(
!root.0.join("docs/taxonomies").exists(),
"the library the manifest points at must not be in the copy"
);
root
}
fn publish(&self, out: &Path) -> (Option<i32>, String) {
publish_from(&self.0, out)
}
fn path(&self) -> &Path {
&self.0
}
}
fn publish_from(root: &Path, out: &Path) -> (Option<i32>, String) {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "publish", "--package", "headwater/standard"])
.arg("--out")
.arg(out)
.arg("--root")
.arg(root)
.output()
.expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn publish_real_source_into(out: &Path) -> (Option<i32>, String) {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.arg("taxonomy")
.arg("publish")
.arg("--from")
.arg(repository().join("taxonomy-source/headwater-standard"))
.arg("--out")
.arg(out)
.arg("--root")
.arg(repository())
.output()
.expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn publish_real_source(out: &Path, json: bool) -> (Option<i32>, String, String) {
let mut command = Command::new(env!("CARGO_BIN_EXE_headwater"));
command
.arg("taxonomy")
.arg("publish")
.arg("--from")
.arg(repository().join("taxonomy-source/headwater-standard"))
.arg("--out")
.arg(out)
.arg("--root")
.arg(repository());
if json {
command.arg("--json");
}
let output = command.output().expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Arm {
Sound,
UnselectableBundle,
UnwrittenMembers,
IncompleteSelection,
}
fn assembly_source(root: &Root, arm: Arm) -> PathBuf {
let source = root.path().join("source/acme-fixture");
write(
&source.join("package.yml"),
"package: acme/fixture\nversion: 1.0.0\ncontents:\n taxonomy: taxonomy.yml\n conformance: conformance.yml\n glossary: glossary.yml\n bundles: bundles\n assemblies: assemblies\n doctrine: doctrine\n templates: templates\n",
);
write(
&source.join("glossary.yml"),
"glossary:\n headwater: the system this fixture is a fixture of\n",
);
write(
&source.join("conformance.yml"),
"conformance:\n format: 1\n rules:\n - name: pin.current\n title: The pin names the package that is installed\n decided_by: tree\n statement: The pin names a version and a digest that the installed artifact carries.\n remediation: Publish the package, write the printed digest into the pin, and vendor it.\n levels:\n - name: L0\n title: Pointed at\n rules: [pin.current]\n",
);
write(
&source.join("taxonomy.yml"),
"taxonomy: acme/fixture\nversion: 1.0.0\npurposes:\n behavior: {intent: state what the system does}\nkinds:\n governed_document: {abstract: true}\n specification: {is_a: governed_document, purpose: behavior}\nshelves:\n specifications: {path: docs/specifications/**, homogeneous: true, kind: specification}\n alphas: {path: docs/alphas/**, homogeneous: true, kind: alpha}\n betas: {path: docs/betas/**, homogeneous: true, kind: beta}\ncore:\n requires:\n - purpose: behavior\n",
);
write(
&source.join("bundles/alpha/bundle.yml"),
"bundle: alpha\nextends: acme/fixture@1.0.0\nrequires: []\nadd:\n kinds.alpha: {is_a: governed_document, purpose: behavior}\n",
);
write(
&source.join("bundles/beta/bundle.yml"),
"bundle: beta\nextends: acme/fixture@1.0.0\nrequires: []\nadd:\n kinds.beta: {is_a: governed_document, purpose: behavior}\n",
);
let selected = match arm {
Arm::UnselectableBundle => "[alpha, missing]",
Arm::IncompleteSelection => "[alpha]",
Arm::Sound | Arm::UnwrittenMembers => "[alpha, beta]",
};
let glue = match arm {
Arm::IncompleteSelection => "",
Arm::Sound | Arm::UnselectableBundle | Arm::UnwrittenMembers => "overlay: overlay.yml\n",
};
write(
&source.join("assemblies/starter/assembly.yml"),
&format!(
"assembly: starter\npackage: acme/starter\nversion: 2.0.0\nfrom:\n package: acme/fixture@1.0.0\n bundles: {selected}\n{glue}"
),
);
write(
&source.join("assemblies/starter/overlay.yml"),
"add:\n relations.connects:\n family: derivation\n from: [alpha]\n to: [beta]\n nucleus: from\n inverse: connected_by\n reciprocal: required\n created_by: scaffold\n",
);
match arm {
Arm::UnwrittenMembers => {
for key in ["doctrine", "templates", "glossary"] {
std::fs::create_dir_all(source.join(key))
.expect("the empty declared directory is made");
}
write(
&source.join("package.yml"),
"package: acme/fixture\nversion: 1.0.0\ncontents:\n taxonomy: taxonomy.yml\n conformance: conformance.yml\n glossary: glossary\n bundles: bundles\n assemblies: assemblies\n doctrine: doctrine\n templates: templates\n",
);
}
Arm::Sound | Arm::UnselectableBundle | Arm::IncompleteSelection => {
write(&source.join("doctrine/guide.md"), "# Fixture doctrine\n");
write(&source.join("templates/decision.md"), "# Decision\n");
}
}
source
}
fn write(path: &Path, text: &str) {
std::fs::create_dir_all(path.parent().expect("the file has a parent"))
.expect("the parent is made");
std::fs::write(path, text).expect("the fixture file is written");
}
fn publish_assembly_from(root: &Path, source: &Path, out: &Path) -> (Option<i32>, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "publish", "--assembly", "starter", "--from"])
.arg(source)
.arg("--out")
.arg(out)
.arg("--root")
.arg(root)
.output()
.expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn publish_plain_from(root: &Path, source: &Path, out: &Path) -> (Option<i32>, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "publish", "--from"])
.arg(source)
.arg("--out")
.arg(out)
.arg("--root")
.arg(root)
.output()
.expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
const PIN_RULE: &str = "taxonomy.pin.diverged";
const PIN_FINDING: &str = "does not hash to it";
fn consumer_run(root: &Path, arguments: &[&str]) -> (Option<i32>, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(arguments)
.arg("--root")
.arg(root)
.output()
.expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn flattened_consumer(root: &Root, digest: &str) -> PathBuf {
let consumer = root.path().join("consumer");
write(
&consumer.join(".headwater/taxonomy.yml"),
&format!(
"taxonomy:\n package: acme/starter\n version: 2.0.0\n digest: {digest}\ncorpus:\n root: docs\n"
),
);
consumer
}
impl Drop for Root {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
fn copy(from: &Path, to: &Path) {
std::fs::create_dir_all(to).expect("the destination is made");
for entry in std::fs::read_dir(from).expect("the source reads") {
let entry = entry.expect("the entry reads").path();
let name = entry.file_name().expect("it has a name");
match entry.is_dir() {
true => copy(&entry, &to.join(name)),
false => {
std::fs::copy(&entry, to.join(name)).expect("the file copies");
}
}
}
}
#[test]
fn a_named_assembly_publishes_one_flattened_package() {
let root = Root::scratch("assembly-publishes");
let source = assembly_source(&root, Arm::Sound);
let out = root.path().join("release");
let (code, stdout, stderr) = publish_assembly_from(root.path(), &source, &out);
assert_eq!(code, Some(0), "{stderr}");
assert!(stdout.contains("published acme/starter 2.0.0"), "{stdout}");
assert!(
out.join("release.yml").is_file(),
"no release record was written"
);
let manifest = std::fs::read_to_string(out.join("package.yml")).expect("the manifest reads");
assert!(manifest.contains("package: acme/starter"), "{manifest}");
assert!(manifest.contains("taxonomy: taxonomy.yml"), "{manifest}");
assert!(manifest.contains("form: flattened"), "{manifest}");
assert!(
!out.join("bundles").exists(),
"a flattened package carries bundles"
);
assert!(out.join("doctrine/starter/guide.md").is_file());
assert!(out.join("templates/starter/decision.md").is_file());
}
#[test]
fn a_flattened_package_carries_the_conformance_rule_set_its_source_declared() {
let root = Root::scratch("assembly-conformance");
let source = assembly_source(&root, Arm::Sound);
let out = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(root.path(), &source, &out);
assert_eq!(code, Some(0), "{stderr}");
let manifest = std::fs::read_to_string(out.join("package.yml")).expect("the manifest reads");
assert!(
manifest.contains("conformance: conformance.yml"),
"{manifest}"
);
assert!(
out.join("conformance.yml").is_file(),
"the manifest declares a conformance rule set the artifact does not carry"
);
assert!(manifest.contains("glossary: glossary.yml"), "{manifest}");
assert!(
out.join("glossary.yml").is_file(),
"a `contents` key no table names did not travel, so the fix is a list rather than a rule"
);
let record = std::fs::read_to_string(out.join("release.yml")).expect("the release reads");
for member in ["conformance.yml", "glossary.yml"] {
assert!(
record.contains(member),
"the release record does not name {member}: {record}"
);
}
}
#[test]
fn a_flattened_manifest_may_not_name_a_member_the_artifact_does_not_carry() {
let root = Root::scratch("assembly-unwritten");
let source = assembly_source(&root, Arm::UnwrittenMembers);
let out = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(root.path(), &source, &out);
assert_eq!(code, Some(1), "{stderr}");
for key in ["doctrine", "templates", "glossary"] {
assert!(stderr.contains(&format!("`contents.{key}`")), "{stderr}");
}
assert!(stderr.contains("doctrine/starter"), "{stderr}");
assert!(stderr.contains("templates/starter"), "{stderr}");
assert!(
!out.exists(),
"the refused run left partial output at {out:?}"
);
}
#[test]
fn a_published_manifest_may_not_name_a_member_the_artifact_does_not_carry() {
let root = Root::scratch("plain-unwritten");
let source = assembly_source(&root, Arm::UnwrittenMembers);
let out = root.path().join("release");
let (code, _stdout, stderr) = publish_plain_from(root.path(), &source, &out);
assert_eq!(code, Some(1), "{stderr}");
for key in ["doctrine", "templates", "glossary"] {
assert!(stderr.contains(&format!("`contents.{key}`")), "{stderr}");
}
assert!(
!out.exists(),
"the refused run left partial output at {out:?}"
);
}
#[test]
fn a_flattened_publish_names_every_contents_key_it_drops() {
let root = Root::scratch("assembly-drops");
let source = assembly_source(&root, Arm::Sound);
write(
&source.join("package.yml"),
"package: acme/fixture\nversion: 1.0.0\ncontents:\n taxonomy: taxonomy.yml\n conformance: conformance.yml\n glossary: glossary.yml\n bundles: bundles\n assemblies: assemblies\n migrations: migrations\n doctrine: doctrine\n templates: templates\n",
);
write(
&source.join("migrations/0-to-1.yml"),
"migration:\n format: 1\n from: \">=0 <1\"\n to: \">=1 <2\"\n\nsteps:\n - subject: overlay_address\n from: purposes.gone\n to: [purposes.behavior]\n because: the case is about what a flattened manifest drops\n",
);
let out = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(root.path(), &source, &out);
assert_eq!(code, Some(0), "{stderr}");
let manifest = std::fs::read_to_string(out.join("package.yml")).expect("the manifest reads");
let contents = manifest
.split_once("contents:")
.and_then(|(_, rest)| rest.split_once("\ndistribution:"))
.map(|(block, _)| block.to_string())
.unwrap_or_else(|| panic!("the flattened manifest has no contents block: {manifest}"));
for key in ["bundles", "assemblies", "migrations"] {
assert!(
!contents.contains(&format!("{key}:")),
"the flattened `contents` declares `{key}`: {contents}"
);
assert!(
!out.join(key).exists(),
"the flattened artifact carries `{key}`, which the manifest does not declare"
);
}
assert!(
stderr.contains("`contents.migrations`"),
"the publish dropped a declared payload and said nothing: {stderr}"
);
assert!(
stderr.contains("has no reader here"),
"the message does not say why the payload was dropped: {stderr}"
);
for key in ["bundles", "assemblies"] {
assert!(
!stderr.contains(&format!("`contents.{key}`")),
"an absorbed key was reported as a loss: {stderr}"
);
}
}
#[test]
fn a_flattened_publish_that_loses_nothing_reports_no_drop() {
let root = Root::scratch("assembly-no-drops");
let source = assembly_source(&root, Arm::Sound);
let out = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(root.path(), &source, &out);
assert_eq!(code, Some(0), "{stderr}");
assert!(
!stderr.contains("nothing in the flattened package carries it"),
"a publish that loses nothing printed a drop notice: {stderr}"
);
}
#[test]
fn a_flattened_assembly_is_pinned_vendored_and_resolved_without_bundle_selection() {
let root = Root::scratch("assembly-consumer");
let source = assembly_source(&root, Arm::Sound);
let artifact = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(root.path(), &source, &artifact);
assert_eq!(code, Some(0), "{stderr}");
let release = headwater_resolve::release::read(
&std::fs::read_to_string(artifact.join("release.yml")).expect("the release reads"),
)
.expect("the published record reads");
let consumer = flattened_consumer(&root, &release.digest);
let declaration = package::consumer(&consumer).expect("the consumer declaration reads");
assert!(
declaration.bundles.is_empty(),
"a flattened consumer carries no bundle selection: {:?}",
declaration.bundles
);
let (code, _stdout, stderr) = consumer_run(
&consumer,
&[
"taxonomy",
"vendor",
artifact.to_str().expect("the artifact is UTF-8"),
],
);
assert_eq!(code, Some(0), "{stderr}");
let installed = consumer.join(".headwater/packages/acme-starter");
assert!(
installed.join("release.yml").is_file(),
"the artifact was not vendored"
);
let (code, _stdout, stderr) = consumer_run(&consumer, &["taxonomy", "resolve"]);
assert_eq!(code, Some(0), "{stderr}");
assert!(
consumer.join(".headwater/taxonomy.lock").is_file(),
"a resolved consumer has a lock"
);
let source_manifest = package::manifest_at(&source).expect("the source manifest reads");
let recipe = assembly::read(root.path(), &source, &source_manifest, "starter")
.expect("the source assembly reads");
let source_resolution = assembly::resolve(root.path(), &source, &source_manifest, &recipe)
.expect("the source assembly resolves");
let installed_manifest =
package::manifest_at(&installed).expect("the installed manifest reads");
let installed_flattened = flatten::Flattened {
manifest: installed_manifest,
taxonomy: std::fs::read_to_string(installed.join("taxonomy.yml"))
.expect("the installed taxonomy reads"),
assets: Vec::new(),
dropped: Vec::new(),
};
assert!(
flatten::equivalent(&source_resolution, &installed_flattened, &recipe),
"the flattened consumer differs from the source assembly after identity is removed"
);
}
#[test]
fn the_shipped_starter_recipe_publishes_vendors_and_resolves() {
let root = Root::scratch("shipped-starter");
let source = repository().join("taxonomy-source/headwater-standard");
let artifact = root.path().join("release");
let source_manifest = package::manifest_at(&source).expect("the source manifest reads");
let recipe = assembly::read(&repository(), &source, &source_manifest, "starter")
.expect("the shipped starter recipe reads");
assert_eq!(recipe.from.bundles.len(), 3, "{:?}", recipe.from.bundles);
assert!(
recipe.overlay.is_none(),
"the shipped recipe declares an overlay, and `validate_glue` admits one only as a \
relation between two selected bundles"
);
let (code, stdout, stderr) = publish_assembly_from(&repository(), &source, &artifact);
assert_eq!(code, Some(0), "{stderr}");
assert!(
stdout.contains(&format!("published {} {}", recipe.package, recipe.version)),
"{stdout}"
);
let manifest =
std::fs::read_to_string(artifact.join("package.yml")).expect("the manifest reads");
assert!(
manifest.contains(&format!("package: {}", recipe.package)),
"{manifest}"
);
assert!(manifest.contains("form: flattened"), "{manifest}");
assert!(
!artifact.join("bundles").exists(),
"a flattened package carries bundles"
);
assert!(
artifact.join("doctrine/starter/starter.md").is_file(),
"the doctrine this package declares did not reach the flattened artifact"
);
let release = headwater_resolve::release::read(
&std::fs::read_to_string(artifact.join("release.yml")).expect("the release reads"),
)
.expect("the published record reads");
let consumer = root.path().join("consumer");
write(
&consumer.join(".headwater/taxonomy.yml"),
&format!(
"taxonomy:\n package: {}\n version: {}\n digest: {}\n overlay: .headwater/overlay.yml\ncorpus:\n root: docs\n",
recipe.package, recipe.version, release.digest
),
);
write(
&consumer.join(".headwater/overlay.yml"),
"add:\n identifier_schemes.decision_id.namespace: ACME\n identifier_schemes.obligation_record_id.namespace: ACME\n",
);
std::fs::create_dir_all(consumer.join("docs")).expect("the corpus root is made");
let declaration = package::consumer(&consumer).expect("the consumer declaration reads");
assert!(
declaration.bundles.is_empty(),
"a flattened consumer carries no bundle selection: {:?}",
declaration.bundles
);
let (code, _stdout, stderr) = consumer_run(
&consumer,
&[
"taxonomy",
"vendor",
artifact.to_str().expect("the artifact is UTF-8"),
],
);
assert_eq!(code, Some(0), "{stderr}");
assert!(
consumer
.join(".headwater/packages/headwater-starter/doctrine/starter/starter.md")
.is_file(),
"the doctrine did not arrive with the vendored package"
);
let doctrine = std::fs::read_to_string(
consumer.join(".headwater/packages/headwater-starter/doctrine/starter/starter.md"),
)
.expect("the vendored doctrine is readable");
assert!(
doctrine.contains("outside_root"),
"the starter doctrine does not name `outside_root`, the second route a language regime has to prose (HW-DR-0084)"
);
assert!(
!doctrine.contains("reaches prose only through the kinds that bind it"),
"the starter doctrine still says a kind is the only route a language regime has to prose"
);
let (code, _stdout, stderr) = consumer_run(&consumer, &["taxonomy", "resolve"]);
assert_eq!(code, Some(0), "{stderr}");
assert!(
consumer.join(".headwater/taxonomy.lock").is_file(),
"a resolved flattened starter has a lock"
);
assert!(
consumer
.join(".headwater/packages/headwater-starter/conformance.yml")
.is_file(),
"the conformance rule set did not arrive with the vendored package"
);
let (code, stdout, stderr) = consumer_run(&consumer, &["conformance"]);
assert_eq!(code, Some(0), "{stderr}");
assert!(stdout.contains("L1"), "{stdout}");
}
#[test]
fn a_pin_that_no_longer_names_the_installed_bytes_fails_check_validate_and_conformance_and_not_resolve(
) {
let root = Root::scratch("pin-recheck-pairing");
let source = repository().join("taxonomy-source/headwater-standard");
let artifact = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(&repository(), &source, &artifact);
assert_eq!(code, Some(0), "{stderr}");
let release = headwater_resolve::release::read(
&std::fs::read_to_string(artifact.join("release.yml")).expect("the release reads"),
)
.expect("the published record reads");
let source_manifest = package::manifest_at(&source).expect("the source manifest reads");
let recipe = assembly::read(&repository(), &source, &source_manifest, "starter")
.expect("the shipped starter recipe reads");
let consumer = root.path().join("consumer");
write(
&consumer.join(".headwater/taxonomy.yml"),
&format!(
"taxonomy:\n package: {}\n version: {}\n digest: {}\n overlay: .headwater/overlay.yml\ncorpus:\n root: docs\n",
recipe.package, recipe.version, release.digest
),
);
write(
&consumer.join(".headwater/overlay.yml"),
"add:\n identifier_schemes.decision_id.namespace: ACME\n identifier_schemes.obligation_record_id.namespace: ACME\n",
);
std::fs::create_dir_all(consumer.join("docs")).expect("the corpus root is made");
let (code, _stdout, stderr) = consumer_run(
&consumer,
&[
"taxonomy",
"vendor",
artifact.to_str().expect("the artifact is UTF-8"),
],
);
assert_eq!(code, Some(0), "{stderr}");
let (code, _stdout, stderr) = consumer_run(&consumer, &["taxonomy", "resolve"]);
assert_eq!(code, Some(0), "{stderr}");
let (code, _stdout, stderr) = consumer_run(&consumer, &["conformance", "--level", "L0"]);
assert_eq!(
code,
Some(0),
"the freshly vendored tree does not reach L0: {stderr}"
);
let (code, stdout, stderr) = consumer_run(&consumer, &["check", "--strict"]);
assert_eq!(
code,
Some(0),
"the freshly vendored tree fails `check --strict`: {stdout}\n{stderr}"
);
assert!(!stdout.contains(PIN_FINDING), "{stdout}");
let (code, _stdout, stderr) = consumer_run(&consumer, &["taxonomy", "validate"]);
assert_eq!(
code,
Some(0),
"the freshly vendored tree fails `validate`: {stderr}"
);
let read_set = root.path().join("clean.readset");
let read_set = read_set.to_str().expect("the read set path is UTF-8");
let (code, _stdout, stderr) = consumer_run(&consumer, &["check", "--read-set", read_set]);
assert_eq!(code, Some(0), "{stderr}");
let recorded = std::fs::read_to_string(read_set).expect("the read set reads");
for needle in [
".headwater/taxonomy.yml",
".headwater/packages/headwater-starter/doctrine/starter/starter.md",
] {
assert!(
recorded.contains(needle),
"the read set does not list `{needle}`: {recorded}"
);
}
let (_code, stdout, _stderr) = consumer_run(&consumer, &["gate", "--read-set", read_set]);
for moved in [".headwater/taxonomy.yml", "starter.md"] {
assert!(
!stdout.contains(moved),
"the gate names `{moved}` over the tree the read set was taken from: {stdout}"
);
}
let stale = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
let authored = consumer.join(".headwater/taxonomy.yml");
let pin = std::fs::read_to_string(&authored).expect("the authored pin reads");
write(&authored, &pin.replace(&release.digest, stale));
let (code, _stdout, stderr) = consumer_run(&consumer, &["taxonomy", "resolve"]);
assert_eq!(
code,
Some(0),
"the resolver read the pinned digest, so this pairing no longer holds: {stderr}"
);
let (code, stdout, stderr) = consumer_run(&consumer, &["conformance"]);
assert_eq!(code, Some(0), "a plain run moved its exit status: {stderr}");
assert!(
stdout.contains("pin.current"),
"the report does not name the reading that found the stale pin: {stdout}"
);
let (code, stdout, stderr) = consumer_run(&consumer, &["conformance", "--level", "L0"]);
assert_ne!(code, Some(0), "a stale pin reached L0: {stdout}\n{stderr}");
assert!(
format!("{stdout}\n{stderr}").contains("pin.current"),
"the refusal does not name the reading: {stdout}\n{stderr}"
);
let (code, stdout, stderr) = consumer_run(&consumer, &["check", "--strict"]);
assert_eq!(
code,
Some(1),
"a stale pin passed `check --strict`: {stdout}\n{stderr}"
);
for needle in [PIN_RULE, PIN_FINDING, stale, release.digest.as_str()] {
assert!(
stdout.contains(needle),
"`check` does not say `{needle}`: {stdout}"
);
}
let (code, _stdout, stderr) = consumer_run(&consumer, &["taxonomy", "validate"]);
assert_eq!(code, Some(1), "a stale pin passed `validate`: {stderr}");
let (_code, stdout, _stderr) = consumer_run(&consumer, &["gate", "--read-set", read_set]);
assert!(
stdout.contains(".headwater/taxonomy.yml"),
"the gate does not name the moved pin: {stdout}"
);
for needle in [stale, release.digest.as_str()] {
assert!(
stderr.contains(needle),
"`validate` does not say `{needle}`: {stderr}"
);
}
write(&authored, &pin);
let installed =
consumer.join(".headwater/packages/headwater-starter/doctrine/starter/starter.md");
let carried = std::fs::read_to_string(&installed).expect("the installed doctrine reads");
std::fs::write(
&installed,
format!("{carried}\n<!-- hand-edited after vendor, never through `vendor` -->\n"),
)
.expect("the hand edit writes");
let (code, _stdout, stderr) = consumer_run(&consumer, &["taxonomy", "resolve"]);
assert_eq!(
code,
Some(0),
"the resolver refused the edited tree, so this pairing no longer holds: {stderr}"
);
let (code, stdout, stderr) = consumer_run(&consumer, &["check", "--strict"]);
assert_eq!(
code,
Some(1),
"an edited member passed `check --strict`: {stdout}\n{stderr}"
);
for needle in [
PIN_RULE,
PIN_FINDING,
release.digest.as_str(),
"doctrine/starter/starter.md",
] {
assert!(
stdout.contains(needle),
"`check` does not say `{needle}`: {stdout}"
);
}
let computed = headwater_resolve::release::digest_of(
&headwater_resolve::release::members(
&consumer.join(".headwater/packages/headwater-starter"),
)
.expect("the installed members read"),
);
assert_ne!(computed, release.digest, "the hand edit moved no byte");
assert!(
stdout.contains(&computed),
"`check` does not name the digest it computed, `{computed}`: {stdout}"
);
let (code, _stdout, stderr) = consumer_run(&consumer, &["taxonomy", "validate"]);
assert_eq!(
code,
Some(1),
"an edited member passed `validate`: {stderr}"
);
let (_code, stdout, _stderr) = consumer_run(&consumer, &["gate", "--read-set", read_set]);
assert!(
stdout.contains("doctrine/starter/starter.md"),
"the gate does not name the member that moved: {stdout}"
);
for needle in [
release.digest.as_str(),
computed.as_str(),
"doctrine/starter/starter.md",
] {
assert!(
stderr.contains(needle),
"`validate` does not say `{needle}`: {stderr}"
);
}
for arguments in [
vec!["conformance"],
vec!["conformance", "--level", "L0"],
vec!["conformance", "--level", "L1"],
] {
let (code, stdout, stderr) = consumer_run(&consumer, &arguments);
assert_eq!(
code,
Some(1),
"an edited member passed `{arguments:?}`: {stdout}\n{stderr}"
);
let said = format!("{stdout}\n{stderr}");
assert!(
said.contains("doctrine/starter/starter.md"),
"the refusal does not name the member that moved: {said}"
);
assert!(
said.contains("no longer match"),
"the refusal does not say the bytes moved: {said}"
);
}
}
#[test]
fn an_invalid_assembly_refuses_before_it_creates_output() {
let root = Root::scratch("assembly-refuses");
let source = assembly_source(&root, Arm::UnselectableBundle);
let out = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(root.path(), &source, &out);
assert_eq!(code, Some(1), "{stderr}");
assert!(stderr.contains("`missing`"), "{stderr}");
assert!(stderr.contains("nothing was published"), "{stderr}");
assert!(
!out.exists(),
"the refused run left partial output at {out:?}"
);
}
#[test]
fn an_assembly_whose_selection_omits_a_bundle_another_bundle_needs_does_not_publish() {
let root = Root::scratch("assembly-incomplete");
let source = assembly_source(&root, Arm::IncompleteSelection);
let out = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(root.path(), &source, &out);
assert_eq!(code, Some(1), "{stderr}");
assert!(stderr.contains("nothing was published"), "{stderr}");
assert!(
stderr.contains("assembly.yml"),
"the refusal names the recipe file to edit: {stderr}"
);
assert!(
stderr.contains("[alpha]"),
"the refusal names the selection: {stderr}"
);
assert!(
stderr.contains("`beta`"),
"the refusal names the dangling name: {stderr}"
);
assert!(
stderr.contains("shelves.betas.kind"),
"the refusal names the address that reads it: {stderr}"
);
assert!(
stderr.contains("`beta` declares 1 of the 1"),
"the advice names the bundle to add back: {stderr}"
);
assert!(
stderr.contains("`from.bundles:`"),
"the advice names the key a publisher edits, not the consumer's: {stderr}"
);
assert!(
!stderr.contains(".headwater/taxonomy.yml"),
"the advice sends a publisher to the recipe and never to a consumer declaration: {stderr}"
);
assert!(
!out.exists(),
"the refused run left partial output at {out:?}"
);
}
#[test]
fn an_assembly_whose_selection_is_complete_still_publishes() {
let root = Root::scratch("assembly-complete");
let source = assembly_source(&root, Arm::Sound);
let out = root.path().join("release");
let (code, stdout, stderr) = publish_assembly_from(root.path(), &source, &out);
assert_eq!(code, Some(0), "{stderr}");
assert!(stdout.contains("published acme/starter 2.0.0"), "{stdout}");
}
#[test]
fn a_publish_that_says_nothing_was_published_wrote_nothing() {
let root = Root::copied("says-nothing");
let out = root.path().join("release");
let (code, first) = root.publish(&out);
assert_eq!(code, Some(1), "{first}");
assert!(
first.contains("headwater: nothing was published"),
"{first}"
);
assert!(
first.contains("package.yml"),
"the refusal does not name the manifest that declares the path: {first}"
);
assert!(
first.contains("../../docs/taxonomies"),
"the refusal does not name the declared value: {first}"
);
assert!(
!out.exists(),
"the run that published nothing left {:?}",
std::fs::read_dir(&out).map(|entries| entries
.filter_map(Result::ok)
.map(|entry| entry.file_name())
.collect::<Vec<_>>())
);
let (code, again) = root.publish(&out);
assert_eq!(code, Some(1), "{again}");
assert_eq!(
again, first,
"the second run met a different refusal, so the first left something behind"
);
}
#[cfg(unix)]
fn mode(at: &Path, bits: u32) {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(at, std::fs::Permissions::from_mode(bits)).expect("the mode is set");
}
#[cfg(unix)]
fn modes_hold(root: &Root) -> bool {
let at = root.path().join("mode-probe");
std::fs::create_dir_all(&at).expect("the probe is made");
std::fs::write(at.join("held"), "x").expect("the probe holds a file");
mode(&at, 0o300);
let held = std::fs::read_dir(&at).is_err();
mode(&at, 0o700);
std::fs::remove_dir_all(&at).expect("the probe goes");
held
}
#[cfg(unix)]
#[test]
fn a_publish_into_a_directory_it_cannot_read_writes_nothing_into_it() {
let holder = Root::scratch("unreadable-out");
if !modes_hold(&holder) {
eprintln!("skipped: this process is root, and root reads through mode 0300");
return;
}
let out = holder.path().join("release");
std::fs::create_dir_all(&out).expect("the caller's directory is made");
std::fs::write(out.join("theirs.txt"), "the caller's own file").expect("their file is written");
mode(&out, 0o300);
let (code, message) = publish_real_source_into(&out);
mode(&out, 0o700);
let left: Vec<String> = std::fs::read_dir(&out)
.expect("it reads once the mode is back")
.filter_map(|entry| entry.ok())
.map(|entry| entry.file_name().to_string_lossy().into_owned())
.collect();
assert_eq!(
left,
vec!["theirs.txt".to_string()],
"the run published into a directory it could not read: {message}"
);
assert_eq!(code, Some(1), "{message}");
assert!(
message.contains("the output directory cannot be read"),
"the refusal does not say why the directory was refused: {message}"
);
}
#[test]
fn an_output_directory_the_caller_made_is_left_empty_rather_than_removed() {
let root = Root::colliding("caller-made");
let out = root.path().join("release");
std::fs::create_dir_all(&out).expect("the caller makes it");
let (code, message) = root.publish(&out);
assert_eq!(code, Some(1), "{message}");
assert!(
message.contains("headwater: nothing was published"),
"{message}"
);
assert!(out.is_dir(), "the caller's own directory was removed");
assert_eq!(
std::fs::read_dir(&out)
.expect("it reads")
.filter_map(Result::ok)
.count(),
0,
"the directory the caller made is not empty again: {message}"
);
}
#[test]
fn a_failed_write_leaves_neither_the_output_directory_nor_the_path_to_it() {
let root = Root::colliding("nested-out");
let nested = root.path().join("nested");
let out = nested.join("a/b/c");
let (code, message) = root.publish(&out);
assert_eq!(code, Some(1), "{message}");
assert!(
message.contains("headwater: nothing was published"),
"{message}"
);
assert!(
!out.exists(),
"the output directory is still there: {message}"
);
assert!(
!nested.exists(),
"the run that published nothing left the directories it made: {message}"
);
}
#[test]
fn from_and_package_together_are_refused_and_from_alone_publishes_the_relocated_source() {
let source = repository().join("taxonomy-source/headwater-standard");
let out_root = Root::scratch("from-flag");
let out = out_root.path().join("release");
let both = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "publish", "--package", "headwater/standard"])
.arg("--from")
.arg(&source)
.arg("--out")
.arg(&out)
.arg("--root")
.arg(repository())
.output()
.expect("the binary runs");
assert_eq!(both.status.code(), Some(1));
let both_err = String::from_utf8_lossy(&both.stderr);
assert!(
both_err.contains("--package") && both_err.contains("--from"),
"the refusal does not name both flags: {both_err}"
);
assert!(!out.exists(), "the refused run wrote into --out anyway");
let from_alone = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "publish"])
.arg("--from")
.arg(&source)
.arg("--out")
.arg(&out)
.arg("--root")
.arg(repository())
.output()
.expect("the binary runs");
let stdout = String::from_utf8_lossy(&from_alone.stdout).into_owned();
assert_eq!(
from_alone.status.code(),
Some(0),
"{}",
String::from_utf8_lossy(&from_alone.stderr)
);
assert!(
stdout.contains("published headwater/standard"),
"the run did not report a publish: {stdout}"
);
assert!(
out.join("release.yml").is_file(),
"publishing `--from` a directory outside `.headwater/packages/` wrote no release record"
);
}
#[test]
fn the_vendored_bundles_agree_with_a_fresh_publish_of_the_maintained_source() {
let root = Root::scratch("vendored-bundles");
let out = root.path().join("release");
let (code, message) = publish_real_source_into(&out);
assert_eq!(
code,
Some(0),
"the publish this comparison depends on failed: {message}"
);
let fresh = out.join("bundles");
let vendored = repository().join(".headwater/packages/headwater-standard/bundles");
let mut fresh_paths = relative_files(&fresh);
let mut vendored_paths = relative_files(&vendored);
fresh_paths.sort();
vendored_paths.sort();
let only_in_fresh: Vec<&String> = fresh_paths
.iter()
.filter(|p| !vendored_paths.contains(p))
.collect();
let only_in_vendored: Vec<&String> = vendored_paths
.iter()
.filter(|p| !fresh_paths.contains(p))
.collect();
let differing: Vec<&String> = fresh_paths
.iter()
.filter(|p| vendored_paths.contains(p))
.filter(|p| {
std::fs::read(fresh.join(p)).expect("the fresh file reads")
!= std::fs::read(vendored.join(p)).expect("the vendored file reads")
})
.collect();
assert!(
only_in_fresh.is_empty() && only_in_vendored.is_empty() && differing.is_empty(),
".headwater/packages/headwater-standard/bundles/ has drifted from docs/taxonomies/, the source \
taxonomy-source/headwater-standard/package.yml declares.\n\
only in a fresh publish, missing from the vendored copy: {only_in_fresh:?}\n\
only in the vendored copy, missing from a fresh publish: {only_in_vendored:?}\n\
present on both sides with different bytes: {differing:?}\n\
Republish and revendor: `headwater taxonomy publish --from \
taxonomy-source/headwater-standard --out <dir> && headwater taxonomy vendor <dir>`"
);
}
fn publish_check(root: &Path, from: &Path, extra: &[&str]) -> (Option<i32>, String, String) {
static RUN: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
let tmp = std::env::temp_dir().join(format!(
"headwater-cli-publish-{}-check-tmp-{}",
std::process::id(),
RUN.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
));
let _ = std::fs::remove_dir_all(&tmp);
std::fs::create_dir_all(&tmp).expect("the private temporary directory is made");
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "publish", "--check", "--from"])
.arg(from)
.args(extra)
.arg("--root")
.arg(root)
.env("TMPDIR", &tmp)
.output()
.expect("the binary runs");
let left = relative_files(&tmp);
let _ = std::fs::remove_dir_all(&tmp);
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
assert!(
left.is_empty(),
"`--check` left its scratch publish behind: {left:?}\nstderr: {stderr}"
);
(
output.status.code(),
String::from_utf8_lossy(&output.stdout).into_owned(),
stderr,
)
}
fn publisher_and_consumer(label: &str) -> Root {
let root = Root::scratch(label);
for path in [
"taxonomy-source/headwater-standard",
".headwater/packages/headwater-standard",
"docs/taxonomies",
] {
copy(&repository().join(path), &root.path().join(path));
}
std::fs::copy(
repository().join(".headwater/taxonomy.yml"),
root.path().join(".headwater/taxonomy.yml"),
)
.expect("the declaration copies");
root
}
fn snapshot(root: &Path) -> Vec<(String, Vec<u8>)> {
let mut files: Vec<(String, Vec<u8>)> = relative_files(root)
.into_iter()
.map(|path| {
let bytes = std::fs::read(root.join(&path)).expect("the file reads");
(path, bytes)
})
.collect();
files.sort();
files
}
#[test]
fn a_source_taxonomy_changed_without_a_republish_fails_publish_check_and_names_the_file() {
let root = publisher_and_consumer("check-stale-taxonomy");
let source = root.path().join("taxonomy-source/headwater-standard");
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(
code,
Some(0),
"a vendored copy that is a fresh publish of its source must pass the check.\nstdout: {stdout}\nstderr: {stderr}"
);
let taxonomy = source.join("taxonomy.yml");
let mut text = std::fs::read_to_string(&taxonomy).expect("the source taxonomy reads");
text.push_str("# an edit nobody republished\n");
std::fs::write(&taxonomy, text).expect("the source taxonomy is edited");
let before = snapshot(root.path());
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(
code,
Some(1),
"a source edited without a republish must fail the check.\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(
stderr.contains("taxonomy.yml"),
"the failure must name the member that moved: {stderr}"
);
assert!(
!stderr.contains("bundles/"),
"only the edited member moved, so no bundle may be named: {stderr}"
);
assert_eq!(
before,
snapshot(root.path()),
"`--check` changed a file in the tree it was checking"
);
}
#[test]
fn publish_check_refuses_out_package_and_a_missing_from() {
let root = publisher_and_consumer("check-refusals");
let source = root.path().join("taxonomy-source/headwater-standard");
let out = root.path().join("release");
let (code, _, stderr) = publish_check(
root.path(),
&source,
&["--out", out.to_str().expect("utf-8")],
);
assert_eq!(code, Some(1), "`--check --out` must be refused: {stderr}");
assert!(stderr.contains("--out"), "{stderr}");
assert!(
!out.exists(),
"a refused `--check --out` wrote the artifact"
);
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args([
"taxonomy",
"publish",
"--check",
"--package",
"headwater/standard",
])
.arg("--root")
.arg(root.path())
.output()
.expect("the binary runs");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(1),
"`--check --package` must be refused: {stderr}"
);
assert!(stderr.contains("`--package`"), "{stderr}");
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "publish", "--check"])
.arg("--root")
.arg(root.path())
.output()
.expect("the binary runs");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
output.status.code(),
Some(1),
"`--check` without `--from` must be refused: {stderr}"
);
assert!(
stderr.contains("Name the source with `--from <dir>`"),
"{stderr}"
);
}
#[test]
fn publish_check_with_no_vendored_copy_is_refused_and_names_the_package() {
let root = publisher_and_consumer("check-no-vendored");
std::fs::remove_dir_all(root.path().join(".headwater/packages/headwater-standard"))
.expect("the vendored copy is removed");
let source = root.path().join("taxonomy-source/headwater-standard");
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(
code,
Some(1),
"no vendored copy must fail the check.\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(stderr.contains("headwater/standard"), "{stderr}");
}
fn replace_line(path: &Path, prefix: &str, line: &str) {
let text = std::fs::read_to_string(path).expect("the file reads");
let mut hit = false;
let edited: Vec<String> = text
.lines()
.map(|each| {
let body = each.trim_start();
match !hit && body.starts_with(prefix) {
true => {
hit = true;
format!("{}{line}", &each[..each.len() - body.len()])
}
false => each.to_string(),
}
})
.collect();
assert!(hit, "no line of {} starts with {prefix}", path.display());
std::fs::write(path, edited.join("\n") + "\n").expect("the file is written");
}
#[test]
fn a_member_the_source_adds_or_drops_is_named_on_its_own_line() {
let root = publisher_and_consumer("check-added");
let source = root.path().join("taxonomy-source/headwater-standard");
std::fs::write(
source.join("doctrine/extra.md"),
"# Extra\n\nA page nobody published.\n",
)
.expect("the added member is written");
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(code, Some(1), "stdout: {stdout}\nstderr: {stderr}");
assert!(
stderr.contains(
"doctrine/extra.md: a fresh publish writes it, and the vendored copy lacks it"
),
"{stderr}"
);
assert!(stderr.contains("the vendored digest is"), "{stderr}");
let root = publisher_and_consumer("check-dropped");
let source = root.path().join("taxonomy-source/headwater-standard");
std::fs::remove_file(
root.path()
.join("docs/taxonomies/diataxis-site/templates/tutorial.md"),
)
.expect("the dropped member is removed");
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(code, Some(1), "stdout: {stdout}\nstderr: {stderr}");
assert!(
stderr.contains(
"bundles/diataxis-site/templates/tutorial.md: the vendored copy carries it, and a fresh publish does not write it"
),
"{stderr}"
);
assert!(stderr.contains("the vendored digest is"), "{stderr}");
}
#[test]
fn the_vendored_record_is_compared_by_what_it_states_and_not_by_its_bytes() {
let root = publisher_and_consumer("check-record-field");
let source = root.path().join("taxonomy-source/headwater-standard");
let record = root
.path()
.join(".headwater/packages/headwater-standard/release.yml");
replace_line(&record, "version:", "version: 0.0.1");
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(
code,
Some(1),
"a vendored record that states another version must fail.\nstdout: {stdout}\nstderr: {stderr}"
);
assert!(stderr.contains("release.yml:"), "{stderr}");
let root = publisher_and_consumer("check-record-header");
let source = root.path().join("taxonomy-source/headwater-standard");
let record = root
.path()
.join(".headwater/packages/headwater-standard/release.yml");
let text = std::fs::read_to_string(&record).expect("the record reads");
std::fs::write(
&record,
format!("# a header an engine release reworded\n{text}"),
)
.expect("the header is edited");
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(
code,
Some(0),
"a reworded comment header alone must pass.\nstdout: {stdout}\nstderr: {stderr}"
);
}
#[test]
fn a_hand_edited_vendored_copy_fails_and_the_message_does_not_blame_the_source() {
let root = publisher_and_consumer("check-hand-edited");
let source = root.path().join("taxonomy-source/headwater-standard");
let vendored = root
.path()
.join(".headwater/packages/headwater-standard/taxonomy.yml");
let mut text = std::fs::read_to_string(&vendored).expect("the vendored taxonomy reads");
text.push_str("# a hand edit\n");
std::fs::write(&vendored, text).expect("the vendored taxonomy is edited");
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(code, Some(1), "stdout: {stdout}\nstderr: {stderr}");
assert!(
stderr.contains("taxonomy.yml: the vendored bytes are"),
"{stderr}"
);
assert!(
stderr.contains("or somebody edited the vendored copy"),
"the message must not say the source changed when it may not have: {stderr}"
);
}
#[test]
fn publish_check_refuses_the_vendored_copy_as_its_source_and_a_copy_with_no_record() {
let root = publisher_and_consumer("check-from-vendored");
let vendored = root.path().join(".headwater/packages/headwater-standard");
let (code, stdout, stderr) = publish_check(root.path(), &vendored, &[]);
assert_eq!(code, Some(1), "stdout: {stdout}\nstderr: {stderr}");
assert!(stderr.contains("names the vendored copy"), "{stderr}");
let root = publisher_and_consumer("check-no-record");
let source = root.path().join("taxonomy-source/headwater-standard");
std::fs::remove_file(
root.path()
.join(".headwater/packages/headwater-standard/release.yml"),
)
.expect("the record is removed");
let (code, stdout, stderr) = publish_check(root.path(), &source, &[]);
assert_eq!(code, Some(1), "stdout: {stdout}\nstderr: {stderr}");
assert!(
stderr.contains("carries no readable release record"),
"{stderr}"
);
}
#[test]
fn this_repository_passes_publish_check_on_its_own_source() {
let (code, stdout, stderr) = publish_check(
&repository(),
&repository().join("taxonomy-source/headwater-standard"),
&[],
);
assert_eq!(
code,
Some(0),
"the vendored copy of headwater/standard is not a fresh publish of \
taxonomy-source/headwater-standard.\nstdout: {stdout}\nstderr: {stderr}"
);
}
#[test]
fn a_fresh_publish_carries_no_bundle_fixtures_and_the_publisher_keeps_its_own() {
let root = Root::scratch("bundle-fixtures");
let out = root.path().join("release");
let (code, message) = publish_real_source_into(&out);
assert_eq!(
code,
Some(0),
"the publish this case depends on failed: {message}"
);
let mut carried: Vec<String> = relative_files(&out.join("bundles"))
.into_iter()
.filter(|path| path.split('/').nth(1) == Some("fixtures"))
.collect();
carried.sort();
assert!(
carried.is_empty(),
"the artifact carries {} bundle fixture files that no consumer verb opens: {carried:#?}",
carried.len()
);
assert!(
repository()
.join("docs/taxonomies/standards-spec/fixtures/README.md")
.is_file(),
"the exception is about the artifact. The publisher's own reference corpora stay where \
they are"
);
}
#[test]
fn no_file_the_artifact_carries_links_into_a_bundle_tree_it_does_not_carry() {
let root = Root::scratch("bundle-links");
let out = root.path().join("release");
let (code, message) = publish_real_source_into(&out);
assert_eq!(
code,
Some(0),
"the publish this case depends on failed: {message}"
);
let mut read = 0usize;
let mut dangling: Vec<String> = Vec::new();
for member in relative_files(&out) {
if !member.ends_with(".md") {
continue;
}
let text = std::fs::read_to_string(out.join(&member)).expect("a member reads");
let directory = Path::new(&member)
.parent()
.unwrap_or(Path::new(""))
.to_owned();
for target in markdown_links(&text) {
let body = target.split('#').next().unwrap_or_default();
if body.is_empty() || body.contains("://") {
continue;
}
let Some(at) = inside_the_artifact(&directory, body) else {
continue;
};
if !at.starts_with("bundles") {
continue;
}
read += 1;
if !out.join(&at).exists() {
dangling.push(format!("{member} -> {target}"));
}
}
}
assert!(
read > 0,
"the scanner found no link into the bundle tree at all, so it held nothing"
);
assert!(
dangling.is_empty(),
"{} of the {read} links into the bundle tree do not resolve inside the artifact. Every \
one of them resolves in `docs/taxonomies/`, so the publish carried the prose and left \
the target behind:\n{dangling:#?}",
dangling.len()
);
}
#[test]
fn every_relative_link_a_carried_file_writes_resolves_inside_the_artifact() {
let root = Root::scratch("relative-links");
let out = root.path().join("release");
let (code, message) = publish_real_source_into(&out);
assert_eq!(
code,
Some(0),
"the publish this case depends on failed: {message}"
);
let mut read = 0usize;
let mut dangling: Vec<String> = Vec::new();
let mut escaping: Vec<String> = Vec::new();
for member in relative_files(&out) {
if !member.ends_with(".md") {
continue;
}
let text = std::fs::read_to_string(out.join(&member)).expect("a member reads");
let directory = Path::new(&member)
.parent()
.unwrap_or(Path::new(""))
.to_owned();
for target in markdown_links(&text) {
let body = target.split('#').next().unwrap_or_default();
if body.is_empty() || body.contains("://") || body.starts_with("mailto:") {
continue;
}
read += 1;
let Some(at) = inside_the_artifact(&directory, body) else {
escaping.push(format!("{member} -> {target}"));
continue;
};
if !out.join(&at).exists() {
dangling.push(format!("{member} -> {target}"));
}
}
}
assert!(
read > 0,
"the scanner found no relative link in any carried file, so it held nothing"
);
assert!(
escaping.is_empty(),
"{} of the {read} relative links a carried file writes climb clear of the artifact root. \
#633 ruled that a reference to a document the artifact does not carry is written as an \
absolute URL, and nothing else in this repository reads these:\n{escaping:#?}",
escaping.len()
);
assert!(
dangling.is_empty(),
"{} of the {read} relative links a carried file writes name a path the artifact does not \
carry:\n{dangling:#?}",
dangling.len()
);
}
#[test]
fn the_real_package_records_exactly_the_references_it_carries() {
let root = Root::scratch("recorded-references");
let out = root.path().join("release");
let (code, message) = publish_real_source_into(&out);
assert_eq!(
code,
Some(0),
"the maintained source no longer publishes, which is the clause this case holds: {message}"
);
let members: Vec<String> = relative_files(&out);
let mut dangling: Vec<(String, String)> = Vec::new();
let mut read = 0usize;
for member in &members {
let Ok(text) = std::fs::read_to_string(out.join(member)) else {
continue;
};
let directory = Path::new(member)
.parent()
.unwrap_or(Path::new(""))
.to_owned();
for written in markdown_links(&text) {
let target = written.split('#').next().unwrap_or_default();
if target.is_empty() || target.contains("://") || target.starts_with("mailto:") {
continue;
}
let Some(at) = inside_the_artifact(&directory, target) else {
continue;
};
read += 1;
if !out.join(&at).exists() {
dangling.push((member.clone(), at.display().to_string()));
}
}
}
dangling.sort();
dangling.dedup();
assert!(
read > 0,
"the walker resolved no reference at all, so it held nothing"
);
let recorded = recorded_references(&out.join("package.yml"));
assert_eq!(
recorded,
dangling,
"the manifest records {} pairs and the artifact carries {} of {read} resolved references \
that resolve nowhere. A pair on the left and not on the right is a repair nobody deleted \
the record of; a pair on the right and not on the left could not have published at all",
recorded.len(),
dangling.len()
);
let account = "references that resolve nowhere inside it";
if recorded.is_empty() {
assert!(
!message.contains(account),
"the manifest records nothing and the publish still reported a population: {message}"
);
} else {
assert!(
message.contains(&format!(
"the artifact records {} {account}",
recorded.len()
)),
"the publish did not report the population it shipped: {message}"
);
}
}
fn recorded_references(manifest: &Path) -> Vec<(String, String)> {
let text = std::fs::read_to_string(manifest).expect("the published manifest reads");
let mut pairs = Vec::new();
let mut member: Option<String> = None;
let mut inside = false;
for line in text.lines() {
if line.starts_with("unresolved_references:") {
inside = true;
continue;
}
if !inside {
continue;
}
if line.starts_with('#') || line.trim().is_empty() {
continue;
}
match line.strip_prefix(" - ") {
Some(target) => match &member {
Some(at) => pairs.push((at.clone(), target.trim().to_string())),
None => panic!("a recorded target with no member above it: {line}"),
},
None => match line.strip_prefix(" ").map(str::trim_end) {
Some(key) if key.ends_with(':') && !key.starts_with(' ') => {
member = Some(key.trim_end_matches(':').to_string());
}
_ => break,
},
}
}
pairs.sort();
pairs
}
#[test]
fn the_shipped_starter_recipe_inherits_no_record_of_another_artifacts_references() {
let root = Root::scratch("starter-record");
let source = repository().join("taxonomy-source/headwater-standard");
let artifact = root.path().join("release");
let (code, _stdout, stderr) = publish_assembly_from(&repository(), &source, &artifact);
assert_eq!(code, Some(0), "{stderr}");
let carried = std::fs::read_to_string(artifact.join("package.yml"))
.expect("the flattened manifest reads");
assert!(
!carried.contains(headwater_resolve::package::RECORDED_REFERENCES),
"the flattened manifest inherited a record written for another artifact:\n{carried}"
);
assert!(
recorded_references(&artifact.join("package.yml")).is_empty(),
"the flattened artifact records a pair it cannot be about"
);
assert!(
!stderr.contains("resolve nowhere inside it"),
"the publish reported a population this artifact does not carry: {stderr}"
);
let planted = root.path().join("planted");
let source = planted.join("source/headwater-standard");
copy_tree(
&repository().join("taxonomy-source/headwater-standard"),
&source,
);
let bundles = planted.join("docs/taxonomies");
copy_tree(&repository().join("docs/taxonomies"), &bundles);
let readme = bundles.join("README.md");
let mut body = std::fs::read_to_string(&readme).expect("the copied README reads");
body.push_str("\n[a planted reference](../../spec/07-distribution-and-federation.md)\n");
std::fs::write(&readme, body).expect("the copied README writes");
let manifest = source.join("package.yml");
let mut declared = std::fs::read_to_string(&manifest).expect("the copied manifest reads");
declared.push_str(&format!(
"\n{}:\n bundles/README.md:\n - spec/07-distribution-and-federation.md\n",
headwater_resolve::package::RECORDED_REFERENCES
));
std::fs::write(&manifest, declared).expect("the copied manifest writes");
let plain = root.path().join("plain");
let (code, _stdout, stderr) = publish_plain_from(&planted, &source, &plain);
assert_eq!(
code,
Some(0),
"the planted source no longer publishes: {stderr}"
);
assert_eq!(
recorded_references(&plain.join("package.yml")),
vec![(
"bundles/README.md".to_string(),
"spec/07-distribution-and-federation.md".to_string()
)],
"the plant did not reach the plain artifact, so the control holds nothing"
);
let flattened = root.path().join("planted-recipe");
let (code, _stdout, stderr) = publish_assembly_from(&planted, &source, &flattened);
assert_eq!(code, Some(0), "{stderr}");
let carried = std::fs::read_to_string(flattened.join("package.yml"))
.expect("the flattened manifest reads");
assert!(
!carried.contains(headwater_resolve::package::RECORDED_REFERENCES),
"a recipe flattened from a source that records a pair inherited the record:\n{carried}"
);
}
fn copy_tree(from: &Path, to: &Path) {
std::fs::create_dir_all(to).expect("the destination directory is made");
for entry in std::fs::read_dir(from).expect("the source directory reads") {
let entry = entry.expect("an entry reads");
let target = to.join(entry.file_name());
if entry.file_type().expect("a file type reads").is_dir() {
copy_tree(&entry.path(), &target);
} else {
std::fs::copy(entry.path(), &target).expect("a file copies");
}
}
}
fn markdown_links(text: &str) -> Vec<String> {
let mut found = Vec::new();
let mut fenced = false;
for line in text.lines() {
let opener = line.trim_start();
if opener.starts_with("```") || opener.starts_with("~~~") {
fenced = !fenced;
continue;
}
if fenced {
continue;
}
let mut outside = String::new();
let mut spanned = false;
for character in line.chars() {
match character {
'`' => spanned = !spanned,
_ if !spanned => outside.push(character),
_ => {}
}
}
let characters: Vec<char> = outside.chars().collect();
let mut at = 0;
while at < characters.len() {
if characters[at] != '[' {
at += 1;
continue;
}
let Some(close) = (at..characters.len()).find(|index| characters[*index] == ']') else {
break;
};
if characters.get(close + 1) != Some(&'(') {
at = close + 1;
continue;
}
let Some(end) = (close + 2..characters.len()).find(|index| characters[*index] == ')')
else {
break;
};
let destination: String = characters[close + 2..end].iter().collect();
let destination = destination
.split_whitespace()
.next()
.unwrap_or_default()
.trim_start_matches('<')
.trim_end_matches('>');
if !destination.is_empty() {
found.push(destination.to_string());
}
at = end + 1;
}
}
found
}
fn inside_the_artifact(directory: &Path, target: &str) -> Option<PathBuf> {
let mut at = PathBuf::new();
for part in directory.join(target).components() {
match part {
std::path::Component::ParentDir => {
if !at.pop() {
return None;
}
}
std::path::Component::CurDir => {}
std::path::Component::Normal(name) => at.push(name),
_ => return None,
}
}
Some(at)
}
#[test]
fn a_json_publish_carries_the_digest_a_consumer_pins() {
let base = Path::new(env!("CARGO_TARGET_TMPDIR")).join("publish-json");
let _ = std::fs::remove_dir_all(&base);
let prose_out = base.join("prose");
let json_out = base.join("json");
let (code, prose, stderr) = publish_real_source(&prose_out, false);
assert_eq!(code, Some(0), "{stderr}");
let (code, document, stderr) = publish_real_source(&json_out, true);
assert_eq!(code, Some(0), "{stderr}");
let unaccounted: Vec<&str> = stderr
.lines()
.filter(|line| !line.contains("references that resolve nowhere inside it"))
.filter(|line| !line.contains(headwater_resolve::package::RECORDED_REFERENCES))
.filter(|line| !line.trim().is_empty())
.collect();
assert!(
unaccounted.is_empty(),
"a JSON run that succeeded accounts for nothing on standard error beyond the population \
the artifact records: {unaccounted:#?}"
);
let record = std::fs::read_to_string(json_out.join("release.yml")).expect("the record reads");
let record = headwater_resolve::release::read(&record).expect("the record parses");
let value = headwater_yaml::load(&document)
.unwrap_or_else(|errors| panic!("the document does not parse: {errors:?}\n{document}"))
.value;
let map = value.as_map().expect("the document is an object");
let text = |key: &str| -> Option<String> {
map.get(key)
.and_then(|spanned| spanned.value.as_scalar())
.map(headwater_yaml::core_schema::as_str)
.map(str::to_string)
};
assert_eq!(
text("version").as_deref(),
Some(headwater_resolve::release::DOCUMENT),
"the document names its own shape"
);
assert_eq!(text("digest").as_deref(), Some(record.digest.as_str()));
assert_eq!(text("out").as_deref(), json_out.to_str());
let package = map
.get("package")
.and_then(|spanned| spanned.value.as_map())
.expect("a `package` member");
let identity = |key: &str| -> Option<String> {
package
.get(key)
.and_then(|spanned| spanned.value.as_scalar())
.map(headwater_yaml::core_schema::as_str)
.map(str::to_string)
};
assert_eq!(identity("name").as_deref(), Some(record.package.as_str()));
assert_eq!(
identity("version").as_deref(),
Some(record.version.as_str())
);
assert_eq!(identity("requires_engine"), record.requires_engine);
let members = map
.get("members")
.and_then(|spanned| spanned.value.as_seq())
.expect("a `members` member");
assert_eq!(members.len(), record.members.len());
for (written, recorded) in members.iter().zip(&record.members) {
let member = written.value.as_map().expect("a member is an object");
let field = |key: &str| -> Option<&str> {
member
.get(key)
.and_then(|spanned| spanned.value.as_scalar())
.map(headwater_yaml::core_schema::as_str)
};
assert_eq!(field("path"), Some(recorded.path.as_str()));
assert_eq!(field("digest"), Some(recorded.digest.as_str()));
}
assert!(prose.contains(&record.digest), "{prose}");
}
fn relative_files(root: &Path) -> Vec<String> {
fn walk(base: &Path, dir: &Path, into: &mut Vec<String>) {
for entry in std::fs::read_dir(dir).expect("the directory reads") {
let entry = entry.expect("the entry reads").path();
if entry.is_dir() {
walk(base, &entry, into);
} else {
into.push(
entry
.strip_prefix(base)
.expect("every entry is under base")
.to_string_lossy()
.into_owned(),
);
}
}
}
let mut into = Vec::new();
walk(root, root, &mut into);
into
}
fn publish_clearing_killed(root: &Path, out: &Path) -> (Option<i32>, String) {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "publish", "--package", "headwater/standard"])
.arg("--clear-killed")
.arg("--out")
.arg(out)
.arg("--root")
.arg(root)
.output()
.expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
#[test]
fn the_flag_over_an_unrelated_directory_refuses_and_every_file_survives() {
let root = Root::copied("clear-killed-unrelated");
let out = root.path().join("artifact");
std::fs::create_dir_all(&out).expect("the caller's directory is made");
std::fs::write(out.join("theirs.txt"), "the caller's own file").expect("their file is written");
let (status, message) = publish_clearing_killed(root.path(), &out);
assert_eq!(status, Some(1), "the flag did not refuse: {message}");
assert!(
message.contains("nothing was published"),
"the refusal is not the publish's own: {message}"
);
assert_eq!(
std::fs::read_to_string(out.join("theirs.txt")).expect("their file reads"),
"the caller's own file"
);
assert_eq!(
relative_files(&out),
vec!["theirs.txt".to_string()],
"the flag wrote or removed something under the caller's directory"
);
}
#[test]
fn the_flag_over_a_killed_runs_residue_clears_it_and_publishes() {
let root = Root::scratch("clear-killed-residue");
let out = root.path().join("artifact");
let mut name = out.file_name().expect("it has a name").to_os_string();
name.push("~staging");
let staging = out.with_file_name(name);
std::fs::create_dir_all(&staging).expect("the staging directory is made");
std::fs::write(
staging.join(".headwater-publish-staging"),
"an earlier run claimed this directory\n",
)
.expect("the marker is written");
std::fs::write(
staging.join(".headwater-publish-direct"),
format!("an earlier run\nThe output path is: {}\n", out.display()),
)
.expect("the note is written");
std::fs::create_dir_all(&out).expect("the output directory is made");
std::fs::write(out.join("taxonomy.yml"), "half of an artifact\n")
.expect("the residue is written");
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.arg("taxonomy")
.arg("publish")
.arg("--clear-killed")
.arg("--from")
.arg(repository().join("taxonomy-source/headwater-standard"))
.arg("--out")
.arg(&out)
.arg("--root")
.arg(repository())
.output()
.expect("the binary runs");
let message = String::from_utf8_lossy(&output.stderr).into_owned();
assert_eq!(
output.status.code(),
Some(0),
"the clear and the publish did not land in one run: {message}"
);
assert!(
!staging.exists(),
"the killed run's staging directory outlived the clear: {message}"
);
assert!(
headwater_resolve::release::at(&out).is_ok(),
"the publish after the clear left no record: {message}"
);
assert!(
message.contains("~staging"),
"the run does not say what it removed: {message}"
);
}
#[derive(Debug, PartialEq, Eq)]
enum Step {
Run(Vec<String>),
Pin,
}
fn maintenance_loop(manifest: &str) -> Vec<Step> {
manifest
.lines()
.filter_map(|line| line.strip_prefix("# "))
.map(str::trim)
.filter(|line| !line.is_empty())
.map(|line| match line.strip_prefix("headwater ") {
Some(rest) => Step::Run(rest.split_whitespace().map(str::to_string).collect()),
None => {
assert!(
line.contains("taxonomy.digest"),
"the instruction block carries a step this case cannot run: {line}"
);
Step::Pin
}
})
.collect()
}
fn run_at(cwd: &Path, arguments: &[String]) -> (Option<i32>, String, String) {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(arguments)
.current_dir(cwd)
.output()
.expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stdout).into_owned(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
fn printed_digest(stdout: &str) -> String {
stdout
.lines()
.find_map(|line| line.trim().strip_prefix("digest "))
.unwrap_or_else(|| panic!("the publish printed no digest: {stdout}"))
.trim()
.to_string()
}
fn declared_version(manifest: &str) -> String {
manifest
.lines()
.find_map(|line| line.strip_prefix("version:"))
.unwrap_or_else(|| panic!("the shipped manifest declares no version: {manifest}"))
.trim()
.to_string()
}
fn pin_digest(consumer: &Path, digest: &str) {
let at = consumer.join(".headwater/taxonomy.yml");
let declaration = std::fs::read_to_string(&at).expect("the consumer declaration reads");
let rewritten = declaration
.lines()
.map(|line| match line.trim_start().starts_with("digest:") {
true => format!(" digest: {digest}"),
false => line.to_string(),
})
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&at, format!("{rewritten}\n")).expect("the pin is written");
}
#[test]
fn the_shipped_maintenance_loop_runs_as_written_over_a_source_that_changed() {
let root = Root::scratch("shipped-maintenance-loop");
let consumer = root.path().join("consumer");
copy(
&repository().join("taxonomy-source/headwater-standard"),
&consumer.join("taxonomy-source/headwater-standard"),
);
copy(
&repository().join("docs/taxonomies"),
&consumer.join("docs/taxonomies"),
);
std::fs::create_dir_all(consumer.join(".headwater")).expect("the block directory is made");
std::fs::copy(
repository().join(".headwater/overlay.yml"),
consumer.join(".headwater/overlay.yml"),
)
.expect("the overlay copies");
let manifest = std::fs::read_to_string(
repository().join("taxonomy-source/headwater-standard/package.yml"),
)
.expect("the shipped manifest reads");
let steps = maintenance_loop(&manifest);
let before = root.path().join("release-before");
let (code, stdout, stderr) = run_at(
&consumer,
&[
"taxonomy".to_string(),
"publish".to_string(),
"--from".to_string(),
"taxonomy-source/headwater-standard".to_string(),
"--out".to_string(),
before.to_str().expect("the path is UTF-8").to_string(),
],
);
assert_eq!(code, Some(0), "{stderr}");
write(
&consumer.join(".headwater/taxonomy.yml"),
&format!(
"taxonomy:\n package: headwater/standard\n version: {}\n digest: {}\n bundles: [design-spec, evidence-and-obligation, decision-record]\n overlay: .headwater/overlay.yml\ncorpus:\n root: docs\n",
declared_version(&manifest),
printed_digest(&stdout)
),
);
for arguments in [
vec![
"taxonomy".to_string(),
"vendor".to_string(),
before.to_str().expect("the path is UTF-8").to_string(),
],
vec!["taxonomy".to_string(), "resolve".to_string()],
] {
let (code, _stdout, stderr) = run_at(&consumer, &arguments);
assert_eq!(code, Some(0), "the settled state is not sound: {stderr}");
}
let source = consumer.join("taxonomy-source/headwater-standard/taxonomy.yml");
let authored = std::fs::read_to_string(&source).expect("the authored source reads");
std::fs::write(
&source,
format!("{authored}\n# an edit to the authored source, which moves the digest\n"),
)
.expect("the edit writes");
let scratch = root.path().join("release-after");
let mut printed = String::new();
for step in &steps {
match step {
Step::Pin => pin_digest(&consumer, &printed_digest(&printed)),
Step::Run(arguments) => {
let arguments: Vec<String> = arguments
.iter()
.map(|argument| match argument.as_str() {
"<scratch-dir>" => scratch.to_str().expect("the path is UTF-8").to_string(),
other => other.to_string(),
})
.collect();
let (code, stdout, stderr) = run_at(&consumer, &arguments);
assert_eq!(
code,
Some(0),
"the shipped instruction block does not run as written. `headwater {}` \
refused after the source changed:\n{stderr}",
arguments.join(" ")
);
printed = stdout;
}
}
}
assert!(
steps.contains(&Step::Pin),
"the instruction block ran green without stating a pin step, so this case no longer \
holds the order it exists for: {steps:?}"
);
let (code, stdout, stderr) = run_at(
&consumer,
&[
"conformance".to_string(),
"--level".to_string(),
"L0".to_string(),
],
);
assert_eq!(
code,
Some(0),
"the loop ran to the end and left a pin that does not name the installed bytes: \
{stdout}\n{stderr}"
);
}
#[test]
fn the_vendor_report_names_both_artifacts_of_one_version() {
let root = Root::scratch("one-version-two-artifacts");
let publisher = root.path().join("publisher");
write(
&publisher.join(".headwater/packages/acme-fixture/package.yml"),
"package: acme/fixture\nversion: 4.2.0\ncontents:\n taxonomy: taxonomy.yml\n doctrine: doctrine/\n",
);
write(
&publisher.join(".headwater/packages/acme-fixture/taxonomy.yml"),
"taxonomy: acme/fixture\nversion: 4.2.0\npurposes:\n rationale: {intent: explain why a choice was made and what it forecloses}\n",
);
write(
&publisher.join(".headwater/packages/acme-fixture/doctrine/method.md"),
"# Method\n\nWhy this taxonomy shelves what it shelves.\n",
);
let first_out = root.path().join("artifact-1");
let (code, _stdout, stderr) = consumer_run(
&publisher,
&[
"taxonomy",
"publish",
"--package",
"acme/fixture",
"--out",
first_out.to_str().expect("the path is UTF-8"),
],
);
assert_eq!(code, Some(0), "{stderr}");
let first = record_at(&first_out);
let adopter = root.path().join("adopter");
std::fs::create_dir_all(&adopter).expect("the adopter root is made");
let (code, stdout, stderr) = consumer_run(
&adopter,
&[
"taxonomy",
"vendor",
first_out.to_str().expect("the path is UTF-8"),
"--expect",
&first.digest,
],
);
assert_eq!(code, Some(0), "{stderr}");
assert!(
!stdout.contains("two sets of bytes"),
"a first install has no record to diverge from, and the report fired anyway:\n{stdout}"
);
write(
&publisher.join(".headwater/packages/acme-fixture/doctrine/second.md"),
"# Second\n\nOne more file, and the version stays where it is.\n",
);
let second_out = root.path().join("artifact-2");
let (code, _stdout, stderr) = consumer_run(
&publisher,
&[
"taxonomy",
"publish",
"--package",
"acme/fixture",
"--out",
second_out.to_str().expect("the path is UTF-8"),
],
);
assert_eq!(code, Some(0), "{stderr}");
let second = record_at(&second_out);
assert_eq!(
first.version, second.version,
"this is an upgrade, not the pair under test"
);
assert_ne!(
first.digest, second.digest,
"the two artifacts are one set of bytes"
);
assert_ne!(
first.members.len(),
second.members.len(),
"the two counts are equal, so a report that printed one of them twice would pass"
);
let (code, stdout, stderr) = consumer_run(
&adopter,
&[
"taxonomy",
"vendor",
second_out.to_str().expect("the path is UTF-8"),
"--expect",
&second.digest,
],
);
assert_eq!(
code,
Some(0),
"this reports and it does not refuse: {stderr}"
);
assert!(
stdout.contains("two sets of bytes"),
"the adopter was told nothing about the second artifact:\n{stdout}"
);
assert!(
stdout.contains(&format!("{} {} files", first.digest, first.members.len())),
"the installed artifact is not named with its own member count:\n{stdout}"
);
assert!(
stdout.contains(&format!(
"{} {} files",
second.digest,
second.members.len()
)),
"the arriving artifact is not named with its own member count:\n{stdout}"
);
assert_eq!(
package::find_version(&adopter, "acme/fixture"),
Some(second.version.clone()),
"the report replaced the install rather than accompanying it"
);
}
fn record_at(out: &Path) -> headwater_resolve::release::Release {
headwater_resolve::release::read(
&std::fs::read_to_string(out.join("release.yml")).expect("the release reads"),
)
.expect("the published record reads")
}
fn tree(dir: &Path) -> std::collections::BTreeMap<PathBuf, Vec<u8>> {
let mut files = std::collections::BTreeMap::new();
let mut stack = vec![dir.to_path_buf()];
while let Some(at) = stack.pop() {
for entry in std::fs::read_dir(&at).expect("the directory reads") {
let path = entry.expect("the entry reads").path();
if path.is_dir() {
stack.push(path);
} else {
let bytes = std::fs::read(&path).expect("the file reads");
files.insert(
path.strip_prefix(dir).expect("under dir").to_path_buf(),
bytes,
);
}
}
}
files
}
fn zipped(artifact: &Path) -> Vec<u8> {
use std::io::Write;
let mut writer = zip::ZipWriter::new(std::io::Cursor::new(Vec::new()));
let options = zip::write::SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated);
for (path, bytes) in tree(artifact) {
let name = path.to_str().expect("the path is UTF-8").replace('\\', "/");
writer.start_file(name, options).expect("the member starts");
writer.write_all(&bytes).expect("the member writes");
}
writer.finish().expect("the archive closes").into_inner()
}
fn serve_artifact(body: Vec<u8>) -> String {
use std::io::{Read, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback port binds");
let base = format!("http://{}", listener.local_addr().expect("the port reads"));
std::thread::spawn(move || {
for stream in listener.incoming() {
let Ok(mut stream) = stream else { continue };
let mut request = [0_u8; 4096];
let read = stream.read(&mut request).unwrap_or(0);
let line = String::from_utf8_lossy(&request[..read]).to_string();
let path = line.split_whitespace().nth(1).unwrap_or("").to_string();
let (head, payload): (String, &[u8]) = match path.as_str() {
"/moved" => (
"HTTP/1.1 302 Found\r\nLocation: /x.zip\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
.to_string(),
&[],
),
"/x.zip" => (
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
),
&body,
),
_ => (
"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"
.to_string(),
&[],
),
};
let _ = stream.write_all(head.as_bytes());
let _ = stream.write_all(payload);
}
});
base
}
#[cfg(feature = "fetch")]
fn published_fixture(root: &Root) -> (PathBuf, headwater_resolve::release::Release) {
let publisher = root.path().join("publisher");
write(
&publisher.join(".headwater/packages/acme-fixture/package.yml"),
"package: acme/fixture\nversion: 4.2.0\ncontents:\n taxonomy: taxonomy.yml\n doctrine: doctrine/\n",
);
write(
&publisher.join(".headwater/packages/acme-fixture/taxonomy.yml"),
"taxonomy: acme/fixture\nversion: 4.2.0\npurposes:\n rationale: {intent: explain why a choice was made and what it forecloses}\n",
);
write(
&publisher.join(".headwater/packages/acme-fixture/doctrine/method.md"),
"# Method\n\nWhy this taxonomy shelves what it shelves.\n",
);
let artifact = root.path().join("artifact");
let (code, _stdout, stderr) = consumer_run(
&publisher,
&[
"taxonomy",
"publish",
"--package",
"acme/fixture",
"--out",
artifact.to_str().expect("the path is UTF-8"),
],
);
assert_eq!(code, Some(0), "{stderr}");
let record = record_at(&artifact);
(artifact, record)
}
#[cfg(feature = "fetch")]
#[test]
fn a_location_vendors_the_bytes_the_same_artifact_vendors_by_path() {
let root = Root::scratch("vendor-location");
let (artifact, record) = published_fixture(&root);
let base = serve_artifact(zipped(&artifact));
let refused = root.path().join("refused");
std::fs::create_dir_all(&refused).expect("the adopter root is made");
let wrong = "sha256:0000000000000000000000000000000000000000000000000000000000000000";
let location = format!("{base}/x.zip");
let (code, _stdout, stderr) = consumer_run(
&refused,
&["taxonomy", "vendor", &location, "--expect", wrong],
);
assert_eq!(code, Some(1), "a wrong digest was accepted:\n{stderr}");
assert!(
stderr.contains(&location) && !stderr.contains("headwater-fetch-"),
"the refusal names the temporary directory rather than the location:\n{stderr}"
);
assert!(
!refused.join(package::PACKAGES).exists(),
"a refused vendor wrote into the package area"
);
let by_path = root.path().join("by-path");
std::fs::create_dir_all(&by_path).expect("the adopter root is made");
let (code, _stdout, stderr) = consumer_run(
&by_path,
&[
"taxonomy",
"vendor",
artifact.to_str().expect("the path is UTF-8"),
"--expect",
&record.digest,
],
);
assert_eq!(code, Some(0), "{stderr}");
let by_location = root.path().join("by-location");
std::fs::create_dir_all(&by_location).expect("the adopter root is made");
let moved = format!("{base}/moved");
let (code, stdout, stderr) = consumer_run(
&by_location,
&["taxonomy", "vendor", &moved, "--expect", &record.digest],
);
assert_eq!(code, Some(0), "{stderr}");
assert!(
stdout.contains(&moved),
"the report does not name the location it fetched:\n{stdout}"
);
let installed_by_path = tree(&by_path.join(package::PACKAGES));
assert!(
!installed_by_path.is_empty(),
"the path form installed nothing"
);
assert_eq!(
installed_by_path,
tree(&by_location.join(package::PACKAGES)),
"a location installed other bytes than the same artifact by path"
);
}
#[cfg(feature = "fetch")]
#[test]
fn a_loopback_location_is_fetched_directly_under_a_proxy_environment() {
use std::io::{Read, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
let root = Root::scratch("vendor-proxy");
let (artifact, record) = published_fixture(&root);
let base = serve_artifact(zipped(&artifact));
let proxy_listener = std::net::TcpListener::bind("127.0.0.1:0").expect("a loopback port binds");
let proxy = format!(
"http://{}",
proxy_listener.local_addr().expect("the port reads")
);
let connections = Arc::new(AtomicUsize::new(0));
let seen = Arc::clone(&connections);
std::thread::spawn(move || {
for stream in proxy_listener.incoming() {
let Ok(mut stream) = stream else { continue };
seen.fetch_add(1, Ordering::SeqCst);
let mut request = [0_u8; 4096];
let _ = stream.read(&mut request);
let _ = stream.write_all(
b"HTTP/1.1 502 Bad Gateway\r\nContent-Length: 0\r\nConnection: close\r\n\r\n",
);
}
});
let by_path = root.path().join("by-path");
std::fs::create_dir_all(&by_path).expect("the adopter root is made");
let (code, _stdout, stderr) = consumer_run(
&by_path,
&[
"taxonomy",
"vendor",
artifact.to_str().expect("the path is UTF-8"),
"--expect",
&record.digest,
],
);
assert_eq!(code, Some(0), "{stderr}");
let upper = base.replacen("http://", "HTTP://", 1);
for (case, location) in [
("lower", format!("{base}/x.zip")),
("upper", format!("{upper}/x.zip")),
] {
let adopter = root.path().join(format!("adopter-{case}"));
std::fs::create_dir_all(&adopter).expect("the adopter root is made");
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["taxonomy", "vendor", &location, "--expect", &record.digest])
.arg("--root")
.arg(&adopter)
.env("HTTP_PROXY", &proxy)
.env("http_proxy", &proxy)
.env("ALL_PROXY", &proxy)
.env("all_proxy", &proxy)
.env_remove("NO_PROXY")
.env_remove("no_proxy")
.output()
.expect("the binary runs");
let stderr = String::from_utf8_lossy(&output.stderr);
assert_eq!(
connections.load(Ordering::SeqCst),
0,
"the plain-http loopback request {location} went through the proxy:\n{stderr}"
);
assert_eq!(output.status.code(), Some(0), "{location}: {stderr}");
assert_eq!(
tree(&by_path.join(package::PACKAGES)),
tree(&adopter.join(package::PACKAGES)),
"{location} under a proxy environment installed other bytes than the artifact by path"
);
}
}