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")
}
const DECLARATION_HEAD: &str = r"# The consumer declaration, written by `headwater init`. It says two things,
# and they are different questions: what schema this repository takes, and what
# tree it walks.
#
# `headwater taxonomy resolve` reads this and writes .headwater/taxonomy.lock. Everything after that
# reads the lock and never these sources.
taxonomy:
package: headwater/standard
";
const NO_PACKAGE_VERSION: &str = r" # INTERVIEW: no package of this name is under `.headwater/packages/`. Two routes
# reach a lock, and each one needs a different field below.
# Copy a package directory into `.headwater/packages/`, and pin `version` at
# the version that package declares. Or run
# `headwater taxonomy vendor <dir-or-location>` on a published artifact,
# unpacked or at the `https://` location of its zip: that verb reads `digest`
# and refuses until it holds the digest the publisher printed, and
# `headwater taxonomy resolve` reads `version` after it, so the vendor route
# needs the digest first and the version as well.
# digest: sha256:<the digest the publisher printed>
version: 0.0.0
";
const DECLARATION_TAIL: &str = r"# A bundle is an optional part of the package, and a selection is add-only.
# INTERVIEW: which traditions does this corpus already follow?
bundles: []
overlay: .headwater/overlay.yml
corpus:
# Proposed from this tree: the directory holding the most Markdown.
root: docs
# An exclusion states a reason. A pattern with none is a silent pass with a
# configuration file in front of it, so the reason is not optional.
# exclude:
# - path: docs/vendor/**
# reason: vendored copies of documents another team owns
";
const OVERLAY: &str = r#"# The adopter overlay, written by `headwater init`. It is an overlay and never a
# resolved taxonomy, so nothing here can weaken the package it sits on: a
# bundle selection is add-only, and an add-only overlay carries no operation
# that removes a base rule.
#
# Every block below is a question this engine cannot answer from a tree. It is
# prose about what this corpus is for, and a corpus does not state it.
#
# INTERVIEW 1 --- what does each purpose answer?
#
# A task is matched against declared purposes before it is matched against any
# text, and it is matched on the `answers` phrases first. Two purposes whose
# phrases share every term separate nothing, and every task then matches both
# equally. Read `headwater/standard`'s purposes, and add the phrases a person here would
# actually type.
#
# add:
# purposes.rationale.answers: ["why is it this way", "what was rejected"]
#
# INTERVIEW 2 --- what identifies a document, and what does the prefix mean?
#
# A relation names its target by identifier. A corpus whose documents carry none
# has no edges, and no check about an edge can say anything about it.
#
# `add` states a value the package leaves unstated, and `override` replaces one
# the package already states, so the operation follows the package rather than
# the taste of the writer. `headwater/standard` declares `decision_id` with no namespace
# and gives `decision` that scheme, so the namespace below is an `add` on a leaf
# the package leaves empty, and the kind below is an `override` because `add`
# over a value the package already states is refused. Replace ACME with the
# prefix this corpus uses.
#
# add:
# identifier_schemes.doc_id: {pattern: "{namespace}-DOC-{slug}", namespace: ACME, allocation: minted-once}
# identifier_schemes.decision_id.namespace: ACME
# override:
# kinds.decision.identifier: {scheme: doc_id}
#
# INTERVIEW 3 --- what does this corpus already write?
#
# Run `headwater infer` once this file resolves. It reports the files that
# classify as nothing, which is the half a payload cannot carry, and the
# documents that state no summary, which nothing will route to.
add: {}
"#;
struct Root {
at: PathBuf,
}
impl Root {
fn over(label: &str) -> Root {
let at =
std::env::temp_dir().join(format!("headwater-cli-init-{}-{label}", std::process::id()));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(at.join("docs")).expect("the corpus directory is there");
std::fs::write(at.join("docs/one.md"), "# a document\n").expect("the document writes");
Root { at }
}
fn with_package(label: &str) -> Root {
let root = Root::over(label);
let directory = root.at.join(".headwater/packages/headwater-standard");
std::fs::create_dir_all(&directory).expect("the package directory is there");
std::fs::copy(
repository().join(".headwater/packages/headwater-standard/package.yml"),
directory.join("package.yml"),
)
.expect("the manifest copies");
root
}
fn init(&self) {
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["init", "--root"])
.arg(&self.at)
.output()
.expect("the binary runs");
assert_eq!(
output.status.code(),
Some(0),
"`headwater init` writes both files:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
fn read(&self, relative: &str) -> String {
std::fs::read_to_string(self.at.join(relative)).expect("the written file reads")
}
fn declared_version(&self) -> String {
declared_version_at(
&self
.at
.join(".headwater/packages/headwater-standard/package.yml"),
)
}
fn beside(&self, label: &str) -> PathBuf {
let at = self.at.with_file_name(format!(
"{}-{label}",
self.at
.file_name()
.expect("the scratch root is named")
.to_string_lossy()
));
let _ = std::fs::remove_dir_all(&at);
std::fs::create_dir_all(&at).expect("the scratch directory is made");
at
}
fn write(&self, relative: &str, body: &str) {
std::fs::write(self.at.join(relative), body).expect("the file writes");
}
fn run(&self, verb: &[&str], argument: Option<&Path>) -> (Option<i32>, String) {
let mut command = Command::new(env!("CARGO_BIN_EXE_headwater"));
command.args(verb);
if let Some(path) = argument {
command.arg(path);
}
let output = command
.arg("--root")
.arg(&self.at)
.output()
.expect("the binary runs");
(
output.status.code(),
String::from_utf8_lossy(&output.stderr).into_owned(),
)
}
}
fn declared_version_at(manifest: &Path) -> String {
let body = std::fs::read_to_string(manifest)
.unwrap_or_else(|error| panic!("{} reads: {error}", manifest.display()));
let lines: Vec<&str> = body
.lines()
.filter(|line| line.starts_with("version:"))
.collect();
assert_eq!(
lines.len(),
1,
"{} declares one version at the top level, and it declares {}",
manifest.display(),
lines.len()
);
lines[0].trim_start_matches("version:").trim().to_string()
}
fn maintained_version() -> String {
declared_version_at(&repository().join("taxonomy-source/headwater-standard/package.yml"))
}
fn publish_maintained_source_into(out: &Path) -> 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");
assert_eq!(
output.status.code(),
Some(0),
"the artifact publishes:\n{}",
String::from_utf8_lossy(&output.stderr)
);
let report = String::from_utf8_lossy(&output.stdout).into_owned();
let stated: Vec<&str> = report
.lines()
.filter_map(|line| line.trim().strip_prefix("digest "))
.collect();
assert_eq!(
stated.len(),
1,
"the publish report states one digest, and it states {}:\n{report}",
stated.len()
);
stated[0].to_string()
}
#[test]
fn the_declaration_of_a_tree_with_no_package_is_written_byte_for_byte() {
let root = Root::over("declaration-no-package");
root.init();
let expected = format!("{DECLARATION_HEAD}{NO_PACKAGE_VERSION}{DECLARATION_TAIL}");
assert_eq!(
root.read(".headwater/taxonomy.yml"),
expected,
"the declaration `headwater init` writes on a blank tree"
);
}
#[test]
fn the_overlay_is_written_byte_for_byte() {
let root = Root::over("overlay");
root.init();
assert_eq!(
root.read(".headwater/overlay.yml"),
OVERLAY,
"the overlay `headwater init` writes"
);
}
#[test]
fn the_declaration_of_a_tree_with_a_package_pins_the_version_that_package_declares() {
let root = Root::with_package("declaration-with-package");
let version = root.declared_version();
root.init();
let expected = format!("{DECLARATION_HEAD} version: {version}\n{DECLARATION_TAIL}");
assert_eq!(
root.read(".headwater/taxonomy.yml"),
expected,
"the declaration `headwater init` writes over a package it found"
);
assert!(
!expected.contains("0.0.0"),
"a package on the tree replaces the interview stub rather than joining it"
);
}
#[test]
fn the_last_line_of_the_overlay_is_the_line_the_tutorial_replaces() {
let root = Root::over("overlay-last-line");
root.init();
let overlay = root.read(".headwater/overlay.yml");
assert!(
overlay.trim_end_matches('\n').ends_with("add: {}"),
"the last line of the overlay is `add: {{}}`, and the tutorial replaces it in place:\n{}",
overlay
.lines()
.rev()
.take(3)
.collect::<Vec<&str>>()
.join("\n")
);
}
#[test]
fn the_vendor_route_the_declaration_names_reaches_a_resolved_version() {
let root = a_root_the_vendor_route_reached("vendor-route");
let (_, stderr) = root.run(&["taxonomy", "resolve"], None);
assert!(
!stderr.contains("this takes headwater/standard"),
"`headwater taxonomy resolve` is past the version the vendor route also needs:\n{stderr}"
);
}
fn a_root_the_vendor_route_reached(label: &str) -> Root {
let root = Root::over(label);
let artifact = root.beside("artifact");
let digest = publish_maintained_source_into(&artifact);
root.init();
let declaration = root.read(".headwater/taxonomy.yml");
let commented = declaration
.lines()
.find(|line| line.trim_start().starts_with("# digest:"))
.unwrap_or_else(|| {
panic!(
"the declaration names the field `taxonomy vendor` reads, and it names none of \
these:\n{declaration}"
)
});
let version = maintained_version();
let pinned = declaration
.replace(commented, &format!(" digest: {digest}"))
.replace(" version: 0.0.0\n", &format!(" version: {version}\n"));
assert!(
pinned.contains(&format!(" digest: {digest}\n")),
"the commented line is replaced by the pin:\n{pinned}"
);
root.write(".headwater/taxonomy.yml", &pinned);
let (code, stderr) = root.run(&["taxonomy", "vendor"], Some(&artifact));
assert_eq!(
code,
Some(0),
"`headwater taxonomy vendor` accepts the artifact the declaration pins:\n{stderr}"
);
assert_eq!(
declared_version_at(
&root
.at
.join(".headwater/packages/headwater-standard/package.yml")
),
version,
"the vendored package is the one the declaration pins"
);
root
}
fn the_interview_2_example(overlay: &str) -> String {
let block: Vec<&str> = overlay
.lines()
.skip_while(|line| !line.starts_with("# INTERVIEW 2"))
.skip(1)
.take_while(|line| !line.starts_with("# INTERVIEW"))
.collect();
let example: Vec<&str> = block
.iter()
.filter_map(|line| line.strip_prefix('#'))
.filter(|rest| rest.starts_with(" "))
.collect();
assert!(
!example.is_empty(),
"the overlay prints an example under `INTERVIEW 2`, and it prints none of these:\n{overlay}"
);
let indent = example
.iter()
.map(|line| line.len() - line.trim_start().len())
.min()
.expect("the example is not empty");
example
.iter()
.map(|line| format!("{}\n", &line[indent..]))
.collect()
}
#[test]
fn the_overlay_example_the_interview_prints_reaches_a_lock() {
let root = a_root_the_vendor_route_reached("interview-example");
let overlay = root.read(".headwater/overlay.yml");
assert!(
overlay.trim_end_matches('\n').ends_with("add: {}"),
"the overlay ends with the line the example replaces:\n{overlay}"
);
let patched = format!(
"{}{}",
overlay.trim_end_matches('\n').trim_end_matches("add: {}"),
the_interview_2_example(&overlay)
);
root.write(".headwater/overlay.yml", &patched);
let (code, stderr) = root.run(&["taxonomy", "resolve"], None);
assert_eq!(
code,
Some(0),
"the example `INTERVIEW 2` prints resolves:\n{stderr}\nthe overlay it was run \
against:\n{patched}"
);
}
fn the_vendor_argument_the_contract_states() -> String {
let contract =
std::fs::read_to_string(repository().join("docs/interfaces/headwater-taxonomy.md"))
.expect("the interface contract reads");
let rows: Vec<String> = contract
.lines()
.filter_map(|line| line.strip_prefix("| `vendor <"))
.filter_map(|rest| {
rest.split_once('>')
.map(|(argument, _)| argument.to_string())
})
.collect();
assert_eq!(
rows.len(),
1,
"the contract states one `vendor` row, and it states {}",
rows.len()
);
rows[0].clone()
}
fn vendor_placeholders(text: &str) -> Vec<String> {
let squeezed = text
.lines()
.map(|line| line.trim_start().trim_start_matches('#').trim())
.collect::<Vec<_>>()
.join(" ");
squeezed
.match_indices("headwater taxonomy vendor <")
.filter_map(|(at, marker)| {
squeezed[at + marker.len()..]
.split_once('>')
.map(|(argument, _)| argument.to_string())
})
.collect()
}
#[cfg(feature = "fetch")]
fn zipped(artifact: &Path) -> Vec<u8> {
use std::io::Write;
let mut members = Vec::new();
let mut pending = vec![artifact.to_path_buf()];
while let Some(dir) = pending.pop() {
for entry in std::fs::read_dir(&dir).expect("the artifact directory reads") {
let path = entry.expect("the entry reads").path();
if path.is_dir() {
pending.push(path);
} else {
members.push(path);
}
}
}
members.sort();
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 in members {
let name = path
.strip_prefix(artifact)
.expect("under the artifact")
.to_str()
.expect("the path is UTF-8")
.replace('\\', "/");
writer.start_file(name, options).expect("the member starts");
writer
.write_all(&std::fs::read(&path).expect("the member reads"))
.expect("the member writes");
}
writer.finish().expect("the archive closes").into_inner()
}
#[cfg(feature = "fetch")]
fn serve(name: &str, 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 location = format!(
"http://{}/{name}",
listener.local_addr().expect("the port reads")
);
let wanted = format!("/{name}");
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 found = line.split_whitespace().nth(1) == Some(wanted.as_str());
let (head, payload): (String, &[u8]) = if found {
(
format!(
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
body.len()
),
&body,
)
} else {
(
"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);
}
});
location
}
#[cfg(feature = "fetch")]
#[test]
fn the_vendor_route_init_names_takes_the_location_a_release_publishes() {
let root = Root::over("vendor-location");
let artifact = root.beside("artifact");
let digest = publish_maintained_source_into(&artifact);
let output = Command::new(env!("CARGO_BIN_EXE_headwater"))
.args(["init", "--root"])
.arg(&root.at)
.output()
.expect("the binary runs");
assert_eq!(
output.status.code(),
Some(0),
"`headwater init` writes both files"
);
let report = String::from_utf8_lossy(&output.stdout).into_owned();
let declaration = root.read(".headwater/taxonomy.yml");
let argument = the_vendor_argument_the_contract_states();
for (name, text) in [
("the printed report", &report),
("the declaration", &declaration),
] {
assert_eq!(
vendor_placeholders(text),
vec![argument.clone()],
"{name} names `headwater taxonomy vendor <{argument}>`, the argument the contract \
states:\n{text}"
);
assert!(
!text.contains("fetches one"),
"{name} no longer says that nothing fetches a package:\n{text}"
);
}
let version = maintained_version();
let commented = declaration
.lines()
.find(|line| line.trim_start().starts_with("# digest:"))
.expect("the declaration names the field `taxonomy vendor` reads");
let pinned = declaration
.replace(commented, &format!(" digest: {digest}"))
.replace(" version: 0.0.0\n", &format!(" version: {version}\n"));
root.write(".headwater/taxonomy.yml", &pinned);
let location = serve(
&format!("headwater-standard-{version}.zip"),
zipped(&artifact),
);
let (code, stderr) = root.run(&["taxonomy", "vendor", location.as_str()], None);
assert_eq!(
code,
Some(0),
"`headwater taxonomy vendor <{argument}>` accepts the location a release \
publishes:\n{stderr}"
);
assert_eq!(
declared_version_at(
&root
.at
.join(".headwater/packages/headwater-standard/package.yml")
),
version,
"the fetched package is the one the declaration pins"
);
let (_, stderr) = root.run(&["taxonomy", "resolve"], None);
assert!(
!stderr.contains("this takes headwater/standard"),
"`headwater taxonomy resolve` is past the version after a fetch:\n{stderr}"
);
}