pub mod cli;
mod config;
mod pipeline;
use std::io::{Read, Write};
use face_core::FaceError;
#[non_exhaustive]
pub struct Io<R: Read, W: Write, E: Write> {
pub stdin: R,
pub stdout: W,
pub stderr: E,
}
impl<R: Read, W: Write, E: Write> Io<R, W, E> {
pub fn new(stdin: R, stdout: W, stderr: E) -> Self {
Self {
stdin,
stdout,
stderr,
}
}
}
pub fn run<R: Read, W: Write, E: Write>(
cli: cli::Cli,
io: &mut Io<R, W, E>,
) -> Result<(), FaceError> {
pipeline::run(cli, io)
}
#[cfg(test)]
mod tests {
use super::*;
use clap::Parser;
use face_core::OutputFormat;
use std::io::Cursor;
fn parse_args<I, S>(args: I) -> cli::Cli
where
I: IntoIterator<Item = S>,
S: Into<std::ffi::OsString> + Clone,
{
cli::Cli::parse_from(args)
}
fn run_with_stdin(cli: cli::Cli, stdin: &[u8]) -> (Result<(), FaceError>, Vec<u8>, Vec<u8>) {
let mut io = Io::new(
Cursor::new(stdin.to_vec()),
Vec::<u8>::new(),
Vec::<u8>::new(),
);
let res = run(cli, &mut io);
(res, io.stdout, io.stderr)
}
#[test]
fn run_help_and_exit() {
let err = cli::Cli::try_parse_from(["face", "--help"]).unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::DisplayHelp);
}
#[test]
fn run_simple_jsonl_grouping() {
let cli = parse_args(["face", "--color=never"]);
let stdin = b"{\"kind\":\"bug\"}\n{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (res, stdout, _stderr) = run_with_stdin(cli, stdin);
res.expect("run ok");
let out = String::from_utf8(stdout).expect("utf-8 output");
assert!(
out.contains("bug"),
"human output should include `bug`: {out:?}"
);
assert!(
out.contains("feat"),
"human output should include `feat`: {out:?}"
);
assert!(out.starts_with("face: 3 items"), "header line: {out:?}");
}
#[test]
fn run_explicit_format_json() {
let cli = parse_args(["face", "--format=json"]);
let stdin = b"{\"kind\":\"bug\"}\n{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (res, stdout, _stderr) = run_with_stdin(cli, stdin);
res.expect("run ok");
let env: face_core::Envelope =
serde_json::from_slice(&stdout).expect("envelope round-trip");
assert_eq!(env.result.input_total, 3);
assert!(!env.clusters.is_empty());
}
#[test]
fn run_explain_short_circuits() {
let cli = parse_args(["face", "--explain"]);
let stdin = b"{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (res, stdout, _stderr) = run_with_stdin(cli, stdin);
res.expect("run ok");
let out = String::from_utf8(stdout).expect("utf-8 output");
assert!(out.contains("input:"), "{out:?}");
assert!(out.contains("strategy:"), "{out:?}");
assert!(out.contains("output:"), "{out:?}");
}
#[test]
fn json_short_flag_overrides_format() {
let cli = parse_args(["face", "-j"]);
let stdin = b"{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (res, stdout, _stderr) = run_with_stdin(cli, stdin);
res.expect("run ok");
let env: face_core::Envelope =
serde_json::from_slice(&stdout).expect("envelope round-trip");
assert_eq!(env.result.input_total, 2);
let _ = OutputFormat::Json;
}
#[test]
fn run_schema_short_circuits() {
let cli = parse_args(["face", "--schema"]);
let stdin = b"{\"kind\":\"bug\"}\n{\"kind\":\"feat\"}\n";
let (res, stdout, _stderr) = run_with_stdin(cli, stdin);
res.expect("run ok");
let out = String::from_utf8(stdout).expect("utf-8 output");
assert!(out.contains("format:"), "{out:?}");
assert!(out.contains("records:"), "{out:?}");
assert!(out.contains("kind"), "schema lists fields: {out:?}");
}
#[test]
fn explain_and_schema_are_mutually_exclusive() {
let cli = parse_args(["face", "--explain", "--schema"]);
let (res, _, _) = run_with_stdin(cli, b"[]");
match res {
Err(FaceError::ConflictingFlags { .. }) => {}
other => panic!("expected ConflictingFlags, got {other:?}"),
}
}
}