Skip to content

Commit

Permalink
Implement saving of test artifacts to a directory
Browse files Browse the repository at this point in the history
  • Loading branch information
dylanmckay committed May 3, 2020
1 parent c8ac450 commit 652ee63
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 19 deletions.
10 changes: 9 additions & 1 deletion src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

#[cfg(feature = "clap")] pub mod clap;

use std::path::PathBuf;
use std::path::{Path, PathBuf};
use std::collections::HashMap;
use std::fmt;
use tempfile::NamedTempFile;
Expand Down Expand Up @@ -82,6 +82,14 @@ impl Config
self.test_paths.push(PathBuf::from(path.into()));
}

/// Gets an iterator over all test search directories.
pub fn test_search_directories(&self) -> impl Iterator<Item=&Path> {
self.test_paths.iter().filter(|p| {
println!("test path file name: {:?}", p.file_name());
p.is_dir()
}).map(PathBuf::as_ref)
}

/// Checks if a given extension will have tests run on it
pub fn is_extension_supported(&self, extension: &str) -> bool {
self.supported_file_extensions.iter().
Expand Down
2 changes: 1 addition & 1 deletion src/config/clap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const SHOW_OPTION_VALUES: &'static [(&'static str, fn(&Config, &mut dyn Write) -
("test-file-paths", |config, writer| {
let test_file_paths = crate::run::find_files::with_config(config).unwrap();
for test_file_path in test_file_paths {
writeln!(writer, "{}", test_file_path)?;
writeln!(writer, "{}", test_file_path.absolute.display())?;
}

Ok(())
Expand Down
12 changes: 6 additions & 6 deletions src/event_handler/default.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,22 +80,22 @@ impl super::EventHandler for EventHandler {
pub fn result(result: &TestResult, verbose: bool, config: &Config) {
match result.overall_result {
TestResultKind::Pass => {
print::success(format!("PASS :: {}", result.path.display()));
print::success(format!("PASS :: {}", result.path.relative.display()));
},
TestResultKind::UnexpectedPass => {
print::failure(format!("UNEXPECTED PASS :: {}", result.path.display()));
print::failure(format!("UNEXPECTED PASS :: {}", result.path.relative.display()));
},
TestResultKind::Skip => {
print::line();
print::warning(format!(
"SKIP :: {} (test does not contain any test commands, perhaps you meant to add a 'CHECK'?)",
result.path.display()));
result.path.relative.display()));
print::line();
},
TestResultKind::Error { ref message } => {
if verbose { print::line(); }

print::error(format!("ERROR :: {}", result.path.display()));
print::error(format!("ERROR :: {}", result.path.relative.display()));

if verbose {
print::textln(message);
Expand All @@ -106,7 +106,7 @@ pub fn result(result: &TestResult, verbose: bool, config: &Config) {
TestResultKind::Fail { ref reason, ref hint } => {
if verbose { print::line(); }

print::failure(format!("FAIL :: {}", result.path.display()));
print::failure(format!("FAIL :: {}", result.path.relative.display()));

// FIXME: improve formatting

Expand All @@ -125,7 +125,7 @@ pub fn result(result: &TestResult, verbose: bool, config: &Config) {
}
},
TestResultKind::ExpectedFailure => {
print::warning(format!("XFAIL :: {}", result.path.display()));
print::warning(format!("XFAIL :: {}", result.path.relative.display()));
},
}

Expand Down
2 changes: 1 addition & 1 deletion src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ pub struct CheckFailureInfo {
pub struct TestResult
{
/// A path to the test.
pub path: PathBuf,
pub path: TestFilePath,
/// The kind of result.
pub overall_result: TestResultKind,
pub individual_run_results: Vec<(TestResultKind, Invocation, run::CommandLine, ProgramOutput)>,
Expand Down
5 changes: 2 additions & 3 deletions src/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ use crate::model::*;

use regex::Regex;
use std::mem;
use std::path::Path;

lazy_static! {
static ref DIRECTIVE_REGEX: Regex = Regex::new("([A-Z-]+):(.*)").unwrap();
static ref IDENTIFIER_REGEX: Regex = Regex::new("^[a-zA-Z_][a-zA-Z0-9_]*$").unwrap();
}

/// Parses a test file
pub fn test_file<P,I>(path: TestFilePath, chars: I) -> Result<TestFile, String>
where P: AsRef<Path>, I: Iterator<Item=char> {
pub fn test_file<I>(path: TestFilePath, chars: I) -> Result<TestFile, String>
where I: Iterator<Item=char> {
let mut commands = Vec::new();
let test_body: String = chars.collect();

Expand Down
176 changes: 174 additions & 2 deletions src/run/find_files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use crate::{Config, model::TestFilePath};

use std;
use std::path::Path;
use std::path::{Path, PathBuf};
use walkdir::WalkDir;

/// Recursively finds tests for the given paths.
Expand All @@ -18,7 +18,8 @@ pub fn with_config(config: &Config) -> Result<Vec<TestFilePath>, String> {
}

let test_paths = absolute_paths.into_iter().map(|absolute_path| {
let relative_path = // TODO: find most specific path in search tree. failing that, find most common path from all test paths and use that. otherwise use a random one like 'test'. maybe rename 'relative_path' to 'relative_path_for_display'.
let relative_path = relative_path::compute(&absolute_path, config).expect("could not compute relative path");

TestFilePath { absolute: absolute_path, relative: relative_path }
}).collect();

Expand All @@ -41,6 +42,177 @@ pub fn in_path(path: &str,
}
}

mod relative_path {
use crate::Config;
use std::path::{Path, PathBuf};

pub fn compute(test_absolute_path: &Path, config: &Config)
-> Option<PathBuf> {
let mut take_path_relative_to_dir = None;

if take_path_relative_to_dir.is_none() {
if let Some(least_specific_parent_test_search_directory_path) =
least_specific_parent_test_search_directory_path(test_absolute_path, config) {
take_path_relative_to_dir = Some(least_specific_parent_test_search_directory_path);
}
}

if take_path_relative_to_dir.is_none() {
if let Some(most_common_test_path_ancestor) =
most_common_test_path_ancestor(test_absolute_path, config) {
take_path_relative_to_dir = Some(most_common_test_path_ancestor);
}
}

take_path_relative_to_dir.map(|relative_to| {
test_absolute_path.strip_prefix(relative_to).expect("relative path computation failed: not a prefix").to_owned()
})
}

/// Attempt to find the most specific prefix directory from the test search paths in the config.
fn least_specific_parent_test_search_directory_path(test_absolute_path: &Path, config: &Config)
-> Option<PathBuf> {
// N.B. we iterate over the test paths here. We don't check for the directory's actual
// existence on the filesystem. This makes testing easier, but also: test paths can only
// be strict prefixes/supersets of other test paths if they ARE directories.
let matching_parent_test_search_directories = config.test_paths.iter()
.filter(|possible_dir_path| test_absolute_path.starts_with(possible_dir_path));

let least_specific_matching_test_search_directory = matching_parent_test_search_directories.min_by_key(|p| p.components().count());

if let Some(least_specific_matching_test_search_directory) = least_specific_matching_test_search_directory {
Some(least_specific_matching_test_search_directory.to_owned())
} else {
None
}
}

/// Otherwise, find the most common path from all the test file paths.
///
/// NOTE: this will return `None` in several cases, such as if there is only one test path,
/// or On windows in the case where there are tests located on several different device drives.
fn most_common_test_path_ancestor(test_absolute_path: &Path, config: &Config)
-> Option<PathBuf> {
// different disk drives at the same time.
{
let initial_current_path_containing_everything_so_far = test_absolute_path.parent().unwrap();
let mut current_path_containing_everything_so_far = initial_current_path_containing_everything_so_far;

for test_path in config.test_paths.iter() {
if !test_path.starts_with(current_path_containing_everything_so_far) {
let common_ancestor = test_path.ancestors().find(|p| current_path_containing_everything_so_far.starts_with(p));

if let Some(common_ancestor) = common_ancestor {
// The common ancestor path may be empty if the files are on different
// devices.
if common_ancestor.file_name().is_some() {
println!("common ancestor: {:?}", common_ancestor.file_name());
current_path_containing_everything_so_far = common_ancestor;
}
} else {
// N.B. we only ever expect no common ancestor on Windows
// where paths may be on different devices. This should be uncommon.
// We cannot use this logic to compute the relative path in this scenario.
}
}
}

if current_path_containing_everything_so_far != initial_current_path_containing_everything_so_far {
Some(current_path_containing_everything_so_far.to_owned())
} else {
None // no common prefix path could be calculated from the test paths
}

}
}

#[cfg(test)]
mod test {
use crate::Config;
use std::path::Path;

#[test]
fn test_compute() {
let config = Config {
test_paths: [
"/home/foo/projects/cool-project/tests/",
"/home/foo/projects/cool-project/tests/run-pass/",
"/home/foo/projects/cool-project/tests/run-fail/",
].iter().map(|p| Path::new(p).to_owned()).collect(),
..Config::default()
};

assert_eq!(super::compute(
&Path::new("/home/foo/projects/cool-project/tests/run-pass/test1.txt"), &config),
Some(Path::new("run-pass/test1.txt").to_owned()));
}

#[test]
fn test_least_specific_parent_test_search_directory_path_when_all_test_paths_are_directories() {
let config = Config {
test_paths: [
"/home/foo/projects/cool-project/tests/",
"/home/foo/projects/cool-project/tests/run-pass/",
"/home/foo/projects/cool-project/tests/run-fail/",
].iter().map(|p| Path::new(p).to_owned()).collect(),
..Config::default()
};

assert_eq!(super::least_specific_parent_test_search_directory_path(
&Path::new("/home/foo/projects/cool-project/tests/run-pass/test1.txt"), &config),
Some(Path::new("/home/foo/projects/cool-project/tests/").to_owned()));
}

#[test]
fn test_least_specific_parent_test_search_directory_path_when_one_test_path_directory() {
let config = Config {
test_paths: [
"/home/foo/projects/cool-project/tests/",
].iter().map(|p| Path::new(p).to_owned()).collect(),
..Config::default()
};

assert_eq!(super::least_specific_parent_test_search_directory_path(
&Path::new("/home/foo/projects/cool-project/tests/run-pass/test1.txt"), &config),
Some(Path::new("/home/foo/projects/cool-project/tests/").to_owned()));
}

#[test]
fn test_most_common_test_path_ancestor_when_all_paths_are_absolute() {
let config = Config {
test_paths: [
"/home/foo/projects/cool-project/tests/run-pass/test1.txt",
"/home/foo/projects/cool-project/tests/run-pass/test2.txt",
"/home/foo/projects/cool-project/tests/run-fail/test3.txt",
].iter().map(|p| Path::new(p).to_owned()).collect(),
..Config::default()
};

assert_eq!(super::most_common_test_path_ancestor(
&Path::new("/home/foo/projects/cool-project/tests/run-pass/test1.txt"), &config),
Some(Path::new("/home/foo/projects/cool-project/tests").to_owned()));
}


#[test]
fn test_most_common_test_path_ancestor_when_all_paths_absolute_on_different_drives() {
let config = Config {
test_paths: [
"C:/tests/run-pass/test1.txt",
"C:/tests/run-pass/test2.txt",
"Z:/tests/run-fail/test3.txt",
"Z:/tests/run-fail/test4.txt",
].iter().map(|p| Path::new(p).to_owned()).collect(),
..Config::default()
};

assert_eq!(super::most_common_test_path_ancestor(
&Path::new("C:/tests/run-pass/test2.txt"), &config),
None);
}
}
}

fn tests_in_dir(path: &str,
config: &Config) -> Result<Vec<String>,String> {
let tests = files_in_dir(path)?.into_iter()
Expand Down
10 changes: 5 additions & 5 deletions src/run/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ pub fn tests<F>(

let mut has_failure = false;
for test_file_path in test_paths {
let test_file = util::parse_test(&test_file_path).unwrap();
let test_file = util::parse_test(test_file_path).unwrap();
let is_successful = self::single_file(&test_file, &mut event_handler, &config, &artifact_config);

if !is_successful { has_failure = true; }
Expand Down Expand Up @@ -160,14 +160,14 @@ mod save_artifacts {

pub fn individual_run_result(run_number: usize, result_kind: &TestResultKind, command_line: &CommandLine, output: &ProgramOutput, test_file: &TestFile, config: &Config) {
let dir_test_file = format!("run-command-{}", run_number);
let dir_run_result = test_file.relative_path.join(format!("run-command-{}", run_number));
let dir_run_result = test_file.path.relative.join(format!("run-command-{}", run_number));

save(&dir_run_result.join("result.txt"), config, || {
format!("{:?}\n", result_kind)
format!("{:#?}\n", result_kind)
});

save(&dir_run_result.join("stdout.txt"), config, || output.stdout);
save(&dir_run_result.join("stderr.txt"), config, || output.stderr);
save(&dir_run_result.join("stdout.txt"), config, || &output.stdout[..]);
save(&dir_run_result.join("stderr.txt"), config, || &output.stderr[..]);
}

fn save<C>(relative_path: &Path, config: &Config, render: impl FnOnce() -> C )
Expand Down

0 comments on commit 652ee63

Please sign in to comment.