use std::path::PathBuf;
use assert_cmd::Command;
const CLEAN: &str = "extends Node\n\n\nfunc _ready() -> void:\n\tprint(\"hi\")\n";
const UNFORMATTED: &str = "extends Node\n\n\nfunc _ready() -> void:\n\tprint( \"hi\" )\n";
const UNCLEAN: &str =
"extends Node\n\n\nfunc DoThing() -> void:\n\tvar MyVar = 1\n\tprint(MyVar)\n";
fn gdck() -> Command {
Command::new(env!("CARGO_BIN_EXE_gdck"))
}
struct Sandbox {
root: PathBuf,
}
impl Sandbox {
fn new(name: &str) -> Self {
let root = std::env::temp_dir().join(format!("gdck-cli-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("should create the sandbox");
Self { root }
}
fn write(&self, name: &str, text: &str) -> PathBuf {
let path = self.root.join(name);
std::fs::write(&path, text).expect("should write the file");
path
}
fn read(&self, name: &str) -> String {
std::fs::read_to_string(self.root.join(name)).expect("should read the file")
}
}
impl Drop for Sandbox {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.root);
}
}
fn text(bytes: &[u8]) -> String {
String::from_utf8(bytes.to_vec()).expect("output should be UTF-8")
}
#[test]
fn nothing_is_written_to_disk_without_fix() {
let sandbox = Sandbox::new("no-write");
sandbox.write("a.gd", UNFORMATTED);
sandbox.write("b.gd", UNCLEAN);
for args in [
vec!["check"],
vec!["check", "--diff"],
vec!["format"],
vec!["format", "--diff"],
vec!["format", "--check"],
vec!["lint"],
vec!["parse"],
] {
let _ = gdck()
.args(&args)
.arg(&sandbox.root)
.arg("--no-config")
.output()
.expect("should run");
assert_eq!(sandbox.read("a.gd"), UNFORMATTED, "{args:?} wrote a.gd");
assert_eq!(sandbox.read("b.gd"), UNCLEAN, "{args:?} wrote b.gd");
}
}
#[test]
fn summaries_go_to_standard_error_leaving_standard_output_empty() {
let sandbox = Sandbox::new("streams-clean");
sandbox.write("a.gd", CLEAN);
for verb in ["check", "format", "lint", "parse", "fix"] {
let output = gdck()
.arg(verb)
.arg(&sandbox.root)
.arg("--no-config")
.output()
.expect("should run");
assert_eq!(
text(&output.stdout),
"",
"`gdck {verb}` put a summary on standard output"
);
assert!(
!output.stderr.is_empty(),
"`gdck {verb}` said nothing at all"
);
}
}
#[test]
fn diagnostics_go_to_standard_output_and_name_their_rule() {
let sandbox = Sandbox::new("diagnostics");
sandbox.write("a.gd", UNCLEAN);
let output = gdck()
.args(["lint", "--no-config"])
.arg(&sandbox.root)
.output()
.expect("should run");
let stdout = text(&output.stdout);
assert!(stdout.contains("[function-name]"), "{stdout}");
assert!(stdout.contains("[variable-name]"), "{stdout}");
assert!(stdout.contains("a.gd:4:"), "{stdout}");
assert!(!stdout.contains("Found 2"), "{stdout}");
assert!(text(&output.stderr).contains("Found 2"));
}
#[test]
fn the_list_of_files_that_would_change_is_the_output_of_a_bare_format() {
let sandbox = Sandbox::new("would-change");
let path = sandbox.write("a.gd", UNFORMATTED);
sandbox.write("b.gd", CLEAN);
let output = gdck()
.args(["format", "--no-config"])
.arg(&sandbox.root)
.output()
.expect("should run");
assert_eq!(text(&output.stdout), format!("{}\n", path.display()));
}
#[test]
fn a_clean_tree_exits_zero() {
let sandbox = Sandbox::new("exit-clean");
sandbox.write("a.gd", CLEAN);
for verb in ["check", "format", "lint", "parse"] {
gdck()
.arg(verb)
.arg(&sandbox.root)
.arg("--no-config")
.assert()
.success();
}
}
#[test]
fn problems_found_exits_one() {
let sandbox = Sandbox::new("exit-problems");
sandbox.write("format.gd", UNFORMATTED);
sandbox.write("lint.gd", UNCLEAN);
sandbox.write("syntax.gd", "func (\n");
for (verb, file) in [
("format", "format.gd"),
("lint", "lint.gd"),
("parse", "syntax.gd"),
("check", "format.gd"),
("check", "lint.gd"),
] {
gdck()
.arg(verb)
.arg(sandbox.root.join(file))
.arg("--no-config")
.assert()
.code(1);
}
}
#[test]
fn a_run_that_cannot_be_completed_exits_two() {
let sandbox = Sandbox::new("exit-error");
sandbox.write("a.gd", CLEAN);
sandbox.write("gdck.toml", "[format]\nline-lenght = 100\n");
let output = gdck()
.arg("check")
.arg(&sandbox.root)
.output()
.expect("should run");
assert_eq!(output.status.code(), Some(2));
let stderr = text(&output.stderr);
assert!(stderr.contains("gdck.toml:2"), "{stderr}");
assert!(stderr.contains("format.line-length"), "{stderr}");
}
#[test]
fn format_fix_writes_and_leaves_the_file_formatted() {
let sandbox = Sandbox::new("format-fix");
sandbox.write("a.gd", UNFORMATTED);
gdck()
.args(["format", "--fix", "--no-config"])
.arg(&sandbox.root)
.assert()
.success();
let after = sandbox.read("a.gd");
assert_ne!(after, UNFORMATTED);
assert!(after.contains("print(\"hi\")"), "{after}");
gdck()
.args(["format", "--no-config"])
.arg(&sandbox.root)
.assert()
.success();
}
#[test]
fn fix_applies_the_lint_fixes_and_the_formatting_together() {
let sandbox = Sandbox::new("fix");
sandbox.write(
"a.gd",
"extends Node\n\n\nfunc _ready() -> void:\n\tprint( 'hi' )\n",
);
gdck()
.args(["fix", "--no-config"])
.arg(&sandbox.root)
.assert()
.success();
assert_eq!(sandbox.read("a.gd"), CLEAN);
}
#[test]
fn standard_input_makes_format_a_filter() {
for args in [
vec!["format", "-", "--no-config"],
vec!["format", "--fix", "-", "--no-config"],
] {
let output = gdck()
.args(&args)
.write_stdin(UNFORMATTED)
.output()
.expect("should run");
assert_eq!(
text(&output.stdout),
CLEAN,
"{args:?} did not emit the formatted file"
);
}
}
#[test]
fn an_already_clean_file_still_comes_back_out_of_the_pipe() {
let output = gdck()
.args(["format", "-", "--no-config"])
.write_stdin(CLEAN)
.output()
.expect("should run");
assert_eq!(text(&output.stdout), CLEAN);
assert_eq!(output.status.code(), Some(0));
}
#[test]
fn a_diff_from_standard_input_is_a_diff_and_not_the_file() {
let output = gdck()
.args(["format", "--diff", "-", "--no-config"])
.write_stdin(UNFORMATTED)
.output()
.expect("should run");
let stdout = text(&output.stdout);
assert!(stdout.starts_with("--- -\n"), "{stdout}");
assert!(stdout.contains("@@"), "{stdout}");
assert!(stdout.contains("-\tprint( \"hi\" )"), "{stdout}");
assert!(stdout.contains("+\tprint(\"hi\")"), "{stdout}");
}
#[test]
fn no_config_ignores_a_file_that_would_otherwise_be_fatal() {
let sandbox = Sandbox::new("no-config");
sandbox.write("a.gd", CLEAN);
sandbox.write("gdck.toml", "[format]\nline-lenght = 100\n");
gdck()
.args(["check", "--no-config"])
.arg(&sandbox.root)
.assert()
.success();
}
#[test]
fn a_named_config_is_read_from_wherever_it_is() {
let sandbox = Sandbox::new("named-config");
sandbox.write("a.gd", CLEAN);
let config = sandbox.write("ci.toml", "[lint]\ndisable = [\"function-name\"]\n");
let output = gdck()
.arg("config")
.arg("--config")
.arg(&config)
.output()
.expect("should run");
assert!(text(&output.stdout).contains("disable = [\"function-name\"]"));
assert!(text(&output.stderr).contains("ci.toml"));
}
#[test]
fn config_prints_a_gdck_toml_that_can_be_read_back() {
let sandbox = Sandbox::new("config-round-trip");
sandbox.write("a.gd", CLEAN);
let first = gdck()
.args(["config", "--no-config"])
.arg(&sandbox.root)
.output()
.expect("should run");
let printed = text(&first.stdout);
assert!(printed.contains("[format]"), "{printed}");
assert!(printed.contains("[lint]"), "{printed}");
sandbox.write("gdck.toml", &printed);
let second = gdck()
.arg("config")
.arg(&sandbox.root)
.output()
.expect("should run");
assert_eq!(second.status.code(), Some(0), "{}", text(&second.stderr));
assert_eq!(
text(&second.stdout),
printed,
"the settings changed on the way through a file"
);
}
#[test]
fn a_gdtoolkit_setting_gdck_cannot_honour_is_a_note_and_not_a_failure() {
let sandbox = Sandbox::new("gdtoolkit-note");
sandbox.write("a.gd", CLEAN);
sandbox.write("gdlintrc", "max-returns: 3\nmax-locals: 15\n");
let output = gdck()
.arg("check")
.arg(&sandbox.root)
.output()
.expect("should run");
assert_eq!(output.status.code(), Some(0));
let stderr = text(&output.stderr);
assert!(stderr.contains("max-locals"), "{stderr}");
assert!(!stderr.contains("max-returns"), "{stderr}");
}
#[test]
fn a_path_that_is_not_there_is_reported_rather_than_ignored() {
let sandbox = Sandbox::new("missing-path");
let output = gdck()
.arg("check")
.arg(sandbox.root.join("nope.gd"))
.arg("--no-config")
.output()
.expect("should run");
assert_eq!(output.status.code(), Some(2));
assert!(text(&output.stderr).contains("nope.gd"));
}
#[test]
fn an_explicit_file_is_processed_whatever_it_is_called() {
let sandbox = Sandbox::new("odd-name");
let path = sandbox.write("odd_name.txt", CLEAN);
gdck()
.arg("parse")
.arg(&path)
.arg("--no-config")
.assert()
.success();
}
#[test]
fn excluded_directories_are_not_walked() {
let sandbox = Sandbox::new("excluded");
sandbox.write("a.gd", CLEAN);
let addons = sandbox.root.join("addons");
std::fs::create_dir_all(&addons).expect("should create");
std::fs::write(addons.join("vendored.gd"), UNCLEAN).expect("should write");
gdck()
.args(["check", "--no-config"])
.arg(&sandbox.root)
.assert()
.success();
gdck()
.args(["lint", "--no-config"])
.arg(addons.join("vendored.gd"))
.assert()
.code(1);
}
#[test]
fn an_empty_tree_is_success_and_says_so() {
let sandbox = Sandbox::new("empty");
let output = gdck()
.args(["check", "--no-config"])
.arg(&sandbox.root)
.output()
.expect("should run");
assert_eq!(output.status.code(), Some(0));
assert_eq!(text(&output.stdout), "");
assert!(text(&output.stderr).contains("No .gd files found."));
}
#[test]
fn every_subcommand_has_help_and_the_binary_has_a_version() {
for verb in ["check", "fix", "format", "lint", "parse", "config"] {
let output = gdck().arg(verb).arg("--help").output().expect("should run");
assert!(output.status.success(), "`gdck {verb} --help` failed");
assert!(
!output.stdout.is_empty(),
"`gdck {verb} --help` said nothing"
);
}
let version = gdck().arg("--version").output().expect("should run");
assert!(text(&version.stdout).contains(env!("CARGO_PKG_VERSION")));
}
#[test]
fn a_bad_invocation_is_refused_rather_than_guessed_at() {
gdck().arg("frobnicate").assert().code(2);
gdck()
.args(["lint", "--no-config", "--config", "ci.toml"])
.assert()
.code(2);
}