Repository: taiki-e/cargo-hack (language: rust)

Issue:
request: option to also enumerate second-order features
A little known thing is you can do:

```
 cargo test --features "foo/bar"
```

Which activates the "bar" feature of the "foo" crate. ( it makes sense when you see how features are defined in Cargo.toml ).

Subsequently, this means there is a way to expand test coverage to test situations where somebody turns on that feature in a different crate, and you get it through cargo composition.

So it would be nice if there was a way to test these, understandably, the set size it may generate could be prohibitively large in some cases, so its not a sensible default.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 637f63cd..dab2067f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,6 +33,8 @@ jobs:
           - 1.36.0
           - 1.38.0
           - 1.39.0
+          - 1.40.0
+          - 1.41.0
           - stable
           - beta
           - nightly
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dafb1649..af70a9b8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -19,10 +19,13 @@ This project adheres to [Semantic Versioning](https://semver.org).
 
 * [Add `--include-features` option.][66] See [#66][66] for details.
 
+* [Add --include-deps-features option.][70] See [#29][29] for details.
+
 * [Fix an issue where using `--features` with `--each-feature` or `--feature-powerset` together would result in the same feature combination being performed multiple times.][64]
 
 * [Improve performance by avoiding reading and parsing Cargo manifest.][73]
 
+[29]: https://github.com/taiki-e/cargo-hack/pull/29
 [42]: https://github.com/taiki-e/cargo-hack/pull/42
 [61]: https://github.com/taiki-e/cargo-hack/pull/61
 [62]: https://github.com/taiki-e/cargo-hack/pull/62
@@ -30,6 +33,7 @@ This project adheres to [Semantic Versioning](https://semver.org).
 [64]: https://github.com/taiki-e/cargo-hack/pull/64
 [65]: https://github.com/taiki-e/cargo-hack/pull/65
 [66]: https://github.com/taiki-e/cargo-hack/pull/66
+[70]: https://github.com/taiki-e/cargo-hack/pull/70
 [73]: https://github.com/taiki-e/cargo-hack/pull/73
 
 ## [0.3.14] - 2020-10-10
diff --git a/Cargo.lock b/Cargo.lock
index fc6aa6c2..ea751173 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -17,6 +17,7 @@ dependencies = [
  "anyhow 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
  "ctrlc 3.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "easy-ext 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustversion 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)",
  "term_size 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -88,6 +89,16 @@ dependencies = [
  "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "rustversion"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)",
+ "syn 1.0.44 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "ryu"
 version = "1.0.5"
@@ -187,6 +198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 "checksum nix 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "83450fe6a6142ddd95fb064b746083fc4ef1705fe81f64a64e1d4b39f54a1055"
 "checksum proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
 "checksum quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37"
+"checksum rustversion 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b9bdc5e856e51e685846fb6c13a1f5e5432946c2c90501bdc76a1319f19e29da"
 "checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
 "checksum serde 1.0.117 (registry+https://github.com/rust-lang/crates.io-index)" = "b88fa983de7720629c9387e9f517353ed404164b1e482c970a90c1a4aaf7dc1a"
 "checksum serde_json 1.0.59 (registry+https://github.com/rust-lang/crates.io-index)" = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
diff --git a/Cargo.toml b/Cargo.toml
index d8a8b514..3fb8e287 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,3 +28,4 @@ toml = "0.5.2"
 
 [dev-dependencies]
 easy-ext = "0.2"
+rustversion = "1"
diff --git a/src/cli.rs b/src/cli.rs
index 12d51f2b..1820c75f 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -211,6 +211,8 @@ pub(crate) struct Args<'a> {
     pub(crate) depth: Option<usize>,
     /// --include-features
     pub(crate) include_features: Vec<&'a str>,
+    /// --include-deps-features
+    pub(crate) include_deps_features: bool,
 
     /// --no-default-features
     pub(crate) no_default_features: bool,
@@ -290,6 +292,7 @@ pub(crate) fn perse_args<'a>(
     raw: &'a RawArgs,
     coloring: &mut Option<Coloring>,
     cargo: &OsStr,
+    version: u32,
 ) -> Result<Args<'a>> {
     let mut iter = raw.0.iter();
     let mut args = iter.by_ref().map(String::as_str).peekable();
@@ -322,6 +325,7 @@ pub(crate) fn perse_args<'a>(
     let mut ignore_unknown_features = false;
     let mut clean_per_run = false;
     let mut depth = None;
+    let mut include_deps_features = false;
 
     let mut exclude_features = Vec::new();
     let mut exclude_no_default_features = false;
@@ -486,6 +490,7 @@ pub(crate) fn perse_args<'a>(
                     continue;
                 }
                 "--exclude-all-features" => parse_flag!(exclude_all_features),
+                "--include-deps-features" => parse_flag!(include_deps_features),
                 "--clean-per-run" => parse_flag!(clean_per_run),
                 "--ignore-unknown-features" => parse_flag!(ignore_unknown_features),
                 "--ignore-non-exist-features" => bail!(
@@ -554,6 +559,10 @@ pub(crate) fn perse_args<'a>(
             bail!(
                 "--include-features can only be used together with either --each-feature or --feature-powerset"
             );
+        } else if include_deps_features {
+            bail!(
+                "--include-deps-features can only be used together with either --each-feature or --feature-powerset"
+            );
         }
     }
     if depth.is_some() && !feature_powerset {
@@ -595,6 +604,10 @@ pub(crate) fn perse_args<'a>(
         bail!("--all-features may not be used together with --feature-powerset");
     }
 
+    if include_deps_features && version < 41 {
+        bail!("--all-features requires Cargo 1.41 or leter")
+    }
+
     if subcommand.is_none() {
         if leading.contains(&"--list") {
             let mut line = ProcessBuilder::new(cargo);
@@ -653,6 +666,7 @@ For more information try --help
         clean_per_run,
         depth,
         include_features,
+        include_deps_features,
 
         no_default_features,
         verbose,
diff --git a/src/context.rs b/src/context.rs
index 75e1f9a1..f4f6b89a 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -3,7 +3,7 @@ use std::{collections::HashMap, env, ffi::OsString, ops::Deref, path::Path};
 use crate::{
     cli::{self, Args, Coloring, RawArgs},
     manifest::Manifest,
-    metadata::{Metadata, Package, PackageId},
+    metadata::{Metadata, Node, Package, PackageId},
     version, ProcessBuilder, Result,
 };
 
@@ -28,7 +28,7 @@ impl<'a> Context<'a> {
             }
         };
 
-        let args = cli::perse_args(args, coloring, &cargo)?;
+        let args = cli::perse_args(args, coloring, &cargo, cargo_version)?;
         assert!(
             args.subcommand.is_some() || args.remove_dev_deps,
             "no subcommand or valid flag specified"
@@ -59,9 +59,9 @@ impl<'a> Context<'a> {
     pub(crate) fn workspace_members(&self) -> impl Iterator<Item = &PackageId> {
         self.metadata.workspace_members.iter()
     }
-    // pub(crate) fn nodes(&self, id: &PackageId) -> &Node {
-    //     &self.metadata.resolve.nodes[id]
-    // }
+    pub(crate) fn nodes(&self, id: &PackageId) -> &Node {
+        &self.metadata.resolve.nodes[id]
+    }
     pub(crate) fn current_package(&self) -> Option<&PackageId> {
         self.metadata.resolve.root.as_ref()
     }
diff --git a/src/main.rs b/src/main.rs
index 3b5bfe5c..d25e68f4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -21,7 +21,7 @@ mod restore;
 mod version;
 
 use anyhow::{bail, Context as _};
-use std::{fmt::Write, fs};
+use std::{borrow::Cow, fmt::Write, fs};
 
 use crate::{
     cli::Coloring,
@@ -82,8 +82,8 @@ enum Kind<'a> {
     NoSubcommand,
     SkipAsPrivate,
     Nomal,
-    Each { features: Vec<&'a str> },
-    Powerset { features: Vec<Vec<&'a str>> },
+    Each { features: Vec<Cow<'a, str>> },
+    Powerset { features: Vec<Vec<Cow<'a, str>>> },
 }
 
 fn determine_kind<'a>(cx: &'a Context<'_>, id: &PackageId, progress: &mut Progress) -> Kind<'a> {
@@ -105,21 +105,52 @@ fn determine_kind<'a>(cx: &'a Context<'_>, id: &PackageId, progress: &mut Progre
             .iter()
             .map(String::as_str)
             .filter(|f| *f != "default" && !cx.exclude_features.contains(f))
+            .map(Cow::Borrowed)
             .collect();
+
         if let Some(opt_deps) = &cx.optional_deps {
-            features.extend(package.dependencies.iter().filter_map(Dependency::as_feature).filter(
-                move |&f| {
-                    !cx.exclude_features.contains(&f)
-                        && (opt_deps.is_empty() || opt_deps.contains(&f))
-                },
-            ));
+            features.extend(
+                package
+                    .dependencies
+                    .iter()
+                    .filter_map(Dependency::as_feature)
+                    .filter(move |f| {
+                        !cx.exclude_features.contains(f)
+                            && (opt_deps.is_empty() || opt_deps.contains(f))
+                    })
+                    .map(Cow::Borrowed),
+            );
         }
+
+        if cx.include_deps_features {
+            let node = cx.nodes(id);
+            let package = cx.packages(id);
+            // TODO: Unpublished dependencies are not included in `node.deps`.
+            for dep in node.deps.iter().filter(|dep| {
+                // ignore if `dep_kinds` is empty (i.e., not Rust 1.41+), target specific or not a normal dependency.
+                dep.dep_kinds.iter().any(|kind| kind.kind.is_none() && kind.target.is_none())
+            }) {
+                let dep_package = cx.packages(&dep.pkg);
+                // TODO: `dep.name` (`resolve.nodes[].deps[].name`) is a valid rust identifier, not a valid feature flag.
+                // And `packages[].dependencies` doesn't have package identifier,
+                // so I'm not sure if there is a way to find the actual feature name exactly.
+                if let Some(d) = package.dependencies.iter().find(|d| d.name == dep_package.name) {
+                    let name = d.rename.as_ref().unwrap_or(&d.name);
+                    features.extend(
+                        dep_package.features.iter().map(|f| Cow::Owned(format!("{}/{}", name, f))),
+                    );
+                }
+                // TODO: Optional deps of `dep_package`.
+            }
+        }
+
         features
     } else {
         cx.include_features
             .iter()
             .filter(|&&f| f != "default" && !cx.exclude_features.contains(&f))
             .copied()
+            .map(Cow::Borrowed)
             .collect()
     };
 
diff --git a/src/metadata.rs b/src/metadata.rs
index 5fbb9eab..5603a591 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -6,7 +6,6 @@ use std::{
     ffi::OsStr,
     fmt,
     path::PathBuf,
-    rc::Rc,
 };
 
 use crate::{cli::Args, Context, ProcessBuilder, Result};
@@ -25,12 +24,12 @@ type ParseResult<T> = Result<T, &'static str>;
 #[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
 pub(crate) struct PackageId {
     /// The underlying string representation of id.
-    repr: Rc<str>,
+    repr: String,
 }
 
 impl PackageId {
     fn new(repr: String) -> Self {
-        Self { repr: repr.into() }
+        Self { repr }
     }
 }
 
@@ -78,9 +77,6 @@ impl Metadata {
                 .remove_array("packages")?
                 .into_iter()
                 .map(|v| Package::from_value(v, version))
-                .filter(|res| {
-                    res.as_ref().map(|(id, _)| workspace_members.contains(id)).unwrap_or(true)
-                })
                 .collect::<Result<_, _>>()?,
             workspace_members,
             resolve: Resolve::from_obj(map.remove_object("resolve")?, version)?,
@@ -91,126 +87,114 @@ impl Metadata {
 
 /// A dependency graph
 pub(crate) struct Resolve {
-    // /// Nodes in a dependencies graph
-    // pub(crate) nodes: HashMap<PackageId, Node>,
+    /// Nodes in a dependencies graph
+    pub(crate) nodes: HashMap<PackageId, Node>,
     // if `None`, cargo-hack called in the root of a virtual workspace
     /// The crate for which the metadata was read.
     pub(crate) root: Option<PackageId>,
 }
 
 impl Resolve {
-    fn from_obj(mut map: Object, _version: u32) -> ParseResult<Self> {
+    fn from_obj(mut map: Object, version: u32) -> ParseResult<Self> {
         Ok(Self {
-            // nodes: map
-            //     .remove_array("nodes")?
-            //     .into_iter()
-            //     .map(|v| Node::from_value(v, version))
-            //     .collect::<Result<_, _>>()?,
+            nodes: map
+                .remove_array("nodes")?
+                .into_iter()
+                .map(|v| Node::from_value(v, version))
+                .collect::<Result<_, _>>()?,
             root: map.remove_nullable("root", into_string)?.map(PackageId::new),
         })
     }
 }
 
-// /// A node in a dependencies graph
-// pub(crate) struct Node {
-//     /// An opaque identifier for a package
-//     pub(crate) id: PackageId,
-//     /// Dependencies in a structured format.
-//     ///
-//     /// This is always empty if running with a version of Cargo older than 1.30.
-//     pub(crate) deps: Vec<NodeDep>,
-//     /// Features enabled on the crate
-//     pub(crate) features: Vec<String>,
-// }
-
-// impl Node {
-//     fn from_value(mut value: Value, version: u32) -> ParseResult<(PackageId, Self)> {
-//         let map = value.as_object_mut().ok_or("nodes")?;
-
-//         let this = Self {
-//             id: map.remove_string("id").map(PackageId::new)?,
-//             // This field was added in Rust 1.30.
-//             deps: if version >= 30 {
-//                 map.remove_array("deps")?
-//                     .into_iter()
-//                     .map(|v| NodeDep::from_value(v, version))
-//                     .collect::<Result<_, _>>()?
-//             } else {
-//                 Vec::new()
-//             },
-//             features: map
-//                 .remove_array("features")?
-//                 .into_iter()
-//                 .map(|v| into_string(v).ok_or("features"))
-//                 .collect::<Result<_, _>>()?,
-//         };
-//         Ok((this.id.clone(), this))
-//     }
-// }
-
-// /// A dependency in a node
-// pub(crate) struct NodeDep {
-//     /// Package ID (opaque unique identifier)
-//     pub(crate) pkg: PackageId,
-//     /// The kinds of dependencies.
-//     ///
-//     /// This is always empty if running with a version of Cargo older than 1.41.
-//     pub(crate) dep_kinds: Vec<DepKindInfo>,
-// }
-
-// impl NodeDep {
-//     fn from_value(mut value: Value, version: u32) -> ParseResult<Self> {
-//         let map = value.as_object_mut().ok_or("deps")?;
-
-//         Ok(Self {
-//             pkg: PackageId::new(map.remove_string("pkg")?),
-//             // This field was added in Rust 1.41.
-//             dep_kinds: if version >= 41 {
-//                 map.remove_array("dep_kinds")?
-//                     .into_iter()
-//                     .map(DepKindInfo::from_value)
-//                     .collect::<Result<_, _>>()?
-//             } else {
-//                 Vec::new()
-//             },
-//         })
-//     }
-// }
-
-// /// Information about a dependency kind.
-// pub(crate) struct DepKindInfo {
-//     /// The kind of dependency.
-//     pub(crate) kind: Option<String>,
-//     /// The target platform for the dependency.
-//     ///
-//     /// This is `None` if it is not a target dependency.
-//     ///
-//     /// By default all platform dependencies are included in the resolve
-//     /// graph. Use Cargo's `--filter-platform` flag if you only want to
-//     /// include dependencies for a specific platform.
-//     pub(crate) target: Option<Platform>,
-// }
-
-// impl DepKindInfo {
-//     fn from_value(mut value: Value) -> ParseResult<Self> {
-//         let map = value.as_object_mut().ok_or("dep_kinds")?;
-
-//         Ok(Self {
-//             kind: map.remove_nullable("kind", into_string)?,
-//             target: map.remove_nullable("target", into_string)?,
-//         })
-//     }
-// }
-
-// type Platform = String;
+/// A node in a dependencies graph
+pub(crate) struct Node {
+    /// Dependencies in a structured format.
+    ///
+    /// This is always empty if running with a version of Cargo older than 1.30.
+    pub(crate) deps: Vec<NodeDep>,
+}
+
+impl Node {
+    fn from_value(mut value: Value, version: u32) -> ParseResult<(PackageId, Self)> {
+        let map = value.as_object_mut().ok_or("nodes")?;
+
+        let id = map.remove_string("id").map(PackageId::new)?;
+        Ok((id, Self {
+            // This field was added in Rust 1.30.
+            deps: if version >= 30 {
+                map.remove_array("deps")?
+                    .into_iter()
+                    .map(|v| NodeDep::from_value(v, version))
+                    .collect::<Result<_, _>>()?
+            } else {
+                Vec::new()
+            },
+        }))
+    }
+}
+
+/// A dependency in a node
+pub(crate) struct NodeDep {
+    /// Package ID (opaque unique identifier)
+    pub(crate) pkg: PackageId,
+    /// The kinds of dependencies.
+    ///
+    /// This is always empty if running with a version of Cargo older than 1.41.
+    pub(crate) dep_kinds: Vec<DepKindInfo>,
+}
+
+impl NodeDep {
+    fn from_value(mut value: Value, version: u32) -> ParseResult<Self> {
+        let map = value.as_object_mut().ok_or("deps")?;
+
+        Ok(Self {
+            pkg: PackageId::new(map.remove_string("pkg")?),
+            // This field was added in Rust 1.41.
+            dep_kinds: if version >= 41 {
+                map.remove_array("dep_kinds")?
+                    .into_iter()
+                    .map(DepKindInfo::from_value)
+                    .collect::<Result<_, _>>()?
+            } else {
+                Vec::new()
+            },
+        })
+    }
+}
+
+/// Information about a dependency kind.
+pub(crate) struct DepKindInfo {
+    /// The kind of dependency.
+    pub(crate) kind: Option<String>,
+    /// The target platform for the dependency.
+    ///
+    /// This is `None` if it is not a target dependency.
+    ///
+    /// By default all platform dependencies are included in the resolve
+    /// graph. Use Cargo's `--filter-platform` flag if you only want to
+    /// include dependencies for a specific platform.
+    pub(crate) target: Option<Platform>,
+}
+
+impl DepKindInfo {
+    fn from_value(mut value: Value) -> ParseResult<Self> {
+        let map = value.as_object_mut().ok_or("dep_kinds")?;
+
+        Ok(Self {
+            kind: map.remove_nullable("kind", into_string)?,
+            target: map.remove_nullable("target", into_string)?,
+        })
+    }
+}
+
+type Platform = String;
 
 pub(crate) struct Package {
     /// Name as given in the `Cargo.toml`
     pub(crate) name: String,
     // /// Version given in the `Cargo.toml`
     // pub(crate) version: String,
-    /// An opaque identifier for a package
-    pub(crate) id: PackageId,
     /// List of dependencies of this particular package
     pub(crate) dependencies: Vec<Dependency>,
     /// Features provided by the crate, mapped to the features required by that feature.
@@ -227,10 +211,10 @@ impl Package {
     fn from_value(mut value: Value, version: u32) -> ParseResult<(PackageId, Self)> {
         let map = value.as_object_mut().ok_or("packages")?;
 
-        let this = Self {
+        let id = PackageId::new(map.remove_string("id")?);
+        Ok((id, Self {
             name: map.remove_string("name")?,
             // version: map.remove_string("version")?,
-            id: PackageId::new(map.remove_string("id")?),
             dependencies: map
                 .remove_array("dependencies")?
                 .into_iter()
@@ -250,8 +234,7 @@ impl Package {
             } else {
                 true
             },
-        };
-        Ok((this.id.clone(), this))
+        }))
     }
 
     pub(crate) fn name_verbose(&self, cx: &Context<'_>) -> Cow<'_, str> {


Test patch:
diff --git a/tests/test.rs b/tests/test.rs
index c2b76534..ef9a174f 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -821,6 +821,48 @@ fn each_feature_all() {
         );
 }
 
+#[rustversion::attr(not(since(1.41)), ignore)]
+#[test]
+fn include_deps_features() {
+    cargo_hack()
+        .args(&["check", "--each-feature", "--include-deps-features"])
+        .test_dir("tests")
+        .assert_success()
+        .assert_stderr_contains("
+            running `cargo check` on cargo-hack (1/21)
+            running `cargo check --no-default-features` on cargo-hack (2/21)
+            running `cargo check --no-default-features --features anyhow/default` on cargo-hack (3/21)
+            running `cargo check --no-default-features --features anyhow/std` on cargo-hack (4/21)
+            running `cargo check --no-default-features --features ctrlc/termination` on cargo-hack (5/21)
+            running `cargo check --no-default-features --features serde_json/alloc` on cargo-hack (6/21)
+            running `cargo check --no-default-features --features serde_json/arbitrary_precision` on cargo-hack (7/21)
+            running `cargo check --no-default-features --features serde_json/default` on cargo-hack (8/21)
+            running `cargo check --no-default-features --features serde_json/float_roundtrip` on cargo-hack (9/21)
+            running `cargo check --no-default-features --features serde_json/preserve_order` on cargo-hack (10/21)
+            running `cargo check --no-default-features --features serde_json/raw_value` on cargo-hack (11/21)
+            running `cargo check --no-default-features --features serde_json/std` on cargo-hack (12/21)
+            running `cargo check --no-default-features --features serde_json/unbounded_depth` on cargo-hack (13/21)
+            running `cargo check --no-default-features --features term_size/debug` on cargo-hack (14/21)
+            running `cargo check --no-default-features --features term_size/default` on cargo-hack (15/21)
+            running `cargo check --no-default-features --features term_size/nightly` on cargo-hack (16/21)
+            running `cargo check --no-default-features --features term_size/travis` on cargo-hack (17/21)
+            running `cargo check --no-default-features --features term_size/unstable` on cargo-hack (18/21)
+            running `cargo check --no-default-features --features toml/default` on cargo-hack (19/21)
+            running `cargo check --no-default-features --features toml/preserve_order` on cargo-hack (20/21)
+            running `cargo check --no-default-features --all-features` on cargo-hack (21/21)
+        ");
+}
+
+#[rustversion::attr(not(before(1.41)), ignore)]
+#[test]
+fn include_deps_features_version_failure() {
+    cargo_hack()
+        .args(&["check", "--each-feature", "--include-deps-features"])
+        .test_dir("tests")
+        .assert_failure()
+        .assert_stderr_contains("--all-features requires Cargo 1.41 or leter");
+}
+
 #[test]
 fn trailing_args() {
     cargo_hack()