#[cfg(feature = "cli")]
use std::env;
#[cfg(feature = "cli")]
use anyhow::Result;
#[cfg(feature = "cli")]
use braillify::cli::run_cli;
#[cfg(feature = "cli")]
fn main() -> Result<()> {
run_cli(env::args().collect())
}
#[cfg(all(test, feature = "cli"))]
mod tests {
use std::io::Write;
use std::sync::OnceLock;
use assert_cmd::assert::OutputAssertExt;
use predicates::prelude::*;
static BUILT_BINARY: OnceLock<escargot::CargoRun> = OnceLock::new();
fn retry_with_backoff<T, E, F, G>(
max_attempts: u32,
mut try_once: F,
backoff_ms: G,
) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
G: Fn(u32) -> u64,
{
let mut last = None;
for attempt in 1..=max_attempts {
match try_once() {
Ok(v) => return Ok(v),
Err(e) => {
last = Some(e);
if attempt < max_attempts {
std::thread::sleep(std::time::Duration::from_millis(backoff_ms(attempt)));
}
}
}
}
Err(last.expect("Err arm guarantees Some on at least one iteration"))
}
fn panic_build_failed(err: &dyn std::fmt::Debug) -> ! {
panic!(
"Failed to build braillify binary for testing after 3 attempts. Last error: {err:?}. This may happen on the first test run. Try running 'cargo build --bin braillify' manually first."
)
}
fn get_built_binary() -> &'static escargot::CargoRun {
BUILT_BINARY.get_or_init(|| {
retry_with_backoff(
3,
|| {
escargot::CargoBuild::new()
.bin("braillify")
.current_release()
.current_target()
.run()
},
|attempt| 200 * u64::from(attempt),
)
.unwrap_or_else(|e| panic_build_failed(&e))
})
}
#[test]
#[should_panic(expected = "Failed to build braillify binary")]
fn panic_build_failed_emits_message() {
panic_build_failed(&"synthetic-error-for-coverage");
}
#[test]
fn retry_succeeds_on_first_attempt() {
let result: Result<i32, ()> = retry_with_backoff(3, || Ok(42), |_| 0);
assert_eq!(result, Ok(42));
}
#[test]
fn retry_succeeds_after_two_failures() {
let mut tries = 0;
let result: Result<i32, &'static str> = retry_with_backoff(
3,
|| {
tries += 1;
if tries < 3 { Err("not yet") } else { Ok(tries) }
},
|_| 0,
);
assert_eq!(result, Ok(3));
}
#[test]
fn retry_returns_final_error_after_max_attempts() {
let mut tries = 0;
let result: Result<i32, &'static str> = retry_with_backoff(
3,
|| {
tries += 1;
Err("always fails")
},
|_| 0,
);
assert_eq!(result, Err("always fails"));
assert_eq!(tries, 3);
}
#[test]
fn retry_backoff_invoked_for_intermediate_attempts() {
use std::cell::RefCell;
let backoffs: RefCell<Vec<u32>> = RefCell::new(Vec::new());
let mut tries = 0;
let _: Result<(), ()> = retry_with_backoff(
3,
|| {
tries += 1;
Err(())
},
|attempt| {
backoffs.borrow_mut().push(attempt);
0
},
);
assert_eq!(*backoffs.borrow(), vec![1, 2]);
}
#[test]
fn test_braillify_integration_single_word() {
let mut cmd = get_built_binary().command();
cmd.arg("안녕");
let assert = cmd
.assert()
.success()
.stdout(predicate::str::is_empty().not());
let stdout = String::from_utf8_lossy(&assert.get_output().stdout);
assert!(
stdout
.chars()
.any(|c| c as u32 >= 0x2800 && c as u32 <= 0x28FF)
);
}
#[test]
fn test_braillify_integration_english() {
let mut cmd = get_built_binary().command();
cmd.arg("hello");
cmd.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_braillify_integration_mixed() {
let mut cmd = get_built_binary().command();
cmd.arg("안녕 hello");
cmd.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_braillify_integration_numbers() {
let mut cmd = get_built_binary().command();
cmd.arg("123");
cmd.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_braillify_pipe_input() {
let mut cmd = get_built_binary().command();
let mut child = cmd
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap();
{
let stdin = child.stdin.as_mut().unwrap();
stdin.write_all("안녕\n".as_bytes()).unwrap();
}
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert!(!output.stdout.is_empty());
}
#[test]
fn test_braillify_help() {
let mut cmd = get_built_binary().command();
cmd.arg("--help");
cmd.assert()
.success()
.stdout(predicate::str::contains("한국어 점자 변환 CLI"));
}
#[test]
fn test_braillify_version() {
let mut cmd = get_built_binary().command();
cmd.arg("--version");
cmd.assert()
.success()
.stdout(predicate::str::contains("braillify"));
}
#[test]
fn test_braillify_no_args() {
let mut cmd = get_built_binary().command();
let mut child = cmd
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.unwrap();
{
let stdin = child.stdin.as_mut().unwrap();
stdin.write_all("안녕\n".as_bytes()).unwrap();
}
let output = child.wait_with_output().unwrap();
assert!(output.status.success());
assert!(!output.stdout.is_empty());
cmd.assert()
.success()
.stdout(predicate::str::contains("braillify REPL"));
}
#[test]
fn test_braillify_empty_input() {
let mut cmd = get_built_binary().command();
cmd.arg("");
cmd.assert().success().stdout(predicate::str::is_empty());
}
#[test]
fn test_braillify_long_text() {
let long_text = "안녕하세요 ".repeat(100);
let mut cmd = get_built_binary().command();
cmd.arg(&long_text);
cmd.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_braillify_special_characters() {
let mut cmd = get_built_binary().command();
cmd.arg("!@#$%^&*()");
cmd.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_braillify_korean_sentences() {
let mut cmd = get_built_binary().command();
cmd.arg("안녕하세요. 오늘 날씨가 좋네요.");
cmd.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_braillify_multiple_spaces() {
let mut cmd = get_built_binary().command();
cmd.arg("안녕 하세요");
cmd.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
#[test]
fn test_braillify_newlines() {
let mut cmd = get_built_binary().command();
cmd.arg("안녕\n하세요");
cmd.assert()
.success()
.stdout(predicate::str::is_empty().not());
}
}