use crate::feature::FeatureMatrix;
use anyhow::Result;
use itertools::Itertools;
use std::{
env::var_os,
ffi::OsString,
path::PathBuf,
process::{Command, Stdio},
sync::LazyLock,
};
use yansi::Paint;
static CARGO: LazyLock<OsString> =
LazyLock::new(|| var_os("CARGO").unwrap_or_else(|| "cargo".into()));
#[derive(Clone, Copy, Debug)]
pub(crate) enum TaskKind {
Build,
Check,
Clippy,
LlvmCov,
Test,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum TaskResult {
Success,
Fail(i32),
}
pub(crate) struct Task {
kind: TaskKind,
package: String,
matrix: FeatureMatrix,
manifest_path: Option<PathBuf>,
args: Vec<String>,
dry_run: bool,
}
impl Task {
pub(crate) fn new(
kind: TaskKind,
package: String,
matrix: FeatureMatrix,
manifest_path: Option<PathBuf>,
args: Vec<String>,
dry_run: bool,
) -> Self {
Self {
kind,
package,
matrix,
manifest_path,
args,
dry_run,
}
}
pub(crate) fn execute(self) -> Result<TaskResult> {
for feature_set in self.matrix {
match self.kind {
TaskKind::Build => print!("{}", Paint::cyan(" Building ").bold()),
TaskKind::Check => print!("{}", Paint::cyan(" Checking ").bold()),
TaskKind::Clippy => print!("{}", Paint::cyan(" Clippy ").bold()),
TaskKind::LlvmCov => print!("{}", Paint::cyan(" Coverage ").bold()),
TaskKind::Test => print!("{}", Paint::cyan(" Testing ").bold()),
}
println!("package={} features=[{feature_set}]", self.package);
let mut cmd = Command::new(CARGO.as_os_str());
let cmd = match self.kind {
TaskKind::Build => cmd.arg("build"),
TaskKind::Check => cmd.arg("check"),
TaskKind::Clippy => cmd.arg("clippy"),
TaskKind::LlvmCov => cmd.arg("llvm-cov"),
TaskKind::Test => cmd.arg("test"),
};
let on_success = || {
println!(
"{} {}",
Paint::cyan(" Result").bold(),
Paint::bright_green("OK")
);
println!();
};
let cmd = cmd
.stderr(Stdio::inherit())
.stdout(Stdio::inherit())
.arg("-p")
.arg(self.package.clone())
.arg("--no-default-features");
let cmd = if feature_set.is_empty() {
cmd
} else {
cmd.arg("-F").arg(feature_set.to_string())
};
let cmd = if let Some(manifest_path) = &self.manifest_path {
cmd.arg("--manifest-path")
.arg(format!("{}", manifest_path.display()))
} else {
cmd
};
let cmd = cmd.args(self.args.clone());
display_command(cmd);
if !self.dry_run {
let output = cmd.output()?;
if output.status.success() {
on_success();
} else {
let code = output.status.code().unwrap_or(-1);
return Ok(TaskResult::Fail(code));
}
}
}
Ok(TaskResult::Success)
}
}
fn display_command(cmd: &Command) {
let args = cmd.get_args().map(|x| x.to_string_lossy()).join(" ");
println!(
"{} {} {}",
Paint::cyan(" Running").bold(),
cmd.get_program().to_string_lossy(),
args
);
println!();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::feature::{Feature, FeatureSet};
fn make_matrix(sets: &[&[&str]]) -> FeatureMatrix {
sets.iter()
.map(|features| {
features
.iter()
.map(|&s| Feature::from(s))
.collect::<FeatureSet>()
})
.collect()
}
#[test]
fn dry_run_empty_matrix_returns_success() {
let task = Task::new(
TaskKind::Build,
"pkg".to_string(),
FeatureMatrix::default(),
None,
vec![],
true,
);
assert!(matches!(task.execute().unwrap(), TaskResult::Success));
}
#[test]
fn dry_run_single_empty_feature_set_returns_success() {
let matrix = make_matrix(&[&[]]);
let task = Task::new(
TaskKind::Check,
"pkg".to_string(),
matrix,
None,
vec![],
true,
);
assert!(matches!(task.execute().unwrap(), TaskResult::Success));
}
#[test]
fn dry_run_nonempty_feature_set_returns_success() {
let matrix = make_matrix(&[&["feat-a", "feat-b"]]);
let task = Task::new(
TaskKind::Clippy,
"pkg".to_string(),
matrix,
None,
vec![],
true,
);
assert!(matches!(task.execute().unwrap(), TaskResult::Success));
}
#[test]
fn dry_run_all_task_kinds() {
let matrix = make_matrix(&[&["feat-a"]]);
for kind in [
TaskKind::Build,
TaskKind::Check,
TaskKind::Clippy,
TaskKind::Test,
TaskKind::LlvmCov,
] {
let task = Task::new(kind, "pkg".to_string(), matrix.clone(), None, vec![], true);
assert!(matches!(task.execute().unwrap(), TaskResult::Success));
}
}
#[test]
fn dry_run_with_manifest_path_returns_success() {
let matrix = make_matrix(&[&["feat-a"]]);
let task = Task::new(
TaskKind::Build,
"pkg".to_string(),
matrix,
Some(PathBuf::from("Cargo.toml")),
vec![],
true,
);
assert!(matches!(task.execute().unwrap(), TaskResult::Success));
}
#[test]
fn dry_run_with_extra_args_returns_success() {
let matrix = make_matrix(&[&[]]);
let task = Task::new(
TaskKind::Clippy,
"pkg".to_string(),
matrix,
None,
vec!["--".to_string(), "-D".to_string(), "warnings".to_string()],
true,
);
assert!(matches!(task.execute().unwrap(), TaskResult::Success));
}
#[test]
fn dry_run_multiple_feature_sets_all_succeed() {
let matrix = make_matrix(&[&[], &["feat-a"], &["feat-b"], &["feat-a", "feat-b"]]);
let task = Task::new(
TaskKind::Build,
"pkg".to_string(),
matrix,
None,
vec![],
true,
);
assert!(matches!(task.execute().unwrap(), TaskResult::Success));
}
}