mod common;
use std::io::Write as _;
use common::{fixture, rete};
use predicates::prelude::PredicateBooleanExt as _;
const TTL: &str = concat!(
"@prefix ex: <http://example.test/> .\n",
"ex:alice ex:knows ex:bob ;\n",
" ex:name \"Alice\"@en .\n",
"ex:bob ex:name \"Bob\"@en .\n",
);
const TRIG: &str = concat!(
"@prefix ex: <http://example.test/> .\n",
"ex:people {\n",
" ex:alice ex:knows ex:bob ;\n",
" ex:name \"Alice\"@en .\n",
" ex:bob ex:name \"Bob\"@en .\n",
"}\n",
);
fn gzip(bytes: &[u8]) -> Vec<u8> {
let mut enc = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
enc.write_all(bytes).unwrap();
enc.finish().unwrap()
}
#[test]
fn gzipped_turtle_builds_identically() {
let f = fixture();
let plain = f.write("g.ttl", TTL);
let gz = f.write("g.ttl.gz", gzip(TTL.as_bytes()));
let out_plain = f.path("plain.rete");
let out_gz = f.path("gz.rete");
for (src, out) in [(&plain, &out_plain), (&gz, &out_gz)] {
rete()
.args(["build"])
.arg(src)
.arg("-o")
.arg(out)
.assert()
.success();
}
assert_eq!(
std::fs::read(&out_plain).unwrap(),
std::fs::read(&out_gz).unwrap(),
"gzip must not change the built file"
);
}
#[test]
fn concatenated_gzip_members_all_read() {
let f = fixture();
let first = "<http://example.test/a> <http://example.test/p> \"1\" .\n";
let second = "<http://example.test/b> <http://example.test/p> \"2\" .\n";
let mut both = gzip(first.as_bytes());
both.extend_from_slice(&gzip(second.as_bytes()));
let src = f.write("both.nt.gz", both);
let out = f.path("both.rete");
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success()
.stdout(predicates::str::contains("2 triples"));
}
#[test]
fn external_build_from_gzipped_turtle_matches_in_ram() {
let f = fixture();
let gz = f.write("g.ttl.gz", gzip(TTL.as_bytes()));
let plain = f.write("g.ttl", TTL);
let out_ext = f.path("ext.rete");
let out_ram = f.path("ram.rete");
rete()
.args(["build"])
.arg(&gz)
.args(["--memory-budget-mb", "64", "-o"])
.arg(&out_ext)
.assert()
.success();
rete()
.args(["build"])
.arg(&plain)
.args(["--no-pyramid", "-o"])
.arg(&out_ram)
.assert()
.success();
assert_eq!(
std::fs::read(&out_ext).unwrap(),
std::fs::read(&out_ram).unwrap(),
"external build of .ttl.gz must equal the in-RAM --no-pyramid build"
);
}
#[test]
fn trig_keeps_its_named_graph() {
let f = fixture();
let src = f.write("g.trig", TRIG);
let out = f.path("g.rete");
rete()
.args(["build"])
.arg(&src)
.arg("-o")
.arg(&out)
.assert()
.success()
.stdout(predicates::str::contains("named graph"));
rete()
.args(["graphs"])
.arg(&out)
.assert()
.success()
.stdout(predicates::str::contains("http://example.test/people"));
}
#[test]
fn collapse_graphs_matches_the_default_graph_build() {
let f = fixture();
let trig = f.write("g.trig", TRIG);
let ttl = f.write("g.ttl", TTL);
let out_collapsed = f.path("collapsed.rete");
let out_ttl = f.path("ttl.rete");
rete()
.args(["build"])
.arg(&trig)
.args(["--collapse-graphs", "-o"])
.arg(&out_collapsed)
.assert()
.success();
rete()
.args(["build"])
.arg(&ttl)
.arg("-o")
.arg(&out_ttl)
.assert()
.success();
assert_eq!(
std::fs::read(&out_collapsed).unwrap(),
std::fs::read(&out_ttl).unwrap(),
"a collapsed TriG must build to the same file as the equivalent Turtle"
);
}
#[test]
fn external_build_keeps_named_graphs_byte_identically() {
let f = fixture();
let src = f.write("g.trig", TRIG);
let out_ext = f.path("ext.rete");
let out_ram = f.path("ram.rete");
rete()
.args(["build"])
.arg(&src)
.args(["--memory-budget-mb", "64", "-o"])
.arg(&out_ext)
.assert()
.success()
.stdout(predicates::str::contains("named graph"));
rete()
.args(["build"])
.arg(&src)
.args(["--no-pyramid", "-o"])
.arg(&out_ram)
.assert()
.success();
assert_eq!(
std::fs::read(&out_ext).unwrap(),
std::fs::read(&out_ram).unwrap(),
"external build of a TriG must equal the in-RAM --no-pyramid build"
);
rete()
.args(["graphs"])
.arg(&out_ext)
.assert()
.success()
.stdout(predicates::str::contains("http://example.test/people"));
let out_flat = f.path("flat.rete");
rete()
.args(["build"])
.arg(&src)
.args(["--collapse-graphs", "--memory-budget-mb", "64", "-o"])
.arg(&out_flat)
.assert()
.success();
}
#[test]
fn merge_folds_named_graph_shards() {
let f = fixture();
let a = f.write(
"a.nq",
concat!(
"<http://ex/s1> <http://ex/p> <http://ex/o1> <http://ex/gA> .\n",
"<http://ex/d> <http://ex/p> <http://ex/o> .\n",
),
);
let b = f.write(
"b.nq",
concat!(
"<http://ex/s2> <http://ex/p> <http://ex/o2> <http://ex/gB> .\n",
"<http://ex/s1> <http://ex/p> <http://ex/o1> <http://ex/gA> .\n",
),
);
let (ra, rb, out) = (f.path("a.rete"), f.path("b.rete"), f.path("all.rete"));
for (src, dst) in [(&a, &ra), (&b, &rb)] {
rete()
.args(["build"])
.arg(src)
.arg("-o")
.arg(dst)
.assert()
.success();
}
rete()
.args(["merge"])
.arg(&ra)
.arg(&rb)
.arg("-o")
.arg(&out)
.assert()
.success();
let graphs = rete().args(["graphs"]).arg(&out).assert().success();
let listed = String::from_utf8(graphs.get_output().stdout.clone()).unwrap();
assert!(listed.contains("http://ex/gA"), "gA missing: {listed}");
assert!(listed.contains("http://ex/gB"), "gB missing: {listed}");
rete()
.args(["info"])
.arg(&out)
.assert()
.success()
.stdout(predicates::str::contains("quad_count: 3"));
}
#[test]
fn merge_reads_its_inputs_inside_the_budget() {
let f = fixture();
let (mut a, mut b) = (String::new(), String::new());
for i in 0..300 {
let shard = if i % 2 == 0 { &mut a } else { &mut b };
shard.push_str(&format!(
"<http://ex/s{i}> <http://ex/p{}> \"value {i}\" <http://ex/g{}> .\n\
<http://ex/s{i}> <http://ex/next> <http://ex/s{}> .\n",
i % 5,
i % 7,
(i + 1) % 300
));
}
let union = f.write("all.nq", format!("{a}{b}"));
let (a, b) = (f.write("a.nq", &a), f.write("b.nq", &b));
let (ra, rb) = (f.path("a.rete"), f.path("b.rete"));
for (src, dst) in [(&a, &ra), (&b, &rb)] {
rete()
.args(["build"])
.arg(src)
.arg("-o")
.arg(dst)
.assert()
.success();
}
let merge = |budget: &str, out: &std::path::Path| {
let run = rete()
.args(["merge"])
.arg(&ra)
.arg(&rb)
.arg("-o")
.arg(out)
.args(["--memory-budget-mb", budget])
.env("RETE_OPEN_DEBUG", "1")
.assert()
.success();
String::from_utf8(run.get_output().stderr.clone()).unwrap()
};
let (m512, m64) = (f.path("m512.rete"), f.path("m64.rete"));
let log = merge("512", &m512);
assert!(!log.contains("EAGER"), "{log}");
assert!(
log.matches("[open_local_ranged]").count() >= 2,
"an input was not opened by range: {log}"
);
assert!(log.contains("[memory-budget] total=128MiB"), "{log}");
assert!(
log.contains("extbuild: budget 512 MiB, 128 MiB of it for reading the input"),
"{log}"
);
merge("64", &m64);
assert_eq!(
std::fs::read(&m512).unwrap(),
std::fs::read(&m64).unwrap(),
"the budget changed the merged file"
);
let built = f.path("built.rete");
rete()
.args(["build"])
.arg(&union)
.args(["--memory-budget-mb", "64", "-o"])
.arg(&built)
.assert()
.success();
assert_eq!(
std::fs::read(&m512).unwrap(),
std::fs::read(&built).unwrap(),
"the merge is not the build of its inputs' union"
);
}
#[test]
fn external_build_writes_the_in_ram_text_index() {
let f = fixture();
let src = f.write(
"g.nq",
"<http://ex/s> <http://ex/p> \"hello world\" .\n\
<http://ex/s> <http://ex/desc> \"has been modified\" .\n\
<http://ex/t> <http://ex/p> \"hello études\"@fr .\n\
<http://ex/s/prov> <http://ex/desc> \"has been created\" <http://ex/s/g> .\n",
);
let scoped = [
"--text-index-graphs",
"all",
"--text-index-exclude",
"http://ex/desc",
];
for extra in [&[][..], &scoped[..]] {
let ram = f.path("ram.rete");
let ext = f.path("ext.rete");
rete()
.args(["build"])
.arg(&src)
.args(["--text-index", "--no-pyramid"])
.args(extra)
.arg("-o")
.arg(&ram)
.assert()
.success();
rete()
.args(["build"])
.arg(&src)
.args(["--text-index", "--memory-budget-mb", "64"])
.args(extra)
.arg("-o")
.arg(&ext)
.assert()
.success()
.stderr(predicates::str::contains("extbuild: text index"));
assert_eq!(
std::fs::read(&ext).unwrap(),
std::fs::read(&ram).unwrap(),
"{extra:?}: memory-bounded and in-RAM builds differ"
);
rete()
.args(["search"])
.arg(&ext)
.args(["--contains", "hello"])
.assert()
.success()
.stdout(predicates::str::contains("<http://ex/s>"))
.stdout(predicates::str::contains("<http://ex/t>"));
}
rete()
.args(["search"])
.arg(f.path("ext.rete"))
.args(["--contains", "modified"])
.assert()
.success()
.stdout(predicates::str::contains("<http://ex/s>").not());
}
#[test]
fn text_index_scope_flags_are_validated() {
let f = fixture();
let src = f.write("g.nt", "<http://ex/s> <http://ex/p> \"hello world\" .\n");
let out = f.path("out.rete");
for (args, why) in [
(
&["--text-index-exclude", "http://ex/p"][..],
"required arguments were not provided",
),
(
&["--text-index-graphs", "all"][..],
"required arguments were not provided",
),
(
&["--text-index", "--text-index-exclude", "description"][..],
"absolute",
),
(
&[
"--text-index",
"--text-index-include",
"http://ex/a",
"--text-index-exclude",
"http://ex/b",
][..],
"cannot be used with",
),
] {
rete()
.args(["build"])
.arg(&src)
.args(args)
.arg("-o")
.arg(&out)
.assert()
.failure()
.stderr(predicates::str::contains(why));
}
}
#[test]
fn external_build_rejects_rdfxml_with_a_clear_error() {
let f = fixture();
let src = f.write("o.rdf", "<rdf:RDF/>");
let out = f.path("ext.rete");
rete()
.args(["build"])
.arg(&src)
.args(["--memory-budget-mb", "64", "-o"])
.arg(&out)
.assert()
.failure()
.stderr(predicates::str::contains("rdfxml"));
}
#[test]
fn turtle_with_anonymous_blank_nodes_builds_on_both_paths() {
let f = fixture();
let ttl = concat!(
"@prefix ex: <http://ex/> .\n",
"ex:a ex:p [ ex:q \"1\" ] .\n",
"ex:b ex:list ( \"x\" \"y\" ) .\n",
);
let src = f.write("bn.ttl", ttl);
let out_plain = f.path("bn.rete");
let out_ext = f.path("bn-ext.rete");
rete()
.args(["build"])
.arg(&src)
.args(["--no-pyramid", "-o"])
.arg(&out_plain)
.assert()
.success()
.stdout(predicates::str::contains("7 triples"));
rete()
.args(["build"])
.arg(&src)
.args(["--memory-budget-mb", "64", "-o"])
.arg(&out_ext)
.assert()
.success()
.stdout(predicates::str::contains("7 triples"));
}
#[test]
fn estimate_reads_gzipped_turtle() {
let f = fixture();
let gz = f.write("g.ttl.gz", gzip(TTL.as_bytes()));
rete()
.args(["estimate"])
.arg(&gz)
.assert()
.success()
.stdout(predicates::str::contains("statements"));
}
#[test]
fn external_build_with_label_index_matches_in_ram() {
let f = fixture();
let nt = "<http://ex/w1> <http://purl.org/dc/terms/title> \"Acta Universitatis\" .\n\
<http://ex/w1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Journal> .\n\
<http://ex/w1> <http://ex/editor> <http://ex/p1> .\n\
<http://ex/p1> <http://www.w3.org/1999/02/22-rdf-syntax-ns#type> <http://ex/Person> .\n\
<http://ex/p1> <http://xmlns.com/foaf/0.1/familyName> \"Svobodová\" .\n\
<http://ex/p1> <http://xmlns.com/foaf/0.1/givenName> \"Jana\" .\n\
<http://ex/Journal> <http://www.w3.org/2000/01/rdf-schema#subClassOf> <http://ex/Venue> .\n";
let src = f.write("l.nt", nt);
let (out_ext, out_ram) = (f.path("ext.rete"), f.path("ram.rete"));
rete()
.args(["build"])
.arg(&src)
.args(["--memory-budget-mb", "64", "--label-index", "-o"])
.arg(&out_ext)
.assert()
.success();
rete()
.args(["build"])
.arg(&src)
.args(["--no-pyramid", "--label-index", "-o"])
.arg(&out_ram)
.assert()
.success();
let ext = std::fs::read(&out_ext).unwrap();
assert_eq!(ext, std::fs::read(&out_ram).unwrap());
let h = rete_core::Header::from_bytes(&ext).unwrap();
assert!(h.label_index_section().is_some() && h.schema_section().is_some());
rete()
.args(["search"])
.arg(&out_ext)
.arg("svob")
.assert()
.success()
.stdout(predicates::str::contains("Svobodová, Jana"));
}