diff --git a/bench/deep-taxonomy/run.sh b/bench/deep-taxonomy/run.sh index 0b2eb7c7ed..698d1f4c7f 100755 --- a/bench/deep-taxonomy/run.sh +++ b/bench/deep-taxonomy/run.sh @@ -66,10 +66,10 @@ for DEPTH in $DEPTHS; do # ---- 2. materialize the N3 forward closure; capture engine time + triple-count -------- # `reason n3 n3 `: stderr carries `reasoned [N3]: ground triples in closure in - # s` (engine-internal timer, robust to machine load); stdout carries ` triples after n3 - # reasoning`. We read the triple-count from stdout and the closure seconds from the stderr timer. + # s` (engine-internal timer, robust to machine load) and, because an output file is given, + # ` triples after n3 reasoning` (#6466). We read both from stderr. "$CLI" reason "$CORPUS" n3 n3 "$CLOSURE" >"$TMP/r.out" 2>"$TMP/r.err" - triples="$(grep -oE '[0-9]+ triples after n3 reasoning' "$TMP/r.out" | grep -oE '^[0-9]+' | head -1)" + triples="$(grep -oE '[0-9]+ triples after n3 reasoning' "$TMP/r.err" | grep -oE '^[0-9]+' | head -1)" closure_s="$(grep -oE 'in [0-9.]+s' "$TMP/r.err" | head -1 | grep -oE '[0-9.]+' | head -1)" # ---- 3. run query.rq over the closure (count mode, min-of-ITERS) ---------------------- diff --git a/bench/owl-sameas/run.sh b/bench/owl-sameas/run.sh index 7039914ed5..c92c1947b9 100755 --- a/bench/owl-sameas/run.sh +++ b/bench/owl-sameas/run.sh @@ -72,11 +72,11 @@ for TIER in $TIERS; do CLOSURE="$CACHE/sameas-${TIER}-closure.nt" # ---- 2. materialise the OWL-RL closure; capture engine time + triple-count ------------ - # `reason ntriples owl `: stdout carries ` triples after owl reasoning` (the - # deduplicated closure size); stderr carries the engine-internal `... in s` timer (robust to - # machine load). We read the triple-count from stdout and the closure seconds from the stderr timer. + # `reason ntriples owl `: because an output file is given, stderr carries both + # ` triples after owl reasoning` (the deduplicated closure size, #6466) and the + # engine-internal `... in s` timer (robust to machine load). We read both from stderr. "$CLI" reason "$CORPUS" ntriples owl "$CLOSURE" >"$TMP/r.out" 2>"$TMP/r.err" - triples="$(grep -oE '[0-9]+ triples after owl reasoning' "$TMP/r.out" | grep -oE '^[0-9]+' | head -1)" + triples="$(grep -oE '[0-9]+ triples after owl reasoning' "$TMP/r.err" | grep -oE '^[0-9]+' | head -1)" closure_s="$(grep -oE 'in [0-9.]+s' "$TMP/r.err" | head -1 | grep -oE '[0-9.]+' | head -1)" # ---- 3. run query.rq over the closure (count mode, min-of-ITERS) ---------------------- diff --git a/crates/sparq-cli/src/main.rs b/crates/sparq-cli/src/main.rs index 79c7544e47..ee310ec30b 100644 --- a/crates/sparq-cli/src/main.rs +++ b/crates/sparq-cli/src/main.rs @@ -1231,8 +1231,16 @@ fn cmd_reason(args: &[String]) { return; } let g = load_with_reasoning(path, format, profile); - println!("{} triples after {profile} reasoning", g.len()); - if let Some(out) = args.get(5) { + let out = args.get(5); + // With an output path the closure is the data product (and may itself be stdout, e.g. + // `/dev/stdout`), so the summary is a diagnostic on stderr; without one the count is + // the command's only result and stays on stdout (#6466). + if out.is_some() { + eprintln!("{} triples after {profile} reasoning", g.len()); + } else { + println!("{} triples after {profile} reasoning", g.len()); + } + if let Some(out) = out { use std::io::Write; let mut w = std::io::BufWriter::new(std::fs::File::create(out).unwrap_or_else(|e| { eprintln!("create {out}: {e}"); diff --git a/crates/sparq-cli/tests/cli_contract.rs b/crates/sparq-cli/tests/cli_contract.rs index 9e4fd4af08..0bc8550191 100644 --- a/crates/sparq-cli/tests/cli_contract.rs +++ b/crates/sparq-cli/tests/cli_contract.rs @@ -588,6 +588,22 @@ fn reason_rdfs_reports_triple_count() { let _ = std::fs::remove_dir_all(&dir); } +/// #6466: with an output path, stdout may BE the closure (`/dev/stdout`), so the summary goes +/// to stderr and stdout carries only N-Triples. +#[cfg(unix)] +#[test] +fn reason_to_dev_stdout_writes_only_the_closure_to_stdout() { + let dir = scratch("reason-stdout"); + let data = write(&dir, "data.nt", NT); + let (code, stdout, stderr) = run3(&["reason", s(&data), "ntriples", "rdfs", "/dev/stdout"]); + assert_eq!(code, 0, "stderr: {stderr}"); + assert!(stderr.contains("triples after rdfs reasoning"), "stderr: {stderr}"); + let lines: Vec<&str> = stdout.lines().filter(|l| !l.trim().is_empty()).collect(); + assert!(!lines.is_empty(), "closure missing from stdout"); + assert!(lines.iter().all(|l| l.ends_with(" .")), "non-N-Triples line on stdout: {stdout}"); + let _ = std::fs::remove_dir_all(&dir); +} + #[test] fn bench_emits_one_tsv_line_per_query_including_construct() { let dir = scratch("bench"); diff --git a/crates/sparq-cli/tests/datalog_cli.rs b/crates/sparq-cli/tests/datalog_cli.rs index ef8f959bea..e502a19141 100644 --- a/crates/sparq-cli/tests/datalog_cli.rs +++ b/crates/sparq-cli/tests/datalog_cli.rs @@ -80,11 +80,11 @@ fn reason_datalog_writes_the_stratified_closure() { let out = dir.join("closure.nt"); let profile = format!("datalog:{}", s(&rules)); - let (code, stdout, stderr) = run3(&["reason", s(&data), "ntriples", &profile, s(&out)]); + let (code, _stdout, stderr) = run3(&["reason", s(&data), "ntriples", &profile, s(&out)]); assert_eq!(code, 0, "stderr: {stderr}"); // The stratification report: 3 rules across 3 strata (deg < Hub < Leaf). assert!(stderr.contains("3 rule(s) in 3 stratum/strata"), "stderr: {stderr}"); - assert!(stdout.contains("triples after"), "stdout: {stdout}"); + assert!(stderr.contains("triples after"), "stderr: {stderr}"); let closure = std::fs::read_to_string(&out).expect("read closure"); // Aggregation: `a` has three edges. diff --git a/crates/sparq-cli/tests/el_cli.rs b/crates/sparq-cli/tests/el_cli.rs index 9c2faf2271..8f70be35fe 100644 --- a/crates/sparq-cli/tests/el_cli.rs +++ b/crates/sparq-cli/tests/el_cli.rs @@ -172,9 +172,9 @@ fn reason_subcommand_accepts_el() { let ttl = write(&dir, "ex.ttl", EXISTENTIAL); let out = dir.join("closure.nt"); - let (code, stdout, stderr) = run3(&["reason", s(&ttl), "turtle", "el", s(&out)]); + let (code, _stdout, stderr) = run3(&["reason", s(&ttl), "turtle", "el", s(&out)]); assert_eq!(code, 0, "stderr: {stderr}"); - assert!(stdout.contains("triples after el reasoning"), "stdout: {stdout}"); + assert!(stderr.contains("triples after el reasoning"), "stderr: {stderr}"); assert!(stderr.contains("classified [OWL 2 EL]"), "stderr: {stderr}"); let closure = std::fs::read_to_string(&out).expect("read closure");