mod common;
use std::path::PathBuf;
use std::process::Command;
use common::{fixture, rete, Fixture};
fn repo_root() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("crate lives two levels under the repo root")
.to_path_buf()
}
fn driver() -> PathBuf {
repo_root().join("scripts/export_scholar_nquads.sh")
}
fn export_report(f: &Fixture, stem: &str, nt: &str) -> String {
let src = f.write(&format!("{stem}.nt"), nt);
let out = f.path(&format!("{stem}.rete"));
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let dump = rete()
.args(["export"])
.arg(&out)
.arg("--sanitize-iris")
.assert()
.success();
String::from_utf8_lossy(&dump.get_output().stderr).into_owned()
}
fn parse_with_driver(f: &Fixture, stem: &str, report: &str) -> Vec<(String, String)> {
let path = f.write(&format!("{stem}.iri.txt"), report);
let out = Command::new("bash")
.arg(driver())
.arg("--parse-report")
.arg(&path)
.current_dir(repo_root())
.output()
.expect("bash is required to run the export driver's parser");
assert!(
out.status.success(),
"the driver could not parse the exporter's own report.\n\
--- report ---\n{report}\n--- stderr ---\n{}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8_lossy(&out.stdout)
.split_whitespace()
.filter_map(|kv| kv.split_once('='))
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
}
#[track_caller]
fn field(parsed: &[(String, String)], key: &str) -> u64 {
parsed
.iter()
.find(|(k, _)| k == key)
.unwrap_or_else(|| panic!("the driver printed no `{key}`: {parsed:?}"))
.1
.parse()
.expect("a count")
}
const EVERY_CLASS: &str = concat!(
"<http://example.org/ok> <http://example.org/p> \"clean control\" .\n",
"<http://example.org/a[b]> <http://example.org/p> \"bracket\" .\n",
"<http://example.org/c#d#e> <http://example.org/p> \"second hash\" .\n",
"<http://example.org/%x> <http://example.org/p> \"bad percent\" .\n",
"<http://example.org/a b> <http://example.org/p> \"raw space\" .\n",
"<noscheme/path> <http://example.org/p> \"relative\" .\n",
"<https://::1> <http://example.org/p> \"unbracketed IPv6\" .\n",
);
#[test]
fn the_driver_parses_the_exporters_own_report() {
let f = fixture();
let cases: &[(&str, &str, u64, u64, u64, u64)] = &[
(
"clean",
"<http://example.org/ok> <http://example.org/p> \"fine\" .\n",
0,
0,
0,
0,
),
(
"repairable_only",
concat!(
"<http://example.org/a[b]> <http://example.org/p> \"bracket\" .\n",
"<http://example.org/c#d#e> <http://example.org/p> \"hash\" .\n",
),
2,
2,
0,
0,
),
(
"relative_only",
"<noscheme/path> <http://example.org/p> \"relative\" .\n",
1,
0,
1,
0,
),
(
"unclassified_only",
"<https://::1> <http://example.org/p> \"unbracketed IPv6\" .\n",
1,
0,
1,
1,
),
("every_class", EVERY_CLASS, 6, 4, 2, 1),
];
for (stem, nt, invalid, repairable, unrepairable, unclassified) in cases {
let report = export_report(&f, stem, nt);
assert!(
report.contains(&format!(
"totals invalid={invalid} repairable={repairable} \
unrepairable={unrepairable} unclassified={unclassified}"
)),
"{stem}: the exporter's own totals line is not what this test expects.\n{report}"
);
let parsed = parse_with_driver(&f, stem, &report);
assert_eq!(field(&parsed, "invalid"), *invalid, "{stem} invalid");
assert_eq!(
field(&parsed, "unrepairable"),
*unrepairable,
"{stem} unrepairable — THIS is the publication gate"
);
assert_eq!(
field(&parsed, "unclassified"),
*unclassified,
"{stem} unclassified"
);
}
}
#[test]
fn every_class_row_reaches_its_own_column() {
let f = fixture();
let report = export_report(&f, "classes", EVERY_CLASS);
let parsed = parse_with_driver(&f, "classes", &report);
for (column, expected) in [
("bracket", 1),
("hash", 1),
("percent", 1),
("forbidden", 1),
("schemeless", 1),
] {
assert_eq!(
field(&parsed, column),
expected,
"{column} did not reach its column — a reason string moved.\n{report}"
);
}
let class_sum: u64 = ["bracket", "hash", "percent", "forbidden", "schemeless"]
.iter()
.map(|c| field(&parsed, c))
.sum::<u64>()
+ field(&parsed, "unclassified");
assert_eq!(
class_sum,
field(&parsed, "invalid"),
"class rows do not account for every invalid occurrence.\n{report}"
);
}
fn verdict(parsed: &[(String, String)]) -> String {
parsed
.iter()
.find(|(k, _)| k == "verdict")
.expect("the driver printed no verdict")
.1
.clone()
}
#[test]
fn an_unbracketed_ipv6_dump_is_refused_not_published() {
let f = fixture();
let report = export_report(
&f,
"gate",
"<https://::1> <http://example.org/p> \"unbracketed IPv6\" .\n",
);
let parsed = parse_with_driver(&f, "gate", &report);
assert_eq!(
field(&parsed, "schemeless"),
0,
"the old `schemeless` gate would have opened, which is the point"
);
assert!(field(&parsed, "unrepairable") > 0);
assert_eq!(
verdict(&parsed),
"refuse",
"the dump must not be published.\n{report}"
);
}
#[test]
fn a_repaired_dump_is_publishable() {
let f = fixture();
for (stem, nt) in [
(
"clean_gate",
"<http://example.org/ok> <http://example.org/p> \"fine\" .\n",
),
(
"repaired_gate",
concat!(
"<http://example.org/a[b]> <http://example.org/p> \"bracket\" .\n",
"<http://example.org/c#d#e> <http://example.org/p> \"hash\" .\n",
),
),
(
"legal_gate",
concat!(
"<http://user:pass@host/> <http://example.org/p> \"userinfo colon\" .\n",
"<http://[::1]:8080/p> <http://example.org/p> \"bracketed IPv6\" .\n",
"<urn:isbn:0451450523> <http://example.org/p> \"no authority\" .\n",
"<mailto:a@b.com> <http://example.org/p> \"path at-sign\" .\n",
"<http://host:8080/a:b> <http://example.org/p> \"path colons\" .\n",
"<http://caf\u{e9}.example/> <http://example.org/p> \"ucschar host\" .\n",
),
),
] {
let report = export_report(&f, stem, nt);
let parsed = parse_with_driver(&f, stem, &report);
assert_eq!(
verdict(&parsed),
"publishable",
"{stem} was refused and should not have been.\n{report}"
);
}
}