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

Issue:
request: some sort of argument to add "optional" deps as features
Any time an optional dependency is specified, that internally becomes visible as a feature flag, eg:

```toml
[dependencies.foo]
version = "1.0"
optional = true
```

Can of course, be enabled with `--features foo`

And rust code can check for it with
```rust
#[cfg(feature = "foo")]
```

And code can activate that path via

```toml
[dependencies.bar]
features = ["foo"]
```

Subsequently, it would be nice to have, at least as an option, these hidden features in `--each-feature` and `--feature-powerset` to test for this case.



Discussion / hints:

Fix patch:
diff --git a/src/cli.rs b/src/cli.rs
index 225e6325..04559293 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -1,4 +1,4 @@
-use std::{env, rc::Rc, str::FromStr};
+use std::{env, mem, rc::Rc, str::FromStr};
 
 use anyhow::{bail, format_err, Error};
 use termcolor::ColorChoice;
@@ -33,13 +33,19 @@ OPTIONS:
         --feature-powerset          Perform for the feature powerset which
                                     includes `--no-default-features` and
                                     default features of the package
+        --optional-deps             Use optional dependencies as features,
+                                    this flag can only be used with either
+                                    `--each-feature` or `--feature-powerset`
+        --skip <FEATURES>           Space-separated list of features to skip,
+                                    this flag can only be used with either
+                                    `--each-feature` or `--feature-powerset`
         --no-dev-deps               Perform without dev-dependencies
         --remove-dev-deps           Equivalent to `--no-dev-deps` except for
                                     does not restore the original `Cargo.toml`
                                     after execution
         --ignore-private            Skip to perform on `publish = false` packages
         --ignore-unknown-features   Skip passing `--features` to `cargo` if that
-                                    feature does not exist in the package.
+                                    feature does not exist in the package
     -v, --verbose                   Use verbose output
                                     (this flag will be propagated to cargo)
         --color <WHEN>              Coloring: auto, always, never
