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 {
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<CmdOutput, std::io::Error> {
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<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::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<Packages, Manager> PackageArgsBuilder<Packages, NoState, Manager> {
}
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;
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;
PackageArgsBuilder {
name,
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;
PackageArgsBuilder {
name,
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,
// 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> {
self.build().run()
}
}
impl PackageArgsBuilder<PackageList, PackageState, Box<dyn SpecificPackageManager>> {
pub fn build(self) -> PackageModule<Box<dyn SpecificPackageManager>> {
impl PackageArgsBuilder<PackageList, PackageState, Box<dyn PackageManager>> {
pub fn build(self) -> PackageModule<Box<dyn PackageManager>> {
let Self { name, state, manager, .. } = self;
PackageModule {

View File

@ -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<String>,
}

View File

@ -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: Manager,
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 {
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> {
fn with_facts(self, facts: &Facts) -> PackageModule<Box<dyn SpecificPackageManager>> {
impl ModuleSetup<PackageModule<Box<dyn PackageManager>>, PackageArgs, (), PackageError> for PackageModule<NoManager> {
fn with_facts(self, facts: &Facts) -> PackageModule<Box<dyn PackageManager>> {
let Self { args, .. } = self;
let manager: Box<dyn SpecificPackageManager> = match facts.os.family() {
OsFamily::Debian => Box::new(DebianPackageManager),
OsFamily::Archlinux => Box::new(ArchlinuxPackageManager),
let manager: Box<dyn PackageManager> = match facts.os.family() {
OsFamily::Debian => Box::new(AptManager),
OsFamily::Archlinux => Box::new(PacmanManager),
};
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 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,
})
)
}
}

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)]
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(),
}
)
}
}

View File

@ -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<String>,
stdin: Option<String>,
chdir: Option<PathBuf>,
env: HashMap<String, String>,
pub program: String,
pub args: Vec<String>,
pub stdin: Option<String>,
pub chdir: Option<PathBuf>,
pub env: HashMap<String, String>,
}
impl Cmd {