diff --git a/crates/cargo-wdk/src/actions/build/mod.rs b/crates/cargo-wdk/src/actions/build/mod.rs index ad1d9159c..20d9d63c4 100644 --- a/crates/cargo-wdk/src/actions/build/mod.rs +++ b/crates/cargo-wdk/src/actions/build/mod.rs @@ -85,6 +85,10 @@ impl<'a> BuildAction<'a> { metadata: &'a Metadata, ) -> Result { // TODO: validate params + anyhow::ensure!( + !params.working_dir.as_os_str().is_empty(), + "working_dir must not be empty" + ); Ok(Self { working_dir: absolute(params.working_dir)?, profile: params.profile, @@ -151,14 +155,13 @@ impl<'a> BuildAction<'a> { ); let mut is_valid_dir_with_rust_projects = false; - for dir in &dirs { - if self.fs.dir_file_type(dir)?.is_dir() - && self.fs.exists(&dir.path().join("Cargo.toml")) - { + for entry in &dirs { + if entry.is_dir && self.fs.exists(&entry.path.join("Cargo.toml")) { debug!( - "Found atleast one valid Rust project directory: {}, continuing with the \ + "Found at least one valid Rust project directory: {}, continuing with the \ build flow", - dir.path() + entry + .path .file_name() .expect( "package sub directory name ended with \"..\" which is not expected" @@ -179,26 +182,24 @@ impl<'a> BuildAction<'a> { info!("Building packages in {}", self.working_dir.display()); let mut failed_atleast_one_project = false; - for dir in dirs { - debug!("Checking dir entry: {}", dir.path().display()); - if !self.fs.dir_file_type(&dir)?.is_dir() - || !self.fs.exists(&dir.path().join("Cargo.toml")) - { + for entry in dirs { + debug!("Checking dir entry: {}", entry.path.display()); + if !entry.is_dir || !self.fs.exists(&entry.path.join("Cargo.toml")) { debug!("Dir entry is not a valid Rust package"); continue; } - let working_dir_path = dir.path(); // Avoids a short-lived temporary - let sub_dir = working_dir_path + let cargo_package_path = entry.path; + let package_dir_name = cargo_package_path .file_name() .expect("package sub directory name ended with \"..\" which is not expected") .to_string_lossy(); - debug!("Building package(s) in dir {sub_dir}"); - if let Err(e) = self.run_from_workspace_root(&dir.path()) { + debug!("Building package(s) in dir {package_dir_name}"); + if let Err(e) = self.run_from_workspace_root(&cargo_package_path) { failed_atleast_one_project = true; err!( - "Error building project: {sub_dir}, error: {:?}", + "Error building project: {package_dir_name}, error: {:?}", anyhow::Error::new(e) ); } diff --git a/crates/cargo-wdk/src/actions/clean/error.rs b/crates/cargo-wdk/src/actions/clean/error.rs new file mode 100644 index 000000000..c71bf9a41 --- /dev/null +++ b/crates/cargo-wdk/src/actions/clean/error.rs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation +// License: MIT OR Apache-2.0 +//! This module defines error types for the clean action module. + +use std::path::PathBuf; + +use thiserror::Error; + +use crate::providers::error::{CommandError, FileError}; + +/// Errors for the clean action layer +#[derive(Error, Debug)] +pub enum CleanActionError { + #[error(transparent)] + FileIo(#[from] FileError), + #[error("No valid rust projects in the current working directory: {0}")] + NoValidRustProjectsInTheDirectory(PathBuf), + #[error("One or more projects failed to clean in the emulated workspace: {0}")] + OneOrMoreRustProjectsFailedToClean(PathBuf), + #[error(transparent)] + CargoClean(#[from] CommandError), +} diff --git a/crates/cargo-wdk/src/actions/clean/mod.rs b/crates/cargo-wdk/src/actions/clean/mod.rs new file mode 100644 index 000000000..557690240 --- /dev/null +++ b/crates/cargo-wdk/src/actions/clean/mod.rs @@ -0,0 +1,423 @@ +// Copyright (c) Microsoft Corporation +// License: MIT OR Apache-2.0 +//! This module contains the `CleanAction` struct and its associated methods +//! for cleaning build artifacts produced by the `build` command. +mod error; + +use std::path::{Path, PathBuf, absolute}; + +use anyhow::Result; +use error::CleanActionError; +use mockall_double::double; +use tracing::{debug, error as err, info}; + +#[double] +use crate::providers::{exec::CommandExec, fs::Fs}; +use crate::trace; + +/// Action that removes build artifacts produced by the `build` command for a +/// driver project or emulated workspace. +pub struct CleanAction<'a> { + working_dir: PathBuf, + verbosity_level: clap_verbosity_flag::Verbosity, + + // Injected deps + command_exec: &'a CommandExec, + fs: &'a Fs, +} + +impl<'a> CleanAction<'a> { + /// Creates a new instance of `CleanAction`. + /// + /// # Arguments + /// * `working_dir` - The working directory for the clean action + /// * `verbosity_level` - The verbosity level for logging + /// * `command_exec` - The command execution provider instance + /// * `fs` - The file system provider instance + /// + /// # Returns + /// * `Result` - A result containing either a new instance of + /// `CleanAction` on success, or an `anyhow::Error`. + /// + /// # Errors + /// * [`anyhow::Error`] - If `working_dir` is not a syntactically valid + /// path, e.g. it is empty + pub fn new( + working_dir: &Path, + verbosity_level: clap_verbosity_flag::Verbosity, + command_exec: &'a CommandExec, + fs: &'a Fs, + ) -> Result { + anyhow::ensure!( + !working_dir.as_os_str().is_empty(), + "working_dir must not be empty" + ); + Ok(Self { + working_dir: absolute(working_dir)?, + verbosity_level, + command_exec, + fs, + }) + } + + /// Entry point method to execute the clean action flow. + /// + /// The detection strategy is: + /// 1. If the working directory has a `Cargo.toml`, run `cargo clean` + /// directly (standalone project or workspace root). + /// 2. Otherwise, treat the directory as an emulated workspace: scan + /// immediate subdirectories for Rust projects and clean each. NOTE: This + /// follows the same logic as the build action. + /// + /// # Returns + /// `Result<(), CleanActionError>` + /// + /// # Errors + /// * `CleanActionError::FileIo` - If there is an IO error. + /// * `CleanActionError::CargoClean` - If there is an error running the + /// `cargo clean` command. + /// * `CleanActionError::NoValidRustProjectsInTheDirectory` - If no valid + /// Rust projects are found in the working directory. + /// * `CleanActionError::OneOrMoreRustProjectsFailedToClean` - If one or + /// more Rust projects fail to clean in an emulated workspace. + pub fn run(&self) -> Result<(), CleanActionError> { + debug!( + "Attempting to clean project at: {}", + self.working_dir.display() + ); + + // Standalone driver/driver workspace support + if self.fs.exists(&self.working_dir.join("Cargo.toml")) { + debug!( + "Found Cargo.toml in {}. Running cargo clean.", + self.working_dir.display() + ); + return self.run_cargo_clean(&self.working_dir); + } + + // Emulated workspaces support + let dirs = self.fs.read_dir_entries(&self.working_dir)?; + debug!( + "Checking for valid Rust projects in the working directory: {}", + self.working_dir.display() + ); + + let mut found_at_least_one_project = false; + let mut failed_at_least_one_project = false; + for entry in dirs { + debug!("Checking dir entry: {}", entry.path.display()); + if !entry.is_dir || !self.fs.exists(&entry.path.join("Cargo.toml")) { + debug!("Dir entry is not a valid Rust package"); + continue; + } + + let cargo_package_path = entry.path; + let package_dir_name = cargo_package_path + .file_name() + .map(|s| s.to_string_lossy().into_owned()) + .unwrap_or_default(); + + // Emit the log only once for the entire emulated workspace, the first + // time a valid Rust project is discovered during the scan. + if !found_at_least_one_project { + info!("Cleaning package(s) in {}", self.working_dir.display()); + } + found_at_least_one_project = true; + debug!("Cleaning package(s) in dir {package_dir_name}"); + if let Err(e) = self.run_cargo_clean(&cargo_package_path) { + failed_at_least_one_project = true; + err!( + "Error cleaning project: {package_dir_name}, error: {:?}", + anyhow::Error::new(e) + ); + } + } + + if !found_at_least_one_project { + return Err(CleanActionError::NoValidRustProjectsInTheDirectory( + self.working_dir.clone(), + )); + } + + debug!("Done cleaning package(s) in {}", self.working_dir.display()); + if failed_at_least_one_project { + return Err(CleanActionError::OneOrMoreRustProjectsFailedToClean( + self.working_dir.clone(), + )); + } + + info!( + "Clean completed successfully for package(s) in {}", + self.working_dir.display() + ); + Ok(()) + } + + /// Runs `cargo clean` in the specified directory. + fn run_cargo_clean(&self, working_dir: &Path) -> Result<(), CleanActionError> { + info!("Running cargo clean in {}", working_dir.display()); + let mut args = vec!["clean"]; + if let Some(flag) = trace::get_cargo_verbose_flags(self.verbosity_level) { + args.push(flag); + } + self.command_exec + .run("cargo", &args, None, Some(working_dir)) + .map_err(CleanActionError::CargoClean)?; + info!("Cleaned project at {}", working_dir.display()); + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use std::{ + io, + os::windows::process::ExitStatusExt, + path::{Path, PathBuf}, + process::{ExitStatus, Output}, + }; + + use mockall::predicate::eq; + use mockall_double::double; + + use super::{CleanAction, error::CleanActionError}; + use crate::providers::{ + error::{CommandError, FileError}, + fs::DirEntryInfo, + }; + #[double] + use crate::providers::{exec::CommandExec, fs::Fs}; + + fn ok_output() -> Output { + Output { + status: ExitStatus::from_raw(0), + stdout: Vec::new(), + stderr: Vec::new(), + } + } + + fn cargo_clean_err() -> CommandError { + CommandError::CommandFailed { + command: "cargo".to_string(), + args: vec!["clean".to_string()], + stdout: "boom".to_string(), + } + } + + /// Sets up `Fs::exists(/Cargo.toml) -> exists`. + fn mock_cargo_toml(fs: &mut Fs, dir: &Path, exists: bool) { + fs.expect_exists() + .with(eq(dir.join("Cargo.toml"))) + .returning(move |_| exists); + } + + /// Sets up `Fs::read_dir_entries(...) -> Ok(entries)` where each entry is + /// `(name relative to working dir, is_dir)`. + fn mock_read_dir(fs: &mut Fs, working_dir: &Path, entries: &[(&str, bool)]) { + let entries: Vec = entries + .iter() + .map(|(name, is_dir)| DirEntryInfo { + path: working_dir.join(name), + is_dir: *is_dir, + }) + .collect(); + fs.expect_read_dir_entries() + .returning(move |_| Ok(entries.clone())); + } + + /// Sets up an expectation for `cargo clean` invoked at `dir`. + fn mock_cargo_clean(exec: &mut CommandExec, dir: &Path, ok: bool) { + let dir = dir.to_owned(); + exec.expect_run() + .withf(move |cmd, args, _env, working_dir| { + cmd == "cargo" && args == ["clean"] && *working_dir == Some(dir.as_path()) + }) + .returning(move |_, _, _, _| { + if ok { + Ok(ok_output()) + } else { + Err(cargo_clean_err()) + } + }); + } + + fn run_action(cwd: &Path, fs: &Fs, exec: &CommandExec) -> Result<(), CleanActionError> { + CleanAction::new(cwd, clap_verbosity_flag::Verbosity::default(), exec, fs) + .expect("CleanAction::new should succeed") + .run() + } + + #[test] + fn new_succeeds_for_valid_args() { + let cwd = PathBuf::from("C:\\tmp"); + let fs = Fs::default(); + let exec = CommandExec::default(); + assert!( + CleanAction::new(&cwd, clap_verbosity_flag::Verbosity::default(), &exec, &fs,).is_ok() + ); + } + + #[test] + fn new_fails_if_working_dir_is_empty() { + let cwd = PathBuf::from(""); + let fs = Fs::default(); + let exec = CommandExec::default(); + let err = CleanAction::new(&cwd, clap_verbosity_flag::Verbosity::default(), &exec, &fs) + .err() + .expect("CleanAction::new should fail for empty working_dir"); + assert_eq!(err.to_string(), "working_dir must not be empty"); + } + + // ---- standalone driver / workspace root --------------------------------- + + #[test] + fn run_invokes_cargo_clean_and_succeeds() { + let cwd = PathBuf::from("C:\\tmp"); + let mut fs = Fs::default(); + let mut exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, true); + mock_cargo_clean(&mut exec, &cwd, true); + assert!(run_action(&cwd, &fs, &exec).is_ok()); + } + + #[test] + fn run_returns_error_when_cargo_clean_fails() { + let cwd = PathBuf::from("C:\\tmp"); + let mut fs = Fs::default(); + let mut exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, true); + mock_cargo_clean(&mut exec, &cwd, false); + assert!(matches!( + run_action(&cwd, &fs, &exec), + Err(CleanActionError::CargoClean(_)) + )); + } + + // ---- emulated workspace ------------------------------------------------- + + #[test] + fn run_returns_error_when_no_cargo_toml_and_no_rust_projects_are_found() { + let cwd = PathBuf::from("C:\\tmp"); + let mut fs = Fs::default(); + let exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, false); + mock_read_dir(&mut fs, &cwd, &[]); + assert!(matches!( + run_action(&cwd, &fs, &exec), + Err(CleanActionError::NoValidRustProjectsInTheDirectory(_)) + )); + } + + #[test] + fn run_cleans_single_rust_project_in_emulated_workspace() { + let cwd = PathBuf::from("C:\\tmp"); + let pkg_a = cwd.join("pkg-a"); + let mut fs = Fs::default(); + let mut exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, false); + mock_read_dir(&mut fs, &cwd, &[("pkg-a", true)]); + mock_cargo_toml(&mut fs, &pkg_a, true); + mock_cargo_clean(&mut exec, &pkg_a, true); + assert!(run_action(&cwd, &fs, &exec).is_ok()); + } + + #[test] + fn run_cleans_multiple_rust_projects_in_emulated_workspace() { + let cwd = PathBuf::from("C:\\tmp"); + let pkg_a = cwd.join("pkg-a"); + let pkg_b = cwd.join("pkg-b"); + let mut fs = Fs::default(); + let mut exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, false); + mock_read_dir(&mut fs, &cwd, &[("pkg-a", true), ("pkg-b", true)]); + mock_cargo_toml(&mut fs, &pkg_a, true); + mock_cargo_toml(&mut fs, &pkg_b, true); + mock_cargo_clean(&mut exec, &pkg_a, true); + mock_cargo_clean(&mut exec, &pkg_b, true); + assert!(run_action(&cwd, &fs, &exec).is_ok()); + } + + #[test] + fn run_skips_non_directory_entries_in_emulated_workspace() { + let cwd = PathBuf::from("C:\\tmp"); + let pkg_a = cwd.join("pkg-a"); + let mut fs = Fs::default(); + let mut exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, false); + // README.md (is_dir=false) is filtered out before any Cargo.toml probe. + mock_read_dir(&mut fs, &cwd, &[("README.md", false), ("pkg-a", true)]); + mock_cargo_toml(&mut fs, &pkg_a, true); + mock_cargo_clean(&mut exec, &pkg_a, true); + assert!(run_action(&cwd, &fs, &exec).is_ok()); + } + + #[test] + fn run_skips_directories_without_cargo_toml_in_emulated_workspace() { + let cwd = PathBuf::from("C:\\tmp"); + let docs = cwd.join("docs"); + let pkg_a = cwd.join("pkg-a"); + let mut fs = Fs::default(); + let mut exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, false); + mock_read_dir(&mut fs, &cwd, &[("docs", true), ("pkg-a", true)]); + mock_cargo_toml(&mut fs, &docs, false); + mock_cargo_toml(&mut fs, &pkg_a, true); + mock_cargo_clean(&mut exec, &pkg_a, true); + assert!(run_action(&cwd, &fs, &exec).is_ok()); + } + + #[test] + fn run_returns_error_when_no_subdir_has_cargo_toml() { + let cwd = PathBuf::from("C:\\tmp"); + let docs = cwd.join("docs"); + let scripts = cwd.join("scripts"); + let mut fs = Fs::default(); + let exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, false); + mock_read_dir(&mut fs, &cwd, &[("docs", true), ("scripts", true)]); + mock_cargo_toml(&mut fs, &docs, false); + mock_cargo_toml(&mut fs, &scripts, false); + assert!(matches!( + run_action(&cwd, &fs, &exec), + Err(CleanActionError::NoValidRustProjectsInTheDirectory(_)) + )); + } + + #[test] + fn run_returns_error_when_one_subproject_fails_to_clean_in_emulated_workspace() { + let cwd = PathBuf::from("C:\\tmp"); + let pkg_ok = cwd.join("pkg-ok"); + let pkg_bad = cwd.join("pkg-bad"); + let mut fs = Fs::default(); + let mut exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, false); + mock_read_dir(&mut fs, &cwd, &[("pkg-ok", true), ("pkg-bad", true)]); + mock_cargo_toml(&mut fs, &pkg_ok, true); + mock_cargo_toml(&mut fs, &pkg_bad, true); + mock_cargo_clean(&mut exec, &pkg_ok, true); + mock_cargo_clean(&mut exec, &pkg_bad, false); + assert!(matches!( + run_action(&cwd, &fs, &exec), + Err(CleanActionError::OneOrMoreRustProjectsFailedToClean(_)) + )); + } + + #[test] + fn run_returns_error_when_read_dir_entries_fails() { + let cwd = PathBuf::from("C:\\tmp"); + let mut fs = Fs::default(); + let exec = CommandExec::default(); + mock_cargo_toml(&mut fs, &cwd, false); + let cwd_clone = cwd.clone(); + fs.expect_read_dir_entries().returning(move |_| { + Err(FileError::ReadDirError( + cwd_clone.clone(), + io::Error::new(io::ErrorKind::PermissionDenied, "denied"), + )) + }); + assert!(matches!( + run_action(&cwd, &fs, &exec), + Err(CleanActionError::FileIo(_)) + )); + } +} diff --git a/crates/cargo-wdk/src/actions/mod.rs b/crates/cargo-wdk/src/actions/mod.rs index 92197b781..e59a880d1 100644 --- a/crates/cargo-wdk/src/actions/mod.rs +++ b/crates/cargo-wdk/src/actions/mod.rs @@ -6,7 +6,9 @@ //! business logic of the cargo-wdk utility are: //! * `new` - New action module //! * `build` - Build action module +//! * `clean` - Clean action module pub mod build; +pub mod clean; pub mod new; use std::{ diff --git a/crates/cargo-wdk/src/cli.rs b/crates/cargo-wdk/src/cli.rs index 907588363..30092729f 100644 --- a/crates/cargo-wdk/src/cli.rs +++ b/crates/cargo-wdk/src/cli.rs @@ -18,6 +18,7 @@ use crate::actions::{ UMDF_STR, WDM_STR, build::{BuildAction, BuildActionParams}, + clean::CleanAction, new::NewAction, }; #[double] @@ -101,6 +102,11 @@ pub enum Subcmd { New(NewArgs), #[clap(name = "build", about = "Build the Windows Driver Kit project")] Build(BuildArgs), + #[clap( + name = "clean", + about = "Clean build artifacts of the Windows Driver Kit project" + )] + Clean, } /// Top level command line interface for cargo wdk @@ -178,6 +184,10 @@ impl Cli { .run()?; Ok(()) } + Subcmd::Clean => { + CleanAction::new(Path::new("."), self.verbose, &command_exec, &fs)?.run()?; + Ok(()) + } } } } diff --git a/crates/cargo-wdk/src/providers/fs.rs b/crates/cargo-wdk/src/providers/fs.rs index a0b526ad3..c714685c8 100644 --- a/crates/cargo-wdk/src/providers/fs.rs +++ b/crates/cargo-wdk/src/providers/fs.rs @@ -11,25 +11,28 @@ #![allow(clippy::unused_self)] use std::{ - fs::{ - DirEntry, - File, - FileType, - OpenOptions, - copy, - create_dir, - create_dir_all, - read_dir, - rename, - }, + fs::{File, OpenOptions, copy, create_dir, create_dir_all, read_dir, rename}, io::{Read, Write}, - path::Path, + path::{Path, PathBuf}, }; use mockall::automock; use super::error::FileError; +/// Owned snapshot of a single directory entry returned by +/// [`Fs::read_dir_entries`] to avoid using `std::fs::DirEntry` directly +/// +/// This struct captures the necessary information about a directory entry - +/// its path and whether it's a directory - at the time of enumeration. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DirEntryInfo { + /// Full path of the entry. + pub path: PathBuf, + /// Whether the entry is a directory, as observed at enumeration time. + pub is_dir: bool, +} + /// Provides limited access to `std::fs` methods #[derive(Default)] pub struct Fs {} @@ -52,16 +55,23 @@ impl Fs { create_dir_all(path).map_err(|e| FileError::CreateDirError(path.to_owned(), e)) } - pub fn dir_file_type(&self, dir: &DirEntry) -> Result { - dir.file_type() - .map_err(|e| FileError::DirFileTypeError(dir.path(), e)) - } - - pub fn read_dir_entries(&self, path: &Path) -> Result, FileError> { + pub fn read_dir_entries(&self, path: &Path) -> Result, FileError> { read_dir(path) .map_err(|e| FileError::ReadDirError(path.to_owned(), e))? - .collect::, std::io::Error>>() - .map_err(|e| FileError::ReadDirEntriesError(path.to_owned(), e)) + .map(|entry_res| { + let entry = + entry_res.map_err(|e| FileError::ReadDirEntriesError(path.to_owned(), e))?; + let entry_path = entry.path(); + let is_dir = entry + .file_type() + .map_err(|e| FileError::DirFileTypeError(entry_path.clone(), e))? + .is_dir(); + Ok(DirEntryInfo { + path: entry_path, + is_dir, + }) + }) + .collect() } pub fn rename(&self, src: &Path, dest: &Path) -> Result<(), FileError> { diff --git a/crates/cargo-wdk/src/providers/mod.rs b/crates/cargo-wdk/src/providers/mod.rs index 1b3a7e81d..d67cb4d7b 100644 --- a/crates/cargo-wdk/src/providers/mod.rs +++ b/crates/cargo-wdk/src/providers/mod.rs @@ -67,7 +67,7 @@ pub mod error { CreateDirError(PathBuf, #[source] io::Error), #[error("Failed to rename file from {0} to {1}")] RenameError(PathBuf, PathBuf, #[source] io::Error), - #[error("Failed to get file type for directory entry {0:#?}")] + #[error("Failed to get file type for directory entry {0}")] DirFileTypeError(PathBuf, #[source] io::Error), #[error("Failed to read directory {0}")] ReadDirError(PathBuf, #[source] io::Error), diff --git a/crates/cargo-wdk/tests/build_command_test.rs b/crates/cargo-wdk/tests/build_command_test.rs index 9aed17ff1..0d34a4aea 100644 --- a/crates/cargo-wdk/tests/build_command_test.rs +++ b/crates/cargo-wdk/tests/build_command_test.rs @@ -173,12 +173,15 @@ fn emulated_workspace_builds_successfully() { let emulated_workspace_path = "tests/emulated-workspace"; let umdf_driver_workspace_path = format!("{emulated_workspace_path}/umdf-driver-workspace"); with_mutex(emulated_workspace_path, || { - run_cargo_clean(&umdf_driver_workspace_path); - run_cargo_clean(&format!("{emulated_workspace_path}/rust-project")); + run_clean_cmd(emulated_workspace_path); + assert_target_dir_does_not_exist(&umdf_driver_workspace_path); + assert_target_dir_does_not_exist(&format!("{emulated_workspace_path}/rust-project")); + let stderr = run_build_cmd(emulated_workspace_path, None, None); assert!(stderr.contains("Building package driver_1")); assert!(stderr.contains("Building package driver_2")); assert!(stderr.contains("Build completed successfully")); + verify_driver_package_files( &umdf_driver_workspace_path, "driver_1", @@ -195,6 +198,12 @@ fn emulated_workspace_builds_successfully() { None, None, ); + + run_clean_cmd(emulated_workspace_path); + assert_target_dir_does_not_exist(&umdf_driver_workspace_path); + assert_target_dir_does_not_exist(&format!("{emulated_workspace_path}/rust-project")); + assert_package_dir_does_not_exist(&umdf_driver_workspace_path, "driver_1", None, "debug"); + assert_package_dir_does_not_exist(&umdf_driver_workspace_path, "driver_2", None, "debug"); }); } @@ -302,7 +311,8 @@ fn clean_build_and_verify_project( project_path.map_or_else(|| format!("tests/{driver_name}"), ToString::to_string); let mutex_name = project_path.clone(); with_mutex(&mutex_name, || { - run_cargo_clean(&project_path); + run_clean_cmd(&project_path); + assert_target_dir_does_not_exist(&project_path); let mut args: Vec<&str> = Vec::new(); if let Some(target_arch) = input_target_arch { @@ -339,6 +349,15 @@ fn clean_build_and_verify_project( target_triple, profile, ); + + run_clean_cmd(&project_path); + assert_target_dir_does_not_exist(&project_path); + assert_package_dir_does_not_exist( + &project_path, + driver_name, + target_triple, + profile.unwrap_or("debug"), + ); }); } @@ -350,12 +369,39 @@ fn to_target_triple(target_arch: &str) -> Option<&'static str> { } } -fn run_cargo_clean(driver_path: &str) { - let mut cmd = Command::new("cargo"); - cmd.args(["clean"]).current_dir(driver_path); +fn run_clean_cmd(path: &str) { + let mut cmd = create_cargo_wdk_cmd("clean", None, None, Some(path)); cmd.assert().success(); } +fn assert_target_dir_does_not_exist(project_path: &str) { + let target_dir = Path::new(project_path).join("target"); + assert!( + !target_dir.exists(), + "Expected target directory to not exist after clean: {}", + target_dir.display() + ); +} + +fn assert_package_dir_does_not_exist( + project_path: &str, + driver_name: &str, + target_triple: Option<&str>, + profile: &str, +) { + let driver_name = driver_name.replace('-', "_"); + let target_folder_path = target_triple.map_or_else( + || format!("{project_path}/target/{profile}"), + |triple| format!("{project_path}/target/{triple}/{profile}"), + ); + let package_dir = PathBuf::from(&target_folder_path).join(format!("{driver_name}_package")); + assert!( + !package_dir.exists(), + "Expected package directory to not exist after clean: {}", + package_dir.display() + ); +} + fn run_build_cmd( path: &str, args: Option<&[&str]>, diff --git a/crates/cargo-wdk/tests/test_utils/mod.rs b/crates/cargo-wdk/tests/test_utils/mod.rs index 7d605b82e..f13da0e45 100644 --- a/crates/cargo-wdk/tests/test_utils/mod.rs +++ b/crates/cargo-wdk/tests/test_utils/mod.rs @@ -235,7 +235,8 @@ impl Drop for NamedMutex { /// /// # Arguments /// -/// * `cmd_name` - Name of the cargo-wdk command. Can be only "new" or "build" +/// * `cmd_name` - Name of the cargo-wdk command. Can be "build", "new", or +/// "clean" /// * `cmd_args` - Optional args for the command /// * `env_vars` - Optional environment variables to overlay for the command /// * `curr_working_dir` - Optional current working directory for the command @@ -246,8 +247,8 @@ pub fn create_cargo_wdk_cmd>( curr_working_dir: Option

, ) -> Command { assert!( - cmd_name == "build" || cmd_name == "new", - "Only 'build' and 'new' commands are supported" + cmd_name == "build" || cmd_name == "new" || cmd_name == "clean", + "Only 'build', 'new', and 'clean' commands are supported" ); let mut cmd = Command::cargo_bin("cargo-wdk").expect("unable to find cargo-wdk binary");