#[cfg(unix)]
mod unix {
use std::io::{self, BufRead, BufReader, Write};
use std::path::PathBuf;
use std::process::{Child, Command, ExitCode, Stdio};
use ctl_term::{ColorMode, Console, DetectOptions, Error, Stream};
use ctl_text::{Context, Line, Measurement, Renderable, Span, Style};
const WRITER_SWITCH: &str = "CTL_TERM_TEST_WRITER";
const COLOUR_VARIABLES: [&str; 5] = [
"CLICOLOR",
"CLICOLOR_FORCE",
"FORCE_COLOR",
"NO_COLOR",
"COLORTERM",
];
const READ_BUFFER: usize = 4096;
const ROWS: usize = 10_000;
#[derive(Debug)]
struct Rows(Vec<Line>);
impl Renderable for Rows {
fn measure(&self, _: &Context) -> Measurement {
Measurement::exact(self.0.iter().map(Line::width).max().unwrap_or(0))
}
fn render(&self, _: &Context) -> Vec<Line> {
self.0.clone()
}
}
pub(super) fn run() -> ExitCode {
if std::env::var_os(WRITER_SWITCH).is_some() {
return write_rows();
}
match start_both() {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
writeln!(io::stderr(), "closed_pipe: {message}").ok();
ExitCode::FAILURE
}
}
}
fn write_rows() -> ExitCode {
let rows = Rows(
(0..ROWS)
.map(|row| {
let mut line = Line::new();
line.push(Span::new(
format!("row {row:05} of a long list"),
Style::new(),
));
line
})
.collect(),
);
let console = Console::from_process(Stream::Stdout, ColorMode::Auto, DetectOptions::new());
match console.print(&rows) {
Ok(()) => ExitCode::from(3),
Err(Error::BrokenPipe(_)) => ExitCode::SUCCESS,
Err(_) => ExitCode::from(4),
}
}
fn spawn_writer(path: PathBuf, stdout: Stdio) -> io::Result<Child> {
let mut writer = Command::new(path);
writer
.env(WRITER_SWITCH, "1")
.stdout(stdout)
.stderr(Stdio::piped());
for name in COLOUR_VARIABLES {
writer.env_remove(name);
}
writer.spawn()
}
fn start_both() -> Result<(), String> {
let (reader, pipe) =
io::pipe().map_err(|error| format!("could not open a pipe: {error}"))?;
let path = std::env::current_exe()
.map_err(|error| format!("could not find the test binary: {error}"))?;
let child = spawn_writer(path, Stdio::from(pipe))
.map_err(|error| format!("could not start the writer: {error}"))?;
let mut reader = BufReader::with_capacity(READ_BUFFER, reader);
let mut first = String::new();
reader
.read_line(&mut first)
.map_err(|error| format!("could not read the first line: {error}"))?;
drop(reader);
let output = child
.wait_with_output()
.map_err(|error| format!("could not wait for the writer: {error}"))?;
let stderr = String::from_utf8_lossy(&output.stderr);
if output.status.code() != Some(0) {
return Err(format!(
"the writer did not see a closed pipe: {:?}; stderr: {stderr}",
output.status
));
}
if !stderr.is_empty() {
return Err(format!("the writer wrote to stderr: {stderr}"));
}
if !first.ends_with('\n') {
return Err(format!(
"the parent read no whole line, only {} bytes",
first.len()
));
}
Ok(())
}
}
#[cfg(unix)]
fn main() -> std::process::ExitCode {
unix::run()
}
#[cfg(not(unix))]
fn main() -> std::process::ExitCode {
use std::io::{self, Write};
writeln!(
io::stderr(),
"closed_pipe: skipped, ctl-term supports and tests Unix only"
)
.ok();
std::process::ExitCode::SUCCESS
}