mod common;
const QUADS: &str = concat!(
"<http://ex/a> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/T> .\n",
"<http://ex/a> <http://ex/p> <http://ex/1> .\n",
"<http://ex/a> <http://ex/p> <http://ex/2> .\n",
"<http://ex/a> <http://ex/q> \"he said \\\"no\\\"\" .\n",
"<http://ex/a> <http://ex/n> \"30\"^^<http://www.w3.org/2001/XMLSchema#integer> .\n",
"<http://ex/a> <http://ex/l> \"h\u{e9}llo\"@en .\n",
"<http://ex/a> <http://ex/b> _:b0 .\n",
"<http://ex/needs%20escaping/x> <http://ex/p> <http://ex/has(paren)> .\n",
"<http://ex/c> <http://ex/p> <http://ex/3> <http://ex/g1> .\n",
"<http://ex/c> <http://ex/p> <http://ex/4> <http://ex/g1> .\n",
"<http://ex/d> <http://ex/p> <http://ex/5> <http://ex/g2> .\n",
);
const NAMED_ONLY: &str = concat!(
"<http://ex/s> <http://ex/p> <http://ex/o> <http://ex/only> .\n",
"<http://ex/s> <http://ex/q> \"x\" <http://ex/only> .\n",
);
fn build(nq: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().unwrap();
let src = dir.path().join("in.nq");
let out = dir.path().join("out.rete");
std::fs::write(&src, nq).unwrap();
common::build(&src, &out, &["--no-pyramid"]);
(dir, out)
}
fn export(file: &std::path::Path, args: &[&str]) -> String {
let out = common::rete()
.arg("export")
.arg(file)
.args(args)
.output()
.unwrap();
assert!(
out.status.success(),
"export {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
String::from_utf8(out.stdout).unwrap()
}
fn quads(file: &std::path::Path) -> Vec<String> {
let mut v: Vec<String> = export(file, &["--format", "nq"])
.lines()
.filter(|l| !l.trim().is_empty())
.map(str::to_string)
.collect();
v.sort();
v
}
fn rebuild(dir: &tempfile::TempDir, text: &str, fmt: &str) -> std::path::PathBuf {
let src = dir.path().join(format!("round.{fmt}"));
let out = dir.path().join(format!("round-{fmt}.rete"));
std::fs::write(&src, text).unwrap();
common::build(&src, &out, &["--no-pyramid", "--format", fmt]);
out
}
#[test]
fn turtle_never_emits_a_trig_graph_block() {
let (_dir, file) = build(NAMED_ONLY);
let ttl = export(&file, &["--format", "ttl"]);
assert!(
!ttl.contains("GRAPH"),
"Turtle must not wrap a graph:\n{ttl}"
);
assert!(!ttl.contains('{'), "nor open a block:\n{ttl}");
assert!(
ttl.contains("<http://ex/s>"),
"…but must write the data:\n{ttl}"
);
let trig = export(&file, &["--format", "trig"]);
assert!(
trig.contains("GRAPH <http://ex/only> {"),
"TriG must wrap it:\n{trig}"
);
}
#[test]
fn a_single_graph_format_says_which_graph_it_chose() {
let (_dir, file) = build(QUADS);
let out = common::rete()
.arg("export")
.arg(&file)
.args(["--format", "ttl"])
.output()
.unwrap();
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("default graph"), "stderr: {err}");
assert!(
err.contains("NOT included") && err.contains("--format trig"),
"it must say the named graphs were dropped, and how to keep them: {err}"
);
}
#[test]
fn an_empty_default_graph_with_one_named_graph_exports_that_one() {
let (_dir, file) = build(NAMED_ONLY);
let out = common::rete()
.arg("export")
.arg(&file)
.args(["--format", "ttl"])
.output()
.unwrap();
assert!(out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("<http://ex/only>"), "stderr: {err}");
assert!(
String::from_utf8_lossy(&out.stdout).contains("<http://ex/o>"),
"the named graph's data must be written"
);
}
#[test]
fn an_empty_default_graph_with_several_named_graphs_is_an_error_that_lists_them() {
let (_dir, file) = build(concat!(
"<http://ex/s> <http://ex/p> <http://ex/o> <http://ex/g1> .\n",
"<http://ex/s> <http://ex/p> <http://ex/o> <http://ex/g2> .\n",
));
let out = common::rete()
.arg("export")
.arg(&file)
.args(["--format", "ttl"])
.output()
.unwrap();
assert!(!out.status.success(), "must not exit 0");
let err = String::from_utf8_lossy(&out.stderr);
for want in [
"<http://ex/g1>",
"<http://ex/g2>",
"--graph",
"--format trig",
] {
assert!(err.contains(want), "error must mention {want}: {err}");
}
assert!(
String::from_utf8_lossy(&out.stdout).is_empty(),
"and must write nothing to stdout"
);
}
#[test]
fn an_unknown_graph_names_the_graphs_that_exist() {
let (_dir, file) = build(QUADS);
let out = common::rete()
.arg("export")
.arg(&file)
.args(["--format", "ttl", "--graph", "http://ex/nope"])
.output()
.unwrap();
assert!(!out.status.success());
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains("<http://ex/g1>"), "{err}");
}
#[test]
fn trig_round_trips_every_graph() {
let (dir, file) = build(QUADS);
let trig = export(&file, &["--format", "trig"]);
let back = rebuild(&dir, &trig, "trig");
assert_eq!(quads(&file), quads(&back), "TriG must be lossless:\n{trig}");
}
#[test]
fn turtle_round_trips_the_graph_it_wrote() {
let (dir, file) = build(QUADS);
let ttl = export(&file, &["--format", "ttl"]);
let back = rebuild(&dir, &ttl, "ttl");
let want = {
let mut v: Vec<String> = export(&file, &["--format", "nq", "--graph", ""])
.lines()
.filter(|l| !l.trim().is_empty())
.map(str::to_string)
.collect();
v.sort();
v
};
assert_eq!(want, quads(&back), "Turtle must round-trip:\n{ttl}");
}
#[test]
fn prefix_compression_does_not_change_the_graph() {
let (dir, file) = build(QUADS);
let with = rebuild(&dir, &export(&file, &["--format", "trig"]), "trig");
let plain = {
let text = export(&file, &["--format", "trig", "--no-prefixes"]);
let src = dir.path().join("plain.trig");
let out = dir.path().join("plain.rete");
std::fs::write(&src, text).unwrap();
common::build(&src, &out, &["--no-pyramid", "--format", "trig"]);
out
};
assert_eq!(quads(&with), quads(&plain));
assert_eq!(quads(&file), quads(&with));
}
#[test]
fn an_iri_whose_local_part_cannot_be_a_qname_is_written_in_full() {
let (_dir, file) = build(QUADS);
let trig = export(&file, &["--format", "trig"]);
assert!(
trig.contains("<http://ex/has(paren)>"),
"must stay a full IRI:\n{trig}"
);
assert!(!trig.contains(":has(paren)"), "never as a QName:\n{trig}");
}
#[test]
fn a_literal_with_an_escaped_quote_survives_verbatim() {
let (_dir, file) = build(QUADS);
for fmt in ["ttl", "trig"] {
let out = export(&file, &["--format", fmt]);
assert!(
out.contains(r#""he said \"no\"""#),
"{fmt} mangled the literal:\n{out}"
);
}
}
#[test]
fn the_dump_is_identical_at_every_memory_budget() {
let (_dir, file) = build(QUADS);
for fmt in ["ttl", "trig"] {
let base = export(&file, &["--format", fmt, "--memory-budget-mb", "4096"]);
for mb in ["1", "16", "0"] {
assert_eq!(
base,
export(&file, &["--format", fmt, "--memory-budget-mb", mb]),
"{fmt} differs at --memory-budget-mb {mb}"
);
}
assert_eq!(
base,
export(&file, &["--format", fmt, "--in-memory"]),
"{fmt} differs under --in-memory"
);
}
}
#[test]
fn a_bound_predicate_still_produces_parseable_output() {
let (dir, file) = build(QUADS);
let trig = export(&file, &["--format", "trig", "--predicate", "http://ex/p"]);
let back = rebuild(&dir, &trig, "trig");
let want = {
let mut v: Vec<String> = export(&file, &["--format", "nq", "--predicate", "http://ex/p"])
.lines()
.filter(|l| !l.trim().is_empty())
.map(str::to_string)
.collect();
v.sort();
v
};
assert_eq!(want, quads(&back), "filtered TriG must round-trip:\n{trig}");
}
#[test]
fn only_used_prefixes_are_declared() {
let (_dir, file) = build(QUADS);
let trig = export(&file, &["--format", "trig"]);
for line in trig.lines().filter(|l| l.starts_with("@prefix ")) {
let name = line
.trim_start_matches("@prefix ")
.split(':')
.next()
.unwrap();
let uses = trig.matches(&format!("{name}:")).count();
assert!(
uses > 1,
"prefix {name} is declared but never used:\n{trig}"
);
}
}