Skip to content

Commit

Permalink
Add Workspace and PackageFilter
Browse files Browse the repository at this point in the history
Moving towards being able to auto-select packages from workspace
subdir
  • Loading branch information
sourcefrog committed Nov 7, 2023
1 parent fd9165c commit 146b5dd
Show file tree
Hide file tree
Showing 11 changed files with 468 additions and 277 deletions.
4 changes: 2 additions & 2 deletions src/build_dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,8 @@ mod test {
#[test]
fn build_dir_debug_form() {
let options = Options::default();
let root = cargo::find_workspace("testdata/tree/factorial".into()).unwrap();
let build_dir = BuildDir::new(&root, &options, &Console::new()).unwrap();
let workspace = Workspace::open(Utf8Path::new("testdata/tree/factorial")).unwrap();
let build_dir = BuildDir::new(&workspace.dir, &options, &Console::new()).unwrap();
let debug_form = format!("{build_dir:?}");
assert!(
Regex::new(r#"^BuildDir \{ path: "[^"]*[/\\]cargo-mutants-factorial[^"]*" \}$"#)
Expand Down
222 changes: 7 additions & 215 deletions src/cargo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ use std::time::{Duration, Instant};

use anyhow::{anyhow, ensure, Context, Result};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_metadata::Metadata;
use itertools::Itertools;
use serde_json::Value;
use tracing::debug_span;
#[allow(unused_imports)]
use tracing::{debug, error, info, span, trace, warn, Level};

use crate::outcome::PhaseResult;
use crate::package::Package;
use crate::process::{get_command_output, Process};
use crate::source::Package;
use crate::*;

/// Run cargo build, check, or test.
Expand Down Expand Up @@ -50,105 +51,20 @@ pub fn run_cargo(
})
}

/// Return the path of the workspace directory enclosing a given directory.
pub fn find_workspace(path: &Utf8Path) -> Result<Utf8PathBuf> {
ensure!(path.is_dir(), "{path:?} is not a directory");
let cargo_bin = cargo_bin(); // needed for lifetime
let argv: Vec<&str> = vec![&cargo_bin, "locate-project", "--workspace"];
let stdout = get_command_output(&argv, path)
.with_context(|| format!("run cargo locate-project in {path:?}"))?;
let val: Value = serde_json::from_str(&stdout).context("parse cargo locate-project output")?;
let cargo_toml_path: Utf8PathBuf = val["root"]
.as_str()
.with_context(|| format!("cargo locate-project output has no root: {stdout:?}"))?
.to_owned()
.into();
debug!(?cargo_toml_path, "Found workspace root manifest");
ensure!(
cargo_toml_path.is_file(),
"cargo locate-project root {cargo_toml_path:?} is not a file"
);
let root = cargo_toml_path
.parent()
.ok_or_else(|| anyhow!("cargo locate-project root {cargo_toml_path:?} has no parent"))?
.to_owned();
ensure!(
root.is_dir(),
"apparent project root directory {root:?} is not a directory"
);
Ok(root)
}

/// Find the root files for each relevant package in the source tree.
///
/// A source tree might include multiple packages (e.g. in a Cargo workspace),
/// and each package might have multiple targets (e.g. a bin and lib). Test targets
/// are excluded here: we run them, but we don't mutate them.
///
/// Each target has one root file, typically but not necessarily called `src/lib.rs`
/// or `src/main.rs`. This function returns a list of all those files.
///
/// After this, there is one more level of discovery, by walking those root files
/// to find `mod` statements, and then recursively walking those files to find
/// all source files.
///
/// Packages are only included if their name is in `include_packages`.
pub fn top_source_files(
workspace_dir: &Utf8Path,
include_packages: &[String],
) -> Result<Vec<Arc<SourceFile>>> {
pub fn run_cargo_metadata(workspace_dir: &Utf8Path) -> Result<Metadata> {
let cargo_toml_path = workspace_dir.join("Cargo.toml");
debug!(?cargo_toml_path, ?workspace_dir, "Find root files");
debug!(?cargo_toml_path, ?workspace_dir, "run cargo metadata");
check_interrupted()?;
let metadata = cargo_metadata::MetadataCommand::new()
.manifest_path(&cargo_toml_path)
.exec()
.context("run cargo metadata")?;

let mut r = Vec::new();
// cargo-metadata output is not obviously ordered so make it deterministic.
for package_metadata in metadata
.workspace_packages()
.iter()
.filter(|p| include_packages.is_empty() || include_packages.contains(&p.name))
.sorted_by_key(|p| &p.name)
{
check_interrupted()?;
let _span = debug_span!("package", name = %package_metadata.name).entered();
let manifest_path = &package_metadata.manifest_path;
debug!(%manifest_path, "walk package");
let relative_manifest_path = manifest_path
.strip_prefix(workspace_dir)
.map_err(|_| {
anyhow!(
"manifest path {manifest_path:?} for package {name:?} is not within the detected source root path {workspace_dir:?}",
name = package_metadata.name
)
})?
.to_owned();
let package = Arc::new(Package {
name: package_metadata.name.clone(),
relative_manifest_path,
});
for source_path in direct_package_sources(workspace_dir, package_metadata)? {
check_interrupted()?;
r.push(Arc::new(SourceFile::new(
workspace_dir,
source_path,
&package,
)?));
}
}
for p in include_packages {
if !r.iter().any(|sf| sf.package.name == *p) {
warn!("package {p} not found in source tree");
}
}
Ok(r)
check_interrupted()?;
Ok(metadata)
}

/// Return the name of the cargo binary.
fn cargo_bin() -> String {
pub fn cargo_bin() -> String {
// When run as a Cargo subcommand, which is the usual/intended case,
// $CARGO tells us the right way to call back into it, so that we get
// the matching toolchain etc.
Expand Down Expand Up @@ -222,46 +138,6 @@ fn rustflags() -> String {
rustflags.join("\x1f")
}

/// Find all the files that are named in the `path` of targets in a Cargo manifest that should be tested.
///
/// These are the starting points for discovering source files.
fn direct_package_sources(
workspace_root: &Utf8Path,
package_metadata: &cargo_metadata::Package,
) -> Result<Vec<Utf8PathBuf>> {
let mut found = Vec::new();
let pkg_dir = package_metadata.manifest_path.parent().unwrap();
for target in &package_metadata.targets {
if should_mutate_target(target) {
if let Ok(relpath) = target
.src_path
.strip_prefix(workspace_root)
.map(ToOwned::to_owned)
{
debug!(
"found mutation target {} of kind {:?}",
relpath, target.kind
);
found.push(relpath);
} else {
warn!("{:?} is not in {:?}", target.src_path, pkg_dir);
}
} else {
debug!(
"skipping target {:?} of kinds {:?}",
target.name, target.kind
);
}
}
found.sort();
found.dedup();
Ok(found)
}

fn should_mutate_target(target: &cargo_metadata::Target) -> bool {
target.kind.iter().any(|k| k.ends_with("lib") || k == "bin")
}

#[cfg(test)]
mod test {
use std::ffi::OsStr;
Expand Down Expand Up @@ -364,88 +240,4 @@ mod test {
]
);
}

#[test]
fn error_opening_outside_of_crate() {
cargo::find_workspace(Utf8Path::new("/")).unwrap_err();
}

#[test]
fn open_subdirectory_of_crate_opens_the_crate() {
let root = cargo::find_workspace(Utf8Path::new("testdata/tree/factorial/src"))
.expect("open source tree from subdirectory");
assert!(root.is_dir());
assert!(root.join("Cargo.toml").is_file());
assert!(root.join("src/bin/factorial.rs").is_file());
assert_eq!(root.file_name().unwrap(), OsStr::new("factorial"));
}

#[test]
fn find_root_from_subdirectory_of_workspace_finds_the_workspace_root() {
let root = cargo::find_workspace(Utf8Path::new("testdata/tree/workspace/main"))
.expect("Find root from within workspace/main");
assert_eq!(root.file_name(), Some("workspace"), "Wrong root: {root:?}");
}

#[test]
fn find_top_source_files_from_subdirectory_of_workspace() {
let root_dir = cargo::find_workspace(Utf8Path::new("testdata/tree/workspace/main"))
.expect("Find workspace root");
let top_source_files = top_source_files(&root_dir, &[]).expect("Find root files");
println!("{top_source_files:#?}");
let paths = top_source_files
.iter()
.map(|sf| sf.tree_relative_path.to_slash_path())
.collect_vec();
// The order here might look strange, but they're actually deterministically
// sorted by the package name, not the path name.
assert_eq!(
paths,
["utils/src/lib.rs", "main/src/main.rs", "main2/src/main.rs"]
);
}

#[test]
fn filter_by_single_package() {
let root_dir = cargo::find_workspace(Utf8Path::new("testdata/tree/workspace/main"))
.expect("Find workspace root");
assert_eq!(
root_dir.file_name(),
Some("workspace"),
"found the workspace root"
);
let top_source_files =
top_source_files(&root_dir, &["main".to_owned()]).expect("Find root files");
println!("{top_source_files:#?}");
assert_eq!(top_source_files.len(), 1);
assert_eq!(
top_source_files
.iter()
.map(|sf| sf.tree_relative_path.clone())
.collect_vec(),
["main/src/main.rs"]
);
}

#[test]
fn filter_by_multiple_packages() {
let root_dir = cargo::find_workspace(Utf8Path::new("testdata/tree/workspace/main"))
.expect("Find workspace root");
assert_eq!(
root_dir.file_name(),
Some("workspace"),
"found the workspace root"
);
let top_source_files =
top_source_files(&root_dir, &["main".to_owned(), "main2".to_owned()])
.expect("Find root files");
println!("{top_source_files:#?}");
assert_eq!(
top_source_files
.iter()
.map(|sf| sf.tree_relative_path.clone())
.collect_vec(),
["main/src/main.rs", "main2/src/main.rs"]
);
}
}
2 changes: 1 addition & 1 deletion src/lab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use crate::cargo::run_cargo;
use crate::console::Console;
use crate::outcome::{LabOutcome, Phase, ScenarioOutcome};
use crate::output::OutputDir;
use crate::source::Package;
use crate::package::Package;
use crate::*;

/// Run all possible mutation experiments.
Expand Down
17 changes: 12 additions & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@ mod mutate;
mod options;
mod outcome;
mod output;
mod package;
mod path;
mod pretty;
mod process;
mod scenario;
mod source;
mod textedit;
mod visit;
pub mod workspace;

use std::env;
use std::io;
Expand Down Expand Up @@ -53,6 +55,8 @@ use crate::path::Utf8PathSlashes;
use crate::scenario::Scenario;
use crate::source::SourceFile;
use crate::visit::walk_tree;
use crate::workspace::PackageFilter;
use crate::workspace::Workspace;

const VERSION: &str = env!("CARGO_PKG_VERSION");
const NAME: &str = env!("CARGO_PKG_NAME");
Expand Down Expand Up @@ -222,25 +226,28 @@ fn main() -> Result<()> {
console.setup_global_trace(args.level)?;
interrupt::install_handler();

let source_path: &Utf8Path = args.dir.as_deref().unwrap_or(Utf8Path::new("."));
let workspace_dir = cargo::find_workspace(source_path)?;
let start_dir: &Utf8Path = args.dir.as_deref().unwrap_or(Utf8Path::new("."));
let workspace = Workspace::open(&start_dir)?;
// let discovered_workspace = discover_packages(start_dir, false, &args.mutate_packages)?;
// let workspace_dir = &discovered_workspace.workspace_dir;
let config = if args.no_config {
config::Config::default()
} else {
config::Config::read_tree_config(&workspace_dir)?
config::Config::read_tree_config(&workspace.dir)?
};
debug!(?config);
let options = Options::new(&args, &config)?;
debug!(?options);
let discovered = walk_tree(&workspace_dir, &args.mutate_packages, &options, &console)?;
let package_filter = PackageFilter::All; // TODO: From args
let discovered = workspace.discover(&package_filter, &options, &console)?;
if args.list_files {
console.clear();
list_files(FmtToIoWrite::new(io::stdout()), discovered, &options)?;
} else if args.list {
console.clear();
list_mutants(FmtToIoWrite::new(io::stdout()), discovered, &options)?;
} else {
let lab_outcome = test_mutants(discovered.mutants, &workspace_dir, options, &console)?;
let lab_outcome = test_mutants(discovered.mutants, &workspace.dir, options, &console)?;
exit(lab_outcome.exit_code());
}
Ok(())
Expand Down
Loading

0 comments on commit 146b5dd

Please sign in to comment.