use std::path::PathBuf;
use std::process::Command;
mod common;
use common::{bin_path, fixture};
#[test]
fn clean_file_exits_zero() {
let output = Command::new(bin_path())
.arg("check")
.arg(fixture("clean.px"))
.output()
.expect("failed to run praxis");
assert!(
output.status.success(),
"clean file should exit 0\nstdout: {}\nstderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr),
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("error(s)"),
"clean file should report no errors, got: {stderr}"
);
}
#[test]
fn bad_byte_file_exits_nonzero_with_diagnostic() {
let output = Command::new(bin_path())
.arg("check")
.arg(fixture("bad_byte.px"))
.output()
.expect("failed to run praxis");
let code = output
.status
.code()
.expect("process was terminated by signal");
assert_eq!(code, 1, "file with a lex error should exit 1");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(stderr.contains("error[T003]"), "missing code: {stderr}");
assert!(
stderr.contains("unexpected character"),
"missing message: {stderr}"
);
assert!(
stderr.contains("bad_byte.px:4:"),
"missing location: {stderr}"
);
assert!(
stderr.contains("var first = @"),
"missing source line: {stderr}"
);
assert!(stderr.contains("^"), "missing caret: {stderr}");
assert!(stderr.contains("2 error(s)"), "missing summary: {stderr}");
}
#[test]
fn parse_error_file_reports_multiple_diagnostics() {
let output = Command::new(bin_path())
.arg("check")
.arg(fixture("parse_error.px"))
.output()
.expect("failed to run praxis");
let code = output
.status
.code()
.expect("process was terminated by signal");
assert_eq!(code, 1, "file with parse errors should exit 1");
let stderr = String::from_utf8_lossy(&output.stderr);
let p001_count = stderr.matches("error[P001]").count();
assert!(
p001_count >= 2,
"expected >=2 parse diagnostics, got {p001_count}: {stderr}"
);
}
#[test]
fn type_error_file_reports_y001() {
let output = Command::new(bin_path())
.arg("check")
.arg(fixture("type_error.px"))
.output()
.expect("failed to run praxis");
let code = output
.status
.code()
.expect("process was terminated by signal");
assert_eq!(code, 1, "file with a type error should exit 1");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("error[Y001]"),
"missing Y001 code: {stderr}"
);
assert!(
stderr.contains("expected Int, found Text"),
"missing type-mismatch message: {stderr}"
);
}
#[test]
fn missing_file_exits_two() {
let output = Command::new(bin_path())
.arg("check")
.arg(fixture("does_not_exist.px"))
.output()
.expect("failed to run praxis");
let code = output
.status
.code()
.expect("process was terminated by signal");
assert_eq!(code, 2, "missing file should exit 2 (usage error)");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("failed to read source file"),
"missing file should explain itself, got: {stderr}"
);
}
#[test]
fn a_subcommand_the_cli_does_not_have_is_rejected() {
for args in [vec!["watch", "prog.px"], vec!["repl"]] {
let output = Command::new(bin_path())
.args(&args)
.output()
.expect("failed to run praxis");
let code = output
.status
.code()
.expect("process was terminated by signal");
let stderr = String::from_utf8_lossy(&output.stderr);
let invocation = args.join(" ");
assert_eq!(code, 2, "`praxis {invocation}` should exit 2: {stderr}");
assert!(
stderr.contains("unrecognized subcommand"),
"`praxis {invocation}` should say the subcommand is unrecognized, got: {stderr}"
);
}
}
#[test]
fn an_unterminated_template_does_not_also_report_a_fabricated_interior() {
let output = Command::new(bin_path())
.arg("check")
.arg(fixture("unterminated_template.px"))
.output()
.expect("failed to run praxis");
assert_eq!(
output.status.code().expect("terminated by signal"),
1,
"an unterminated template must fail the check"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("error[T002]") && stderr.contains("unterminated backtick template"),
"the truthful report is missing: {stderr}"
);
assert!(
!stderr.contains("nested template"),
"reported a nested template the source does not contain: {stderr}"
);
assert!(
!stderr.contains("error[I030]"),
"reported about an interior the token does not have: {stderr}"
);
}
#[test]
fn an_unterminated_template_names_its_own_line_and_nothing_else() {
let output = Command::new(bin_path())
.arg("check")
.arg(fixture("unterminated_template.px"))
.output()
.expect("failed to run praxis");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("praxis: 1 error(s)"),
"one typo is one error (ADR-094): {stderr}"
);
for cascade in ["error[P001]", "error[Y001]", "error[I000]", "error[Y023]"] {
assert!(
!stderr.contains(cascade),
"{cascade} is damage the unterminated token used to cause: {stderr}"
);
}
let caret = stderr
.lines()
.find(|l| l.contains('^'))
.expect("the report draws a caret");
let width = caret.chars().filter(|c| *c == '^').count();
assert!(
(1..=40).contains(&width),
"the caret must cover the template, not the file: {width} carets in {caret:?}"
);
}
#[test]
fn no_help_page_leaks_an_implementation_marker() {
for args in [
vec!["--help"],
vec!["run", "--help"],
vec!["check", "--help"],
vec!["lsp", "--help"],
] {
let out = Command::new(bin_path())
.args(&args)
.output()
.expect("failed to run praxis");
let help = String::from_utf8_lossy(&out.stdout).to_string()
+ &String::from_utf8_lossy(&out.stderr);
for marker in ["§", "Milestone", "(M1", "M6)", "M10)", "M11)"] {
assert!(
!help.contains(marker),
"`praxis {}` prints `{marker}`:\n{help}",
args.join(" ")
);
}
}
}
fn scratch_dir() -> PathBuf {
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR"))
.join(format!("check-tests-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("create this process's scratch directory");
dir
}
fn check_source(name: &str, src: &str) -> (i32, String) {
let path = scratch_dir().join(name);
std::fs::write(&path, src).expect("write the source");
let out = Command::new(bin_path())
.arg("check")
.arg(&path)
.arg("--color")
.arg("never")
.output()
.expect("failed to run praxis");
(
out.status.code().unwrap_or(-1),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn a_char_literal_that_is_not_one_character_is_reported_at_the_literal() {
let (code, stderr) = check_source("char_len.px", "var two = '##'\nvar none = ''\n");
assert_eq!(code, 1, "{stderr}");
assert!(stderr.contains("error[T007]"), "{stderr}");
assert!(
stderr.contains("a character literal holds exactly one character"),
"{stderr}"
);
assert!(
stderr.contains("empty character literal"),
"the two messages are distinct: {stderr}"
);
assert!(
stderr.contains("^^^^"),
"the caret covers the whole literal: {stderr}"
);
assert!(stderr.contains("\"##\""), "{stderr}");
assert!(stderr.contains("2 error(s)"), "{stderr}");
}
#[test]
fn an_unterminated_char_literal_does_not_cascade() {
let (code, stderr) = check_source("char_unterminated.px", "var c = 'a\nout(c)\n");
assert_eq!(code, 1, "{stderr}");
assert!(stderr.contains("error[T006]"), "{stderr}");
assert!(
stderr.contains("unterminated character literal"),
"{stderr}"
);
assert!(stderr.contains("1 error(s)"), "exactly one: {stderr}");
for cascade in ["P001", "P002", "T003", "N001"] {
assert!(
!stderr.contains(cascade),
"a `{cascade}` cascade is back: {stderr}"
);
}
}
#[test]
fn a_char_literals_escapes_are_a_text_literals() {
let (code, stderr) = check_source(
"char_escapes.px",
"var a = '\\n'\nvar b = '\\''\nvar c = '\\\\'\nout(a)\nout(b)\nout(c)\n",
);
assert_eq!(code, 0, "{stderr}");
let (code, stderr) = check_source("char_bad_escape.px", "var a = '\\u{41}'\n");
assert_eq!(code, 1, "{stderr}");
assert!(stderr.contains("error[T005]"), "{stderr}");
assert!(
stderr.contains("invalid escape in character literal"),
"{stderr}"
);
}