use std::path::Path;
use std::process::{Command, Output};
const TWO_VIOLATIONS: &str =
"extends Node\n\nvar BadName: int = 5\n\nfunc DoThing() -> void:\n\tpass\n";
struct Project {
_dir: tempfile::TempDir,
config: std::path::PathBuf,
script: std::path::PathBuf,
}
fn project(config_body: &str, source: &str) -> Project {
let dir = tempfile::tempdir().expect("temp dir");
let config = dir.path().join("gdstyle.toml");
let script = dir.path().join("bad_test.gd");
std::fs::write(&config, config_body).expect("write config");
std::fs::write(&script, source).expect("write script");
Project {
_dir: dir,
config,
script,
}
}
fn check(config: &Path, script: &Path, extra: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_gdstyle"))
.arg("check")
.arg(script)
.arg("--config")
.arg(config)
.arg("--no-color")
.args(extra)
.output()
.expect("run gdstyle")
}
fn stdout_of(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn stderr_of(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
#[test]
fn error_severity_makes_check_exit_1() {
let p = project(
"[rules]\n\
\"naming/variable-name-snake-case\" = \"error\"\n\
\"naming/function-name-snake-case\" = \"error\"\n",
TWO_VIOLATIONS,
);
let output = check(&p.config, &p.script, &[]);
let stdout = stdout_of(&output);
assert_eq!(output.status.code(), Some(1), "stdout:\n{}", stdout);
assert!(
stdout.contains("2 errors found"),
"summary should count errors; got:\n{}",
stdout
);
assert!(
!stdout.contains("warning"),
"no diagnostic should print as a warning; got:\n{}",
stdout
);
}
#[test]
fn warnings_alone_still_exit_0() {
let p = project("[rules]\n", TWO_VIOLATIONS);
let output = check(&p.config, &p.script, &[]);
let stdout = stdout_of(&output);
assert_eq!(output.status.code(), Some(0), "stdout:\n{}", stdout);
assert!(stdout.contains("2 warnings found"), "got:\n{}", stdout);
}
#[test]
fn max_warnings_fails_when_the_count_exceeds_the_limit() {
let p = project("[rules]\n", TWO_VIOLATIONS);
let output = check(&p.config, &p.script, &["--max-warnings", "1"]);
assert_eq!(
output.status.code(),
Some(1),
"stdout:\n{}",
stdout_of(&output)
);
assert!(
stderr_of(&output).contains("2 warnings found, exceeding the --max-warnings limit of 1"),
"should explain why it failed; got stderr:\n{}",
stderr_of(&output)
);
}
#[test]
fn max_warnings_passes_when_the_count_equals_the_limit() {
let p = project("[rules]\n", TWO_VIOLATIONS);
let output = check(&p.config, &p.script, &["--max-warnings", "2"]);
assert_eq!(
output.status.code(),
Some(0),
"stdout:\n{}",
stdout_of(&output)
);
}
#[test]
fn max_warnings_ignores_diagnostics_escalated_to_error() {
let p = project(
"[rules]\n\"naming/variable-name-snake-case\" = \"error\"\n\
\"naming/function-name-snake-case\" = \"off\"\n",
TWO_VIOLATIONS,
);
let output = check(&p.config, &p.script, &["--max-warnings", "0"]);
assert_eq!(output.status.code(), Some(1));
assert!(
!stderr_of(&output).contains("--max-warnings"),
"an error should fail before the warning budget is consulted; got stderr:\n{}",
stderr_of(&output)
);
}
#[test]
fn max_warnings_keeps_json_stdout_parsable() {
let p = project("[rules]\n", TWO_VIOLATIONS);
let output = Command::new(env!("CARGO_BIN_EXE_gdstyle"))
.arg("check")
.arg(&p.script)
.arg("--config")
.arg(&p.config)
.arg("--format")
.arg("json")
.arg("--max-warnings")
.arg("0")
.output()
.expect("run gdstyle");
assert_eq!(output.status.code(), Some(1));
let stdout = stdout_of(&output);
let parsed: serde_json::Value = serde_json::from_str(&stdout)
.unwrap_or_else(|e| panic!("stdout should be valid JSON ({}); got:\n{}", e, stdout));
assert_eq!(parsed.as_array().map(Vec::len), Some(2));
}
#[test]
fn unknown_rule_names_are_reported() {
let p = project(
"[rules]\n\
\"naming/variable-snake-case\" = \"error\"\n\
\"format/tabs\" = \"off\"\n",
TWO_VIOLATIONS,
);
let output = check(&p.config, &p.script, &[]);
let stderr = stderr_of(&output);
assert!(
stderr.contains("unknown rule names in [rules]: format/tabs, naming/variable-snake-case"),
"both typos should be listed, sorted; got stderr:\n{}",
stderr
);
assert!(
stderr.contains("gdstyle rules"),
"should point at the rule listing; got stderr:\n{}",
stderr
);
}
#[test]
fn a_correct_config_reports_no_unknown_rules() {
let p = project(
"[rules]\n\"naming/variable-name-snake-case\" = \"error\"\n",
TWO_VIOLATIONS,
);
let output = check(&p.config, &p.script, &[]);
assert!(
!stderr_of(&output).contains("unknown rule"),
"a valid config must stay quiet; got stderr:\n{}",
stderr_of(&output)
);
}
#[test]
fn fmt_also_reports_unknown_rule_names() {
let p = project("[rules]\n\"format/tabs\" = \"off\"\n", TWO_VIOLATIONS);
let output = Command::new(env!("CARGO_BIN_EXE_gdstyle"))
.arg("fmt")
.arg(&p.script)
.arg("--config")
.arg(&p.config)
.arg("--no-color")
.output()
.expect("run gdstyle");
assert!(
stderr_of(&output).contains("unknown rule name in [rules]: format/tabs"),
"a single typo should be singular; got stderr:\n{}",
stderr_of(&output)
);
}
#[test]
fn the_generated_config_names_only_real_rules() {
let dir = tempfile::tempdir().expect("temp dir");
let status = Command::new(env!("CARGO_BIN_EXE_gdstyle"))
.arg("init")
.current_dir(dir.path())
.status()
.expect("run gdstyle init");
assert!(status.success());
let template = std::fs::read_to_string(dir.path().join("gdstyle.toml")).expect("read template");
let known = gdstyle::rules::all_rule_names();
let named: Vec<&str> = template
.lines()
.skip_while(|line| line.trim() != "[rules]")
.filter(|line| line.contains('='))
.filter_map(|line| line.split('"').nth(1))
.collect();
assert!(!named.is_empty(), "template should name some rules");
for name in named {
assert!(
known.contains(&name),
"template names {:?}, which is not in the rule registry",
name
);
}
}
#[test]
fn issue_29_full_user_config() {
let fixtures = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("issue29");
let output = check(
&fixtures.join("gdstyle.toml"),
&fixtures.join("bad_test.gd"),
&[],
);
let stdout = stdout_of(&output);
assert_eq!(output.status.code(), Some(1), "stdout:\n{}", stdout);
assert!(stdout.contains("2 errors found"), "got:\n{}", stdout);
for expected in [
"3:1 error variable name \'BadName\'",
"5:1 error function name \'DoThing\'",
] {
assert!(
stdout.contains(expected),
"missing {:?}; got:\n{}",
expected,
stdout
);
}
assert!(
!stdout.contains("warning"),
"nothing should report as a warning; got:\n{}",
stdout
);
assert!(
!stderr_of(&output).contains("unknown rule"),
"the reported config is valid; got stderr:\n{}",
stderr_of(&output)
);
}
#[test]
fn issue_32_reserved_identifiers_fail_check() {
let p = project(
"[rules]\n",
"extends Node\n\n\nfunc _prose(namespace: String) -> Dictionary:\n\treturn { \"namespace\": namespace }\n\n\nfunc trait(id: String) -> float:\n\treturn 0.0\n",
);
let output = check(&p.config, &p.script, &[]);
let stdout = stdout_of(&output);
assert_eq!(output.status.code(), Some(1), "stdout:\n{}", stdout);
assert!(
stdout.contains("4:13 error 'namespace' is a reserved word"),
"got:\n{}",
stdout
);
assert!(
stdout.contains("8:6 error 'trait' is a reserved word"),
"got:\n{}",
stdout
);
assert!(stdout.contains("2 errors found"), "got:\n{}", stdout);
}