mod common;
use clap::CommandFactory;
use common::*;
use tempfile::tempdir;
#[test]
fn bare_largo_prints_command_overview() {
let out = largo().output().expect("largo should spawn");
assert_eq!(out.status.code(), Some(2), "bare largo is a usage error");
let all = format!("{}{}", stdout(&out), stderr(&out));
let all = strip_ansi(&all);
assert!(all.contains("Commands:"), "must list the command overview:\n{all}");
for cmd in ["new", "init", "build", "run", "check", "publish"] {
assert!(all.contains(cmd), "overview must mention `{cmd}`:\n{all}");
}
}
#[test]
fn every_subcommand_has_about_and_examples() {
let cmd = logicaffeine_cli::cli::Cli::command();
for sub in cmd.get_subcommands() {
let name = sub.get_name();
if name == "help" {
continue;
}
assert!(
sub.get_about().is_some(),
"subcommand `{name}` is missing an about string"
);
if sub.is_hide_set() {
continue;
}
let after = sub
.get_after_help()
.or_else(|| sub.get_after_long_help())
.map(|s| s.to_string())
.unwrap_or_default();
assert!(
after.contains("Examples"),
"subcommand `{name}` is missing an Examples section in after_help"
);
}
}
#[test]
fn quiet_flag_silences_check() {
let dir = tempdir().unwrap();
scaffold(dir.path(), "quiet_proj");
let out = largo_in(dir.path(), &["check", "-q"]);
assert_eq!(out.status.code(), Some(0), "check -q should succeed: {}", stderr(&out));
assert_eq!(stdout(&out), "", "quiet check must print nothing on stdout");
}
#[test]
fn verbose_flag_accepted() {
let dir = tempdir().unwrap();
scaffold(dir.path(), "verbose_proj");
let out = largo_in(dir.path(), &["-v", "check"]);
assert_eq!(out.status.code(), Some(0), "-v check should succeed: {}", stderr(&out));
}
#[test]
fn default_piped_output_has_no_ansi() {
let dir = tempdir().unwrap();
let out = largo_in(dir.path(), &["build"]);
assert!(
!has_ansi(&stderr(&out)),
"piped stderr must not contain ANSI by default:\n{:?}",
stderr(&out)
);
}
#[test]
fn color_always_forces_ansi_on_piped_error() {
let dir = tempdir().unwrap();
let out = largo_in(dir.path(), &["--color", "always", "build"]);
assert!(
has_ansi(&stderr(&out)),
"--color always must emit ANSI into the pipe:\n{:?}",
stderr(&out)
);
}
#[test]
fn no_color_env_wins_over_auto() {
let dir = tempdir().unwrap();
let out = largo()
.args(["build"])
.current_dir(dir.path())
.env("NO_COLOR", "1")
.output()
.expect("largo should spawn");
assert!(
!has_ansi(&stderr(&out)),
"NO_COLOR must strip ANSI:\n{:?}",
stderr(&out)
);
}
#[test]
fn unknown_subcommand_exits_2() {
let out = largo().arg("frobnicate").output().expect("largo should spawn");
assert_eq!(out.status.code(), Some(2));
}
#[test]
fn error_style_has_error_and_help_lines() {
let dir = tempdir().unwrap();
let out = largo_in(dir.path(), &["build"]);
assert_eq!(out.status.code(), Some(1));
let err = strip_ansi(&stderr(&out));
assert!(
err.lines().next().is_some_and(|l| l.starts_with("error:")),
"first stderr line must start with `error:`:\n{err}"
);
assert!(
err.lines().any(|l| l.starts_with("help:")),
"stderr must carry a `help:` hint line:\n{err}"
);
}
#[test]
fn new_rejects_hostile_project_names() {
for bad in ["", "x/y", "x\\y", "-flag", "a b", "o'brien", "..", "a\"b"] {
let dir = tempdir().unwrap();
let out = largo()
.args(["new", "--", bad])
.current_dir(dir.path())
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(1),
"name {bad:?} must be rejected, got: {}",
stdout(&out)
);
let err = strip_ansi(&stderr(&out));
assert!(err.contains("name"), "error must explain the name rule for {bad:?}:\n{err}");
}
}
#[test]
fn new_accepts_conventional_names() {
for good in ["hello", "my_project", "app2", "wire-codec"] {
let dir = tempdir().unwrap();
let out = largo().args(["new", good]).current_dir(dir.path()).output().unwrap();
assert_eq!(out.status.code(), Some(0), "name {good:?}: {}", stderr(&out));
assert!(dir.path().join(good).join("Largo.toml").exists());
}
}
#[test]
fn deep_expression_chain_is_a_diagnostic_not_a_crash() {
let dir = tempdir().unwrap();
scaffold(dir.path(), "deep_chain");
let mut program = String::from("# Main\n\n## Main\n\nShow 1");
for _ in 1..5000 {
program.push_str(" + 1");
}
program.push_str(".\n");
std::fs::write(dir.path().join("src/main.lg"), program).unwrap();
let out = largo_in(dir.path(), &["check"]);
assert_eq!(
out.status.code(),
Some(1),
"must exit 1 (a signal death reports None): {:?}",
out.status
);
let err = strip_ansi(&stderr(&out));
assert!(err.contains("deep"), "must name the problem:\n{err}");
assert!(
err.contains("LOGOS_MAX_AST_DEPTH"),
"must teach the environment override:\n{err}"
);
}
#[test]
fn depth_limit_is_environment_tunable() {
let dir = tempdir().unwrap();
scaffold(dir.path(), "tunable");
let mut program = String::from("# Main\n\n## Main\n\nShow 1");
for _ in 1..200 {
program.push_str(" + 1");
}
program.push_str(".\n");
std::fs::write(dir.path().join("src/main.lg"), program).unwrap();
let default_run = largo_in(dir.path(), &["check"]);
assert_eq!(default_run.status.code(), Some(1), "200 > default limit");
let raised = largo()
.args(["check"])
.current_dir(dir.path())
.env("LOGOS_MAX_AST_DEPTH", "1024")
.output()
.unwrap();
assert_eq!(
raised.status.code(),
Some(0),
"raised limit must accept the 200-term chain: {}",
stderr(&raised)
);
}
#[test]
fn raised_depth_limit_carries_its_own_stack() {
let dir = tempdir().unwrap();
scaffold(dir.path(), "big_machine");
let mut program = String::from("# Main\n\n## Main\n\nShow 1");
for _ in 1..5000 {
program.push_str(" + 1");
}
program.push_str(".\n");
std::fs::write(dir.path().join("src/main.lg"), program).unwrap();
let out = largo()
.args(["check"])
.current_dir(dir.path())
.env("LOGOS_MAX_AST_DEPTH", "8192")
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(0),
"a raised limit must genuinely work (None = signal death): {:?}\n{}",
out.status,
stderr(&out)
);
}
#[test]
fn check_deep_passes_on_a_clean_program() {
let dir = tempdir().unwrap();
scaffold(dir.path(), "deep_clean");
let out = largo_in(dir.path(), &["check", "--deep"]);
assert_eq!(out.status.code(), Some(0), "deep check: {}", stderr(&out));
assert!(
stdout(&out).contains("Deep check passed"),
"must confirm the rustc pass:\n{}",
stdout(&out)
);
}
#[test]
fn test_verb_is_reserved_and_hidden() {
let out = largo().arg("test").output().expect("largo should spawn");
assert_eq!(out.status.code(), Some(2), "reserved verb is a usage error");
let err = strip_ansi(&stderr(&out));
assert!(
err.contains("reserved") && err.contains("test framework"),
"must explain the verb is reserved for the LOGOS test framework:\n{err}"
);
let cmd = logicaffeine_cli::cli::Cli::command();
let test_sub = cmd
.get_subcommands()
.find(|s| s.get_name() == "test")
.expect("the `test` verb must exist (reserved)");
assert!(test_sub.is_hide_set(), "`test` must be hidden from help");
}