@@ -73,7 +79,7 @@ pub(crate) struct Args {
     pub(crate) package: Vec<String>,
     /// --exclude <SPEC>...
     pub(crate) exclude: Vec<String>,
-    /// --all, --workspace
+    /// --workspace, (--all)
     pub(crate) workspace: bool,
     /// --each-feature
     pub(crate) each_feature: bool,
@@ -89,6 +95,8 @@ pub(crate) struct Args {
     pub(crate) ignore_private: bool,
     /// --ignore-unknown-features, (--ignore-non-exist-features)
     pub(crate) ignore_unknown_features: bool,
+    /// --optional-deps
+    pub(crate) optional_deps: bool,
 
     // flags that will be propagated to cargo
     /// --features <FEATURES>...
@@ -168,6 +176,7 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
     let mut ignore_private = false;
     let mut ignore_unknown_features = false;
     let mut ignore_non_exist_features = false;
+    let mut optional_deps = false;
 
     let res = (|| -> Result<()> {
         while let Some(arg) = args.next() {
@@ -264,34 +273,34 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
                     workspace = Some(arg);
                 }
                 "--no-dev-deps" => {
-                    if no_dev_deps {
+                    if mem::replace(&mut no_dev_deps, true) {
                         return Err(multi_arg(&arg, subcommand.as_ref()));
                     }
-                    no_dev_deps = true;
                 }
                 "--remove-dev-deps" => {
-                    if remove_dev_deps {
+                    if mem::replace(&mut remove_dev_deps, true) {
                         return Err(multi_arg(&arg, subcommand.as_ref()));
                     }
-                    remove_dev_deps = true;
                 }
                 "--each-feature" => {
-                    if each_feature {
+                    if mem::replace(&mut each_feature, true) {
                         return Err(multi_arg(&arg, subcommand.as_ref()));
                     }
-                    each_feature = true;
                 }
                 "--feature-powerset" => {
-                    if feature_powerset {
+                    if mem::replace(&mut feature_powerset, true) {
                         return Err(multi_arg(&arg, subcommand.as_ref()));
                     }
-                    feature_powerset = true;
                 }
                 "--ignore-private" => {
-                    if ignore_private {
+                    if mem::replace(&mut ignore_private, true) {
+                        return Err(multi_arg(&arg, subcommand.as_ref()));
+                    }
+                }
+                "--optional-deps" => {
+                    if mem::replace(&mut optional_deps, true) {
                         return Err(multi_arg(&arg, subcommand.as_ref()));
                     }
-                    ignore_private = true;
                 }
                 "--ignore-unknown-features" => {
                     if ignore_unknown_features || ignore_non_exist_features {
@@ -331,10 +340,12 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
     if !exclude.is_empty() && workspace.is_none() {
         bail!("--exclude can only be used together with --workspace");
     }
-
     if !skip.is_empty() && (!each_feature && !feature_powerset) {
         bail!("--skip can only be used with either --each-feature or --feature-powerset");
     }
+    if optional_deps && (!each_feature && !feature_powerset) {
+        bail!("--optional-deps can only be used with either --each-feature or --feature-powerset");
+    }
 
     if let Some(subcommand) = &subcommand {
         if subcommand == "test" || subcommand == "bench" {
@@ -417,6 +428,7 @@ For more information try --help
         remove_dev_deps,
         ignore_private,
         ignore_unknown_features: ignore_unknown_features || ignore_non_exist_features,
+        optional_deps,
 
         features,
         color,
diff --git a/src/manifest.rs b/src/manifest.rs
index 2fbd0852..ccd58a21 100644
--- a/src/manifest.rs
+++ b/src/manifest.rs
@@ -86,9 +86,7 @@ mod de {
 
     #[derive(Debug, Deserialize)]
     pub(crate) struct Package {
-        pub(crate) edition: Option<String>,
         pub(crate) name: String,
-        pub(crate) version: String,
         #[serde(default)]
         pub(crate) publish: Publish,
     }
diff --git a/src/metadata.rs b/src/metadata.rs
index 149da3cd..ad1cfdbe 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -18,6 +18,8 @@ use crate::{Args, Result};
 // * https://github.com/rust-lang/cargo/blob/0.40.0/src/cargo/core/package.rs#L57-L80
 // * https://github.com/oli-obk/cargo_metadata
 
+// As cargo_metadata does not preserve the order of feature flags, use our own structs.
+
 #[derive(Debug, Deserialize)]
 pub(crate) struct Metadata {
     /// A list of all crates referenced by this crate (and the crate itself)
@@ -30,12 +32,26 @@ pub(crate) struct Metadata {
 pub(crate) struct Package {
     /// Name as given in the `Cargo.toml`
     pub(crate) name: String,
+    /// 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.
     pub(crate) features: BTreeMap<String, Vec<String>>,
     /// Path containing the `Cargo.toml`
     pub(crate) manifest_path: PathBuf,
 }
 
+#[derive(Debug, Deserialize)]
+/// A dependency of the main crate
+pub(crate) struct Dependency {
+    /// Name as given in the `Cargo.toml`
+    pub(crate) name: String,
+    /// Whether this dependency is required or optional
+    pub(crate) optional: bool,
+    /// If the dependency is renamed, this is the new name for the dependency
+    /// as a string.  None if it is not renamed.
+    pub(crate) rename: Option<String>,
+}
+
 impl Metadata {
     pub(crate) fn new(args: &Args) -> Result<Self> {
         let cargo = env::var_os("CARGO").unwrap_or_else(|| OsString::from("cargo"));
@@ -70,3 +86,9 @@ impl Package {
         }
     }
 }
+
+impl Dependency {
+    pub(crate) fn as_feature(&self) -> Option<&String> {
+        if self.optional { Some(self.rename.as_ref().unwrap_or(&self.name)) } else { None }
+    }
+}
diff --git a/src/package.rs b/src/package.rs
index d6058718..782f37a9 100644
--- a/src/package.rs
+++ b/src/package.rs
@@ -50,6 +50,7 @@ impl<'a> Package<'a> {
 
 impl Deref for Package<'_> {
     type Target = metadata::Package;
+
     fn deref(&self) -> &Self::Target {
         self.package
     }
@@ -64,20 +65,44 @@ pub(crate) enum Kind<'a> {
 
 impl<'a> Kind<'a> {
     fn collect(args: &'a Args, package: &'a metadata::Package) -> (Self, usize) {
-        if (!args.each_feature && !args.feature_powerset) || package.features.is_empty() {
-            return (Kind::Nomal { show_progress: args.each_feature || args.feature_powerset }, 1);
+        if !args.each_feature && !args.feature_powerset {
+            return (Kind::Nomal { show_progress: false }, 1);
         }
 
         let features =
             package.features.keys().filter(|k| (*k != "default" && !args.skip.contains(k)));
+        let opt_deps = if args.optional_deps {
+            Some(package.dependencies.iter().filter_map(|dep| dep.as_feature()))
+        } else {
+            None
+        };
+
         if args.each_feature {
-            let features: Vec<_> = features.collect();
+            let features: Vec<_> = if let Some(opt_deps) = opt_deps {
+                features.chain(opt_deps).collect()
+            } else {
+                features.collect()
+            };
+
+            if package.features.is_empty() && features.is_empty() {
+                return (Kind::Nomal { show_progress: true }, 1);
+            }
+
             // +1: default features
             // +1: no default features
             let total = features.len() + 2;
             (Kind::Each { features }, total)
         } else if args.feature_powerset {
-            let features = powerset(features);
+            let features = if let Some(opt_deps) = opt_deps {
+                powerset(features.chain(opt_deps))
+            } else {
+                powerset(features)
+            };
+
+            if package.features.is_empty() && features.is_empty() {
+                return (Kind::Nomal { show_progress: true }, 1);
+            }
+
             // +1: default features
             // +1: no default features
             // -1: the first element of a powerset is `[]`
diff --git a/src/process.rs b/src/process.rs
index fc610c01..c0d3d3a4 100644
--- a/src/process.rs
+++ b/src/process.rs
@@ -68,7 +68,9 @@ impl ProcessBuilder {
     pub(crate) fn features(&mut self, args: &Args, package: &Package) -> &mut Self {
         if args.ignore_unknown_features {
             self.append_features(args.features.iter().filter(|f| {
-                if package.features.get(*f).is_some() {
+                if package.features.get(*f).is_some()
+                    || package.dependencies.iter().any(|dep| dep.as_feature() == Some(*f))
+                {
                     true
                 } else {
                     // ignored


Test patch:
diff --git a/tests/fixtures/no_dev_deps/.cargo/config b/tests/fixtures/no_dev_deps/.cargo/config
new file mode 100644
index 00000000..88403f3b
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/.cargo/config
@@ -0,0 +1,2 @@
+[build]
+target-dir = "../../../target"
diff --git a/tests/fixtures/no_dev_deps/Cargo.toml b/tests/fixtures/no_dev_deps/Cargo.toml
new file mode 100644
index 00000000..b7e201a3
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/Cargo.toml
@@ -0,0 +1,26 @@
+[package]
+name = "no_dev_deps"
+version = "0.1.0"
+authors = ["Taiki Endo <te316e89@gmail.com>"]
+edition = "2018"
+publish = false
+
+[workspace]
+members = [
+    "member1",
+    "member2",
+    "member3",
+    ".",
+]
+
+[dependencies]
+
+[dev-dependencies]
+
+[target.'cfg(unix)'.dev-dependencies]
+
+[features]
+default = []
+a = []
+b = []
+c = []
diff --git a/tests/fixtures/no_dev_deps/member1/Cargo.toml b/tests/fixtures/no_dev_deps/member1/Cargo.toml
new file mode 100644
index 00000000..0d22b1ee
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/member1/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "member1"
+version = "0.1.0"
+authors = ["Taiki Endo <te316e89@gmail.com>"]
+edition = "2018"
+
+[target.'cfg(unix)'.dev-dependencies]
+
+[features]
+default = []
+a = []
+b = []
+c = []
+
+[dependencies]
+
+[dev-dependencies]
diff --git a/tests/fixtures/no_dev_deps/member1/src/main.rs b/tests/fixtures/no_dev_deps/member1/src/main.rs
new file mode 100644
index 00000000..b5733ff2
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/member1/src/main.rs
@@ -0,0 +1,11 @@
+fn main() {
+    println!("hello!");
+    #[cfg(feature = "default")]
+    println!("default");
+    #[cfg(feature = "a")]
+    println!("a");
+    #[cfg(feature = "b")]
+    println!("b");
+    #[cfg(feature = "c")]
+    println!("c");
+}
diff --git a/tests/fixtures/no_dev_deps/member2/Cargo.toml b/tests/fixtures/no_dev_deps/member2/Cargo.toml
new file mode 100644
index 00000000..6d9ee428
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/member2/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "member2"
+version = "0.1.0"
+authors = ["Taiki Endo <te316e89@gmail.com>"]
+edition = "2018"
+publish = false
+
+[features]
+default = []
+a = []
+b = []
+c = []
+
+[dependencies]
+
+[dev-dependencies]
+
+[target.'cfg(unix)'.dev-dependencies]
diff --git a/tests/fixtures/no_dev_deps/member2/src/main.rs b/tests/fixtures/no_dev_deps/member2/src/main.rs
new file mode 100644
index 00000000..b5733ff2
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/member2/src/main.rs
@@ -0,0 +1,11 @@
+fn main() {
+    println!("hello!");
+    #[cfg(feature = "default")]
+    println!("default");
+    #[cfg(feature = "a")]
+    println!("a");
+    #[cfg(feature = "b")]
+    println!("b");
+    #[cfg(feature = "c")]
+    println!("c");
+}
diff --git a/tests/fixtures/no_dev_deps/member3/Cargo.toml b/tests/fixtures/no_dev_deps/member3/Cargo.toml
new file mode 100644
index 00000000..9ce5ee5c
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/member3/Cargo.toml
@@ -0,0 +1,19 @@
+[package]
+name = "member3"
+version = "0.1.0"
+authors = ["Taiki Endo <te316e89@gmail.com>"]
+edition = "2018"
+publish = true
+
+[dependencies]
+member2 = { path = "../member2" }
+
+[dev-dependencies]
+
+[target.'cfg(unix)'.dev-dependencies]
+
+[features]
+default = ["a"]
+a = []
+b = []
+c = []
diff --git a/tests/fixtures/no_dev_deps/member3/src/main.rs b/tests/fixtures/no_dev_deps/member3/src/main.rs
new file mode 100644
index 00000000..b5733ff2
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/member3/src/main.rs
@@ -0,0 +1,11 @@
+fn main() {
+    println!("hello!");
+    #[cfg(feature = "default")]
+    println!("default");
+    #[cfg(feature = "a")]
+    println!("a");
+    #[cfg(feature = "b")]
+    println!("b");
+    #[cfg(feature = "c")]
+    println!("c");
+}
diff --git a/tests/fixtures/no_dev_deps/src/main.rs b/tests/fixtures/no_dev_deps/src/main.rs
new file mode 100644
index 00000000..915fe672
--- /dev/null
+++ b/tests/fixtures/no_dev_deps/src/main.rs
@@ -0,0 +1,21 @@
+fn main() {
+    println!("hello!");
+    #[cfg(feature = "default")]
+    println!("default");
+    #[cfg(feature = "a")]
+    println!("a");
+    #[cfg(feature = "b")]
+    println!("b");
+    #[cfg(feature = "c")]
+    println!("c");
+}
+
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn test() {}
+
+    #[ignore]
+    #[test]
+    fn test_ignored() {}
+}
diff --git a/tests/fixtures/optional_deps/.cargo/config b/tests/fixtures/optional_deps/.cargo/config
new file mode 100644
index 00000000..88403f3b
--- /dev/null
+++ b/tests/fixtures/optional_deps/.cargo/config
@@ -0,0 +1,2 @@
+[build]
+target-dir = "../../../target"
diff --git a/tests/fixtures/optional_deps/Cargo.toml b/tests/fixtures/optional_deps/Cargo.toml
new file mode 100644
index 00000000..174422b0
--- /dev/null
+++ b/tests/fixtures/optional_deps/Cargo.toml
@@ -0,0 +1,15 @@
+[package]
+name = "optional_deps"
+version = "0.1.0"
+authors = ["Taiki Endo <te316e89@gmail.com>"]
+edition = "2018"
+publish = false
+
+[workspace]
+
+[dependencies]
+real = { path = "../real", optional = true }
+member2 = { path = "../real/member2" }
+renemed = { path = "../real/member3", package = "member3", optional = true }
+
+[features]
diff --git a/tests/fixtures/optional_deps/src/main.rs b/tests/fixtures/optional_deps/src/main.rs
new file mode 100644
index 00000000..83c3f66f
--- /dev/null
+++ b/tests/fixtures/optional_deps/src/main.rs
@@ -0,0 +1,10 @@
+fn main() {
+    #[cfg(feature = "member2")]
+    println!("member2");
+    #[cfg(feature = "member3")]
+    println!("member3");
+    #[cfg(feature = "real")]
+    println!("real");
+    #[cfg(feature = "renemed")]
+    println!("renemed");
+}
diff --git a/tests/test.rs b/tests/test.rs
index 305649b8..e0263b14 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -91,7 +91,52 @@ impl Output {
 }
 
 #[test]
-fn test_real() {
+fn multi_arg() {
+    // --package, -p, --exclude, --features, --skip, and --verbose are allowed.
+
+    for flag in &[
+        "--examples",
+        "--workspace",
+        "--all",
+        "--each-feature",
+        "--feature-powerset",
+        "--no-dev-deps",
+        "--remove-dev-deps",
+        "--ignore-private",
+        "--ignore-unknown-features",
+        "--optional-deps",
+    ][..]
+    {
+        cargo_hack()
+            .args(&["check", flag, flag])
+            .current_dir(test_dir("tests/fixtures/real"))
+            .output()
+            .unwrap()
+            .assert_failure()
+            .assert_stderr_contains(&format!(
+                "The argument '{}' was provided more than once, but cannot be used multiple times",
+                flag
+            ));
+    }
+
+    for (flag, msg) in
+        &[("--manifest-path", "--manifest-path <PATH>"), ("--color", "--color <WHEN>")][..]
+    {
+        cargo_hack()
+            .args(&["check", flag, "auto", flag, "auto"])
+            .current_dir(test_dir("tests/fixtures/real"))
+            .output()
+            .unwrap()
+            .assert_failure()
+            .assert_stderr_contains(&format!(
+                "The argument '{}' was provided more than once, but cannot be used multiple times",
+                msg
+            ));
+    }
+}
+
+#[test]
+fn real_manifest() {
     cargo_hack()
         .args(&["check"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -104,7 +149,7 @@ fn test_real() {
         .assert_stderr_contains("running `cargo check` on real");
 
     cargo_hack()
-        .args(&["check", "--all"])
+        .args(&["check", "--workspace"])
         .current_dir(test_dir("tests/fixtures/real"))
         .output()
         .unwrap()
@@ -116,7 +161,7 @@ fn test_real() {
 }
 
 #[test]
-fn test_virtual() {
+fn virtual_manifest() {
     cargo_hack()
         .args(&["check"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -137,7 +182,7 @@ fn test_virtual() {
 }
 
 #[test]
-fn test_real_all_in_subcrate() {
+fn real_all_in_subcrate() {
     cargo_hack()
         .args(&["check"])
         .current_dir(test_dir("tests/fixtures/real/member2"))
@@ -162,7 +207,7 @@ fn test_real_all_in_subcrate() {
 }
 
 #[test]
-fn test_virtual_all_in_subcrate() {
+fn virtual_all_in_subcrate() {
     cargo_hack()
         .args(&["check"])
         .current_dir(test_dir("tests/fixtures/virtual/member1"))
@@ -183,7 +228,7 @@ fn test_virtual_all_in_subcrate() {
 }
 
 #[test]
-fn test_real_ignore_private() {
+fn real_ignore_private() {
     cargo_hack()
         .args(&["check", "--ignore-private"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -214,7 +259,7 @@ fn test_real_ignore_private() {
 }
 
 #[test]
-fn test_virtual_ignore_private() {
+fn virtual_ignore_private() {
     cargo_hack()
         .args(&["check", "--ignore-private"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -239,7 +284,7 @@ fn test_virtual_ignore_private() {
 }
 
 #[test]
-fn test_package() {
+fn package() {
     cargo_hack()
         .args(&["check", "--package", "member1"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -251,7 +296,7 @@ fn test_package() {
 }
 
 #[test]
-fn test_package_no_packages() {
+fn package_no_packages() {
     cargo_hack()
         .args(&["check", "--package", "foo"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -262,32 +307,30 @@ fn test_package_no_packages() {
 }
 
 #[test]
-fn test_exclude() {
+fn exclude() {
     cargo_hack()
-        .args(&["check", "--all", "--exclude", "foo"])
+        .args(&["check", "--all", "--exclude", "member1"])
         .current_dir(test_dir("tests/fixtures/virtual"))
         .output()
         .unwrap()
         .assert_success()
-        .assert_stderr_contains("excluded package(s) foo not found in workspace")
-        .assert_stderr_contains("running `cargo check` on member1")
+        .assert_stderr_not_contains("running `cargo check` on member1")
         .assert_stderr_contains("running `cargo check` on member2");
-}
 
-#[test]
-fn test_exclude_not_found() {
+    // not_found is warning
     cargo_hack()
-        .args(&["check", "--all", "--exclude", "member1"])
+        .args(&["check", "--all", "--exclude", "foo"])
         .current_dir(test_dir("tests/fixtures/virtual"))
         .output()
         .unwrap()
         .assert_success()
-        .assert_stderr_not_contains("running `cargo check` on member1")
+        .assert_stderr_contains("excluded package(s) foo not found in workspace")
+        .assert_stderr_contains("running `cargo check` on member1")
         .assert_stderr_contains("running `cargo check` on member2");
 }
 
 #[test]
-fn test_exclude_not_with_all() {
+fn exclude_not_with_all() {
     cargo_hack()
         .args(&["check", "--exclude", "member1"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -298,7 +341,7 @@ fn test_exclude_not_with_all() {
 }
 
 #[test]
-fn test_remove_dev_deps_with_devs() {
+fn remove_dev_deps_with_devs() {
     for flag in &[
         "--example",
         "--examples",
@@ -336,10 +379,10 @@ fn test_remove_dev_deps_with_devs() {
 }
 
 #[test]
-fn test_no_dev_deps() {
+fn no_dev_deps() {
     cargo_hack()
         .args(&["check", "--no-dev-deps", "--remove-dev-deps"])
-        .current_dir(test_dir("tests/fixtures/real"))
+        .current_dir(test_dir("tests/fixtures/no_dev_deps"))
         .output()
         .unwrap()
         .assert_failure()
@@ -347,21 +390,19 @@ fn test_no_dev_deps() {
 
     cargo_hack()
         .args(&["check", "--no-dev-deps"])
-        .current_dir(test_dir("tests/fixtures/real"))
+        .current_dir(test_dir("tests/fixtures/no_dev_deps"))
         .output()
         .unwrap()
         .assert_success()
-        .assert_stderr_contains("running `cargo check` on real")
+        .assert_stderr_contains("running `cargo check` on no_dev_deps")
         .assert_stderr_contains(
             "`--no-dev-deps` flag removes dev-dependencies from real `Cargo.toml` while cargo-hack is running and restores it when finished",
         );
-}
 
-#[test]
-fn test_no_dev_deps_all() {
+    // with --all
     cargo_hack()
         .args(&["check", "--no-dev-deps", "--all"])
-        .current_dir(test_dir("tests/fixtures/real"))
+        .current_dir(test_dir("tests/fixtures/no_dev_deps"))
         .output()
         .unwrap()
         .assert_success()
@@ -371,7 +412,7 @@ fn test_no_dev_deps_all() {
 }
 
 #[test]
-fn test_no_dev_deps_with_devs() {
+fn no_dev_deps_with_devs() {
     for flag in &[
         "--example",
         "--examples",
@@ -409,7 +450,7 @@ fn test_no_dev_deps_with_devs() {
 }
 
 #[test]
-fn test_ignore_unknown_features() {
+fn ignore_unknown_features() {
     cargo_hack()
         .args(&["check", "--ignore-unknown-features", "--no-default-features", "--features", "f"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -422,10 +463,8 @@ fn test_ignore_unknown_features() {
         .assert_stderr_contains(
             "running `cargo check --no-default-features --features f` on member2",
         );
-}
 
-#[test]
-fn test_ignore_non_exist_features() {
+    // --ignore-non-exist-features is a deprecated alias of --ignore-unknown-features
     cargo_hack()
         .args(&["check", "--ignore-non-exist-features", "--features=f"])
         .current_dir(test_dir("tests/fixtures/virtual"))
@@ -440,7 +479,7 @@ fn test_ignore_non_exist_features() {
 }
 
 #[test]
-fn test_each_feature() {
+fn each_feature() {
     cargo_hack()
         .args(&["check", "--each-feature"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -458,10 +497,31 @@ fn test_each_feature() {
         .assert_stderr_contains(
             "running `cargo check --no-default-features --features c` on real (5/5)",
         );
+
+    // with other feature
+    cargo_hack()
+        .args(&["check", "--each-feature", "--features=a"])
+        .current_dir(test_dir("tests/fixtures/real"))
+        .output()
+        .unwrap()
+        .assert_success()
+        .assert_stderr_contains("running `cargo check --features a` on real (1/5)")
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a` on real (2/5)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a,a` on real (3/5)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a,b` on real (4/5)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a,c` on real (5/5)",
+        );
 }
 
 #[test]
-fn test_feature_powerset() {
+fn feature_powerset() {
     cargo_hack()
         .args(&["check", "--feature-powerset"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -494,7 +554,7 @@ fn test_feature_powerset() {
 }
 
 #[test]
-fn test_skip_failure() {
+fn skip_failure() {
     cargo_hack()
         .args(&["check", "--skip", "a"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -507,7 +567,7 @@ fn test_skip_failure() {
 }
 
 #[test]
-fn test_each_feature_skip_success() {
+fn each_feature_skip_success() {
     cargo_hack()
         .args(&["check", "--each-feature", "--skip", "a"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -528,7 +588,7 @@ fn test_each_feature_skip_success() {
 }
 
 #[test]
-fn test_powerset_skip_success() {
+fn powerset_skip_success() {
     cargo_hack()
         .args(&["check", "--feature-powerset", "--skip", "a"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -557,30 +617,7 @@ fn test_powerset_skip_success() {
 }
 
 #[test]
-fn test_each_feature2() {
-    cargo_hack()
-        .args(&["check", "--each-feature", "--features=a"])
-        .current_dir(test_dir("tests/fixtures/real"))
-        .output()
-        .unwrap()
-        .assert_success()
-        .assert_stderr_contains("running `cargo check --features a` on real (1/5)")
-        .assert_stderr_contains(
-            "running `cargo check --no-default-features --features a` on real (2/5)",
-        )
-        .assert_stderr_contains(
-            "running `cargo check --no-default-features --features a,a` on real (3/5)",
-        )
-        .assert_stderr_contains(
-            "running `cargo check --no-default-features --features a,b` on real (4/5)",
-        )
-        .assert_stderr_contains(
-            "running `cargo check --no-default-features --features a,c` on real (5/5)",
-        );
-}
-
-#[test]
-fn test_each_feature_all() {
+fn each_feature_all() {
     cargo_hack()
         .args(&["check", "--each-feature", "--workspace"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -634,7 +671,7 @@ fn test_each_feature_all() {
 }
 
 #[test]
-fn test_args2() {
+fn trailing_args() {
     cargo_hack()
         .args(&["test", "--", "--ignored"])
         .current_dir(test_dir("tests/fixtures/real"))
@@ -647,7 +684,7 @@ fn test_args2() {
 }
 
 #[test]
-fn test_package_collision() {
+fn package_collision() {
     cargo_hack()
         .args(&["check"])
         .current_dir(test_dir("tests/fixtures/package_collision"))
@@ -659,7 +696,7 @@ fn test_package_collision() {
 }
 
 #[test]
-fn test_not_find_manifest() {
+fn not_find_manifest() {
     cargo_hack()
         .args(&["check"])
         .current_dir(test_dir("tests/fixtures/virtual/dir/not_find_manifest"))
@@ -700,3 +737,51 @@ fn test_not_find_manifest() {
         .assert_stderr_contains("running `cargo check` on member2")
         .assert_stderr_contains("running `cargo check` on not_find_manifest");
 }
+
+#[test]
+fn optional_deps() {
+    cargo_hack()
+        .args(&["run", "--features=real,member2,renemed", "--ignore-unknown-features"])
+        .current_dir(test_dir("tests/fixtures/optional_deps"))
+        .output()
+        .unwrap()
+        .assert_success()
+        .assert_stderr_contains("skipped applying unknown `member2` feature to optional_deps")
+        .assert_stderr_contains("running `cargo run --features real,renemed` on optional_deps")
+        .assert_stdout_contains("renemed")
+        .assert_stdout_contains("real")
+        .assert_stdout_not_contains("member3")
+        .assert_stdout_not_contains("member2");
+
+    cargo_hack()
+        .args(&["check", "--each-feature"])
+        .current_dir(test_dir("tests/fixtures/optional_deps"))
+        .output()
+        .unwrap()
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on optional_deps (1/1)")
+        .assert_stderr_not_contains("running `cargo check --no-default-features` on optional_deps")
+        .assert_stderr_not_contains(
+            "running `cargo check --no-default-features --features real` on optional_deps",
+        )
+        .assert_stderr_not_contains(
+            "running `cargo check  --no-default-features--features renemed` on optional_deps",
+        );
+
+    cargo_hack()
+        .args(&["check", "--each-feature", "--optional-deps"])
+        .current_dir(test_dir("tests/fixtures/optional_deps"))
+        .output()
+        .unwrap()
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on optional_deps (1/4)")
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features` on optional_deps (2/4)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features real` on optional_deps (3/4)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features renemed` on optional_deps (4/4)",
+        );
+}