Skip to content

Commit 826d3ec

Browse files
authored
cargo pgrx regress output is no longer fully buffered (#2095)
Prior to this PR, `cargo pgrx regress` would buffer the entire `pg_regress` output, decorate each line, and then output the results to stdout. Now it does it line-by-line. It has annoyed me that I couldn't watch the test suite progress as it runs. Now I can and you can too!
1 parent d68c9cc commit 826d3ec

File tree

1 file changed

+97
-86
lines changed

1 file changed

+97
-86
lines changed

cargo-pgrx/src/command/regress.rs

Lines changed: 97 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use pgrx_pg_config::{createdb, dropdb, PgConfig};
1717
use std::collections::HashSet;
1818
use std::env::temp_dir;
1919
use std::fs::{DirEntry, File};
20-
use std::io::{IsTerminal, Write};
20+
use std::io::{BufRead, BufReader, IsTerminal, Write};
2121
use std::path::{Path, PathBuf};
2222
use std::process::{Command, ExitStatus, Stdio};
2323

@@ -424,11 +424,8 @@ fn run_tests(
424424
.parent()
425425
.expect("test file should be in a directory named `sql/`")
426426
.to_path_buf();
427-
let (status, output) = pg_regress(pg_config, pg_regress_bin, dbname, &input_dir, test_files)?;
428-
429-
println!("{output}");
430-
431-
Ok(status.success())
427+
pg_regress(pg_config, pg_regress_bin, dbname, &input_dir, test_files)
428+
.map(|status| status.success())
432429
}
433430

434431
fn create_regress_output(
@@ -446,7 +443,7 @@ fn create_regress_output(
446443
.parent()
447444
.expect("test file should be in a directory named `sql/`")
448445
.to_path_buf();
449-
let (status, output) = pg_regress(pg_config, pg_regress_bin, dbname, &input_dir, &[test_file])?;
446+
let status = pg_regress(pg_config, pg_regress_bin, dbname, &input_dir, &[test_file])?;
450447

451448
if !status.success() {
452449
// pg_regress returned with an error code, but that is most likely because the test's output file
@@ -457,7 +454,6 @@ fn create_regress_output(
457454
if out_file.exists() {
458455
return Ok(Some(out_file));
459456
} else {
460-
println!("{output}");
461457
std::process::exit(status.code().unwrap_or(1));
462458
}
463459
}
@@ -471,7 +467,7 @@ fn pg_regress(
471467
dbname: &str,
472468
input_dir: impl AsRef<Path>,
473469
tests: &[&DirEntry],
474-
) -> eyre::Result<(ExitStatus, String)> {
470+
) -> eyre::Result<ExitStatus> {
475471
if tests.is_empty() {
476472
eyre::bail!("no tests to run");
477473
}
@@ -520,101 +516,116 @@ fn pg_regress(
520516

521517
tracing::trace!("running {command:?}");
522518

523-
let output = command.output()?;
524-
let stdout = decorate_output(&String::from_utf8_lossy(&output.stdout));
525-
let stderr = decorate_output(&String::from_utf8_lossy(&output.stderr));
519+
let mut child = command.spawn()?;
520+
let (Some(stdout), Some(stderr)) = (child.stdout.take(), child.stderr.take()) else {
521+
panic!("unable to take stdout or stderr from pg_regress process");
522+
};
526523

527-
let cmd_output = if !stdout.is_empty() && !stderr.is_empty() {
528-
format!("{stdout}\n{stderr}")
529-
} else if !stdout.is_empty() {
530-
stdout.to_string()
531-
} else {
532-
stderr.to_string()
533-
}
534-
.trim()
535-
.to_string();
524+
let output_monitor = std::thread::spawn(move || {
525+
let mut passed_cnt = 0;
526+
let mut failed_cnt = 0;
527+
let stdout = BufReader::new(stdout);
528+
let stderr = BufReader::new(stderr);
529+
for line in stdout.lines().chain(stderr.lines()) {
530+
let line = line.unwrap();
531+
let Some((line, result)) = decorate_output(line) else {
532+
continue;
533+
};
534+
535+
match result {
536+
Some(TestResult::Passed) => passed_cnt += 1,
537+
Some(TestResult::Failed) => failed_cnt += 1,
538+
None => (),
539+
}
540+
541+
println!("{line}");
542+
}
543+
(passed_cnt, failed_cnt)
544+
});
545+
let status = child.wait()?;
546+
let (passed_cnt, failed_cnt) =
547+
output_monitor.join().map_err(|_| eyre::eyre!("failed to join output monitor thread"))?;
548+
println!("passed={passed_cnt} failed={failed_cnt}");
536549

537550
#[cfg(not(target_os = "windows"))]
538551
{
539552
std::fs::remove_file(launcher_script)?;
540553
}
541554

542-
Ok((output.status, cmd_output))
555+
Ok(status)
556+
}
557+
558+
enum TestResult {
559+
Passed,
560+
Failed,
543561
}
544562

545-
fn decorate_output(input: &str) -> String {
546-
let mut decorated = String::with_capacity(input.len());
547-
let (mut total_passed, mut total_failed) = (0, 0);
548-
for line in input.lines() {
549-
let mut line = line.to_string();
550-
let mut is_old_line = false;
551-
let mut is_new_line = false;
552-
553-
if line.starts_with("ok") {
554-
// for pg_regress from pg16 forward, rewrite the "ok" into a colored PASS"
555-
is_new_line = true;
556-
} else if line.starts_with("not ok") {
557-
// for pg_regress from pg16 forward, rewrite the "no ok" into a colored FAIL"
558-
line = line.replace("not ok", "not_ok"); // to make parsing easier down below
559-
is_new_line = true;
560-
} else if line.contains("... ok") || line.contains("... FAILED") {
561-
is_old_line = true;
563+
fn decorate_output(mut line: String) -> Option<(String, Option<TestResult>)> {
564+
let mut decorated = String::with_capacity(line.len());
565+
let mut test_result: Option<TestResult> = None;
566+
let mut is_old_line = false;
567+
let mut is_new_line = false;
568+
569+
if line.starts_with("ok") {
570+
// for pg_regress from pg16 forward, rewrite the "ok" into a colored PASS"
571+
is_new_line = true;
572+
} else if line.starts_with("not ok") {
573+
// for pg_regress from pg16 forward, rewrite the "no ok" into a colored FAIL"
574+
line = line.replace("not ok", "not_ok"); // to make parsing easier down below
575+
is_new_line = true;
576+
} else if line.contains("... ok") || line.contains("... FAILED") {
577+
is_old_line = true;
578+
}
579+
580+
let parsed_test_line = if is_new_line {
581+
fn split_line(line: &str) -> Option<(&str, bool, &str, &str)> {
582+
let mut parts = line.split_whitespace();
583+
584+
let passed = parts.next()? == "ok";
585+
parts.next()?; // throw away the test number
586+
parts.next()?; // throw away the dash (-)
587+
let test_name = parts.next()?;
588+
let execution_time = parts.next()?;
589+
let execution_units = parts.next()?;
590+
Some((test_name, passed, execution_time, execution_units))
562591
}
592+
split_line(&line)
593+
} else if is_old_line {
594+
fn split_line(line: &str) -> Option<(&str, bool, &str, &str)> {
595+
let mut parts = line.split_whitespace();
596+
597+
parts.next()?; // throw away "test"
598+
let test_name = parts.next()?;
599+
parts.next()?; // throw away "..."
600+
let passed = parts.next()? == "ok";
601+
let execution_time = parts.next()?;
602+
let execution_units = parts.next()?;
603+
Some((test_name, passed, execution_time, execution_units))
604+
}
605+
split_line(&line)
606+
} else {
607+
// not a line we care about
608+
return None;
609+
};
563610

564-
let parsed_test_line = if is_new_line {
565-
fn split_line(line: &str) -> Option<(&str, bool, &str, &str)> {
566-
let mut parts = line.split_whitespace();
567-
568-
let passed = parts.next()? == "ok";
569-
parts.next()?; // throw away the test number
570-
parts.next()?; // throw away the dash (-)
571-
let test_name = parts.next()?;
572-
let execution_time = parts.next()?;
573-
let execution_units = parts.next()?;
574-
Some((test_name, passed, execution_time, execution_units))
575-
}
576-
split_line(&line)
577-
} else if is_old_line {
578-
fn split_line(line: &str) -> Option<(&str, bool, &str, &str)> {
579-
let mut parts = line.split_whitespace();
580-
581-
parts.next()?; // throw away "test"
582-
let test_name = parts.next()?;
583-
parts.next()?; // throw away "..."
584-
let passed = parts.next()? == "ok";
585-
let execution_time = parts.next()?;
586-
let execution_units = parts.next()?;
587-
Some((test_name, passed, execution_time, execution_units))
588-
}
589-
split_line(&line)
611+
if let Some((test_name, passed, execution_time, execution_units)) = parsed_test_line {
612+
if passed {
613+
test_result = Some(TestResult::Passed);
590614
} else {
591-
// not a line we care about
592-
continue;
593-
};
615+
test_result = Some(TestResult::Failed);
616+
}
594617

595-
if let Some((test_name, passed, execution_time, execution_units)) = parsed_test_line {
618+
decorated.push_str(&format!(
619+
"{} {test_name} {execution_time}{execution_units}",
596620
if passed {
597-
total_passed += 1
621+
"PASS".bold().bright_green().to_string()
598622
} else {
599-
total_failed += 1
623+
"FAIL".bold().bright_red().to_string()
600624
}
601-
602-
decorated.push_str(&format!(
603-
"{} {test_name} {execution_time}{execution_units}\n",
604-
if passed {
605-
"PASS".bold().bright_green().to_string()
606-
} else {
607-
"FAIL".bold().bright_red().to_string()
608-
}
609-
))
610-
}
611-
}
612-
613-
if total_passed + total_failed > 0 {
614-
decorated.push_str(&format!("passed={total_passed}, failed={total_failed}\n"))
625+
))
615626
}
616627

617-
decorated
628+
Some((decorated, test_result))
618629
}
619630

620631
fn make_test_name(entry: &DirEntry) -> String {

0 commit comments

Comments
 (0)