Rename SpecificPackageManager trait PackageManager, add some unit tests

This commit is contained in:
selfhoster selfhoster 2023-04-21 15:13:03 +02:00
parent 1245db813b
commit d2ef322761
6 changed files with 380 additions and 46 deletions

View File

@ -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 { /// This structure is mostly used as an indirection for testing purposes.
Cmd::new("apt").env("DEBIAN_FRONTEND", "noninteractive") /// 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<CmdOutput, std::io::Error> {
self.0.run()
}
} }
#[derive(Debug)] #[derive(Debug)]
pub struct DebianPackageManager; pub struct AptManager;
impl SpecificPackageManager for DebianPackageManager { impl PackageManager for AptManager {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"apt" "apt"
} }
fn update(&self) -> Result<(), PackageError> { fn update(&self) -> Result<(), PackageError> {
let res = apt().arg("update").run()?; let res = AptCmd::update().run()?;
if ! res.success { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@ -24,7 +55,7 @@ impl SpecificPackageManager for DebianPackageManager {
} }
fn install(&self, list: PackageList) -> Result<(), PackageError> { 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 { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@ -33,7 +64,7 @@ impl SpecificPackageManager for DebianPackageManager {
} }
fn remove(&self, list: PackageList) -> Result<(), PackageError> { 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 { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@ -41,3 +72,100 @@ impl SpecificPackageManager for DebianPackageManager {
Ok(()) 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<String, String> = 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<String, String> = 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<String, String> = 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<String, String> = 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<String, String> = 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,
}
)
}
}

View File

@ -1,6 +1,5 @@
use super::{PackageArgs, IntoPackageList, PackageList, PackageState, PackageModule, PackageStatus, PackageError, SpecificPackageManager}; use super::{PackageArgs, IntoPackageList, PackageList, PackageState, PackageModule, PackageStatus, PackageError, PackageManager};
use super::apt::DebianPackageManager; use super::{apt::AptManager, pacman::PacmanManager};
use super::pacman::ArchlinuxPackageManager;
use std::boxed::Box; use std::boxed::Box;
use crate::Module; use crate::Module;
@ -50,7 +49,7 @@ impl<Packages, Manager> PackageArgsBuilder<Packages, NoState, Manager> {
} }
impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> { impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> {
pub fn with(self, manager: impl SpecificPackageManager + 'static) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> { pub fn with(self, manager: impl PackageManager + 'static) -> PackageArgsBuilder<Packages, State, Box<dyn PackageManager>> {
let Self { name, state, .. } = self; let Self { name, state, .. } = self;
PackageArgsBuilder { PackageArgsBuilder {
@ -60,21 +59,21 @@ impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> {
} }
} }
pub fn with_apt(self) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> { pub fn with_apt(self) -> PackageArgsBuilder<Packages, State, Box<dyn PackageManager>> {
let Self { name, state, .. } = self; let Self { name, state, .. } = self;
PackageArgsBuilder { PackageArgsBuilder {
name, name,
state, state,
manager: Box::new(DebianPackageManager), manager: Box::new(AptManager),
} }
} }
pub fn with_pacman(self) -> PackageArgsBuilder<Packages, State, Box<dyn SpecificPackageManager>> { pub fn with_pacman(self) -> PackageArgsBuilder<Packages, State, Box<dyn PackageManager>> {
let Self { name, state, .. } = self; let Self { name, state, .. } = self;
PackageArgsBuilder { PackageArgsBuilder {
name, name,
state, state,
manager: Box::new(ArchlinuxPackageManager), manager: Box::new(PacmanManager),
} }
} }
} }
@ -82,14 +81,14 @@ impl<Packages, State> PackageArgsBuilder<Packages, State, NoManager> {
// We can only RUN the args when a package manager has been set. Alternatively, // 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. // when called inside a playbook, the package manager can be populated automatically from facts.
impl PackageArgsBuilder<PackageList, PackageState, Box<dyn SpecificPackageManager>> { impl PackageArgsBuilder<PackageList, PackageState, Box<dyn PackageManager>> {
pub fn run(self) -> Result<PackageStatus, PackageError> { pub fn run(self) -> Result<PackageStatus, PackageError> {
self.build().run() self.build().run()
} }
} }
impl PackageArgsBuilder<PackageList, PackageState, Box<dyn SpecificPackageManager>> { impl PackageArgsBuilder<PackageList, PackageState, Box<dyn PackageManager>> {
pub fn build(self) -> PackageModule<Box<dyn SpecificPackageManager>> { pub fn build(self) -> PackageModule<Box<dyn PackageManager>> {
let Self { name, state, manager, .. } = self; let Self { name, state, manager, .. } = self;
PackageModule { PackageModule {

View File

@ -1,6 +1,7 @@
use serde::Serialize; use serde::Serialize;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
#[serde(transparent)]
pub struct PackageList { pub struct PackageList {
pub list: Vec<String>, pub list: Vec<String>,
} }

View File

@ -10,35 +10,35 @@ pub mod list;
pub use list::{PackageList, IntoPackageList}; pub use list::{PackageList, IntoPackageList};
pub mod pacman; pub mod pacman;
use pacman::ArchlinuxPackageManager; use pacman::PacmanManager;
pub mod apt; pub mod apt;
use apt::DebianPackageManager; use apt::AptManager;
pub mod builder; pub mod builder;
pub use builder::{PackageArgsBuilder, NoManager}; pub use builder::{PackageArgsBuilder, NoManager};
pub type PackageStatus = (); pub type PackageStatus = ();
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct PackageArgs { pub struct PackageArgs {
name: PackageList, name: PackageList,
state: PackageState, state: PackageState,
} }
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum PackageState { pub enum PackageState {
Present, Present,
Absent, Absent,
Latest, Latest,
} }
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
pub struct PackageModule<Manager> { pub struct PackageModule<Manager> {
manager: Manager, manager: Manager,
args: PackageArgs, args: PackageArgs,
} }
impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn SpecificPackageManager>> { impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn PackageManager>> {
fn serialize_args(&self) -> serde_json::Value { fn serialize_args(&self) -> serde_json::Value {
serde_json::to_value(&self.args).unwrap() serde_json::to_value(&self.args).unwrap()
} }
@ -62,13 +62,13 @@ impl Module<PackageArgs, (), PackageError> for PackageModule<Box<dyn SpecificPac
} }
} }
impl ModuleSetup<PackageModule<Box<dyn SpecificPackageManager>>, PackageArgs, (), PackageError> for PackageModule<NoManager> { impl ModuleSetup<PackageModule<Box<dyn PackageManager>>, PackageArgs, (), PackageError> for PackageModule<NoManager> {
fn with_facts(self, facts: &Facts) -> PackageModule<Box<dyn SpecificPackageManager>> { fn with_facts(self, facts: &Facts) -> PackageModule<Box<dyn PackageManager>> {
let Self { args, .. } = self; let Self { args, .. } = self;
let manager: Box<dyn SpecificPackageManager> = match facts.os.family() { let manager: Box<dyn PackageManager> = match facts.os.family() {
OsFamily::Debian => Box::new(DebianPackageManager), OsFamily::Debian => Box::new(AptManager),
OsFamily::Archlinux => Box::new(ArchlinuxPackageManager), OsFamily::Archlinux => Box::new(PacmanManager),
}; };
PackageModule { PackageModule {
@ -97,9 +97,82 @@ impl From<std::io::Error> for PackageError {
} }
} }
pub trait SpecificPackageManager: std::fmt::Debug { pub trait PackageManager: std::fmt::Debug {
fn name(&self) -> &'static str; fn name(&self) -> &'static str;
fn update(&self) -> Result<(), PackageError>; fn update(&self) -> Result<(), PackageError>;
fn install(&self, list: PackageList) -> Result<(), PackageError>; fn install(&self, list: PackageList) -> Result<(), PackageError>;
fn remove(&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,
})
)
}
}

View File

@ -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<CmdOutput, std::io::Error> {
self.0.run()
}
}
use super::{PackageError, PackageList, SpecificPackageManager};
#[derive(Debug)] #[derive(Debug)]
pub struct ArchlinuxPackageManager; pub struct PacmanManager;
impl SpecificPackageManager for ArchlinuxPackageManager { impl PackageManager for PacmanManager {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"pacman" "pacman"
} }
fn update(&self) -> Result<(), PackageError> { fn update(&self) -> Result<(), PackageError> {
let res = Cmd::new("pacman").arg("-Sy").run()?; let res = PacmanCmd::update().run()?;
if ! res.success { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@ -21,7 +57,7 @@ impl SpecificPackageManager for ArchlinuxPackageManager {
} }
fn install(&self, list: PackageList) -> Result<(), PackageError> { 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 { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@ -31,7 +67,7 @@ impl SpecificPackageManager for ArchlinuxPackageManager {
} }
fn remove(&self, list: PackageList) -> Result<(), PackageError> { 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 { if ! res.success {
return Err(PackageError::CmdFail(res)); return Err(PackageError::CmdFail(res));
@ -40,3 +76,100 @@ impl SpecificPackageManager for ArchlinuxPackageManager {
Ok(()) 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(),
}
)
}
}

View File

@ -3,13 +3,13 @@ use duct::cmd as duct_cmd;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
#[derive(Clone, Debug, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Cmd { pub struct Cmd {
program: String, pub program: String,
args: Vec<String>, pub args: Vec<String>,
stdin: Option<String>, pub stdin: Option<String>,
chdir: Option<PathBuf>, pub chdir: Option<PathBuf>,
env: HashMap<String, String>, pub env: HashMap<String, String>,
} }
impl Cmd { impl Cmd {