mod common;
use std::io::Read;
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/b> <http://ex/p> <http://ex/3> <http://ex/g1> .\n",
"<http://ex/c> <http://ex/p> <http://ex/4> <http://ex/g2> .\n",
);
const NAMED_ONLY: &str = 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",
);
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 run(file: &std::path::Path, args: &[&str]) -> std::process::Output {
common::rete()
.arg("export")
.arg(file)
.args(args)
.output()
.unwrap()
}
fn ok(file: &std::path::Path, args: &[&str]) -> Vec<u8> {
let out = run(file, args);
assert!(
out.status.success(),
"export {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
out.stdout
}
fn gunzip(bytes: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
flate2::read::GzDecoder::new(bytes)
.read_to_end(&mut v)
.unwrap();
v
}
#[test]
fn compressed_output_decodes_to_exactly_the_uncompressed_dump() {
let (_dir, file) = build(QUADS);
for fmt in ["nq", "trig", "jsonld"] {
let plain = ok(&file, &["--format", fmt]);
assert!(!plain.is_empty(), "{fmt} produced nothing");
let z = ok(&file, &["--format", fmt, "--compress", "zstd"]);
assert_eq!(
zstd::decode_all(&z[..]).unwrap(),
plain,
"{fmt}: zstd output must decode to the plain dump"
);
let g = ok(&file, &["--format", fmt, "--compress", "gzip"]);
assert_eq!(
gunzip(&g),
plain,
"{fmt}: gzip output must decode to the plain dump"
);
}
}
#[test]
fn turtle_compresses_too() {
let (_dir, file) = build(QUADS);
let plain = ok(&file, &["--format", "ttl"]);
let z = ok(&file, &["--format", "ttl", "--compress", "zstd"]);
assert_eq!(zstd::decode_all(&z[..]).unwrap(), plain);
}
#[test]
fn the_default_is_still_uncompressed_and_unchanged() {
let (_dir, file) = build(QUADS);
for fmt in ["nq", "ttl", "trig"] {
let implicit = ok(&file, &["--format", fmt]);
let explicit = ok(&file, &["--format", fmt, "--compress", "none"]);
assert_eq!(
implicit, explicit,
"{fmt}: --compress none must be the default"
);
assert!(
implicit.starts_with(b"<") || implicit.starts_with(b"@") || implicit.starts_with(b"G"),
"{fmt}: the default must still be text, got {:?}",
&implicit[..implicit.len().min(8)]
);
}
}
#[test]
fn stdout_carries_the_frame_and_nothing_else() {
let (_dir, file) = build(QUADS);
let z = ok(&file, &["--format", "nq", "--compress", "zstd"]);
assert_eq!(
&z[..4],
&[0x28, 0xB5, 0x2F, 0xFD],
"zstd magic must start the stream"
);
let g = ok(&file, &["--format", "nq", "--compress", "gzip"]);
assert_eq!(&g[..2], &[0x1F, 0x8B], "gzip magic must start the stream");
}
#[test]
fn every_diagnostic_goes_to_stderr_when_the_stream_is_binary() {
let (_dir, file) = build(concat!(
"<http://ex/a[b]> <http://ex/p> <http://ex/o> .\n",
"<http://ex/c> <http://ex/p> <http://ex/o> <http://ex/g1> .\n",
));
let out = run(
&file,
&["--format", "ttl", "--compress", "zstd", "--sanitize-iris"],
);
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("percent-encoded"),
"the report belongs on stderr: {err}"
);
assert!(
err.contains("zstd-compressed"),
"…as does the compression note: {err}"
);
let text = zstd::decode_all(&out.stdout[..]).expect("stdout must be a clean zstd frame");
assert!(String::from_utf8(text).unwrap().contains("%5Bb%5D"));
}
#[test]
fn an_error_before_any_data_leaves_stdout_empty() {
let (_dir, file) = build(NAMED_ONLY);
let out = run(&file, &["--format", "ttl", "--compress", "zstd"]);
assert!(!out.status.success(), "must not exit 0");
assert!(
out.stdout.is_empty(),
"stdout must be empty, got {} bytes",
out.stdout.len()
);
}
#[test]
fn a_bad_level_fails_before_the_export_starts() {
let (_dir, file) = build(QUADS);
for args in [
vec!["--compress", "zstd", "--compress-level", "23"],
vec!["--compress", "gzip", "--compress-level", "10"],
vec!["--compress-level", "3"],
] {
let out = run(&file, &args);
assert!(!out.status.success(), "{args:?} should have failed");
assert!(out.stdout.is_empty(), "{args:?} wrote to stdout");
}
}
#[test]
fn an_unknown_codec_is_rejected_by_the_parser() {
let (_dir, file) = build(QUADS);
let out = run(&file, &["--compress", "brotli"]);
assert!(!out.status.success());
assert!(out.stdout.is_empty());
}
#[test]
fn every_accepted_level_produces_a_decodable_frame() {
let (_dir, file) = build(QUADS);
let plain = ok(&file, &["--format", "nq"]);
for level in ["-7", "-1", "1", "3", "6", "12", "19", "22"] {
let z = ok(
&file,
&[
"--format",
"nq",
"--compress",
"zstd",
"--compress-level",
level,
],
);
assert_eq!(
zstd::decode_all(&z[..]).unwrap(),
plain,
"zstd level {level}"
);
}
for level in ["1", "6", "9"] {
let g = ok(
&file,
&[
"--format",
"nq",
"--compress",
"gzip",
"--compress-level",
level,
],
);
assert_eq!(gunzip(&g), plain, "gzip level {level}");
}
}
#[test]
fn a_higher_level_is_not_larger() {
let (_dir, file) = build(&QUADS.repeat(400));
let low = ok(
&file,
&[
"--format",
"nq",
"--compress",
"zstd",
"--compress-level",
"1",
],
);
let high = ok(
&file,
&[
"--format",
"nq",
"--compress",
"zstd",
"--compress-level",
"19",
],
);
assert!(
high.len() < low.len(),
"level 19 ({}) should beat level 1 ({})",
high.len(),
low.len()
);
}
#[test]
fn compression_does_not_break_budget_invariance() {
let (_dir, file) = build(QUADS);
for fmt in ["nq", "ttl", "trig"] {
let base = ok(
&file,
&[
"--format",
fmt,
"--compress",
"zstd",
"--memory-budget-mb",
"4096",
],
);
for mb in ["1", "16", "0"] {
assert_eq!(
base,
ok(
&file,
&[
"--format",
fmt,
"--compress",
"zstd",
"--memory-budget-mb",
mb
]
),
"{fmt} differs at --memory-budget-mb {mb}"
);
}
}
}