mod common;
use common::{fixture, rete};
const REPAIRABLE: &str = concat!(
"<http://example.org/ok> <http://example.org/p> \"fine\" .\n",
"<http://example.org/raw/caf\u{e9}> <http://example.org/p> \"a raw ucschar is a valid IRI\" .\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 escape\" .\n",
"<http://example.org/a b> <http://example.org/p> \"raw space\" .\n",
);
const UNREPAIRABLE: &str = concat!(
"<http://example.org/ok> <http://example.org/p> \"fine\" .\n",
"<noscheme/path> <http://example.org/p> \"no scheme\" .\n",
);
const UNBRACKETED_IPV6: &str = concat!(
"<http://example.org/ok> <http://example.org/p> \"fine\" .\n",
"<https://github.com/steamfoundry> <http://example.org/homepage> <https://::1> .\n",
);
fn stderr_of(out: &std::process::Output) -> String {
String::from_utf8_lossy(&out.stderr).into_owned()
}
#[test]
fn a_graph_with_invalid_iris_still_builds_and_says_how_many() {
let f = fixture();
let src = f.write("bad.nt", REPAIRABLE);
let out = f.path("bad.rete");
let assert = rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let err = stderr_of(assert.get_output());
assert!(
err.contains("4 statement(s) carry an invalid IRI (4 IRI occurrence(s))"),
"no count in:\n{err}"
);
assert!(
err.contains("'[' or ']' outside an IP-literal host"),
"{err}"
);
assert!(err.contains("more than one '#'"), "{err}");
assert!(err.contains("'%' not followed by two hex digits"), "{err}");
assert!(
err.contains("a character the IRIREF grammar excludes"),
"{err}"
);
assert!(err.contains("--sanitize-iris"), "{err}");
assert!(err.contains("--strict"), "{err}");
assert!(out.exists(), "the build must still produce a file");
}
#[test]
fn a_clean_graph_produces_no_warning() {
let f = fixture();
let src = f.write(
"ok.nt",
concat!(
"<http://example.org/ok> <http://example.org/p> \"fine\" .\n",
"<http://[::1]:7878/sparql> <http://example.org/p> \"an IP-literal host is legal\" .\n",
"<urn:uuid:0000> <http://example.org/p> \"so is a URN\" .\n",
"<http://example.org/a#frag> <http://example.org/p> \"one '#' is the fragment\" .\n",
),
);
let assert = rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(f.path("ok.rete"))
.assert()
.success();
let err = stderr_of(assert.get_output());
assert!(!err.contains("invalid IRI"), "false positive:\n{err}");
}
#[test]
fn strict_refuses_and_names_the_offending_iri() {
let f = fixture();
let src = f.write("bad.nt", REPAIRABLE);
let assert = rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(f.path("strict.rete"))
.arg("--strict")
.assert()
.failure();
let err = stderr_of(assert.get_output());
assert!(
err.contains("invalid IRI <http://example.org/a[b]>"),
"{err}"
);
assert!(
err.contains("'[' or ']' outside an IP-literal host"),
"{err}"
);
assert!(err.contains("hint: `--strict` refuses input"), "{err}");
}
#[test]
fn the_external_build_audits_too() {
let f = fixture();
let src = f.write("bad.nt", REPAIRABLE);
let assert = rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(f.path("ext.rete"))
.args(["--memory-budget-mb", "64"])
.assert()
.success();
let err = stderr_of(assert.get_output());
assert!(err.contains("4 statement(s) carry an invalid IRI"), "{err}");
}
#[test]
fn validate_reports_the_audit_and_can_be_strict() {
let f = fixture();
let src = f.write("bad.nt", REPAIRABLE);
let assert = rete().args(["validate"]).arg(&src).assert().success();
let err = stderr_of(assert.get_output());
assert!(err.contains("carry an invalid IRI"), "{err}");
rete()
.args(["validate"])
.arg(&src)
.arg("--strict")
.assert()
.failure();
}
#[test]
fn export_is_unchanged_without_the_flag() {
let f = fixture();
let src = f.write("bad.nt", REPAIRABLE);
let out = f.path("bad.rete");
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let dump = rete().args(["export"]).arg(&out).assert().success();
let text = String::from_utf8_lossy(&dump.get_output().stdout).into_owned();
assert!(text.contains("<http://example.org/a[b]>"), "{text}");
assert!(text.contains("<http://example.org/c#d#e>"), "{text}");
}
#[test]
fn sanitize_iris_percent_encodes_and_reports() {
let f = fixture();
let src = f.write("bad.nt", REPAIRABLE);
let out = f.path("bad.rete");
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let dump = rete()
.args(["export"])
.arg(&out)
.arg("--sanitize-iris")
.assert()
.success();
let text = String::from_utf8_lossy(&dump.get_output().stdout).into_owned();
let err = stderr_of(dump.get_output());
assert!(text.contains("<http://example.org/a%5Bb%5D>"), "{text}");
assert!(text.contains("<http://example.org/c#d%23e>"), "{text}");
assert!(text.contains("<http://example.org/%25x>"), "{text}");
assert!(text.contains("<http://example.org/a%20b>"), "{text}");
assert!(text.contains("<http://example.org/ok>"), "{text}");
assert!(
text.contains("<http://example.org/raw/caf\u{e9}>"),
"{text}"
);
assert!(
!text.contains("caf%"),
"a valid ucschar was escaped:\n{text}"
);
assert!(err.contains("percent-encoded 4 IRI occurrence(s)"), "{err}");
assert!(
err.contains("no longer joins against the source graph"),
"{err}"
);
}
#[test]
fn sanitize_iris_admits_what_it_cannot_repair() {
let f = fixture();
let src = f.write("rel.nt", UNREPAIRABLE);
let out = f.path("rel.rete");
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let dump = rete()
.args(["export"])
.arg(&out)
.arg("--sanitize-iris")
.assert()
.success();
let text = String::from_utf8_lossy(&dump.get_output().stdout).into_owned();
let err = stderr_of(dump.get_output());
assert!(
text.contains("<noscheme/path>"),
"written verbatim:\n{text}"
);
assert!(err.contains("CANNOT be repaired"), "{err}");
assert!(err.contains("still not valid N-Quads"), "{err}");
}
#[test]
fn sanitize_iris_says_so_when_there_was_nothing_to_do() {
let f = fixture();
let src = f.write(
"ok.nt",
"<http://example.org/ok> <http://example.org/p> \"fine\" .\n",
);
let out = f.path("ok.rete");
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let dump = rete()
.args(["export"])
.arg(&out)
.arg("--sanitize-iris")
.assert()
.success();
assert!(
stderr_of(dump.get_output()).contains("no invalid IRIs found"),
"{}",
stderr_of(dump.get_output())
);
}
#[test]
fn sanitize_iris_covers_the_graph_term() {
let f = fixture();
let src = f.write(
"g.nq",
"<http://example.org/s> <http://example.org/p> \"o\" <http://example.org/g[1]> .\n",
);
let out = f.path("g.rete");
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let dump = rete()
.args(["export"])
.arg(&out)
.arg("--sanitize-iris")
.assert()
.success();
let text = String::from_utf8_lossy(&dump.get_output().stdout).into_owned();
assert!(text.contains("<http://example.org/g%5B1%5D>"), "{text}");
}
#[test]
fn an_unclassified_defect_is_counted_and_blocks() {
let f = fixture();
let src = f.write("ipv6.nt", UNBRACKETED_IPV6);
let out = f.path("ipv6.rete");
let build = rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let berr = stderr_of(build.get_output());
assert!(
berr.contains("1 statement(s) carry an invalid IRI"),
"the build audit missed it:\n{berr}"
);
assert!(berr.contains("RFC 3987"), "{berr}");
let dump = rete()
.args(["export"])
.arg(&out)
.arg("--sanitize-iris")
.assert()
.success();
let text = String::from_utf8_lossy(&dump.get_output().stdout).into_owned();
let err = stderr_of(dump.get_output());
assert!(
err.contains("totals invalid=1 repairable=0 unrepairable=1 unclassified=1"),
"the totals line is what the export gate reads:\n{err}"
);
assert!(err.contains("CANNOT be repaired"), "{err}");
assert!(err.contains("still not valid N-Quads"), "{err}");
assert!(
err.contains("no repair class recognises it"),
"the report must say the taxonomy did not recognise it:\n{err}"
);
assert!(
text.contains("<https://::1>"),
"an unrepairable IRI is written verbatim:\n{text}"
);
}
#[test]
fn a_clean_dump_reports_zero_totals_and_is_untouched() {
let f = fixture();
let src = f.write(
"clean.nt",
concat!(
"<http://user:pass@host/> <http://example.org/p> \"colon in userinfo\" .\n",
"<http://[::1]:8080/p> <http://example.org/p> \"bracketed IPv6 with a port\" .\n",
"<urn:isbn:0451450523> <http://example.org/p> \"no authority\" .\n",
"<mailto:a@b.com> <http://example.org/p> \"'@' is path here\" .\n",
"<http://host:8080/a:b> <http://example.org/p> \"colons in the path\" .\n",
"<http://caf\u{e9}.example/> <http://example.org/p> \"RFC 3987 ucschar\" .\n",
),
);
let out = f.path("clean.rete");
let build = rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
assert!(
!stderr_of(build.get_output()).contains("invalid IRI"),
"false positive on legal IRIs:\n{}",
stderr_of(build.get_output())
);
let plain = rete().args(["export"]).arg(&out).assert().success();
let sanitized = rete()
.args(["export"])
.arg(&out)
.arg("--sanitize-iris")
.assert()
.success();
let err = stderr_of(sanitized.get_output());
assert!(
err.contains("totals invalid=0 repairable=0 unrepairable=0 unclassified=0"),
"{err}"
);
assert_eq!(
plain.get_output().stdout,
sanitized.get_output().stdout,
"--sanitize-iris rewrote a dump that was already valid"
);
}
#[test]
fn a_fully_repairable_dump_reports_zero_unrepairable() {
let f = fixture();
let src = f.write("rep.nt", REPAIRABLE);
let out = f.path("rep.rete");
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success();
let dump = rete()
.args(["export"])
.arg(&out)
.arg("--sanitize-iris")
.assert()
.success();
let err = stderr_of(dump.get_output());
assert!(
err.contains("totals invalid=4 repairable=4 unrepairable=0 unclassified=0"),
"{err}"
);
}