extern crate difference;
extern crate regex;
extern crate term;
extern crate log;
extern crate simple_logger;
use difference::{Changeset, Difference};
use std::fs;
use std::io::{Cursor, Write};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use term::Terminal;
struct TestConfig
{
crate_path: PathBuf,
data_path: PathBuf,
}
#[test]
fn check_expansions()
{
let root_path = find_root().unwrap();
let crate_path = root_path.join("intercom-attributes");
let config = TestConfig {
data_path: crate_path.join("tests/data"),
crate_path,
};
build_crate("intercom");
build_crate("intercom-fmt");
let failed = test_path(&config, "macro", TestMode::Macro)
+ test_path(&config, "ui", TestMode::UI)
+ test_path(&config, "run", TestMode::Run);
assert_eq!(failed, 0, "{} tests failed", failed);
}
struct OutputResult
{
message: String,
actual: String,
expected_path: Option<String>,
changeset: Changeset,
}
impl OutputResult
{
pub fn show_diff(&self, ctx_size: usize, t: &mut dyn Terminal<Output = std::io::Stdout>)
{
enum Lines
{
Expected(String),
Actual(String),
Same(String),
}
let mut all_lines = vec![];
for cs in &self.changeset.diffs {
match cs {
Difference::Same(ref x) => all_lines.push(
x.lines()
.map(|l| Lines::Same(format!(" {}", l)))
.collect::<Vec<_>>(),
),
Difference::Add(ref x) => all_lines.push(
x.lines()
.map(|l| Lines::Expected(format!("E {}", l)))
.collect::<Vec<_>>(),
),
Difference::Rem(ref x) => all_lines.push(
x.lines()
.map(|l| Lines::Actual(format!("A {}", l)))
.collect::<Vec<_>>(),
),
}
}
let all_lines = all_lines.into_iter().flat_map(|i| i).collect::<Vec<_>>();
let mut ctx_counter = 0;
let mut ctx_lines = vec![];
for l in &all_lines {
match l {
Lines::Expected(..) | Lines::Actual(..) => ctx_counter = ctx_size * 2 + 1,
_ if ctx_counter > 0 => ctx_counter -= 1,
_ => {}
}
ctx_lines.push(ctx_counter);
}
for _ in 0..ctx_size {
if ctx_counter > 0 {
ctx_counter -= 1
}
ctx_lines.push(ctx_counter);
}
let mut skip = false;
for i in 0..all_lines.len() {
if ctx_lines[i + ctx_size] == 0 {
if !skip {
writeln!(t, "...snip").expect("Write failed");
skip = true;
}
continue;
}
skip = false;
match all_lines[i] {
Lines::Same(ref x) => {
t.reset().unwrap();
writeln!(t, "{}", x).expect("Write failed");
}
Lines::Expected(ref x) => {
t.fg(term::color::GREEN).unwrap();
writeln!(t, "{}", x).expect("Write failed");
}
Lines::Actual(ref x) => {
t.fg(term::color::RED).unwrap();
writeln!(t, "{}", x).expect("Write failed");
}
}
}
t.reset().unwrap();
}
}
fn build(cwd: &str, path: &str, mode: TestMode) -> (bool, String, String)
{
#[cfg(debug_assertions)]
let conf = "debug";
#[cfg(not(debug_assertions))]
let conf = "release";
let crate_type = match mode {
TestMode::Run => "bin",
_ => "lib",
};
let mut cmd = std::process::Command::new("rustc");
#[rustfmt::skip]
cmd.current_dir(cwd)
.env("CARGO_PKG_NAME", "TestLib")
.args(&[
"--crate-name", "testcrate",
"--crate-type", crate_type,
"--out-dir", "tests/out",
"-L", &format!("all=../target/{}/deps", conf),
"--extern", &format!("intercom=../target/{}/libintercom.rlib", conf),
path,
]);
if let TestMode::Macro = mode {
cmd.arg("-Zunpretty=expanded");
}
let output = cmd.output().expect("Failed to execute");
(
output.status.success(),
String::from_utf8(output.stdout).expect("Bad output"),
String::from_utf8(output.stderr).expect("Bad stderr"),
)
}
fn format(code: &str) -> String
{
let intercom_fmt = find_intercom_fmt().unwrap();
let mut formatter = Command::new(&intercom_fmt)
.arg("-p")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn()
.expect(&format!("Failed to launch formatter {:?}", intercom_fmt));
{
let mut stdin = formatter.stdin.as_mut().unwrap();
std::io::copy(&mut Cursor::new(code.as_bytes()), &mut stdin)
.expect("Failed to send data for formatting.");
}
let output = formatter
.wait_with_output()
.expect("Failed to read formatter results.");
let status = output.status;
assert!(
status.success() && status.code() == Some(0),
"Formatting failed with status \"{:?}\".",
status.code()
);
String::from_utf8(output.stdout).expect("Bad output")
}
fn build_crate(module: &str)
{
let mut cmd = Command::new("cargo");
cmd.arg("build");
#[cfg(not(debug_assertions))]
{
cmd.arg("--release");
}
let status = cmd
.current_dir(find_root().unwrap().join(module))
.status()
.expect(&format!("Failed to build crate \"{0}\"", module));
assert!(status.success());
}
fn find_root() -> std::io::Result<PathBuf>
{
let mut root_path = std::env::current_exe()?;
loop {
if root_path.join("Cargo.toml").exists() {
break;
}
assert!(root_path.pop());
}
Ok(root_path)
}
fn find_intercom_fmt() -> std::io::Result<PathBuf>
{
let mut intercom_fmt_dir = std::env::current_exe()?;
assert!(intercom_fmt_dir.pop()); let original_path = format!("{:?}", intercom_fmt_dir);
loop {
if let Some(intercom_fmt) = has_intercom_fmt(&intercom_fmt_dir) {
return Ok(intercom_fmt);
}
assert!(
intercom_fmt_dir.pop(),
"Could not locate intercom-fmt. Search started from \"{0}\".",
original_path
);
}
}
fn has_intercom_fmt(dir: &PathBuf) -> Option<PathBuf>
{
#[cfg(windows)]
let intercom_fmt = dir.join("intercom-fmt.exe");
#[cfg(not(windows))]
let intercom_fmt = dir.join("intercom-fmt");
if intercom_fmt.exists() && intercom_fmt.metadata().unwrap().is_file() {
Some(intercom_fmt)
} else {
None
}
}
#[derive(Clone, Copy, PartialEq)]
enum TestMode
{
Macro,
UI,
Run,
}
fn test_path(config: &TestConfig, sub_path: &str, mode: TestMode) -> usize
{
let test_data = fs::read_dir(config.data_path.join(sub_path)).unwrap();
let source_paths = test_data
.into_iter()
.map(|e| e.expect("Failed to read entry").path())
.map(|p| p.to_str().unwrap().to_owned())
.map(|s| s.replace("\\", "/"))
.filter(|p| p.ends_with(".rs"));
let mut failed = 0;
let timestamp_re = regex::RegexBuilder::new(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3}")
.multi_line(true)
.build()
.unwrap();
let bracket_re = regex::Regex::new(r"\[0x.*?\]").unwrap();
for source_path in source_paths {
println!("Testing {}", source_path);
let (success, mut result_stdout, mut result_stderr) =
build(config.crate_path.to_str().unwrap(), &source_path, mode);
if success && mode == TestMode::Run {
let mut cmd = std::process::Command::new("tests/out/testcrate");
let output = cmd.output().expect("Failed to execute");
assert!(output.status.success());
result_stdout = String::from_utf8(output.stdout).expect("Bad output");
result_stderr = String::from_utf8(output.stderr).expect("Bad stderr");
let result_stdout_clean = timestamp_re.replace_all(&result_stdout, "<TIMESTAMP>");
let result_stderr_clean = timestamp_re.replace_all(&result_stderr, "<TIMESTAMP>");
let result_stdout_clean = bracket_re.replace_all(&result_stdout_clean, "[xxxxxxxx]");
let result_stderr_clean = bracket_re.replace_all(&result_stderr_clean, "[xxxxxxxx]");
result_stdout = result_stdout_clean.to_string();
result_stderr = result_stderr_clean.to_string();
}
let result_stdout = result_stdout.replace("\r", "");
let result_stderr = result_stderr.replace("\r", "");
let mut results = vec![];
match mode {
TestMode::Macro => results.extend(assert_test_output(
result_stdout,
result_stderr,
&source_path,
|s| format(s.trim()),
)),
TestMode::Run | TestMode::UI => results.extend(assert_test_output(
result_stdout,
result_stderr,
&source_path,
|s| s,
)),
}
let mut t = term::stdout().unwrap_or_else(|| {
Box::new(term::TerminfoTerminal::new_with_terminfo(
std::io::stdout(),
term::terminfo::TermInfo::from_name("ansi").expect("ansi terminal not supported"),
))
});
for r in results {
if std::env::var("UPDATE_TARGETS").is_ok() && r.expected_path.is_some() {
let mut target_file = fs::File::create(r.expected_path.as_ref().unwrap()).unwrap();
target_file.write_all(r.actual.as_bytes()).expect(&format!(
"Writing target file {} failed",
r.expected_path.as_ref().unwrap()
));
writeln!(
t,
"-----------------------------------------------------------------"
)
.unwrap();
writeln!(t, "UPDATED: {}", r.expected_path.unwrap()).unwrap();
writeln!(
t,
"-----------------------------------------------------------------"
)
.unwrap();
} else {
writeln!(
t,
"-----------------------------------------------------------------"
)
.unwrap();
writeln!(t, "FAIL: {}", r.message).unwrap();
writeln!(t, "").unwrap();
r.show_diff(5, &mut *t);
writeln!(t, "").unwrap();
writeln!(
t,
"-----------------------------------------------------------------"
)
.unwrap();
failed += 1;
}
}
}
failed
}
fn assert_test_output(
result_stdout: String,
result_stderr: String,
source_path: &str,
sanitize: impl Fn(String) -> String,
) -> Vec<OutputResult>
{
vec![
assert_output_with(result_stdout, source_path, "stdout", |s| {
sanitize(strip_path(source_path, s))
}),
assert_output_with(result_stderr, source_path, "stderr", |s| {
strip_path(source_path, s)
}),
]
.into_iter()
.flat_map(|v| v)
.collect()
}
fn assert_output_with(
actual: String,
actual_path: &str,
output_kind: &str,
sanitize: impl Fn(String) -> String,
) -> Option<OutputResult>
{
let name = Path::new(actual_path)
.file_name()
.unwrap()
.to_string_lossy();
let actual = sanitize(actual.clone());
let expected_path_string = format!("{}.{}", actual_path, output_kind);
let expected_path = Path::new(&expected_path_string);
if expected_path.exists() {
let expected = std::fs::read_to_string(expected_path)
.unwrap()
.replace("\r", "");
let expected = sanitize(expected);
if std::env::var("FORCE_UPDATE_TARGETS").is_ok() {
Some(OutputResult {
message: format!("Forcing update"),
changeset: Changeset::new(&actual, &expected, "\n"),
actual: actual,
expected_path: Some(expected_path_string),
})
} else if expected != actual {
Some(OutputResult {
message: format!("{} {} output differs", name, output_kind),
changeset: Changeset::new(&actual, &expected, "\n"),
actual: actual,
expected_path: Some(expected_path_string),
})
} else {
None
}
} else if !actual.is_empty() {
Some(OutputResult {
message: format!("{} {} output was not expected", name, output_kind),
changeset: Changeset::new(&actual, "", "\n"),
actual: actual,
expected_path: None,
})
} else {
None
}
}
fn strip_path(path: &str, source: String) -> String
{
let root = Path::new(path).parent().unwrap().to_string_lossy() + "/";
source.replace(root.as_ref(), "")
}