From d2ef3227610fb1f3ff1a8215d75de09d52eee53d Mon Sep 17 00:00:00 2001 From: selfhoster1312 Date: Fri, 21 Apr 2023 15:13:03 +0200 Subject: [PATCH] Rename SpecificPackageManager trait PackageManager, add some unit tests --- src/modules/package/apt.rs | 146 ++++++++++++++++++++++++++++++-- src/modules/package/builder.rs | 21 +++-- src/modules/package/list.rs | 3 +- src/modules/package/mod.rs | 97 +++++++++++++++++++--- src/modules/package/pacman.rs | 147 +++++++++++++++++++++++++++++++-- src/utils/cmd.rs | 12 +-- 6 files changed, 380 insertions(+), 46 deletions(-) diff --git a/src/modules/package/apt.rs b/src/modules/package/apt.rs index cecf295..98cd9f4 100644 --- a/src/modules/package/apt.rs +++ b/src/modules/package/apt.rs @@ -1,21 +1,52 @@ -use crate::utils::cmd::Cmd; +use crate::utils::cmd::{Cmd, CmdOutput}; -use super::{PackageError, PackageList, SpecificPackageManager}; +use super::{PackageError, PackageList, PackageManager}; -pub fn apt() -> Cmd { - Cmd::new("apt").env("DEBIAN_FRONTEND", "noninteractive") +/// This structure is mostly used as an indirection for testing purposes. +/// But you can use it to build your own apt commands. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct AptCmd(pub Cmd); + +impl AptCmd { + pub fn new() -> AptCmd { + AptCmd( + Cmd::new("apt").env("DEBIAN_FRONTEND", "noninteractive") + ) + } + + pub fn update() -> AptCmd { + let mut cmd = Self::new(); + cmd.0 = cmd.0.arg("update"); + cmd + } + + pub fn install(list: PackageList) -> AptCmd { + let mut cmd = Self::new(); + cmd.0 = cmd.0.arg("install").args(list.list()); + cmd + } + + pub fn remove(list: PackageList) -> AptCmd { + let mut cmd = Self::new(); + cmd.0 = cmd.0.arg("remove").args(list.list()); + cmd + } + + pub fn run(self) -> Result { + self.0.run() + } } #[derive(Debug)] -pub struct DebianPackageManager; +pub struct AptManager; -impl SpecificPackageManager for DebianPackageManager { +impl PackageManager for AptManager { fn name(&self) -> &'static str { "apt" } fn update(&self) -> Result<(), PackageError> { - let res = apt().arg("update").run()?; + let res = AptCmd::update().run()?; if ! res.success { return Err(PackageError::CmdFail(res)); @@ -24,7 +55,7 @@ impl SpecificPackageManager for DebianPackageManager { } fn install(&self, list: PackageList) -> Result<(), PackageError> { - let res = apt().arg("install").args(list.list()).run()?; + let res = AptCmd::install(list).run()?; if ! res.success { return Err(PackageError::CmdFail(res)); @@ -33,7 +64,7 @@ impl SpecificPackageManager for DebianPackageManager { } fn remove(&self, list: PackageList) -> Result<(), PackageError> { - let res = apt().arg("remove").args(list.list()).run()?; + let res = AptCmd::remove(list).run()?; if ! res.success { return Err(PackageError::CmdFail(res)); @@ -41,3 +72,100 @@ impl SpecificPackageManager for DebianPackageManager { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use super::super::IntoPackageList; + use std::collections::HashMap; + + #[test] + pub fn update_cmd() { + let cmd = AptCmd::update(); + let mut env: HashMap = HashMap::new(); + env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); + + assert_eq!( + cmd.0, + Cmd { + program: "apt".to_string(), + args: vec!("update".to_string()), + stdin: None, + chdir: None, + env, + } + ) + } + + #[test] + pub fn install_cmd() { + let cmd = AptCmd::install(vec!("sl").into_package_list()); + let mut env: HashMap = HashMap::new(); + env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); + + assert_eq!( + cmd.0, + Cmd { + program: "apt".to_string(), + args: vec!("install", "sl").iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env, + } + ) + } + + #[test] + pub fn install_cmd_multi() { + let cmd = AptCmd::install(vec!("sl", "hello").into_package_list()); + let mut env: HashMap = HashMap::new(); + env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); + + assert_eq!( + cmd.0, + Cmd { + program: "apt".to_string(), + args: vec!("install", "sl", "hello").iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env, + } + ) + } + + #[test] + pub fn remove_cmd() { + let cmd = AptCmd::remove(vec!("sl").into_package_list()); + let mut env: HashMap = HashMap::new(); + env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); + + assert_eq!( + cmd.0, + Cmd { + program: "apt".to_string(), + args: vec!("remove", "sl").iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env, + } + ) + } + + #[test] + pub fn remove_cmd_multi() { + let cmd = AptCmd::remove(vec!("sl", "hello").into_package_list()); + let mut env: HashMap = HashMap::new(); + env.insert("DEBIAN_FRONTEND".to_string(), "noninteractive".to_string()); + + assert_eq!( + cmd.0, + Cmd { + program: "apt".to_string(), + args: vec!("remove", "sl", "hello").iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env, + } + ) + } +} diff --git a/src/modules/package/builder.rs b/src/modules/package/builder.rs index 4e39fb3..f95642e 100644 --- a/src/modules/package/builder.rs +++ b/src/modules/package/builder.rs @@ -1,6 +1,5 @@ -use super::{PackageArgs, IntoPackageList, PackageList, PackageState, PackageModule, PackageStatus, PackageError, SpecificPackageManager}; -use super::apt::DebianPackageManager; -use super::pacman::ArchlinuxPackageManager; +use super::{PackageArgs, IntoPackageList, PackageList, PackageState, PackageModule, PackageStatus, PackageError, PackageManager}; +use super::{apt::AptManager, pacman::PacmanManager}; use std::boxed::Box; use crate::Module; @@ -50,7 +49,7 @@ impl PackageArgsBuilder { } impl PackageArgsBuilder { - pub fn with(self, manager: impl SpecificPackageManager + 'static) -> PackageArgsBuilder> { + pub fn with(self, manager: impl PackageManager + 'static) -> PackageArgsBuilder> { let Self { name, state, .. } = self; PackageArgsBuilder { @@ -60,21 +59,21 @@ impl PackageArgsBuilder { } } - pub fn with_apt(self) -> PackageArgsBuilder> { + pub fn with_apt(self) -> PackageArgsBuilder> { let Self { name, state, .. } = self; PackageArgsBuilder { name, state, - manager: Box::new(DebianPackageManager), + manager: Box::new(AptManager), } } - pub fn with_pacman(self) -> PackageArgsBuilder> { + pub fn with_pacman(self) -> PackageArgsBuilder> { let Self { name, state, .. } = self; PackageArgsBuilder { name, state, - manager: Box::new(ArchlinuxPackageManager), + manager: Box::new(PacmanManager), } } } @@ -82,14 +81,14 @@ impl PackageArgsBuilder { // We can only RUN the args when a package manager has been set. Alternatively, // when called inside a playbook, the package manager can be populated automatically from facts. -impl PackageArgsBuilder> { +impl PackageArgsBuilder> { pub fn run(self) -> Result { self.build().run() } } -impl PackageArgsBuilder> { - pub fn build(self) -> PackageModule> { +impl PackageArgsBuilder> { + pub fn build(self) -> PackageModule> { let Self { name, state, manager, .. } = self; PackageModule { diff --git a/src/modules/package/list.rs b/src/modules/package/list.rs index d2e2176..1a3e7b7 100644 --- a/src/modules/package/list.rs +++ b/src/modules/package/list.rs @@ -1,6 +1,7 @@ use serde::Serialize; -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +#[serde(transparent)] pub struct PackageList { pub list: Vec, } diff --git a/src/modules/package/mod.rs b/src/modules/package/mod.rs index b121454..67b9143 100644 --- a/src/modules/package/mod.rs +++ b/src/modules/package/mod.rs @@ -10,35 +10,35 @@ pub mod list; pub use list::{PackageList, IntoPackageList}; pub mod pacman; -use pacman::ArchlinuxPackageManager; +use pacman::PacmanManager; pub mod apt; -use apt::DebianPackageManager; +use apt::AptManager; pub mod builder; pub use builder::{PackageArgsBuilder, NoManager}; pub type PackageStatus = (); -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct PackageArgs { name: PackageList, state: PackageState, } -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub enum PackageState { Present, Absent, Latest, } -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub struct PackageModule { manager: Manager, args: PackageArgs, } -impl Module for PackageModule> { +impl Module for PackageModule> { fn serialize_args(&self) -> serde_json::Value { serde_json::to_value(&self.args).unwrap() } @@ -62,13 +62,13 @@ impl Module for PackageModule>, PackageArgs, (), PackageError> for PackageModule { - fn with_facts(self, facts: &Facts) -> PackageModule> { +impl ModuleSetup>, PackageArgs, (), PackageError> for PackageModule { + fn with_facts(self, facts: &Facts) -> PackageModule> { let Self { args, .. } = self; - let manager: Box = match facts.os.family() { - OsFamily::Debian => Box::new(DebianPackageManager), - OsFamily::Archlinux => Box::new(ArchlinuxPackageManager), + let manager: Box = match facts.os.family() { + OsFamily::Debian => Box::new(AptManager), + OsFamily::Archlinux => Box::new(PacmanManager), }; PackageModule { @@ -97,9 +97,82 @@ impl From for PackageError { } } -pub trait SpecificPackageManager: std::fmt::Debug { +pub trait PackageManager: std::fmt::Debug { fn name(&self) -> &'static str; fn update(&self) -> Result<(), PackageError>; fn install(&self, list: PackageList) -> Result<(), PackageError>; fn remove(&self, list: PackageList) -> Result<(), PackageError>; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn ensure_present() { + let pkg = PackageModule::new() + .name("hello") + .state(PackageState::Present) + .with_apt() + .build(); + + assert_eq!( + pkg.serialize_args(), + serde_json::json!({ + "name": vec!("hello"), + "state": PackageState::Present, + }) + ); + } + + #[test] + fn ensure_present_multi() { + let pkg = PackageModule::new() + .name(&[ "hello", "sl" ]) + .state(PackageState::Present) + .with_apt() + .build(); + + assert_eq!( + pkg.serialize_args(), + serde_json::json!({ + "name": vec!("hello", "sl"), + "state": PackageState::Present, + }) + ) + } + + #[test] + fn ensure_absent() { + let pkg = PackageModule::new() + .name("hello") + .state(PackageState::Absent) + .with_apt() + .build(); + + assert_eq!( + pkg.serialize_args(), + serde_json::json!({ + "name": vec!("hello"), + "state": PackageState::Absent, + }) + ) + } + + #[test] + fn ensure_absent_multi() { + let pkg = PackageModule::new() + .name(&[ "hello", "sl" ]) + .state(PackageState::Absent) + .with_apt() + .build(); + + assert_eq!( + pkg.serialize_args(), + serde_json::json!({ + "name": vec!("hello", "sl"), + "state": PackageState::Absent, + }) + ) + } } \ No newline at end of file diff --git a/src/modules/package/pacman.rs b/src/modules/package/pacman.rs index cce89a0..c4070ba 100644 --- a/src/modules/package/pacman.rs +++ b/src/modules/package/pacman.rs @@ -1,17 +1,53 @@ -use crate::utils::cmd::Cmd; +use crate::utils::cmd::{Cmd, CmdOutput}; + +use super::{PackageError, PackageList, PackageManager}; + +/// This structure is mostly used as an indirection for testing purposes. +/// But you can use it to build your own pacman commands. +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] +pub struct PacmanCmd(pub Cmd); + +impl PacmanCmd { + pub fn new() -> PacmanCmd { + PacmanCmd( + Cmd::new("pacman").arg("--noconfirm").arg("--needed") + ) + } + + pub fn update() -> PacmanCmd { + let mut cmd = Self::new(); + cmd.0 = cmd.0.arg("-Sy"); + cmd + } + + pub fn install(list: PackageList) -> PacmanCmd { + let mut cmd = Self::new(); + cmd.0 = cmd.0.arg("-S").args(list.list()); + cmd + } + + pub fn remove(list: PackageList) -> PacmanCmd { + let mut cmd = Self::new(); + cmd.0 = cmd.0.arg("-R").args(list.list()); + cmd + } + + pub fn run(self) -> Result { + self.0.run() + } +} -use super::{PackageError, PackageList, SpecificPackageManager}; #[derive(Debug)] -pub struct ArchlinuxPackageManager; +pub struct PacmanManager; -impl SpecificPackageManager for ArchlinuxPackageManager { +impl PackageManager for PacmanManager { fn name(&self) -> &'static str { "pacman" } fn update(&self) -> Result<(), PackageError> { - let res = Cmd::new("pacman").arg("-Sy").run()?; + let res = PacmanCmd::update().run()?; if ! res.success { return Err(PackageError::CmdFail(res)); @@ -21,7 +57,7 @@ impl SpecificPackageManager for ArchlinuxPackageManager { } fn install(&self, list: PackageList) -> Result<(), PackageError> { - let res = Cmd::new("pacman").arg("-S").args(list.list()).run()?; + let res = PacmanCmd::install(list).run()?; if ! res.success { return Err(PackageError::CmdFail(res)); @@ -31,7 +67,7 @@ impl SpecificPackageManager for ArchlinuxPackageManager { } fn remove(&self, list: PackageList) -> Result<(), PackageError> { - let res = Cmd::new("pacman").arg("-R").args(list.list()).run()?; + let res = PacmanCmd::remove(list).run()?; if ! res.success { return Err(PackageError::CmdFail(res)); @@ -40,3 +76,100 @@ impl SpecificPackageManager for ArchlinuxPackageManager { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use super::super::IntoPackageList; + use std::collections::HashMap; + + #[test] + pub fn update_cmd() { + let cmd = PacmanCmd::update(); + let mut args: Vec<&str> = vec!("--noconfirm", "--needed"); + args.push("-Sy"); + + assert_eq!( + cmd.0, + Cmd { + program: "pacman".to_string(), + args: args.iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env: HashMap::new(), + } + ) + } + + #[test] + pub fn install_cmd() { + let cmd = PacmanCmd::install(vec!("sl").into_package_list()); + let mut args: Vec<&str> = vec!("--noconfirm", "--needed"); + args.extend(& [ "-S", "sl" ]); + + assert_eq!( + cmd.0, + Cmd { + program: "pacman".to_string(), + args: args.iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env: HashMap::new(), + } + ) + } + + #[test] + pub fn install_cmd_multi() { + let cmd = PacmanCmd::install(vec!("sl", "hello").into_package_list()); + let mut args: Vec<&str> = vec!("--noconfirm", "--needed"); + args.extend(& [ "-S", "sl", "hello" ]); + + assert_eq!( + cmd.0, + Cmd { + program: "pacman".to_string(), + args: args.iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env: HashMap::new(), + } + ) + } + + #[test] + pub fn remove_cmd() { + let cmd = PacmanCmd::remove(vec!("sl").into_package_list()); + let mut args: Vec<&str> = vec!("--noconfirm", "--needed"); + args.extend(& [ "-R", "sl" ]); + + assert_eq!( + cmd.0, + Cmd { + program: "pacman".to_string(), + args: args.iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env: HashMap::new(), + } + ) + } + + #[test] + pub fn remove_cmd_multi() { + let cmd = PacmanCmd::remove(vec!("sl", "hello").into_package_list()); + let mut args: Vec<&str> = vec!("--noconfirm", "--needed"); + args.extend(& [ "-R", "sl", "hello" ]); + + assert_eq!( + cmd.0, + Cmd { + program: "pacman".to_string(), + args: args.iter().map(|x| x.to_string()).collect(), + stdin: None, + chdir: None, + env: HashMap::new(), + } + ) + } +} diff --git a/src/utils/cmd.rs b/src/utils/cmd.rs index a38dc89..71c7320 100644 --- a/src/utils/cmd.rs +++ b/src/utils/cmd.rs @@ -3,13 +3,13 @@ use duct::cmd as duct_cmd; use std::collections::HashMap; use std::path::PathBuf; -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize)] pub struct Cmd { - program: String, - args: Vec, - stdin: Option, - chdir: Option, - env: HashMap, + pub program: String, + pub args: Vec, + pub stdin: Option, + pub chdir: Option, + pub env: HashMap, } impl Cmd {