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

Issue:
Add --include-features (and add --exclude-features as alias of --skip)
* [ ] Add --include-features option (#66)
* [x] Add --exclude-features as an alias of --skip (#65)
* [x] Rename --skip-no-default-features to --exclude-no-default-features (#65)
* [x] Rename --skip-all-features to --exclude-all-features (#65)

Pull request description:
Add --include-features option
```text
--include-features <FEATURES>...
    Include only the specified features in the feature combinations instead of package features.

    This flag can only be used together with either --each-feature flag or --feature-powerset flag.
```

Closes #52

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac8965b..e8da586 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,20 +6,28 @@ This project adheres to [Semantic Versioning](https://semver.org).
 
 ## [Unreleased]
 
-* Remove `--ignore-non-exist-features` flag, use `--ignore-unknown-features` flag instead.
+* [Remove `--ignore-non-exist-features` flag.][62] Use `--ignore-unknown-features` flag instead.
 
-* Treat `--all-features` flag as one of feature combinations. See [#42][42] for details.
+* [Treat `--all-features` flag as one of feature combinations.][61] See [#42][42] for details.
 
-* Add `--exclude-all-features` flag. See [#42][42] for details.
+* Add `--exclude-all-features` flag. ([#61][61], [#65][65]) See [#42][42] for details.
 
-* Add `--exclude-features` option. This is an alias of `--skip` option.
+* [Add `--exclude-features` option. This is an alias of `--skip` option.][65]
 
-* Rename `--skip-no-default-features` flag to `--exclude-no-default-features`.
+* [Rename `--skip-no-default-features` flag to `--exclude-no-default-features`.][65]
   The old name can be used as an alias, but is deprecated.
 
-* Fix an issue where using `--features` with `--each-feature` or `--feature-powerset` together would result in the same feature combination being performed multiple times.
+* [Add `--include-features` option.][66] See [#66][66] 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]
 
 [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
+[63]: https://github.com/taiki-e/cargo-hack/pull/63
+[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
 
 ## [0.3.14] - 2020-10-10
 
diff --git a/src/cli.rs b/src/cli.rs
index 8983f04..650c5b7 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -31,6 +31,8 @@ const HELP: &[(&str, &str, &str, &[&str])] = &[
     ("", "--skip <FEATURES>...", "Alias for --exclude-features", &[]),
     ("", "--exclude-features <FEATURES>...", "Space-separated list of features to exclude", &[
         "To exclude run of default feature, using value `--exclude-features default`.",
+        "To exclude run of just --no-default-features flag, using --exclude-no-default-features flag.",
+        "To exclude run of just --all-features flag, using --exclude-all-features flag.",
         "This flag can only be used together with either --each-feature flag or --feature-powerset flag.",
     ]),
     ("", "--exclude-no-default-features", "Exclude run of just --no-default-features flag", &[
@@ -48,6 +50,14 @@ const HELP: &[(&str, &str, &str, &[&str])] = &[
             "This flag can only be used together with --feature-powerset flag.",
         ],
     ),
+    (
+        "",
+        "--include-features <FEATURES>...",
+        "Include only the specified features in the feature combinations instead of package features",
+        &[
+            "This flag can only be used together with either --each-feature flag or --feature-powerset flag.",
+        ],
+    ),
     ("", "--no-dev-deps", "Perform without dev-dependencies", &[
         "Note that this flag removes dev-dependencies from real `Cargo.toml` while cargo-hack is running and restores it when finished.",
     ]),
@@ -62,9 +72,7 @@ const HELP: &[(&str, &str, &str, &[&str])] = &[
         "",
         "--ignore-unknown-features",
         "Skip passing --features flag to `cargo` if that feature does not exist in the package",
-        &[
-            "This flag can only be used in the root of a virtual workspace or together with --workspace.",
-        ],
+        &["This flag can only be used together with either --features or --include-features."],
     ),
     ("", "--clean-per-run", "Remove artifacts for that package before running the command", &[
         "If used this flag with --workspace, --each-feature, or --feature-powerset, artifacts will be removed before each run.",
@@ -197,6 +205,8 @@ pub(crate) struct Args {
     pub(crate) clean_per_run: bool,
     /// --depth <NUM>
     pub(crate) depth: Option<usize>,
+    /// --include-features
+    pub(crate) include_features: Vec<String>,
 
     /// --no-default-features
     pub(crate) no_default_features: bool,
@@ -278,6 +288,7 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
     let mut exclude = Vec::new();
     let mut features = Vec::new();
     let mut optional_deps = None;
+    let mut include_features = Vec::new();
 
     let mut workspace = None;
     let mut no_dev_deps = false;
@@ -408,6 +419,13 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
                 "--exclude-features",
                 "--exclude-features <FEATURES>..."
             );
+            parse_multi_opt!(
+                include_features,
+                true,
+                true,
+                "--include-features",
+                "--include-features <FEATURES>..."
+            );
 
             if arg.starts_with("--optional-deps") {
                 if optional_deps.is_some() {
@@ -497,10 +515,10 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
         // in the root of a virtual workspace as well?
         bail!("--exclude can only be used together with --workspace");
     }
-    if ignore_unknown_features && features.is_empty() {
-        // TODO: Once https://github.com/taiki-e/cargo-hack/issues/52 implemented,
-        // allow --include-features.
-        bail!("--ignore-unknown-features can only be used together with --features");
+    if ignore_unknown_features && features.is_empty() && include_features.is_empty() {
+        bail!(
+            "--ignore-unknown-features can only be used together with either --features or --include-features"
+        );
     }
     if !each_feature && !feature_powerset {
         if optional_deps.is_some() {
@@ -519,6 +537,10 @@ pub(crate) fn args(coloring: &mut Option<Coloring>) -> Result<Option<Args>> {
             bail!(
                 "--exclude-all-features can only be used together with either --each-feature or --feature-powerset"
             );
+        } else if !include_features.is_empty() {
+            bail!(
+                "--include-features can only be used together with either --each-feature or --feature-powerset"
+            );
         }
     }
     if depth.is_some() && !feature_powerset {
@@ -617,6 +639,7 @@ For more information try --help
         optional_deps,
         clean_per_run,
         depth,
+        include_features,
 
         no_default_features,
         verbose,
diff --git a/src/package.rs b/src/package.rs
index f2f6bf8..801d6d7 100644
--- a/src/package.rs
+++ b/src/package.rs
@@ -62,24 +62,34 @@ impl<'a> Kind<'a> {
             return Kind::Nomal { show_progress: false };
         }
 
-        let features = package
-            .features
-            .keys()
-            .filter(|f| *f != "default" && !args.exclude_features.contains(f));
-        let opt_deps = args.optional_deps.as_ref().map(|opt_deps| {
-            package.dependencies.iter().filter_map(Dependency::as_feature).filter(move |f| {
-                !args.exclude_features.contains(f) && (opt_deps.is_empty() || opt_deps.contains(f))
-            })
-        });
+        let features = if args.include_features.is_empty() {
+            let mut features: Vec<_> = package
+                .features
+                .keys()
+                .filter(|f| *f != "default" && !args.exclude_features.contains(f))
+                .collect();
+            if let Some(opt_deps) = &args.optional_deps {
+                features.extend(
+                    package.dependencies.iter().filter_map(Dependency::as_feature).filter(
+                        move |f| {
+                            !args.exclude_features.contains(f)
+                                && (opt_deps.is_empty() || opt_deps.contains(f))
+                        },
+                    ),
+                );
+            }
+            features
+        } else {
+            args.include_features
+                .iter()
+                .filter(|f| *f != "default" && !args.exclude_features.contains(f))
+                .collect()
+        };
 
         if args.each_feature {
-            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() {
+            if (package.features.is_empty() || !args.include_features.is_empty())
+                && features.is_empty()
+            {
                 *total += 1;
                 Kind::Nomal { show_progress: true }
             } else {
@@ -90,13 +100,11 @@ impl<'a> Kind<'a> {
                 Kind::Each { features }
             }
         } else if args.feature_powerset {
-            let features = if let Some(opt_deps) = opt_deps {
-                powerset(features.chain(opt_deps), args.depth)
-            } else {
-                powerset(features, args.depth)
-            };
+            let features = powerset(features, args.depth);
 
-            if package.features.is_empty() && features.is_empty() {
+            if (package.features.is_empty() || !args.include_features.is_empty())
+                && features.is_empty()
+            {
                 *total += 1;
                 Kind::Nomal { show_progress: true }
             } else {


Test patch:
diff --git a/tests/long-help.txt b/tests/long-help.txt
index d02bcb3..265a626 100644
--- a/tests/long-help.txt
+++ b/tests/long-help.txt
@@ -52,6 +52,10 @@ OPTIONS:
 
             To exclude run of default feature, using value `--exclude-features default`.
 
+            To exclude run of just --no-default-features flag, using --exclude-no-default-features flag.
+
+            To exclude run of just --all-features flag, using --exclude-all-features flag.
+
             This flag can only be used together with either --each-feature flag or --feature-powerset flag.
 
         --exclude-no-default-features
@@ -71,6 +75,11 @@ OPTIONS:
 
             This flag can only be used together with --feature-powerset flag.
 
+        --include-features <FEATURES>...
+            Include only the specified features in the feature combinations instead of package features.
+
+            This flag can only be used together with either --each-feature flag or --feature-powerset flag.
+
         --no-dev-deps
             Perform without dev-dependencies.
 
@@ -85,7 +94,7 @@ OPTIONS:
         --ignore-unknown-features
             Skip passing --features flag to `cargo` if that feature does not exist in the package.
 
-            This flag can only be used in the root of a virtual workspace or together with --workspace.
+            This flag can only be used together with either --features or --include-features.
 
         --clean-per-run
             Remove artifacts for that package before running the command.
diff --git a/tests/short-help.txt b/tests/short-help.txt
index b2e180f..73302f8 100644
--- a/tests/short-help.txt
+++ b/tests/short-help.txt
@@ -21,6 +21,7 @@ OPTIONS:
         --exclude-no-default-features    Exclude run of just --no-default-features flag
         --exclude-all-features           Exclude run of just --all-features flag
         --depth <NUM>                    Specify a max number of simultaneous feature flags of --feature-powerset
+        --include-features <FEATURES>... Include only the specified features in the feature combinations instead of package features
         --no-dev-deps                    Perform without dev-dependencies
         --remove-dev-deps                Equivalent to --no-dev-deps flag except for does not restore the original `Cargo.toml` after performed
         --ignore-private                 Skip to perform on `publish = false` packages
diff --git a/tests/test.rs b/tests/test.rs
index 98c7c4c..45c0813 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -498,7 +498,7 @@ fn ignore_unknown_features_failure() {
         .unwrap()
         .assert_failure()
         .assert_stderr_contains(
-            "--ignore-unknown-features can only be used together with --features",
+            "--ignore-unknown-features can only be used together with either --features or --include-features",
         );
 }
 
@@ -654,6 +654,49 @@ fn depth_failure() {
         .assert_stderr_contains("--depth can only be used together with --feature-powerset");
 }
 
+#[test]
+fn include_features() {
+    cargo_hack()
+        .args(&["check", "--each-feature", "--include-features", "a,b"])
+        .current_dir(test_dir("tests/fixtures/real"))
+        .output()
+        .unwrap()
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on real (1/5)")
+        .assert_stderr_contains("running `cargo check --no-default-features` on real (2/5)")
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a` on real (3/5)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features b` on real (4/5)",
+        )
+        .assert_stderr_not_contains("--features c")
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --all-features` on real (5/5)",
+        );
+
+    cargo_hack()
+        .args(&["check", "--feature-powerset", "--include-features", "a,b"])
+        .current_dir(test_dir("tests/fixtures/real"))
+        .output()
+        .unwrap()
+        .assert_success()
+        .assert_stderr_contains("running `cargo check` on real (1/6)")
+        .assert_stderr_contains("running `cargo check --no-default-features` on real (2/6)")
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a` on real (3/6)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features b` on real (4/6)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --features a,b` on real (5/6)",
+        )
+        .assert_stderr_contains(
+            "running `cargo check --no-default-features --all-features` on real (6/6)",
+        );
+}
+
 #[test]
 fn exclude_features_failure() {
     cargo_hack()