Skip to content

Commit

Permalink
cargo-insta: fix --manifest-path <virtual-workspace>
Browse files Browse the repository at this point in the history
Prior to this change --manifest-path only worked properly if it pointed
to the root of a non-virtual workspace.

Rewrite all the cargo interaction to use cargo-metadata which crucially
provides `Metadata::resolve::root` which is used to determine which
package --manifest-path was pointing to.

https://doc.rust-lang.org/cargo/reference/workspaces.html#virtual-workspace
  • Loading branch information
tamird committed Sep 28, 2023
1 parent 9623b3a commit 71d09ba
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 175 deletions.
42 changes: 42 additions & 0 deletions cargo-insta/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions cargo-insta/Cargo.toml
Expand Up @@ -13,6 +13,7 @@ readme = "README.md"

[dependencies]
insta = { version = "=1.33.0", path = "..", features = ["json", "yaml", "redactions", "_cargo_insta_internal"] }
cargo_metadata = { version = "0.18.0", default-features = false }
console = "0.15.4"
structopt = { version = "0.3.26", default-features = false }
serde = { version = "1.0.117", features = ["derive"] }
Expand Down
214 changes: 51 additions & 163 deletions cargo-insta/src/cargo.rs
@@ -1,190 +1,78 @@
use std::collections::HashSet;
use std::env;
use std::error::Error;
use std::fs;
use std::path::{Path, PathBuf};
use std::process;

use serde::Deserialize;
pub(crate) use cargo_metadata::{Metadata, Package};

use crate::utils::err_msg;

#[derive(Deserialize, Clone, Debug)]
struct Target {
src_path: PathBuf,
kind: HashSet<String>,
}

#[derive(Deserialize, Clone, Debug)]
pub(crate) struct Package {
name: String,
version: String,
id: String,
manifest_path: PathBuf,
targets: Vec<Target>,
}

