xtask/cmd/power_set/
mod.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use anyhow::Context;
4
5mod utils;
6
7use utils::{XTaskMetadata, parse_features, test_package_features};
8
9use crate::cmd::IGNORED_PACKAGES;
10use crate::utils::{cargo_cmd, comma_delimited};
11
12#[derive(Debug, Clone, clap::Parser)]
13pub struct PowerSet {
14    #[clap(long, value_delimiter = ',')]
15    #[clap(alias = "feature")]
16    /// Features to test
17    features: Vec<String>,
18    #[clap(long, value_delimiter = ',')]
19    #[clap(alias = "exclude-feature")]
20    /// Features to exclude from testing
21    exclude_features: Vec<String>,
22    #[clap(long, short, value_delimiter = ',')]
23    #[clap(alias = "package")]
24    /// Packages to test
25    packages: Vec<String>,
26    #[clap(long, short, value_delimiter = ',')]
27    #[clap(alias = "exclude-package")]
28    /// Packages to exclude from testing
29    exclude_packages: Vec<String>,
30    #[clap(long, default_value = "0")]
31    /// Number of tests to skip
32    skip: usize,
33    #[clap(long, default_value = "true")]
34    /// Fail fast
35    fail_fast: bool,
36    #[clap(long, default_value = "target/power-set")]
37    /// Target directory
38    target_dir: String,
39    #[clap(long, action = clap::ArgAction::SetTrue)]
40    /// Override target directory
41    no_override_target_dir: bool,
42    #[clap(name = "command", default_value = "clippy")]
43    /// Command to run
44    command: String,
45    #[clap(long, short, action = clap::ArgAction::SetTrue)]
46    #[clap(alias = "dry-run")]
47    /// Dry run
48    dry_run: bool,
49    #[clap(last = true)]
50    /// Additional arguments to pass to the command
51    args: Vec<String>,
52}
53
54impl PowerSet {
55    pub fn run(self) -> anyhow::Result<()> {
56        let start = std::time::Instant::now();
57
58        let metadata = crate::utils::metadata()?;
59
60        let mut tests = BTreeMap::new();
61
62        let features = self.features.into_iter().map(|f| f.to_lowercase()).collect::<BTreeSet<_>>();
63
64        let (added_global_features, added_package_features) = parse_features(features.iter().map(|f| f.as_str()));
65        let (excluded_global_features, excluded_package_features) =
66            parse_features(self.exclude_features.iter().map(|f| f.as_str()));
67
68        let ignored_packages = self
69            .exclude_packages
70            .into_iter()
71            .chain(IGNORED_PACKAGES.iter().map(|p| p.to_string()))
72            .map(|p| p.to_lowercase())
73            .collect::<BTreeSet<_>>();
74        let packages = self.packages.into_iter().map(|p| p.to_lowercase()).collect::<BTreeSet<_>>();
75
76        let xtask_metadata = metadata
77            .workspace_packages()
78            .iter()
79            .map(|p| {
80                XTaskMetadata::from_package(p).with_context(|| format!("failed to get metadata for package {}", p.name))
81            })
82            .collect::<anyhow::Result<Vec<_>>>()?;
83
84        // For each package in the workspace, run tests
85        for (package, xtask_metadata) in metadata.workspace_packages().iter().zip(xtask_metadata.iter()) {
86            if ignored_packages.contains(&package.name.to_lowercase())
87                || !(packages.is_empty() || packages.contains(&package.name.to_lowercase()))
88                || xtask_metadata.skip
89            {
90                continue;
91            }
92
93            let added_features = added_package_features
94                .get(package.name.as_str())
95                .into_iter()
96                .flatten()
97                .chain(added_global_features.iter())
98                .copied()
99                .filter(|s| package.features.contains_key(*s));
100            let excluded_features = excluded_package_features
101                .get(package.name.as_str())
102                .into_iter()
103                .flatten()
104                .chain(excluded_global_features.iter())
105                .copied()
106                .filter(|s| package.features.contains_key(*s));
107
108            let features = test_package_features(package, added_features, excluded_features, xtask_metadata)
109                .with_context(|| package.name.clone())?;
110
111            tests.insert(package.name.as_str(), features);
112        }
113
114        let total = tests.values().map(|s| s.len()).sum::<usize>();
115
116        if self.dry_run {
117            println!("dry run: {} packages with a total of {} feature sets", tests.len(), total);
118
119            for (package, sets) in tests.iter() {
120                println!("dry run: {} with {} feature sets: {:#?}", package, sets.len(), sets);
121            }
122
123            return Ok(());
124        }
125
126        let mut i = 0;
127        let mut failed = Vec::new();
128
129        for (package, power_set) in tests.iter() {
130            for features in power_set.iter() {
131                if i < self.skip {
132                    i += 1;
133                    continue;
134                }
135
136                let mut cmd = cargo_cmd();
137                cmd.arg(&self.command);
138                cmd.arg("--no-default-features");
139                if !features.is_empty() {
140                    cmd.arg("--features").arg(comma_delimited(features.iter()));
141                }
142                cmd.arg("--package").arg(package);
143
144                if !self.no_override_target_dir {
145                    cmd.arg("--target-dir").arg(&self.target_dir);
146                }
147
148                cmd.args(&self.args);
149
150                println!("executing {cmd:?} ({i}/{total})");
151
152                if !cmd.status()?.success() {
153                    failed.push((*package, features));
154                    if self.fail_fast {
155                        anyhow::bail!(
156                            "failed to execute command for package {} with features {:?} after {:?}",
157                            package,
158                            features,
159                            start.elapsed()
160                        );
161                    }
162                }
163
164                i += 1;
165            }
166        }
167
168        if !failed.is_empty() {
169            eprintln!("failed to execute command for the following:");
170            for (package, features) in failed {
171                eprintln!("  {package} with features {features:?}");
172            }
173
174            anyhow::bail!("failed to execute command for some packages after {:?}", start.elapsed());
175        }
176
177        println!("all commands executed successfully after {:?}", start.elapsed());
178
179        Ok(())
180    }
181}