blob: cca7cf94ba9e7d4d086b8f384781714516dfd842 [file] [log] [blame] [edit]
use crate::cargo::{self, Metadata};
use crate::dependencies::{self, Dependency};
use crate::directory::Directory;
use crate::env::Update;
use crate::error::{Error, Result};
use crate::flock::Lock;
use crate::manifest::{Bin, Build, Config, Manifest, Name, Package, Workspace};
use crate::message::{self, Fail, Warn};
use crate::normalize::{self, Context, Variations};
use crate::{features, rustflags, Expected, Runner, Test};
use std::collections::BTreeMap as Map;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fs::{self, File};
use std::mem;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub struct Project {
pub dir: Directory,
source_dir: Directory,
pub target_dir: Directory,
pub name: String,
update: Update,
pub has_pass: bool,
has_compile_fail: bool,
pub features: Option<Vec<String>>,
pub workspace: Directory,
pub path_dependencies: Vec<PathDependency>,
manifest: Manifest,
}
#[derive(Debug)]
pub struct PathDependency {
pub name: String,
pub normalized_path: Directory,
}
impl Runner {
pub fn run(&mut self) {
let mut tests = expand_globs(&self.tests);
filter(&mut tests);
let (project, _lock) = (|| {
let project = self.prepare(&tests)?;
let lock = Lock::acquire(path!(project.dir / ".lock"));
self.write(&project)?;
Ok((project, lock))
})()
.unwrap_or_else(|err| {
message::prepare_fail(err);
panic!("tests failed");
});
print!("\n\n");
let len = tests.len();
let mut failures = 0;
if tests.is_empty() {
message::no_tests_enabled();
} else {
for test in tests {
if let Err(err) = test.run(&project) {
failures += 1;
message::test_fail(err);
}
}
}
print!("\n\n");
if failures > 0 && project.name != "trybuild-tests" {
panic!("{} of {} tests failed", failures, len);
}
}
fn prepare(&self, tests: &[ExpandedTest]) -> Result<Project> {
let Metadata {
target_directory: target_dir,
workspace_root: workspace,
packages,
} = cargo::metadata()?;
let crate_name = env::var("CARGO_PKG_NAME").map_err(Error::PkgName)?;
let mut has_pass = false;
let mut has_compile_fail = false;
for e in tests {
match e.test.expected {
Expected::Pass => has_pass = true,
Expected::CompileFail => has_compile_fail = true,
}
}
let source_dir = env::var_os("CARGO_MANIFEST_DIR")
.map(Directory::from)
.ok_or(Error::ProjectDir)?;
let source_manifest = dependencies::get_manifest(&source_dir);
let mut features = features::find();
let path_dependencies = source_manifest
.dependencies
.iter()
.filter_map(|(name, dep)| {
let path = dep.path.as_ref()?;
if packages.iter().any(|p| &p.name == name) {
// Skip path dependencies coming from the workspace itself
None
} else {
Some(PathDependency {
name: name.clone(),
normalized_path: path.canonicalize().ok()?,
})
}
})
.collect();
let project_dir = path!(target_dir / "tests" / crate_name /);
fs::create_dir_all(&project_dir)?;
let project_name = format!("{}-tests", crate_name);
let manifest = self.make_manifest(
crate_name,
&workspace,
&project_name,
&source_dir,
tests,
source_manifest,
);
if let Some(enabled_features) = &mut features {
enabled_features.retain(|feature| manifest.features.contains_key(feature));
}
Ok(Project {
dir: project_dir,
source_dir,
target_dir,
name: project_name,
update: Update::env()?,
has_pass,
has_compile_fail,
features,
workspace,
path_dependencies,
manifest,
})
}
fn write(&self, project: &Project) -> Result<()> {
let manifest_toml = toml::to_string(&project.manifest)?;
let config = self.make_config();
let config_toml = toml::to_string(&config)?;
fs::create_dir_all(path!(project.dir / ".cargo"))?;
fs::write(path!(project.dir / ".cargo" / "config"), config_toml)?;
fs::write(path!(project.dir / "Cargo.toml"), manifest_toml)?;
fs::write(path!(project.dir / "main.rs"), b"fn main() {}\n")?;
cargo::build_dependencies(project)?;
Ok(())
}
fn make_manifest(
&self,
crate_name: String,
workspace: &Directory,
project_name: &str,
source_dir: &Directory,
tests: &[ExpandedTest],
source_manifest: dependencies::Manifest,
) -> Manifest {
let workspace_manifest = dependencies::get_workspace_manifest(workspace);
let features = source_manifest
.features
.keys()
.map(|feature| {
let enable = format!("{}/{}", crate_name, feature);
(feature.clone(), vec![enable])
})
.collect();
let mut manifest = Manifest {
package: Package {
name: project_name.to_owned(),
version: "0.0.0".to_owned(),
edition: source_manifest.package.edition,
publish: false,
},
features,
dependencies: Map::new(),
target: source_manifest.target,
bins: Vec::new(),
workspace: Some(Workspace {}),
// Within a workspace, only the [patch] and [replace] sections in
// the workspace root's Cargo.toml are applied by Cargo.
patch: workspace_manifest.patch,
replace: workspace_manifest.replace,
};
manifest.dependencies.extend(source_manifest.dependencies);
manifest
.dependencies
.extend(source_manifest.dev_dependencies);
for target in manifest.target.values_mut() {
let dev_dependencies = mem::replace(&mut target.dev_dependencies, Map::new());
target.dependencies.extend(dev_dependencies);
}
manifest.dependencies.insert(
crate_name,
Dependency {
version: None,
path: Some(source_dir.clone()),
default_features: false,
features: Vec::new(),
git: None,
branch: None,
tag: None,
rev: None,
rest: Map::new(),
},
);
manifest.bins.push(Bin {
name: Name(project_name.to_owned()),
path: Path::new("main.rs").to_owned(),
});
for expanded in tests {
if expanded.error.is_none() {
manifest.bins.push(Bin {
name: expanded.name.clone(),
path: source_dir.join(&expanded.test.path),
});
}
}
manifest
}
fn make_config(&self) -> Config {
Config {
build: Build {
rustflags: rustflags::make_vec(),
},
}
}
}
impl Test {
fn run(&self, project: &Project, name: &Name) -> Result<()> {
let show_expected = project.has_pass && project.has_compile_fail;
message::begin_test(self, show_expected);
check_exists(&self.path)?;
let output = cargo::build_test(project, name)?;
let success = output.status.success();
let stdout = output.stdout;
let stderr = normalize::diagnostics(
output.stderr,
Context {
krate: &name.0,
source_dir: &project.source_dir,
workspace: &project.workspace,
input_file: &self.path,
path_dependencies: &project.path_dependencies,
},
);
let check = match self.expected {
Expected::Pass => Test::check_pass,
Expected::CompileFail => Test::check_compile_fail,
};
check(self, project, name, success, stdout, stderr)
}
fn check_pass(
&self,
project: &Project,
name: &Name,
success: bool,
build_stdout: Vec<u8>,
variations: Variations,
) -> Result<()> {
let preferred = variations.preferred();
if !success {
message::failed_to_build(preferred);
return Err(Error::CargoFail);
}
let mut output = cargo::run_test(project, name)?;
output.stdout.splice(..0, build_stdout);
message::output(preferred, &output);
if output.status.success() {
Ok(())
} else {
Err(Error::RunFailed)
}
}
fn check_compile_fail(
&self,
project: &Project,
_name: &Name,
success: bool,
build_stdout: Vec<u8>,
variations: Variations,
) -> Result<()> {
let preferred = variations.preferred();
if success {
message::should_not_have_compiled();
message::fail_output(Fail, &build_stdout);
message::warnings(preferred);
return Err(Error::ShouldNotHaveCompiled);
}
let stderr_path = self.path.with_extension("stderr");
if !stderr_path.exists() {
match project.update {
Update::Wip => {
let wip_dir = Path::new("wip");
fs::create_dir_all(wip_dir)?;
let gitignore_path = wip_dir.join(".gitignore");
fs::write(gitignore_path, "*\n")?;
let stderr_name = stderr_path
.file_name()
.unwrap_or_else(|| OsStr::new("test.stderr"));
let wip_path = wip_dir.join(stderr_name);
message::write_stderr_wip(&wip_path, &stderr_path, preferred);
fs::write(wip_path, preferred).map_err(Error::WriteStderr)?;
}
Update::Overwrite => {
message::overwrite_stderr(&stderr_path, preferred);
fs::write(stderr_path, preferred).map_err(Error::WriteStderr)?;
}
}
message::fail_output(Warn, &build_stdout);
return Ok(());
}
let expected = fs::read_to_string(&stderr_path)
.map_err(Error::ReadStderr)?
.replace("\r\n", "\n");
if variations.any(|stderr| expected == stderr) {
message::ok();
return Ok(());
}
match project.update {
Update::Wip => {
message::mismatch(&expected, preferred);
Err(Error::Mismatch)
}
Update::Overwrite => {
message::overwrite_stderr(&stderr_path, preferred);
fs::write(stderr_path, preferred).map_err(Error::WriteStderr)?;
Ok(())
}
}
}
}
fn check_exists(path: &Path) -> Result<()> {
if path.exists() {
return Ok(());
}
match File::open(path) {
Ok(_) => Ok(()),
Err(err) => Err(Error::Open(path.to_owned(), err)),
}
}
#[derive(Debug)]
struct ExpandedTest {
name: Name,
test: Test,
error: Option<Error>,
}
fn expand_globs(tests: &[Test]) -> Vec<ExpandedTest> {
fn glob(pattern: &str) -> Result<Vec<PathBuf>> {
let mut paths = glob::glob(pattern)?
.map(|entry| entry.map_err(Error::from))
.collect::<Result<Vec<PathBuf>>>()?;
paths.sort();
Ok(paths)
}
fn bin_name(i: usize) -> Name {
Name(format!("trybuild{:03}", i))
}
let mut vec = Vec::new();
for test in tests {
let mut expanded = ExpandedTest {
name: bin_name(vec.len()),
test: test.clone(),
error: None,
};
if let Some(utf8) = test.path.to_str() {
if utf8.contains('*') {
match glob(utf8) {
Ok(paths) => {
for path in paths {
vec.push(ExpandedTest {
name: bin_name(vec.len()),
test: Test {
path,
expected: expanded.test.expected,
},
error: None,
});
}
continue;
}
Err(error) => expanded.error = Some(error),
}
}
}
vec.push(expanded);
}
vec
}
impl ExpandedTest {
fn run(self, project: &Project) -> Result<()> {
match self.error {
None => self.test.run(project, &self.name),
Some(error) => {
let show_expected = false;
message::begin_test(&self.test, show_expected);
Err(error)
}
}
}
}
// Filter which test cases are run by trybuild.
//
// $ cargo test -- ui trybuild=tuple_structs.rs
//
// The first argument after `--` must be the trybuild test name i.e. the name of
// the function that has the #[test] attribute and calls trybuild. That's to get
// Cargo to run the test at all. The next argument starting with `trybuild=`
// provides a filename filter. Only test cases whose filename contains the
// filter string will be run.
#[allow(clippy::needless_collect)] // false positive https://github.com/rust-lang/rust-clippy/issues/5991
fn filter(tests: &mut Vec<ExpandedTest>) {
let filters = env::args_os()
.flat_map(OsString::into_string)
.filter_map(|mut arg| {
const PREFIX: &str = "trybuild=";
if arg.starts_with(PREFIX) && arg != PREFIX {
Some(arg.split_off(PREFIX.len()))
} else {
None
}
})
.collect::<Vec<String>>();
if filters.is_empty() {
return;
}
tests.retain(|t| {
filters
.iter()
.any(|f| t.test.path.to_string_lossy().contains(f))
});
}