diff --git a/Cargo.lock b/Cargo.lock index 3741b77fdc4..fe9c1c652d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -173,6 +173,8 @@ dependencies = [ "either 1.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "guppy 0.3.1", "guppy-cmdlib 0.1.0", + "once_cell 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proptest 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -644,6 +646,7 @@ version = "0.1.0" dependencies = [ "anyhow 1.0.28 (registry+https://github.com/rust-lang/crates.io-index)", "guppy 0.3.1", + "proptest 0.9.6 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.3.13 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/fixtures/README.md b/fixtures/README.md index f9ccfd3e987..1ba641df557 100644 --- a/fixtures/README.md +++ b/fixtures/README.md @@ -8,5 +8,7 @@ The fixtures are organized into several folders. * `small`: relatively simple examples that cover basic and some edge case functionality * `large`: complex examples pulled from real-world Rust repositories, that test a variety of edge cases -* `invalid`: examples that are *representable* as cargo metadata (i.e. they are valid JSON and follow the general - schema) but are *invalid* in some way; `cargo metadata` should never be able to generate these +* `invalid`: examples that are [*representable*](https://oleb.net/blog/2018/03/making-illegal-states-unrepresentable/) + as cargo metadata (i.e. they are valid JSON and follow the general schema) but are *invalid* in some way; `cargo + metadata` should never be able to generate these +* `workspace`: real workspaces, used for comparison testing with Cargo diff --git a/fixtures/workspace/inside-outside/external/.gitignore b/fixtures/workspace/inside-outside/external/.gitignore new file mode 100644 index 00000000000..96ef6c0b944 --- /dev/null +++ b/fixtures/workspace/inside-outside/external/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/fixtures/workspace/inside-outside/external/Cargo.toml b/fixtures/workspace/inside-outside/external/Cargo.toml new file mode 100644 index 00000000000..913eacf44f1 --- /dev/null +++ b/fixtures/workspace/inside-outside/external/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "external" +version = "0.1.0" +authors = ["Fake Author "] +edition = "2018" + +[dependencies] +transitive = { path = "../transitive" } +bytes = { version = "0.5", optional = true } + +[dev-dependencies] +transitive = { path = "../transitive", features = ["dev-feature"] } + +[build-dependencies] +transitive = { path = "../transitive", features = ["build-feature"] } + +[features] +internal-dev-feature = [] +internal-build-feature = [] +main-dev-feature = [] +main-build-feature = [] +macro-normal-feature = [] +macro-build-feature = [] +macro-dev-feature = [] +side-feature = ["bytes"] + +[workspace] diff --git a/fixtures/workspace/inside-outside/external/src/lib.rs b/fixtures/workspace/inside-outside/external/src/lib.rs new file mode 100644 index 00000000000..31e1bb209f9 --- /dev/null +++ b/fixtures/workspace/inside-outside/external/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/fixtures/workspace/inside-outside/inactive/.gitignore b/fixtures/workspace/inside-outside/inactive/.gitignore new file mode 100644 index 00000000000..96ef6c0b944 --- /dev/null +++ b/fixtures/workspace/inside-outside/inactive/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/fixtures/workspace/inside-outside/inactive/Cargo.toml b/fixtures/workspace/inside-outside/inactive/Cargo.toml new file mode 100644 index 00000000000..bfceb459b31 --- /dev/null +++ b/fixtures/workspace/inside-outside/inactive/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "inactive" +version = "0.1.0" +authors = ["Fake Author "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +transitive = { path = "../transitive", features = ["inactive-normal"] } + +[build-dependencies] +transitive = { path = "../transitive", features = ["inactive-build"] } + +[dev-dependencies] +transitive = { path = "../transitive", features = ["inactive-dev"] } + +[features] +extra = ["transitive/extra"] + +[workspace] diff --git a/fixtures/workspace/inside-outside/inactive/src/lib.rs b/fixtures/workspace/inside-outside/inactive/src/lib.rs new file mode 100644 index 00000000000..31e1bb209f9 --- /dev/null +++ b/fixtures/workspace/inside-outside/inactive/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/fixtures/workspace/inside-outside/transitive/.gitignore b/fixtures/workspace/inside-outside/transitive/.gitignore new file mode 100644 index 00000000000..96ef6c0b944 --- /dev/null +++ b/fixtures/workspace/inside-outside/transitive/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/fixtures/workspace/inside-outside/transitive/Cargo.toml b/fixtures/workspace/inside-outside/transitive/Cargo.toml new file mode 100644 index 00000000000..9ad0cf4d759 --- /dev/null +++ b/fixtures/workspace/inside-outside/transitive/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "transitive" +version = "0.1.0" +authors = ["Fake Author "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +build-feature = [] +dev-feature = [] +inactive-normal = [] +inactive-build = [] +inactive-dev = [] +extra = [] + +[workspace] diff --git a/fixtures/workspace/inside-outside/transitive/src/lib.rs b/fixtures/workspace/inside-outside/transitive/src/lib.rs new file mode 100644 index 00000000000..31e1bb209f9 --- /dev/null +++ b/fixtures/workspace/inside-outside/transitive/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/fixtures/workspace/inside-outside/workspace/.gitignore b/fixtures/workspace/inside-outside/workspace/.gitignore new file mode 100644 index 00000000000..ea8c4bf7f35 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/.gitignore @@ -0,0 +1 @@ +/target diff --git a/fixtures/workspace/inside-outside/workspace/Cargo.lock b/fixtures/workspace/inside-outside/workspace/Cargo.lock new file mode 100644 index 00000000000..3b3d1984970 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/Cargo.lock @@ -0,0 +1,75 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "bytes" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "130aac562c0dd69c56b3b1cc8ffd2e17be31d0b6c25b61c96b76231aa23e39e1" + +[[package]] +name = "external" +version = "0.1.0" +dependencies = [ + "bytes", + "transitive", +] + +[[package]] +name = "inactive" +version = "0.1.0" +dependencies = [ + "transitive", +] + +[[package]] +name = "internal" +version = "0.1.0" +dependencies = [ + "bytes", + "external", + "internal-macro", + "lazy_static", +] + +[[package]] +name = "internal-macro" +version = "0.1.0" +dependencies = [ + "external", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + +[[package]] +name = "main" +version = "0.1.0" +dependencies = [ + "external", + "inactive", + "internal", + "internal-macro", +] + +[[package]] +name = "side" +version = "0.1.0" +dependencies = [ + "external", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "transitive" +version = "0.1.0" diff --git a/fixtures/workspace/inside-outside/workspace/Cargo.toml b/fixtures/workspace/inside-outside/workspace/Cargo.toml new file mode 100644 index 00000000000..70fc568e06a --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["main", "internal", "internal-macro", "side"] diff --git a/fixtures/workspace/inside-outside/workspace/internal-macro/Cargo.toml b/fixtures/workspace/inside-outside/workspace/internal-macro/Cargo.toml new file mode 100644 index 00000000000..48e046aea90 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/internal-macro/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "internal-macro" +version = "0.1.0" +authors = ["Fake Author "] +edition = "2018" + +[lib] +proc-macro = true + +[dependencies] +external = { path = "../../external", features = ["macro-normal-feature"] } + +[build-dependencies] +external = { path = "../../external", features = ["macro-build-feature"] } + +[dev-dependencies] +external = { path = "../../external", features = ["macro-dev-feature"] } + +[features] +main-build-feature = [] +internal-normal-feature = [] diff --git a/fixtures/workspace/inside-outside/workspace/internal-macro/build.rs b/fixtures/workspace/inside-outside/workspace/internal-macro/build.rs new file mode 100644 index 00000000000..e69de29bb2d diff --git a/fixtures/workspace/inside-outside/workspace/internal-macro/src/lib.rs b/fixtures/workspace/inside-outside/workspace/internal-macro/src/lib.rs new file mode 100644 index 00000000000..31e1bb209f9 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/internal-macro/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/fixtures/workspace/inside-outside/workspace/internal/Cargo.toml b/fixtures/workspace/inside-outside/workspace/internal/Cargo.toml new file mode 100644 index 00000000000..6bd6c46c4cc --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/internal/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "internal" +version = "0.1.0" +authors = ["Fake Author "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lazy_static = "1" +external = { path = "../../external" } +internal-macro = { path = "../internal-macro", features = ["internal-normal-feature"] } + +[dev-dependencies] +lazy_static = {version = "1", features = ["spin"] } +bytes = "0.5" +external = { path = "../../external", features = ["internal-dev-feature"] } + +[build-dependencies] +external = { path = "../../external", features = ["internal-build-feature"] } + +[features] +dev-feature = [] +build-feature = [] diff --git a/fixtures/workspace/inside-outside/workspace/internal/src/lib.rs b/fixtures/workspace/inside-outside/workspace/internal/src/lib.rs new file mode 100644 index 00000000000..31e1bb209f9 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/internal/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/fixtures/workspace/inside-outside/workspace/main/Cargo.toml b/fixtures/workspace/inside-outside/workspace/main/Cargo.toml new file mode 100644 index 00000000000..0edafdeb335 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/main/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "main" +version = "0.1.0" +authors = ["Fake Author "] +edition = "2018" +build = "build.rs" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +external = { path = "../../external" } +internal = { path = "../internal" } + +[build-dependencies] +external = { path = "../../external", features = ["main-build-feature"] } +internal = { path = "../internal", features = ["build-feature"] } +internal-macro = { path = "../internal-macro", features = ["main-build-feature"] } + +[dev-dependencies] +external = { path = "../../external", features = ["main-dev-feature"] } +internal = { path = "../internal", features = ["dev-feature"] } + +[target.'cfg(all(unix, not(unix)))'.build-dependencies] +inactive = { path = "../../inactive" } + +[features] +inactive-extra = ["inactive/extra"] diff --git a/fixtures/workspace/inside-outside/workspace/main/build.rs b/fixtures/workspace/inside-outside/workspace/main/build.rs new file mode 100644 index 00000000000..f79c691f085 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/main/build.rs @@ -0,0 +1,2 @@ +fn main() { +} diff --git a/fixtures/workspace/inside-outside/workspace/main/src/lib.rs b/fixtures/workspace/inside-outside/workspace/main/src/lib.rs new file mode 100644 index 00000000000..31e1bb209f9 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/main/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/fixtures/workspace/inside-outside/workspace/side/Cargo.toml b/fixtures/workspace/inside-outside/workspace/side/Cargo.toml new file mode 100644 index 00000000000..718d982ea22 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/side/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "side" +version = "0.1.0" +authors = ["Fake Author "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +external = { path = "../../external", features = ["side-feature"] } diff --git a/fixtures/workspace/inside-outside/workspace/side/src/lib.rs b/fixtures/workspace/inside-outside/workspace/side/src/lib.rs new file mode 100644 index 00000000000..31e1bb209f9 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/side/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/fixtures/workspace/inside-outside/workspace/src/lib.rs b/fixtures/workspace/inside-outside/workspace/src/lib.rs new file mode 100644 index 00000000000..31e1bb209f9 --- /dev/null +++ b/fixtures/workspace/inside-outside/workspace/src/lib.rs @@ -0,0 +1,7 @@ +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + assert_eq!(2 + 2, 4); + } +} diff --git a/guppy-cmdlib/Cargo.toml b/guppy-cmdlib/Cargo.toml index 4cb4f6a50bc..e75e29e0d88 100644 --- a/guppy-cmdlib/Cargo.toml +++ b/guppy-cmdlib/Cargo.toml @@ -11,3 +11,7 @@ edition = "2018" anyhow = "1.0.28" guppy = { path = "../guppy" } structopt = "0.3" +proptest = { version = "0.9", optional = true } + +[features] +proptest09 = ["proptest", "guppy/proptest09"] diff --git a/guppy-cmdlib/src/lib.rs b/guppy-cmdlib/src/lib.rs index e8c2ade8095..fda0be12cb4 100644 --- a/guppy-cmdlib/src/lib.rs +++ b/guppy-cmdlib/src/lib.rs @@ -5,6 +5,9 @@ //! //! This library allows translating command-line arguments into guppy's data structures. +#[cfg(feature = "proptest09")] +pub mod proptest; + use anyhow::Result; use guppy::graph::feature::{ all_filter, default_filter, feature_filter, none_filter, FeatureFilter, FeatureQuery, @@ -66,11 +69,11 @@ impl PackagesAndFeatures { /// Context for invoking the `cargo metadata` command. /// /// The options mirror Cargo's. -#[derive(Debug, StructOpt)] +#[derive(Clone, Debug, StructOpt)] pub struct CargoMetadataOptions { /// Path to Cargo.toml #[structopt(long = "manifest-path")] - manifest_path: Option, + pub manifest_path: Option, } impl CargoMetadataOptions { diff --git a/guppy-cmdlib/src/proptest.rs b/guppy-cmdlib/src/proptest.rs new file mode 100644 index 00000000000..574f2928d54 --- /dev/null +++ b/guppy-cmdlib/src/proptest.rs @@ -0,0 +1,33 @@ +// Copyright (c) The cargo-guppy Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +//! Proptest support. + +use crate::PackagesAndFeatures; +use guppy::graph::PackageGraph; +use proptest::collection::hash_set; +use proptest::prelude::*; + +impl PackagesAndFeatures { + pub fn strategy<'g>(graph: &'g PackageGraph) -> impl Strategy + 'g { + let workspace = graph.workspace(); + ( + // The lower bound of 0 is important because 0 means the whole workspace. + hash_set(workspace.prop09_name_strategy(), 0..8), + any::(), + any::(), + ) + .prop_map(move |(packages, all_features, no_default_features)| { + // TODO: select features from these packages (probably requires flat_map :/ ) + Self { + packages: packages + .into_iter() + .map(|package| package.to_string()) + .collect(), + features: vec![], + all_features, + no_default_features, + } + }) + } +} diff --git a/guppy/src/graph/build.rs b/guppy/src/graph/build.rs index 85285b7e3eb..3294a91e818 100644 --- a/guppy/src/graph/build.rs +++ b/guppy/src/graph/build.rs @@ -108,6 +108,8 @@ impl WorkspaceImpl { root: workspace_root, members_by_path, members_by_name, + #[cfg(feature = "proptest09")] + name_list: OnceCell::new(), }) } } diff --git a/guppy/src/graph/graph_impl.rs b/guppy/src/graph/graph_impl.rs index cfaf356f8e7..58df4ebb0f9 100644 --- a/guppy/src/graph/graph_impl.rs +++ b/guppy/src/graph/graph_impl.rs @@ -463,7 +463,7 @@ impl<'g> DependsCache<'g> { #[derive(Clone, Debug)] pub struct Workspace<'g> { graph: &'g PackageGraph, - inner: &'g WorkspaceImpl, + pub(super) inner: &'g WorkspaceImpl, } impl<'g> Workspace<'g> { @@ -472,6 +472,11 @@ impl<'g> Workspace<'g> { &self.inner.root } + /// Returns the number of packages in this workspace. + pub fn member_count(&self) -> usize { + self.inner.members_by_path.len() + } + /// Returns an iterator over workspace paths and package metadatas, sorted by the path /// they're in. pub fn members( @@ -522,6 +527,9 @@ pub(super) struct WorkspaceImpl { // This is a BTreeMap to allow presenting data in sorted order. pub(super) members_by_path: BTreeMap, pub(super) members_by_name: BTreeMap, PackageId>, + // Cache for members by name (only used for proptests) + #[cfg(feature = "proptest09")] + pub(super) name_list: OnceCell>>, } /// Information about a specific package in a `PackageGraph`. diff --git a/guppy/src/graph/proptest09.rs b/guppy/src/graph/proptest09.rs index 1e3191458c8..624d6ba4950 100644 --- a/guppy/src/graph/proptest09.rs +++ b/guppy/src/graph/proptest09.rs @@ -1,7 +1,7 @@ // Copyright (c) The cargo-guppy Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 -use crate::graph::{PackageGraph, PackageLink, PackageQuery, PackageResolver}; +use crate::graph::{PackageGraph, PackageLink, PackageQuery, PackageResolver, Workspace}; use crate::PackageId; use fixedbitset::FixedBitSet; use petgraph::prelude::*; @@ -63,6 +63,46 @@ impl PackageGraph { } } +/// ## Helpers for property testing +/// +/// The methods in this section allow a `Workspace` to be used in property-based testing +/// scenarios. +/// +/// Currently, [proptest 0.9](https://docs.rs/proptest/0.9) is supported if the `proptest09` feature +/// is enabled. +impl<'g> Workspace<'g> { + /// Returns a `Strategy` that generates random package names from this workspace. + /// + /// Requires the `proptest09` feature to be enabled. + /// + /// ## Panics + /// + /// Panics if there are no packages in this `Workspace`. + pub fn prop09_name_strategy(&self) -> impl Strategy + 'g { + let name_list = self.name_list(); + (0..name_list.len()).prop_map(move |idx| name_list[idx].as_ref()) + } + + /// Returns a `Strategy` that generates random package IDs from this workspace. + /// + /// Requires the `proptest09` feature to be enabled. + /// + /// ## Panics + /// + /// Panics if there are no packages in this `Workspace`. + pub fn prop09_id_strategy(&self) -> impl Strategy + 'g { + let members_by_name = &self.inner.members_by_name; + self.prop09_name_strategy() + .prop_map(move |name| &members_by_name[name]) + } + + fn name_list(&self) -> &'g [Box] { + self.inner + .name_list + .get_or_init(|| self.inner.members_by_name.keys().cloned().collect()) + } +} + /// A randomly generated package resolver. /// /// Created by `PackageGraph::prop09_resolver_strategy`. Requires the `proptest09` feature to be diff --git a/tools/cargo-compare/Cargo.toml b/tools/cargo-compare/Cargo.toml index 57ef7f13b73..e1b91b47490 100644 --- a/tools/cargo-compare/Cargo.toml +++ b/tools/cargo-compare/Cargo.toml @@ -16,3 +16,9 @@ guppy = { path = "../../guppy" } guppy-cmdlib = { path = "../../guppy-cmdlib" } structopt = "0.3" tempfile = "3.1.0" + +[dev-dependencies] +guppy-cmdlib = { path = "../../guppy-cmdlib", features = ["proptest09"] } +once_cell = "1.3.1" + +proptest = "0.9" diff --git a/tools/cargo-compare/src/common.rs b/tools/cargo-compare/src/common.rs index 30d218e29a6..5a29a3b6e5a 100644 --- a/tools/cargo-compare/src/common.rs +++ b/tools/cargo-compare/src/common.rs @@ -12,7 +12,7 @@ use cargo::ops::resolve_ws_with_opts; use cargo::Config; use guppy::graph::cargo::{CargoOptions, CargoResolverVersion, CargoSet}; use guppy::graph::feature::FeatureSet; -use guppy::graph::{DependencyDirection, PackageGraph}; +use guppy::graph::DependencyDirection; use guppy::{PackageId, Platform, TargetFeatures}; use guppy_cmdlib::{CargoMetadataOptions, PackagesAndFeatures}; use std::collections::{BTreeMap, BTreeSet}; @@ -45,7 +45,7 @@ pub struct GuppyCargoCommon { impl GuppyCargoCommon { /// Resolves data for this query using Cargo. - pub fn resolve_cargo(&self, ctx: &GlobalContext) -> Result { + pub fn resolve_cargo(&self, ctx: &GlobalContext<'_>) -> Result { let config = self.cargo_make_config(ctx)?; let root_manifest = self.cargo_discover_root(&config)?; let workspace = self.cargo_make_workspace(&config, &root_manifest)?; @@ -114,8 +114,8 @@ impl GuppyCargoCommon { } /// Resolves data for this query using Guppy. - pub fn resolve_guppy(&self, _ctx: &GlobalContext, graph: &PackageGraph) -> Result { - let feature_query = self.pf.make_feature_query(graph)?; + pub fn resolve_guppy(&self, ctx: &GlobalContext<'_>) -> Result { + let feature_query = self.pf.make_feature_query(ctx.graph())?; // Note that guppy is more flexible than cargo here -- with the v1 feature resolver, it can // evaluate dependencies one of three ways: diff --git a/tools/cargo-compare/src/diff.rs b/tools/cargo-compare/src/diff.rs index 3f66781af3f..4f3d6577681 100644 --- a/tools/cargo-compare/src/diff.rs +++ b/tools/cargo-compare/src/diff.rs @@ -13,15 +13,14 @@ use structopt::StructOpt; #[derive(Debug, StructOpt)] pub struct DiffOpts { #[structopt(flatten)] - common: GuppyCargoCommon, + pub common: GuppyCargoCommon, } impl DiffOpts { /// Executes this command. pub fn exec(self, ctx: &GlobalContext) -> Result<()> { let cargo_map = self.common.resolve_cargo(ctx)?; - let graph = self.common.metadata_opts.make_command().build_graph()?; - let guppy_map = self.common.resolve_guppy(ctx, &graph)?; + let guppy_map = self.common.resolve_guppy(ctx)?; // As of 2020-04-30, Cargo's APIs don't let users tell the difference between the package // being missing entirely, and the package being present but with no features. diff --git a/tools/cargo-compare/src/lib.rs b/tools/cargo-compare/src/lib.rs index 2e4c9aad1be..e46689f2990 100644 --- a/tools/cargo-compare/src/lib.rs +++ b/tools/cargo-compare/src/lib.rs @@ -6,6 +6,7 @@ use crate::diff::DiffOpts; use anyhow::Result; use either::Either; +use guppy::graph::PackageGraph; use std::env; use std::path::{Path, PathBuf}; use structopt::StructOpt; @@ -13,6 +14,8 @@ use tempfile::TempDir; pub mod common; pub mod diff; +#[cfg(test)] +mod tests; pub mod type_conversions; #[derive(Debug, StructOpt)] @@ -24,11 +27,13 @@ pub struct CargoCompare { impl CargoCompare { pub fn exec(self) -> Result<()> { - // Don't use the temporary home here so that Cargo caches can be reused. - let ctx = GlobalContext::new(false)?; - match self.cmd { - Command::Diff(opts) => opts.exec(&ctx), + Command::Diff(opts) => { + // Don't use the temporary home here so that Cargo caches can be reused. + let graph = opts.common.metadata_opts.make_command().build_graph()?; + let ctx = GlobalContext::new(false, &graph)?; + opts.exec(&ctx) + } } } } @@ -42,18 +47,22 @@ enum Command { /// Global context for Cargo comparisons. #[derive(Debug)] -pub struct GlobalContext { +pub struct GlobalContext<'g> { home_dir: Either, + graph: &'g PackageGraph, } -impl GlobalContext { - pub fn new(temp_home: bool) -> Result { +impl<'g> GlobalContext<'g> { + pub fn new(temp_home: bool, graph: &'g PackageGraph) -> Result { let home = if temp_home { Either::Left(TempDir::new()?) } else { Either::Right(env::current_dir()?) }; - Ok(Self { home_dir: home }) + Ok(Self { + home_dir: home, + graph, + }) } pub fn home_dir(&self) -> &Path { @@ -62,4 +71,8 @@ impl GlobalContext { Either::Right(home_dir) => home_dir.as_path(), } } + + pub fn graph(&self) -> &'g PackageGraph { + self.graph + } } diff --git a/tools/cargo-compare/src/tests/fixtures.rs b/tools/cargo-compare/src/tests/fixtures.rs new file mode 100644 index 00000000000..94fb4e24ef0 --- /dev/null +++ b/tools/cargo-compare/src/tests/fixtures.rs @@ -0,0 +1,100 @@ +// Copyright (c) The cargo-guppy Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::common::GuppyCargoCommon; +use guppy::graph::PackageGraph; +use guppy_cmdlib::{CargoMetadataOptions, PackagesAndFeatures}; +use once_cell::sync::Lazy; +use proptest::prelude::*; +use std::path::Path; + +// --- +// Paths to fixtures, relative to the cargo-compare directory (the one with Cargo.toml) +// --- +pub(super) static INSIDE_OUTSIDE_WORKSPACE: &str = + "../../fixtures/workspace/inside-outside/workspace"; +pub(super) static CARGO_GUPPY_WORKSPACE: &str = "."; + +#[derive(Debug)] +pub struct Fixture { + metadata_opts: CargoMetadataOptions, + graph: PackageGraph, +} + +macro_rules! define_fixture { + ($name: ident, $path: ident) => { + pub(crate) fn $name() -> &'static Fixture { + static FIXTURE: Lazy = Lazy::new(|| Fixture::new($path)); + &*FIXTURE + } + }; +} + +static CARGO_MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR"); + +impl Fixture { + pub fn new(workspace_dir: &str) -> Self { + // Assume that the workspace is relative to `CARGO_MANIFEST_DIR`. + let workspace_dir = Path::new(CARGO_MANIFEST_DIR).join(workspace_dir); + if !workspace_dir.is_dir() { + panic!( + "workspace_dir {} is not a directory", + workspace_dir.display() + ); + } + let metadata_opts = CargoMetadataOptions { + manifest_path: Some(workspace_dir.join("Cargo.toml")), + }; + let graph = metadata_opts + .make_command() + .build_graph() + .expect("constructing package graph worked"); + + Self { + metadata_opts, + graph, + } + } + + // --- + // Fixtures + // --- + + define_fixture!(inside_outside, INSIDE_OUTSIDE_WORKSPACE); + define_fixture!(cargo_guppy, CARGO_GUPPY_WORKSPACE); + + // --- + + pub fn graph(&self) -> &PackageGraph { + &self.graph + } + + /// Returns the number of proptest iterations that should be run for this fixture. + pub fn num_proptests(&self) -> u32 { + // Large graphs (like cargo-guppy's) can only really do a tiny number of proptests + // reasonably. It would be cool to figure out a way to speed it up (maybe through + // parallelization?) + if self.graph.package_count() > 100 { + 2 + } else { + 16 + } + } + + pub fn common_strategy<'a>(&'a self) -> impl Strategy + 'a { + let metadata_opts = &self.metadata_opts; + ( + PackagesAndFeatures::strategy(self.graph()), + any::(), + any::(), + // TODO: random target_platform generation + ) + .prop_map(move |(pf, include_dev, v2)| GuppyCargoCommon { + pf, + include_dev, + v2, + target_platform: None, + metadata_opts: metadata_opts.clone(), + }) + } +} diff --git a/tools/cargo-compare/src/tests/mod.rs b/tools/cargo-compare/src/tests/mod.rs new file mode 100644 index 00000000000..d14670e1d3c --- /dev/null +++ b/tools/cargo-compare/src/tests/mod.rs @@ -0,0 +1,7 @@ +// Copyright (c) The cargo-guppy Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +mod fixtures; +#[macro_use] +mod proptest_helpers; +mod workspace_tests; diff --git a/tools/cargo-compare/src/tests/proptest_helpers.rs b/tools/cargo-compare/src/tests/proptest_helpers.rs new file mode 100644 index 00000000000..d4e8fb1d009 --- /dev/null +++ b/tools/cargo-compare/src/tests/proptest_helpers.rs @@ -0,0 +1,35 @@ +// Copyright (c) The cargo-guppy Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use crate::common::GuppyCargoCommon; +use crate::diff::DiffOpts; +use crate::GlobalContext; +use guppy::graph::PackageGraph; + +macro_rules! proptest_suite { + ($name: ident) => { + mod $name { + use crate::tests::fixtures::Fixture; + use crate::tests::proptest_helpers::*; + use proptest::prelude::*; + + #[test] + fn proptest_compare() { + let fixture = Fixture::$name(); + // cargo is pretty slow, so limit the number of test cases. + proptest!(ProptestConfig::with_cases(fixture.num_proptests()), |( + common in fixture.common_strategy(), + )| { + compare(fixture.graph(), common); + }); + } + } + } +} + +/// Test that there is no diff between guppy and cargo for the same query. +pub(super) fn compare(graph: &PackageGraph, common: GuppyCargoCommon) { + let diff_opts = DiffOpts { common }; + let ctx = GlobalContext::new(true, graph).expect("context created"); + diff_opts.exec(&ctx).expect("no errors and no diff found"); +} diff --git a/tools/cargo-compare/src/tests/workspace_tests.rs b/tools/cargo-compare/src/tests/workspace_tests.rs new file mode 100644 index 00000000000..1b8ee7547e4 --- /dev/null +++ b/tools/cargo-compare/src/tests/workspace_tests.rs @@ -0,0 +1,5 @@ +// Copyright (c) The cargo-guppy Contributors +// SPDX-License-Identifier: MIT OR Apache-2.0 + +proptest_suite!(inside_outside); +proptest_suite!(cargo_guppy);