#[derive(Deserialize, Debug)]
pub(crate) struct Metadata {
packages: Vec<Package>,
workspace_members: Vec<String>,
workspace_root: String,
}
pub(crate) fn find_snapshot_roots(package: &Package) -> Vec<PathBuf> {
let mut roots = Vec::new();

impl Metadata {
pub(crate) fn workspace_root(&self) -> &Path {
Path::new(&self.workspace_root)
// the manifest path's parent is always a snapshot container. For
// a rationale see GH-70. But generally a user would expect to be
// able to put a snapshot into foo/snapshots instead of foo/src/snapshots.
if let Some(manifest) = package.manifest_path.parent() {
roots.push(manifest.as_std_path().to_path_buf());
}
}

#[derive(Deserialize, Debug)]
struct ProjectLocation {
root: PathBuf,
}

impl Package {
pub(crate) fn manifest_path(&self) -> &Path {
&self.manifest_path
}

pub(crate) fn name(&self) -> &str {
&self.name
}

pub(crate) fn version(&self) -> &str {
&self.version
}

pub(crate) fn find_snapshot_roots(&self) -> Vec<PathBuf> {
let mut roots = Vec::new();

// the manifest path's parent is always a snapshot container. For
// a rationale see GH-70. But generally a user would expect to be
// able to put a snapshot into foo/snapshots instead of foo/src/snapshots.
if let Some(manifest) = self.manifest_path.parent() {
roots.push(manifest.to_path_buf());
// additionally check all targets.
for target in &package.targets {
// custom build scripts we can safely skip over. In the past this
// caused issues with duplicate paths but that's resolved in other
// ways now. We do not want to pick up snapshots in such places
// though.
if target.kind.iter().any(|kind| kind == "custom-build") {
continue;
}

// additionally check all targets.
for target in &self.targets {
// custom build scripts we can safely skip over. In the past this
// caused issues with duplicate paths but that's resolved in other
// ways now. We do not want to pick up snapshots in such places
// though.
if target.kind.contains("custom-build") {
continue;
}

// this gives us the containing source folder. Typically this is
// something like crate/src.
let root = target.src_path.parent().unwrap();
roots.push(root.to_path_buf());
}
// this gives us the containing source folder. Typically this is
// something like crate/src.
let root = target.src_path.parent().unwrap().as_std_path();
roots.push(root.to_path_buf());
}

// reduce roots to avoid traversing into paths twice. If we have both
// /foo and /foo/bar as roots we would only walk into /foo. Otherwise
// we would encounter paths twice. If we don't skip them here we run
// into issues where the existence of a build script causes a snapshot
// to be picked up twice since the same path is determined. (GH-15)
roots.sort_by_key(|x| x.as_os_str().len());
let mut reduced_roots = vec![];
for root in roots {
if !reduced_roots.iter().any(|x| root.starts_with(x)) {
reduced_roots.push(root);
}
// reduce roots to avoid traversing into paths twice. If we have both
// /foo and /foo/bar as roots we would only walk into /foo. Otherwise
// we would encounter paths twice. If we don't skip them here we run
// into issues where the existence of a build script causes a snapshot
// to be picked up twice since the same path is determined. (GH-15)
roots.sort_by_key(|x| x.as_os_str().len());
let mut reduced_roots = vec![];
for root in roots {
if !reduced_roots.iter().any(|x| root.starts_with(x)) {
reduced_roots.push(root);
}

reduced_roots
}
}

pub(crate) fn get_cargo() -> String {
env::var("CARGO")
.ok()
.unwrap_or_else(|| "cargo".to_string())
reduced_roots
}

pub(crate) fn get_package_metadata(
manifest_path: Option<&Path>,
) -> Result<Metadata, Box<dyn Error>> {
let mut cmd = process::Command::new(get_cargo());
cmd.arg("metadata")
.arg("--no-deps")
.arg("--format-version=1");
) -> cargo_metadata::Result<Metadata> {
let mut cmd = cargo_metadata::MetadataCommand::new();
if let Some(manifest_path) = manifest_path {
if !fs::metadata(manifest_path)
.ok()
.map_or(false, |x| x.is_file())
{
return Err(err_msg(
"the manifest-path must be a path to a Cargo.toml file",
));
}
cmd.arg("--manifest-path").arg(manifest_path.as_os_str());
cmd.manifest_path(manifest_path);
}
let output = cmd.output()?;
if !output.status.success() {
let msg = String::from_utf8_lossy(&output.stderr);
return Err(err_msg(format!(
"cargo erroried getting metadata: {}",
msg.trim()
)));
}
Ok(serde_json::from_slice(&output.stdout)?)
cmd.exec()
}

fn get_default_manifest() -> Result<Option<PathBuf>, Box<dyn Error>> {
let output = process::Command::new(get_cargo())
.arg("locate-project")
.output()?;
if output.status.success() {
let loc: ProjectLocation = serde_json::from_slice(&output.stdout)?;
Ok(Some(loc.root))
} else {
Ok(None)
}
}

fn find_all_packages(metadata: &Metadata) -> Vec<Package> {
metadata
.packages
.iter()
.filter_map(|package| {
if metadata.workspace_members.contains(&package.id) {
Some(package.clone())
} else {
None
}
})
.collect()
}

pub(crate) fn find_packages(
metadata: &Metadata,
all: bool,
) -> Result<Vec<Package>, Box<dyn Error>> {
pub(crate) fn find_packages(metadata: Metadata, all: bool) -> Result<Vec<Package>, Box<dyn Error>> {
let Metadata {
packages,
workspace_members,
resolve,
..
} = metadata;
if all {
Ok(find_all_packages(metadata))
let mut packages = packages;
packages.retain(|Package { id, .. }| workspace_members.contains(id));
Ok(packages)
} else {
let default_manifest = get_default_manifest()?
.ok_or_else(|| {
err_msg(
"Could not find `Cargo.toml` in the current folder or any parent directory.",
)
})?
.canonicalize()?;
let mut rv = vec![];
for package in &metadata.packages {
if package.manifest_path.canonicalize()? == default_manifest {
rv.push(package.clone());
}
}
if rv.is_empty() {
// if we don't find anything we're in a workspace root that has no
// root member in which case --all is implied.
Ok(find_all_packages(metadata))
} else {
Ok(rv)
}
let root = resolve
.and_then(|cargo_metadata::Resolve { root, .. }| root)
.and_then(|root| packages.into_iter().find(|Package { id, .. }| *id == root))
.ok_or_else(|| err_msg("could not find root package. Try running with --all"))?;
Ok(vec![root])
}
}
26 changes: 16 additions & 10 deletions cargo-insta/src/cli.rs
@@ -1,6 +1,7 @@
use std::borrow::{Borrow, Cow};
use std::collections::HashSet;
use std::error::Error;
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::{env, fs};
use std::{io, process};
Expand All @@ -15,7 +16,7 @@ use structopt::clap::AppSettings;
use structopt::StructOpt;
use uuid::Uuid;

use crate::cargo::{find_packages, get_cargo, get_package_metadata, Package};
use crate::cargo::{find_packages, find_snapshot_roots, get_package_metadata, Package};
use crate::container::{Operation, SnapshotContainer};
use crate::utils::{err_msg, QuietExit};
use crate::walk::{find_snapshots, make_deletion_walker, make_snapshot_walker, FindFlags};
Expand Down Expand Up @@ -240,10 +241,10 @@ fn query_snapshot(
) -> Result<Operation, Box<dyn Error>> {
loop {
term.clear_screen()?;
let (pkg_name, pkg_version) = if let Some(pkg) = pkg {
(pkg.name(), pkg.version())
let (pkg_name, pkg_version): (_, &dyn Display) = if let Some(pkg) = pkg {
(pkg.name.as_str(), &pkg.version)
} else {
("unknown package", "unknown version")
("unknown package", &"unknown version")
};

println!(
Expand Down Expand Up @@ -396,10 +397,11 @@ fn handle_target_args(target_args: &TargetArgs) -> Result<LocationInfo<'_>, Box<
})
} else {
let metadata = get_package_metadata(manifest_path.as_ref().map(|x| x.as_path()))?;
let packages = find_packages(&metadata, target_args.all || target_args.workspace)?;
let tool_config = ToolConfig::from_workspace(metadata.workspace_root())?;
let workspace_root = metadata.workspace_root.as_std_path().to_path_buf();
let packages = find_packages(metadata, target_args.all || target_args.workspace)?;
let tool_config = ToolConfig::from_workspace(&workspace_root)?;
Ok(LocationInfo {
workspace_root: metadata.workspace_root().to_path_buf(),
workspace_root,
packages: Some(packages),
exts,
find_flags: get_find_flags(&tool_config, target_args),
Expand All @@ -421,7 +423,7 @@ fn load_snapshot_containers<'a>(
let mut snapshot_containers = vec![];
if let Some(ref packages) = loc.packages {
for package in packages.iter() {
for root in package.find_snapshot_roots() {
for root in find_snapshot_roots(package) {
roots.insert(root.clone());
for snapshot_container in find_snapshots(&root, &loc.exts, loc.find_flags) {
snapshot_containers.push((snapshot_container?, Some(package)));
Expand Down Expand Up @@ -776,14 +778,18 @@ fn prepare_test_runner<'snapshot_ref>(
extra_args: &[&str],
snapshot_ref_file: Option<&'snapshot_ref Path>,
) -> Result<(process::Command, Option<Cow<'snapshot_ref, Path>>, bool), Box<dyn Error>> {
let cargo = env::var_os("CARGO");
let cargo = cargo
.as_deref()
.unwrap_or_else(|| std::ffi::OsStr::new("cargo"));
let mut proc = match test_runner {
TestRunner::CargoTest | TestRunner::Auto => {
let mut proc = process::Command::new(get_cargo());
let mut proc = process::Command::new(cargo);
proc.arg("test");
proc
}
TestRunner::Nextest => {
let mut proc = process::Command::new(get_cargo());
let mut proc = process::Command::new(cargo);
proc.arg("nextest");
proc.arg("run");
proc
Expand Down

0 comments on commit 71d09ba

Please sign in to comment.