Skip to content

Commit 146b5dd

Browse files
committed
Add Workspace and PackageFilter
Moving towards being able to auto-select packages from workspace subdir
1 parent fd9165c commit 146b5dd

File tree

11 files changed

+468
-277
lines changed

11 files changed

+468
-277
lines changed

src/build_dir.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,8 @@ mod test {
152152
#[test]
153153
fn build_dir_debug_form() {
154154
let options = Options::default();
155-
let root = cargo::find_workspace("testdata/tree/factorial".into()).unwrap();
156-
let build_dir = BuildDir::new(&root, &options, &Console::new()).unwrap();
155+
let workspace = Workspace::open(Utf8Path::new("testdata/tree/factorial")).unwrap();
156+
let build_dir = BuildDir::new(&workspace.dir, &options, &Console::new()).unwrap();
157157
let debug_form = format!("{build_dir:?}");
158158
assert!(
159159
Regex::new(r#"^BuildDir \{ path: "[^"]*[/\\]cargo-mutants-factorial[^"]*" \}$"#)

src/cargo.rs

Lines changed: 7 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@ use std::time::{Duration, Instant};
88

99
use anyhow::{anyhow, ensure, Context, Result};
1010
use camino::{Utf8Path, Utf8PathBuf};
11+
use cargo_metadata::Metadata;
1112
use itertools::Itertools;
1213
use serde_json::Value;
1314
use tracing::debug_span;
1415
#[allow(unused_imports)]
1516
use tracing::{debug, error, info, span, trace, warn, Level};
1617

1718
use crate::outcome::PhaseResult;
19+
use crate::package::Package;
1820
use crate::process::{get_command_output, Process};
19-
use crate::source::Package;
2021
use crate::*;
2122

2223
/// Run cargo build, check, or test.
@@ -50,105 +51,20 @@ pub fn run_cargo(
5051
})
5152
}
5253

53-
/// Return the path of the workspace directory enclosing a given directory.
54-
pub fn find_workspace(path: &Utf8Path) -> Result<Utf8PathBuf> {
55-
ensure!(path.is_dir(), "{path:?} is not a directory");
56-
let cargo_bin = cargo_bin(); // needed for lifetime
57-
let argv: Vec<&str> = vec![&cargo_bin, "locate-project", "--workspace"];
58-
let stdout = get_command_output(&argv, path)
59-
.with_context(|| format!("run cargo locate-project in {path:?}"))?;
60-
let val: Value = serde_json::from_str(&stdout).context("parse cargo locate-project output")?;
61-
let cargo_toml_path: Utf8PathBuf = val["root"]
62-
.as_str()
63-
.with_context(|| format!("cargo locate-project output has no root: {stdout:?}"))?
64-
.to_owned()
65-
.into();
66-
debug!(?cargo_toml_path, "Found workspace root manifest");
67-
ensure!(
68-
cargo_toml_path.is_file(),
69-
"cargo locate-project root {cargo_toml_path:?} is not a file"
70-
);
71-
let root = cargo_toml_path
72-
.parent()
73-
.ok_or_else(|| anyhow!("cargo locate-project root {cargo_toml_path:?} has no parent"))?
74-
.to_owned();
75-
ensure!(
76-
root.is_dir(),
77-
"apparent project root directory {root:?} is not a directory"
78-
);
79-
Ok(root)
80-
}
81-
82-
/// Find the root files for each relevant package in the source tree.
83-
///
84-
/// A source tree might include multiple packages (e.g. in a Cargo workspace),
85-
/// and each package might have multiple targets (e.g. a bin and lib). Test targets
86-
/// are excluded here: we run them, but we don't mutate them.
87-
///
88-
/// Each target has one root file, typically but not necessarily called `src/lib.rs`
89-
/// or `src/main.rs`. This function returns a list of all those files.
90-
///
91-
/// After this, there is one more level of discovery, by walking those root files
92-
/// to find `mod` statements, and then recursively walking those files to find
93-
/// all source files.
94-
///
95-
/// Packages are only included if their name is in `include_packages`.
96-
pub fn top_source_files(
97-
workspace_dir: &Utf8Path,
98-
include_packages: &[String],
99-
) -> Result<Vec<Arc<SourceFile>>> {
54+
pub fn run_cargo_metadata(workspace_dir: &Utf8Path) -> Result<Metadata> {
10055
let cargo_toml_path = workspace_dir.join("Cargo.toml");
101-
debug!(?cargo_toml_path, ?workspace_dir, "Find root files");
56+
debug!(?cargo_toml_path, ?workspace_dir, "run cargo metadata");
10257
check_interrupted()?;
10358
let metadata = cargo_metadata::MetadataCommand::new()
10459
.manifest_path(&cargo_toml_path)
10560
.exec()
10661
.context("run cargo metadata")?;
107-
108-
let mut r = Vec::new();
109-
// cargo-metadata output is not obviously ordered so make it deterministic.
110-
for package_metadata in metadata
111-
.workspace_packages()
112-
.iter()
113-
.filter(|p| include_packages.is_empty() || include_packages.contains(&p.name))
114-
.sorted_by_key(|p| &p.name)
115-
{
116-
check_interrupted()?;
117-
let _span = debug_span!("package", name = %package_metadata.name).entered();
118-
let manifest_path = &package_metadata.manifest_path;
119-
debug!(%manifest_path, "walk package");
120-
let relative_manifest_path = manifest_path
121-
.strip_prefix(workspace_dir)
122-
.map_err(|_| {
123-
anyhow!(
124-
"manifest path {manifest_path:?} for package {name:?} is not within the detected source root path {workspace_dir:?}",
125-
name = package_metadata.name
126-
)
127-
})?
128-
.to_owned();
129-
let package = Arc::new(Package {
130-
name: package_metadata.name.clone(),
131-
relative_manifest_path,
132-
});
133-
for source_path in direct_package_sources(workspace_dir, package_metadata)? {
134-
check_interrupted()?;
135-
r.push(Arc::new(SourceFile::new(
136-
workspace_dir,
137-
source_path,
138-
&package,
139-
)?));
140-
}
141-
}
142-
for p in include_packages {
143-
if !r.iter().any(|sf| sf.package.name == *p) {
144-
warn!("package {p} not found in source tree");
145-
}
146-
}
147-
Ok(r)
62+
check_interrupted()?;
63+
Ok(metadata)
14864
}
14965

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

225-
/// Find all the files that are named in the `path` of targets in a Cargo manifest that should be tested.
226-
///
227-
/// These are the starting points for discovering source files.
228-
fn direct_package_sources(
229-
workspace_root: &Utf8Path,
230-
package_metadata: &cargo_metadata::Package,
231-
) -> Result<Vec<Utf8PathBuf>> {
232-
let mut found = Vec::new();
233-
let pkg_dir = package_metadata.manifest_path.parent().unwrap();
234-
for target in &package_metadata.targets {
235-
if should_mutate_target(target) {
236-
if let Ok(relpath) = target
237-
.src_path
238-
.strip_prefix(workspace_root)
239-
.map(ToOwned::to_owned)
240-
{
241-
debug!(
242-
"found mutation target {} of kind {:?}",
243-
relpath, target.kind
244-
);
245-
found.push(relpath);
246-
} else {
247-
warn!("{:?} is not in {:?}", target.src_path, pkg_dir);
248-
}
249-
} else {
250-
debug!(
251-
"skipping target {:?} of kinds {:?}",
252-
target.name, target.kind
253-
);
254-
}
255-
}
256-
found.sort();
257-
found.dedup();
258-
Ok(found)
259-
}
260-
261-
fn should_mutate_target(target: &cargo_metadata::Target) -> bool {
262-
target.kind.iter().any(|k| k.ends_with("lib") || k == "bin")
263-
}
264-
265141
#[cfg(test)]
266142
mod test {
267143
use std::ffi::OsStr;
@@ -364,88 +240,4 @@ mod test {
364240
]
365241
);
366242
}
367-
368-
#[test]
369-
fn error_opening_outside_of_crate() {
370-
cargo::find_workspace(Utf8Path::new("/")).unwrap_err();
371-
}
372-
373-
#[test]
374-
fn open_subdirectory_of_crate_opens_the_crate() {
375-
let root = cargo::find_workspace(Utf8Path::new("testdata/tree/factorial/src"))
376-
.expect("open source tree from subdirectory");
377-
assert!(root.is_dir());
378-
assert!(root.join("Cargo.toml").is_file());
379-
assert!(root.join("src/bin/factorial.rs").is_file());
380-
assert_eq!(root.file_name().unwrap(), OsStr::new("factorial"));
381-
}
382-
383-
#[test]
384-
fn find_root_from_subdirectory_of_workspace_finds_the_workspace_root() {
385-
let root = cargo::find_workspace(Utf8Path::new("testdata/tree/workspace/main"))
386-
.expect("Find root from within workspace/main");
387-
assert_eq!(root.file_name(), Some("workspace"), "Wrong root: {root:?}");
388-
}
389-
390-
#[test]
391-
fn find_top_source_files_from_subdirectory_of_workspace() {
392-
let root_dir = cargo::find_workspace(Utf8Path::new("testdata/tree/workspace/main"))
393-
.expect("Find workspace root");
394-
let top_source_files = top_source_files(&root_dir, &[]).expect("Find root files");
395-
println!("{top_source_files:#?}");
396-
let paths = top_source_files
397-
.iter()
398-
.map(|sf| sf.tree_relative_path.to_slash_path())
399-
.collect_vec();
400-
// The order here might look strange, but they're actually deterministically
401-
// sorted by the package name, not the path name.
402-
assert_eq!(
403-
paths,
404-
["utils/src/lib.rs", "main/src/main.rs", "main2/src/main.rs"]
405-
);
406-
}
407-
408-
#[test]
409-
fn filter_by_single_package() {
410-
let root_dir = cargo::find_workspace(Utf8Path::new("testdata/tree/workspace/main"))
411-
.expect("Find workspace root");
412-
assert_eq!(
413-
root_dir.file_name(),
414-
Some("workspace"),
415-
"found the workspace root"
416-
);
417-
let top_source_files =
418-
top_source_files(&root_dir, &["main".to_owned()]).expect("Find root files");
419-
println!("{top_source_files:#?}");
420-
assert_eq!(top_source_files.len(), 1);
421-
assert_eq!(
422-
top_source_files
423-
.iter()
424-
.map(|sf| sf.tree_relative_path.clone())
425-
.collect_vec(),
426-
["main/src/main.rs"]
427-
);
428-
}
429-
430-
#[test]
431-
fn filter_by_multiple_packages() {
432-
let root_dir = cargo::find_workspace(Utf8Path::new("testdata/tree/workspace/main"))
433-
.expect("Find workspace root");
434-
assert_eq!(
435-
root_dir.file_name(),
436-
Some("workspace"),
437-
"found the workspace root"
438-
);
439-
let top_source_files =
440-
top_source_files(&root_dir, &["main".to_owned(), "main2".to_owned()])
441-
.expect("Find root files");
442-
println!("{top_source_files:#?}");
443-
assert_eq!(
444-
top_source_files
445-
.iter()
446-
.map(|sf| sf.tree_relative_path.clone())
447-
.collect_vec(),
448-
["main/src/main.rs", "main2/src/main.rs"]
449-
);
450-
}
451243
}

src/lab.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::cargo::run_cargo;
1717
use crate::console::Console;
1818
use crate::outcome::{LabOutcome, Phase, ScenarioOutcome};
1919
use crate::output::OutputDir;
20-
use crate::source::Package;
20+
use crate::package::Package;
2121
use crate::*;
2222

2323
/// Run all possible mutation experiments.

src/main.rs

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ mod mutate;
1717
mod options;
1818
mod outcome;
1919
mod output;
20+
mod package;
2021
mod path;
2122
mod pretty;
2223
mod process;
2324
mod scenario;
2425
mod source;
2526
mod textedit;
2627
mod visit;
28+
pub mod workspace;
2729

2830
use std::env;
2931
use std::io;
@@ -53,6 +55,8 @@ use crate::path::Utf8PathSlashes;
5355
use crate::scenario::Scenario;
5456
use crate::source::SourceFile;
5557
use crate::visit::walk_tree;
58+
use crate::workspace::PackageFilter;
59+
use crate::workspace::Workspace;
5660

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

225-
let source_path: &Utf8Path = args.dir.as_deref().unwrap_or(Utf8Path::new("."));
226-
let workspace_dir = cargo::find_workspace(source_path)?;
229+
let start_dir: &Utf8Path = args.dir.as_deref().unwrap_or(Utf8Path::new("."));
230+
let workspace = Workspace::open(&start_dir)?;
231+
// let discovered_workspace = discover_packages(start_dir, false, &args.mutate_packages)?;
232+
// let workspace_dir = &discovered_workspace.workspace_dir;
227233
let config = if args.no_config {
228234
config::Config::default()
229235
} else {
230-
config::Config::read_tree_config(&workspace_dir)?
236+
config::Config::read_tree_config(&workspace.dir)?
231237
};
232238
debug!(?config);
233239
let options = Options::new(&args, &config)?;
234240
debug!(?options);
235-
let discovered = walk_tree(&workspace_dir, &args.mutate_packages, &options, &console)?;
241+
let package_filter = PackageFilter::All; // TODO: From args
242+
let discovered = workspace.discover(&package_filter, &options, &console)?;
236243
if args.list_files {
237244
console.clear();
238245
list_files(FmtToIoWrite::new(io::stdout()), discovered, &options)?;
239246
} else if args.list {
240247
console.clear();
241248
list_mutants(FmtToIoWrite::new(io::stdout()), discovered, &options)?;
242249
} else {
243-
let lab_outcome = test_mutants(discovered.mutants, &workspace_dir, options, &console)?;
250+
let lab_outcome = test_mutants(discovered.mutants, &workspace.dir, options, &console)?;
244251
exit(lab_outcome.exit_code());
245252
}
246253
Ok(())

0 commit comments

Comments
 (0)