#![warn(anonymous_parameters, bad_style, missing_docs)]
#![warn(
unused,
unused_extern_crates,
unused_import_braces,
unused_qualifications
)]
#![warn(unsafe_code)]
use std::env;
use std::fs::{self, File};
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process;
const DATE_RE: &str = "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]:[0-5][0-9]";
const VERSION_RE: &str = "[0-9]+\\.[0-9]+\\.[0-9]+";
fn self_dir() -> PathBuf {
let self_exe = env::current_exe().expect("Cannot get self's executable path");
let dir = self_exe.parent().expect("Cannot get self's directory");
assert!(dir.ends_with("target/debug/deps") || dir.ends_with("target/release/deps"));
dir.to_owned()
}
fn bin_path<P: AsRef<Path>>(name: P) -> PathBuf {
let test_dir = self_dir();
let debug_or_release_dir = test_dir.parent().expect("Failed to get parent directory");
debug_or_release_dir
.join(name)
.with_extension(env::consts::EXE_EXTENSION)
}
fn src_path(name: &str) -> PathBuf {
let test_dir = self_dir();
let debug_or_release_dir = test_dir.parent().expect("Failed to get parent directory");
let target_dir = debug_or_release_dir
.parent()
.expect("Failed to get parent directory");
let dir = target_dir.parent().expect("Failed to get parent directory");
assert!(dir.join("Cargo.toml").exists());
dir.join(name)
}
fn src_str(p: &str) -> String {
src_path(p)
.to_str()
.expect("Need paths to be valid strings")
.to_owned()
}
enum Behavior {
Null,
File(PathBuf),
Literal(String),
}
fn read_golden(path: &Path) -> String {
let mut f = File::open(path).expect("Failed to open golden data file");
let mut golden = vec![];
f.read_to_end(&mut golden)
.expect("Failed to read golden data file");
let raw = String::from_utf8(golden).expect("Golden data file is not valid UTF-8");
let golden = if cfg!(target_os = "windows") {
raw.replace("\r\n", "\n")
} else {
raw
};
let version_re = regex::Regex::new(VERSION_RE).unwrap();
assert!(
!version_re.is_match(&golden),
"Golden file {} contains a version number",
path.display()
);
let date_re = regex::Regex::new(DATE_RE).unwrap();
assert!(
!date_re.is_match(&golden),
"Golden file {} contains a date",
path.display()
);
golden
}
fn apply_mocks(input: String) -> String {
let version_re = regex::Regex::new(VERSION_RE).unwrap();
let input = version_re.replace_all(&input, "X.Y.Z").to_owned();
let date_re = regex::Regex::new(DATE_RE).unwrap();
date_re.replace_all(&input, "YYYY-MM-DD HH:MM").into()
}
fn check<P: AsRef<Path>>(
bin: P,
args: &[&str],
exp_code: i32,
stdin_behavior: Behavior,
stdout_behavior: Behavior,
stderr_behavior: Behavior,
) {
let golden_stdin = match stdin_behavior {
Behavior::Null => process::Stdio::null(),
Behavior::File(path) => File::open(path).unwrap().into(),
Behavior::Literal(_) => panic!("Literals not supported for stdin"),
};
let exp_stdout = match stdout_behavior {
Behavior::Null => "".to_owned(),
Behavior::File(path) => read_golden(&path),
Behavior::Literal(text) => text,
};
let exp_stderr = match stderr_behavior {
Behavior::Null => "".to_owned(),
Behavior::File(path) => read_golden(&path),
Behavior::Literal(text) => text,
};
let result = process::Command::new(bin.as_ref())
.args(args)
.stdin(golden_stdin)
.output()
.expect("Failed to execute subprocess");
let code = result
.status
.code()
.expect("Subprocess didn't exit cleanly");
let stdout =
apply_mocks(String::from_utf8(result.stdout).expect("Stdout not is not valid UTF-8"));
let stderr =
apply_mocks(String::from_utf8(result.stderr).expect("Stderr not is not valid UTF-8"));
if exp_code != code || exp_stdout != stdout || exp_stderr != stderr {
eprintln!("Exit code: {}", code);
eprintln!("stdout:\n{}", stdout);
eprintln!("stderr:\n{}", stderr);
assert_eq!(exp_code, code);
assert_eq!(exp_stdout, stdout);
assert_eq!(exp_stderr, stderr);
}
}
#[test]
fn test_cli_help() {
fn check_with_args(args: &[&str]) {
check(
&bin_path("endbasic"),
args,
0,
Behavior::Null,
Behavior::File(src_path("tests/cli/help.out")),
Behavior::Null,
);
}
check_with_args(&["-h"]);
check_with_args(&["--help"]);
check_with_args(&["--version", "--help"]);
check_with_args(&["the", "--help", "flag always wins"]);
}
#[cfg(not(target_os = "linux"))]
#[test]
fn test_cli_program_name_uses_arg0() {
struct DeleteOnDrop<'a> {
path: &'a Path,
}
impl<'a> Drop for DeleteOnDrop<'a> {
fn drop(&mut self) {
let _best_effort_removal = fs::remove_file(self.path);
}
}
let original = bin_path("endbasic");
let custom = self_dir()
.join("custom-name")
.with_extension(env::consts::EXE_EXTENSION);
let _delete_custom = DeleteOnDrop { path: &custom };
fs::copy(&original, &custom).unwrap();
check(
&custom,
&["one", "two", "three"],
2,
Behavior::Null,
Behavior::Null,
Behavior::Literal(
"Usage error: Too many arguments\nType custom-name --help for more information\n"
.to_owned(),
),
);
}
#[test]
fn test_cli_too_many_args() {
check(
&bin_path("endbasic"),
&["foo", "bar"],
2,
Behavior::Null,
Behavior::Null,
Behavior::Literal(
"Usage error: Too many arguments\nType endbasic --help for more information\n"
.to_owned(),
),
);
}
#[test]
fn test_cli_unknown_option() {
check(
&bin_path("endbasic"),
&["-Z", "some-file"],
2,
Behavior::Null,
Behavior::Null,
Behavior::Literal(
"Usage error: Unrecognized option: 'Z'\nType endbasic --help for more information\n"
.to_owned(),
),
);
}
#[test]
fn test_cli_version() {
fn check_with_args(args: &[&str]) {
check(
&bin_path("endbasic"),
args,
0,
Behavior::Null,
Behavior::File(src_path("tests/cli/version.out")),
Behavior::Null,
);
}
check_with_args(&["--version"]);
check_with_args(&["the", "--version", "flag wins over arguments"]);
}
#[test]
fn test_example_custom_commands() {
check(
bin_path("examples/custom-commands"),
&[],
0,
Behavior::Null,
Behavior::File(src_path("examples/custom-commands.out")),
Behavior::Null,
);
}
#[test]
fn test_example_minimal() {
check(
bin_path("examples/minimal"),
&[],
0,
Behavior::Null,
Behavior::File(src_path("examples/minimal.out")),
Behavior::Null,
);
}
#[test]
fn test_lang_control_flow() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/control-flow.bas")],
0,
Behavior::Null,
Behavior::File(src_path("tests/lang/control-flow.out")),
Behavior::Null,
);
}
#[test]
fn test_lang_exec_error() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/exec-error.bas")],
1,
Behavior::Null,
Behavior::File(src_path("tests/lang/exec-error.out")),
Behavior::File(src_path("tests/lang/exec-error.err")),
);
}
#[test]
fn test_lang_hello() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/hello.bas")],
0,
Behavior::Null,
Behavior::File(src_path("tests/lang/hello.out")),
Behavior::Null,
);
}
#[test]
fn test_lang_lexer_error() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/lexer-error.bas")],
1,
Behavior::Null,
Behavior::File(src_path("tests/lang/lexer-error.out")),
Behavior::File(src_path("tests/lang/lexer-error.err")),
);
}
#[test]
fn test_lang_no_repl_commands() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/no-repl-commands.bas")],
1,
Behavior::Null,
Behavior::File(src_path("tests/lang/no-repl-commands.out")),
Behavior::File(src_path("tests/lang/no-repl-commands.err")),
);
}
#[test]
fn test_lang_parser_error() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/parser-error.bas")],
1,
Behavior::Null,
Behavior::File(src_path("tests/lang/parser-error.out")),
Behavior::File(src_path("tests/lang/parser-error.err")),
);
}
#[test]
fn test_lang_types() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/types.bas")],
0,
Behavior::Null,
Behavior::File(src_path("tests/lang/types.out")),
Behavior::Null,
);
}
#[test]
fn test_lang_utf8() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/utf8.bas")],
0,
Behavior::File(src_path("tests/lang/utf8.in")),
Behavior::File(src_path("tests/lang/utf8.out")),
Behavior::Null,
);
}
#[test]
fn test_lang_yes_no() {
check(
bin_path("endbasic"),
&[&src_str("tests/lang/yes-no.bas")],
0,
Behavior::File(src_path("tests/lang/yes-no.in")),
Behavior::File(src_path("tests/lang/yes-no.out")),
Behavior::Null,
);
}
#[test]
fn test_repl_dir() {
let dir = tempfile::tempdir().unwrap();
let subdir = dir.path().join("subdir"); check(
bin_path("endbasic"),
&["--programs-dir", subdir.to_str().unwrap()],
0,
Behavior::File(src_path("tests/repl/dir.in")),
Behavior::File(src_path("tests/repl/dir.out")),
Behavior::Null,
);
}
#[test]
fn test_repl_editor() {
check(
bin_path("endbasic"),
&[],
0,
Behavior::File(src_path("tests/repl/editor.in")),
Behavior::File(src_path("tests/repl/editor.out")),
Behavior::Null,
);
}
#[test]
fn test_repl_help() {
check(
bin_path("endbasic"),
&[],
0,
Behavior::File(src_path("tests/repl/help.in")),
Behavior::File(src_path("tests/repl/help.out")),
Behavior::Null,
);
}
#[test]
fn test_repl_interactive() {
check(
bin_path("endbasic"),
&[],
0,
Behavior::File(src_path("tests/repl/interactive.in")),
Behavior::File(src_path("tests/repl/interactive.out")),
Behavior::Null,
);
}
#[test]
fn test_repl_load_save() {
let dir = tempfile::tempdir().unwrap();
fs::copy(
&src_path("tests/lang/hello.bas"),
&dir.path().join("hello.bas"),
)
.unwrap();
assert!(!dir.path().join("hello2.bas").exists());
check(
bin_path("endbasic"),
&["--programs-dir", dir.path().to_str().unwrap()],
0,
Behavior::File(src_path("tests/repl/load-save.in")),
Behavior::File(src_path("tests/repl/load-save.out")),
Behavior::Null,
);
assert!(dir.path().join("hello2.bas").exists());
}
#[test]
fn test_repl_state_sharing() {
check(
bin_path("endbasic"),
&[],
0,
Behavior::File(src_path("tests/repl/state-sharing.in")),
Behavior::File(src_path("tests/repl/state-sharing.out")),
Behavior::Null,
);
}