use std::io::Write as _;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use serde::Serialize;
use crate::error::RkError;
static STDOUT_CLOSED: AtomicBool = AtomicBool::new(false);
static STDOUT_ERROR: Mutex<Option<std::io::Error>> = Mutex::new(None);
fn to_stdout(text: &str) {
to_stdout_bytes(text.as_bytes());
}
fn to_stdout_bytes(bytes: &[u8]) {
if STDOUT_CLOSED.load(Ordering::Relaxed) {
return;
}
let Ok(mut retained) = STDOUT_ERROR.lock() else {
return;
};
if retained.is_some() {
return;
}
let mut stdout = std::io::stdout().lock();
let outcome = stdout.write_all(bytes).and_then(|()| stdout.flush());
if let Err(source) = outcome {
if source.kind() == std::io::ErrorKind::BrokenPipe {
STDOUT_CLOSED.store(true, Ordering::Relaxed);
} else {
*retained = Some(source);
}
}
}
#[must_use]
pub fn take_stdout_failure() -> Option<std::io::Error> {
STDOUT_ERROR.lock().ok().and_then(|mut held| held.take())
}
fn to_stderr(text: &str) {
let _ = writeln!(std::io::stderr(), "{text}");
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Human,
Json,
}
#[derive(Debug, Clone, Copy)]
pub struct Output {
format: Format,
}
impl Output {
#[must_use]
pub const fn new(json: bool) -> Self {
Self {
format: if json { Format::Json } else { Format::Human },
}
}
#[must_use]
pub const fn human() -> Self {
Self {
format: Format::Human,
}
}
#[must_use]
pub const fn is_json(&self) -> bool {
matches!(self.format, Format::Json)
}
pub fn result_line(&self, line: impl AsRef<str>) {
if !self.is_json() {
to_stdout(&format!("{}\n", line.as_ref()));
}
}
pub fn result_raw(&self, text: &str) {
if !self.is_json() {
to_stdout(text);
}
}
pub fn result_bytes(&self, bytes: &[u8]) {
if !self.is_json() {
to_stdout(&String::from_utf8_lossy(bytes));
}
}
pub fn emit<T: Serialize>(&self, report: &T) -> Result<(), RkError> {
if self.is_json() {
let text = serde_json::to_string_pretty(report).map_err(anyhow::Error::from)?;
to_stdout(&format!("{text}\n"));
}
Ok(())
}
pub fn event<T: Serialize>(&self, event: &T) {
if self.is_json() {
if let Ok(line) = serde_json::to_string(event) {
to_stdout(&format!("{line}\n"));
}
}
}
pub fn frame(&self, line: impl AsRef<str>) {
if !self.is_json() {
to_stderr(line.as_ref());
}
}
pub fn warn(&self, line: impl AsRef<str>) {
to_stderr(&format!("warning: {}", line.as_ref()));
}
pub fn child_passthrough(&self, stream: crate::events::ChildStream, bytes: &[u8]) {
if self.is_json() {
return;
}
match stream {
crate::events::ChildStream::Stdout => to_stdout_bytes(bytes),
crate::events::ChildStream::Stderr => {
let _ = std::io::stderr().lock().write_all(bytes);
}
}
}
pub fn next(&self, lines: &[String]) {
if self.is_json() || lines.is_empty() {
return;
}
let mut block = String::from("Next:\n");
for line in lines {
block.push_str(" ");
block.push_str(line);
block.push('\n');
}
to_stdout(&block);
}
}
pub fn render_error(err: &RkError, json: bool) {
if json {
let diagnostic = err.diagnostic();
match serde_json::to_string(&diagnostic) {
Ok(line) => to_stderr(&line),
Err(_) => to_stderr(
r#"{"schema":"rk.diagnostic/1","reason":"internal","message":"a diagnostic failed to serialize"}"#,
),
}
return;
}
match err {
RkError::Refusal(diagnostic)
| RkError::Missing(diagnostic)
| RkError::CheckFailed(diagnostic)
| RkError::Subprocess(diagnostic) => to_stderr(&diagnostic.render_human()),
_ => to_stderr(&format!("error: {err}")),
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
#[test]
fn no_handler_prints_past_the_boundary() {
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut offenders = Vec::new();
scan(&root, &mut offenders);
assert!(
offenders.is_empty(),
"these print directly instead of using the output boundary: {offenders:?}"
);
}
fn scan(dir: &std::path::Path, offenders: &mut Vec<String>) {
for entry in std::fs::read_dir(dir).expect("the source directory reads") {
let path = entry.expect("the entry reads").path();
if path.is_dir() {
scan(&path, offenders);
continue;
}
if path.extension().is_none_or(|ext| ext != "rs")
|| path.file_name().is_some_and(|name| name == "output.rs")
{
continue;
}
let text = std::fs::read_to_string(&path).expect("the source reads");
for (idx, line) in text.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("//") {
continue;
}
for needle in [
"println!",
"print!",
"eprintln!",
"eprint!",
"io::stdout(",
"io::stderr(",
] {
if trimmed.contains(needle) {
offenders.push(format!("{}:{}", path.display(), idx + 1));
}
}
}
}
}
}