WIP: 26-integration-tests #35
@ -84,7 +84,3 @@ Once you have that installed, just run
|
||||
cargo build --release
|
||||
```
|
||||
and you will find an executable in `target/release`.
|
||||
|
||||
### Unit tests
|
||||
|
||||
In order for the unit tests to pass, you will need `python3`.
|
||||
|
||||
200
src/command.rs
200
src/command.rs
@ -1,200 +0,0 @@
|
||||
use crate::log;
|
||||
use crate::logger::{LogLevel, Logger};
|
||||
use std::error::Error;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::io;
|
||||
use std::iter::once;
|
||||
use std::process::{Command, ExitStatus, Output};
|
||||
|
||||
pub trait LogRunnable {
|
||||
fn run(&mut self, logger: &Logger) -> Result<(), CommandSpecificError<ExecutionError>>;
|
||||
fn collect_output(&mut self) -> Result<Output, CommandSpecificError<ExecutionError>>;
|
||||
fn collect_full_output(&mut self) -> Result<Output, CommandSpecificError<StartError>>;
|
||||
}
|
||||
|
||||
impl LogRunnable for Command {
|
||||
fn run(&mut self, logger: &Logger) -> Result<(), CommandSpecificError<ExecutionError>> {
|
||||
run(self, logger).map_err(|error| CommandSpecificError {
|
||||
command: self,
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_output(&mut self) -> Result<Output, CommandSpecificError<ExecutionError>> {
|
||||
collect_output(self, None).map_err(|error| CommandSpecificError {
|
||||
command: self,
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
fn collect_full_output(&mut self) -> Result<Output, CommandSpecificError<StartError>> {
|
||||
collect_full_output(self).map_err(|error| CommandSpecificError {
|
||||
command: self,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn run(command: &mut Command, logger: &Logger) -> Result<(), ExecutionError> {
|
||||
match logger.level {
|
||||
LogLevel::Debug | LogLevel::Info => {
|
||||
let status = command.status()?;
|
||||
if !status.success() {
|
||||
Err(status)?;
|
||||
}
|
||||
}
|
||||
LogLevel::Error => {
|
||||
collect_output(command, Some(logger))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_output(
|
||||
command: &mut Command,
|
||||
logger: Option<&Logger>,
|
||||
) -> Result<Output, ExecutionError> {
|
||||
let output = collect_full_output(command)?;
|
||||
if !output.status.success() {
|
||||
if let Some(logger) = logger {
|
||||
log!(logger, error, "{}", String::from_utf8_lossy(&output.stdout));
|
||||
log!(logger, error, "{}", String::from_utf8_lossy(&output.stderr));
|
||||
}
|
||||
Err(output.status)?;
|
||||
}
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn collect_full_output(command: &mut Command) -> Result<Output, StartError> {
|
||||
Ok(command.output()?)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandSpecificError<'a, E> {
|
||||
pub command: &'a Command,
|
||||
pub error: E,
|
||||
}
|
||||
|
||||
impl<E> Display for CommandSpecificError<'_, E>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Failed to execute command '{}': {}",
|
||||
command_to_string(self.command),
|
||||
self.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn command_to_string(command: &Command) -> String {
|
||||
once(command.get_program().to_string_lossy().to_string())
|
||||
.chain(command.get_args().map(|arg| {
|
||||
let arg_str = arg.to_string_lossy();
|
||||
if arg_str.contains(' ') {
|
||||
format!("\"{arg_str}\"")
|
||||
} else {
|
||||
arg_str.to_string()
|
||||
}
|
||||
}))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
impl<E> Error for CommandSpecificError<'_, E> where E: Debug + Display {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StartError(io::Error);
|
||||
|
||||
impl From<io::Error> for StartError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
StartError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StartError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Failed to start command: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for StartError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ExecutionError {
|
||||
StartError(StartError),
|
||||
BadExitStatus(ExitStatus),
|
||||
}
|
||||
|
||||
impl From<io::Error> for ExecutionError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::StartError(StartError(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StartError> for ExecutionError {
|
||||
fn from(value: StartError) -> Self {
|
||||
Self::StartError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ExitStatus> for ExecutionError {
|
||||
fn from(value: ExitStatus) -> Self {
|
||||
Self::BadExitStatus(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ExecutionError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ExecutionError::StartError(e) => Display::fmt(e, f),
|
||||
ExecutionError::BadExitStatus(status) => write!(f, "Command failed with {}", status),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ExecutionError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::command::{CommandSpecificError, ExecutionError, LogRunnable};
|
||||
use crate::logger::Logger;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
#[test]
|
||||
fn test_unknown_command() {
|
||||
let mut command = Command::new("python7");
|
||||
let Err(
|
||||
e @ CommandSpecificError {
|
||||
error: ExecutionError::StartError(_),
|
||||
..
|
||||
},
|
||||
) = command
|
||||
.args([PathBuf::from("test-ressources/python/exit_1.py")])
|
||||
.run(&Logger::default())
|
||||
else {
|
||||
panic!("command shouldn't exist");
|
||||
};
|
||||
assert_eq!(e.to_string(), "Failed to execute command 'python7': Failed to start command: No such file or directory (os error 2)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error() {
|
||||
let mut command = Command::new("python3");
|
||||
let Err(
|
||||
e @ CommandSpecificError {
|
||||
error: ExecutionError::BadExitStatus(_),
|
||||
..
|
||||
},
|
||||
) = command
|
||||
.arg("test-ressources/python/exit_1.py")
|
||||
.run(&Logger::default())
|
||||
else {
|
||||
panic!("command should return exit-code 1")
|
||||
};
|
||||
assert_eq!(e.to_string(), "Failed to execute command 'python3 test-ressources/python/exit_1.py': Command failed with exit status: 1");
|
||||
}
|
||||
}
|
||||
96
src/environment.rs
Normal file
96
src/environment.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use crate::shell_interface::{
|
||||
build_command_from_shell_command, CommandOutput, CommandResult, ExitStatus, ShellCommand,
|
||||
ShellInterface, StartError,
|
||||
};
|
||||
use std::env::VarError;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::path::PathBuf;
|
||||
use std::{env, io};
|
||||
|
||||
pub trait Environment {
|
||||
fn args_os(&self) -> Vec<OsString>;
|
||||
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||
where
|
||||
K: AsRef<OsStr>;
|
||||
fn var<K>(&self, key: K) -> Result<String, VarError>
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
{
|
||||
self
|
||||
.var_os(key)
|
||||
.ok_or(VarError::NotPresent)
|
||||
.and_then(|s| s.into_string().map_err(VarError::NotUnicode))
|
||||
}
|
||||
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
V: AsRef<OsStr>;
|
||||
fn get_home_directory(&self) -> Option<PathBuf>;
|
||||
fn read_line(&mut self) -> Result<String, io::Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Prod;
|
||||
|
||||
impl Environment for Prod {
|
||||
fn args_os(&self) -> Vec<OsString> {
|
||||
env::args_os().collect()
|
||||
}
|
||||
|
||||
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||
where
|
||||
K: AsRef<OsStr>
|
||||
{
|
||||
env::var_os(key)
|
||||
}
|
||||
|
||||
fn var<K>(&self, key: K) -> Result<String, VarError>
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
{
|
||||
env::var(key)
|
||||
}
|
||||
|
||||
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
V: AsRef<OsStr>,
|
||||
{
|
||||
env::set_var(key, value);
|
||||
}
|
||||
|
||||
fn get_home_directory(&self) -> Option<PathBuf> {
|
||||
homedir::my_home().ok().flatten()
|
||||
}
|
||||
|
||||
fn read_line(&mut self) -> Result<String, io::Error> {
|
||||
let mut buffer = String::new();
|
||||
io::stdin().read_line(&mut buffer)?;
|
||||
Ok(buffer.trim().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellInterface for Prod {
|
||||
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError> {
|
||||
CommandResult {
|
||||
result: build_command_from_shell_command(&command)
|
||||
.status()
|
||||
.map(ExitStatus::from)
|
||||
.map_err(StartError::from),
|
||||
command,
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_command_output(
|
||||
&mut self,
|
||||
command: ShellCommand,
|
||||
) -> CommandResult<CommandOutput, StartError> {
|
||||
CommandResult {
|
||||
result: build_command_from_shell_command(&command)
|
||||
.output()
|
||||
.map(CommandOutput::from)
|
||||
.map_err(StartError::from),
|
||||
command,
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/integration_test.rs
Normal file
90
src/integration_test.rs
Normal file
@ -0,0 +1,90 @@
|
||||
use crate::environment::Environment;
|
||||
use crate::shell_interface::{
|
||||
CommandOutput, CommandResult, ExitStatus, ShellCommand, ShellInterface, StartError,
|
||||
};
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::io::Error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TestEnvironment {
|
||||
/// passed command line arguments
|
||||
args_os: Vec<OsString>,
|
||||
/// set environment variables - we assume a pure environment by default
|
||||
env_vars: HashMap<OsString, OsString>,
|
||||
/// home directory, relative to the target/test folder
|
||||
home_dir: PathBuf,
|
||||
/// pending lines of std input
|
||||
stdin: VecDeque<String>,
|
||||
/// whether an ssh agent has been started successfully
|
||||
ssh_agent_started: bool,
|
||||
// TODO ssh servers and local server
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SshServer {
|
||||
pub name: String,
|
||||
pub home_dir: FsEntry,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FsEntry {
|
||||
pub name: OsString,
|
||||
pub kind: FsEntryKind,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum FsEntryKind {
|
||||
Directory(Dir),
|
||||
File {
|
||||
contents: String,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Dir {
|
||||
pub contents: Vec<FsEntry>,
|
||||
}
|
||||
|
||||
impl Environment for TestEnvironment {
|
||||
fn args_os(&self) -> Vec<OsString> {
|
||||
self.args_os.clone()
|
||||
}
|
||||
|
||||
fn var_os<K>(&self, key: K) -> Option<OsString>
|
||||
where
|
||||
K: AsRef<OsStr>
|
||||
{
|
||||
self.env_vars.get(key.as_ref()).map(|s| s.into())
|
||||
}
|
||||
|
||||
fn set_var<K, V>(&mut self, key: K, value: V)
|
||||
where
|
||||
K: AsRef<OsStr>,
|
||||
V: AsRef<OsStr>,
|
||||
{
|
||||
self.env_vars.insert(key.as_ref().into(), value.as_ref().into());
|
||||
}
|
||||
|
||||
fn get_home_directory(&self) -> Option<PathBuf> {
|
||||
PathBuf::from("target/integration_test").join(&self.home_dir).into()
|
||||
}
|
||||
|
||||
fn read_line(&mut self) -> Result<String, Error> {
|
||||
self.stdin.pop_front().ok_or_else(|| Error::other("Unexpected call to read_line: No input prepared"))
|
||||
}
|
||||
}
|
||||
|
||||
impl ShellInterface for TestEnvironment {
|
||||
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn collect_command_output(
|
||||
&mut self,
|
||||
command: ShellCommand,
|
||||
) -> CommandResult<CommandOutput, StartError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
1237
src/main.rs
1237
src/main.rs
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,3 @@
|
||||
use crate::get_home_directory;
|
||||
use std::cell::LazyCell;
|
||||
use std::error::Error;
|
||||
use std::fmt::{Display, Formatter};
|
||||
@ -6,7 +5,6 @@ use std::fs;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::ops::Deref;
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ServerReference {
|
||||
@ -15,6 +13,19 @@ pub enum ServerReference {
|
||||
}
|
||||
|
||||
impl ServerReference {
|
||||
pub fn from_str<F>(s: &str, get_home_directory: F) -> Result<Self, ServerReferenceParseError>
|
||||
where
|
||||
F: FnOnce() -> Result<PathBuf, String>,
|
||||
{
|
||||
Server::from_str(
|
||||
s,
|
||||
RelativeLocalPathAnker::CurrentDirectory,
|
||||
get_home_directory,
|
||||
)
|
||||
.map(Self::Resolved)
|
||||
.or_else(|_| Ok(Self::Identifier(s.to_string())))
|
||||
}
|
||||
|
||||
pub fn get_identifier(&self) -> &str {
|
||||
match self {
|
||||
ServerReference::Resolved(server) => server.address.identifier(),
|
||||
@ -68,16 +79,6 @@ impl ServerReference {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ServerReference {
|
||||
type Err = ServerReferenceParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
Server::from_str(s, RelativeLocalPathAnker::CurrentDirectory)
|
||||
.map(Self::Resolved)
|
||||
.or_else(|_| Ok(Self::Identifier(s.to_string())))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ServerReference {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.get_identifier() == other.get_identifier()
|
||||
@ -126,10 +127,14 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(
|
||||
pub fn from_str<F>(
|
||||
s: &str,
|
||||
relative_local_path_anker: RelativeLocalPathAnker,
|
||||
) -> Result<Self, ServerParseError> {
|
||||
get_home_directory: F,
|
||||
) -> Result<Self, ServerParseError>
|
||||
where
|
||||
F: FnOnce() -> Result<PathBuf, String>,
|
||||
{
|
||||
s.split_once(':')
|
||||
.ok_or(ServerParseError::MissingServerDirectory)
|
||||
.and_then(|(identifier, server_directory)| {
|
||||
@ -245,13 +250,12 @@ impl Error for ServerParseError {}
|
||||
mod test_server_reference {
|
||||
use crate::server::{Server, ServerAddress, ServerReference};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_from_str() {
|
||||
assert_eq!(
|
||||
ServerReference::Identifier("foo".to_string()),
|
||||
ServerReference::from_str("foo").unwrap()
|
||||
ServerReference::from_str("foo", || panic!("shouldn't be called")).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
ServerReference::Resolved(Server {
|
||||
@ -260,7 +264,7 @@ mod test_server_reference {
|
||||
},
|
||||
server_directory_path: PathBuf::from("server/creative2")
|
||||
}),
|
||||
ServerReference::from_str("crea:server/creative2").unwrap()
|
||||
ServerReference::from_str("crea:server/creative2", || panic!("shouldn't be called")).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
562
src/shell_interface.rs
Normal file
562
src/shell_interface.rs
Normal file
@ -0,0 +1,562 @@
|
||||
use crate::logger::{LogLevel, Logger};
|
||||
use crate::server::{Server, ServerAddress};
|
||||
use crate::{log, osf};
|
||||
use std::error::Error;
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::{Debug, Display, Formatter};
|
||||
use std::iter::once;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Output};
|
||||
use std::{io, process};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EnvCommand<'a, E> {
|
||||
command: ShellCommand,
|
||||
environment: &'a mut E,
|
||||
}
|
||||
|
||||
impl<E> EnvCommand<'_, E>
|
||||
where
|
||||
E: ShellInterface,
|
||||
{
|
||||
pub fn run(self) -> CommandResult<ExitStatus, StartError> {
|
||||
self.environment.run_command(self.command)
|
||||
}
|
||||
|
||||
pub fn output(self) -> CommandResult<CommandOutput, StartError> {
|
||||
self.environment.collect_command_output(self.command)
|
||||
}
|
||||
|
||||
pub fn run_logged(self, logger: &Logger) -> CommandResult<LoggedRunOutput, StartError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match logger.level {
|
||||
LogLevel::Debug | LogLevel::Info => {
|
||||
let res = self.run();
|
||||
CommandResult {
|
||||
result: res.result.map(LoggedRunOutput::from),
|
||||
command: res.command,
|
||||
}
|
||||
}
|
||||
LogLevel::Error => {
|
||||
let res = self.output();
|
||||
CommandResult {
|
||||
result: res.result.map(LoggedRunOutput::from),
|
||||
command: res.command,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ShellCommand {
|
||||
Ssh {
|
||||
address: String,
|
||||
server_command: ServerCommand,
|
||||
},
|
||||
Scp {
|
||||
source: ScpParam,
|
||||
destination: ScpParam,
|
||||
},
|
||||
SshAgent,
|
||||
ShhAdd,
|
||||
Editor(Vec<OsString>),
|
||||
Execute {
|
||||
working_directory: PathBuf,
|
||||
command: Vec<OsString>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ShellCommand {
|
||||
pub fn in_env<E>(self, environment: &mut E) -> EnvCommand<E> {
|
||||
EnvCommand {
|
||||
command: self,
|
||||
environment,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ServerCommand {
|
||||
Realpath {
|
||||
path: PathBuf,
|
||||
},
|
||||
Ls {
|
||||
dir: PathBuf,
|
||||
},
|
||||
Rm {
|
||||
file: PathBuf,
|
||||
},
|
||||
Mv {
|
||||
source: PathBuf,
|
||||
destination: PathBuf,
|
||||
},
|
||||
Execute {
|
||||
working_directory: PathBuf,
|
||||
command: OsString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ScpParam {
|
||||
pub server: Option<String>,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
impl<P> From<(&Server, P)> for ScpParam
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
fn from((server, path): (&Server, P)) -> Self {
|
||||
Self {
|
||||
server: match &server.address {
|
||||
ServerAddress::Ssh { ssh_address } => Some(ssh_address.into()),
|
||||
ServerAddress::Localhost => None,
|
||||
},
|
||||
path: PathBuf::from(path.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Path> for ScpParam {
|
||||
fn from(value: &Path) -> Self {
|
||||
Self {
|
||||
server: None,
|
||||
path: PathBuf::from(value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ScpParam> for OsString {
|
||||
fn from(value: &ScpParam) -> Self {
|
||||
let mut builder = osf!();
|
||||
if let Some(server) = &value.server {
|
||||
builder += format!("{server}:");
|
||||
}
|
||||
builder += &value.path;
|
||||
builder.build()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ShellCommand {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
command_to_string(&build_command_from_shell_command(self))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn command_to_string(command: &Command) -> String {
|
||||
once(command.get_program().to_string_lossy().to_string())
|
||||
.chain(command.get_args().map(|arg| {
|
||||
let arg_str = arg.to_string_lossy();
|
||||
if arg_str.contains(' ') {
|
||||
format!("\"{arg_str}\"")
|
||||
} else {
|
||||
arg_str.to_string()
|
||||
}
|
||||
}))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
}
|
||||
|
||||
macro_rules! cmd {
|
||||
($programm: expr $(, $arg:expr )*)=> {{
|
||||
#[allow(unused_mut)]
|
||||
let mut cmd = Command::new($programm);
|
||||
$( cmd.arg($arg); )*
|
||||
cmd
|
||||
}};
|
||||
}
|
||||
|
||||
pub fn build_command_from_shell_command(shell_command: &ShellCommand) -> Command {
|
||||
match shell_command {
|
||||
ShellCommand::Ssh {
|
||||
address,
|
||||
server_command,
|
||||
} => cmd!(
|
||||
"ssh",
|
||||
address,
|
||||
match server_command {
|
||||
ServerCommand::Realpath { path } => osf!("realpath -e ") + path,
|
||||
ServerCommand::Ls { dir } => osf!("ls ") + dir,
|
||||
ServerCommand::Rm { file } => osf!("rm ") + file,
|
||||
ServerCommand::Mv {
|
||||
source,
|
||||
destination,
|
||||
} => osf!("mv ") + source + " " + destination,
|
||||
ServerCommand::Execute {
|
||||
working_directory,
|
||||
command,
|
||||
} => osf!("cd ") + working_directory + "; " + command,
|
||||
}
|
||||
),
|
||||
ShellCommand::Scp {
|
||||
source,
|
||||
destination,
|
||||
} => cmd!("scp", OsString::from(source), OsString::from(destination)),
|
||||
ShellCommand::SshAgent => cmd!("ssh-agent", "-s"),
|
||||
ShellCommand::ShhAdd => cmd!("ssh-add"),
|
||||
ShellCommand::Editor(args) => {
|
||||
let mut args = args.clone();
|
||||
let mut cmd = cmd!(args.remove(0));
|
||||
cmd.args(args);
|
||||
cmd
|
||||
}
|
||||
ShellCommand::Execute {
|
||||
working_directory,
|
||||
command,
|
||||
} => {
|
||||
let mut args = command.clone();
|
||||
let mut cmd = cmd!(args.remove(0));
|
||||
cmd.args(args).current_dir(working_directory);
|
||||
cmd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ShellInterface {
|
||||
fn run_command(&mut self, command: ShellCommand) -> CommandResult<ExitStatus, StartError>;
|
||||
fn collect_command_output(
|
||||
&mut self,
|
||||
command: ShellCommand,
|
||||
) -> CommandResult<CommandOutput, StartError>;
|
||||
}
|
||||
|
||||
pub trait MaybeCast<T> {
|
||||
fn maybe_cast(&self) -> Option<&T>;
|
||||
}
|
||||
|
||||
impl<T> MaybeCast<T> for T {
|
||||
fn maybe_cast(&self) -> Option<&T> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LoggedRunOutput {
|
||||
ExitStatus(ExitStatus),
|
||||
CommandOutput(CommandOutput),
|
||||
}
|
||||
|
||||
impl From<ExitStatus> for LoggedRunOutput {
|
||||
fn from(value: ExitStatus) -> Self {
|
||||
Self::ExitStatus(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CommandOutput> for LoggedRunOutput {
|
||||
fn from(value: CommandOutput) -> Self {
|
||||
Self::CommandOutput(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ExitStatus> for LoggedRunOutput {
|
||||
fn as_ref(&self) -> &ExitStatus {
|
||||
match self {
|
||||
LoggedRunOutput::ExitStatus(status) => status,
|
||||
LoggedRunOutput::CommandOutput(output) => output.as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MaybeCast<CommandOutput> for LoggedRunOutput {
|
||||
fn maybe_cast(&self) -> Option<&CommandOutput> {
|
||||
match self {
|
||||
LoggedRunOutput::ExitStatus(_) => None,
|
||||
LoggedRunOutput::CommandOutput(output) => Some(output),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandResult<T, E> {
|
||||
pub command: ShellCommand,
|
||||
pub result: Result<T, E>,
|
||||
}
|
||||
|
||||
impl<T, E> CommandResult<T, E> {
|
||||
pub fn into_result(self) -> Result<T, CommandError<E>> {
|
||||
self.result.map_err(|error| CommandError {
|
||||
command: self.command,
|
||||
error,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CommandResult<T, StartError> {
|
||||
pub fn and_expect_success(self) -> CommandResult<T, ExecutionError<T>>
|
||||
where
|
||||
T: AsRef<ExitStatus>,
|
||||
{
|
||||
CommandResult {
|
||||
result: self.result.map_err(ExecutionError::from).and_then(|t| {
|
||||
if t.as_ref().success {
|
||||
Ok(t)
|
||||
} else {
|
||||
Err(ExecutionError::BadExitStatus(t))
|
||||
}
|
||||
}),
|
||||
command: self.command,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> CommandResult<T, ExecutionError<T>> {
|
||||
pub fn into_result_with_error_logging(
|
||||
self,
|
||||
logger: &Logger,
|
||||
) -> Result<T, CommandError<ExecutionError<T>>>
|
||||
where
|
||||
T: MaybeCast<CommandOutput>,
|
||||
{
|
||||
self.result.map_err(|error| {
|
||||
if let ExecutionError::BadExitStatus(t) = &error {
|
||||
if let Some(output) = t.maybe_cast() {
|
||||
log!(logger, error, "{}", output.stdout.to_string_lossy());
|
||||
log!(logger, error, "{}", output.stderr.to_string_lossy());
|
||||
}
|
||||
}
|
||||
CommandError {
|
||||
command: self.command,
|
||||
error,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CommandOutput {
|
||||
pub stdout: OsString,
|
||||
pub stderr: OsString,
|
||||
pub status: ExitStatus,
|
||||
}
|
||||
|
||||
impl From<Output> for CommandOutput {
|
||||
fn from(value: Output) -> Self {
|
||||
Self {
|
||||
stdout: os_string_from_ssh_output(value.stdout),
|
||||
stderr: os_string_from_ssh_output(value.stderr),
|
||||
status: value.status.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn os_string_from_ssh_output(output: Vec<u8>) -> OsString {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStringExt;
|
||||
OsString::from_vec(output)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
OsString::from_wide(output.iter().map(|&b| b as u16).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ExitStatus> for CommandOutput {
|
||||
fn as_ref(&self) -> &ExitStatus {
|
||||
&self.status
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExitStatus {
|
||||
pub success: bool,
|
||||
pub string_form: String,
|
||||
#[allow(dead_code)]
|
||||
pub code: Option<i32>,
|
||||
}
|
||||
|
||||
impl From<process::ExitStatus> for ExitStatus {
|
||||
fn from(value: process::ExitStatus) -> Self {
|
||||
Self {
|
||||
success: value.success(),
|
||||
string_form: value.to_string(),
|
||||
code: value.code(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<ExitStatus> for ExitStatus {
|
||||
fn as_ref(&self) -> &ExitStatus {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ExitStatus {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
Display::fmt(&self.string_form, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommandError<E> {
|
||||
pub command: ShellCommand,
|
||||
pub error: E,
|
||||
}
|
||||
|
||||
impl<E> From<CommandError<E>> for String
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn from(value: CommandError<E>) -> Self {
|
||||
value.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Display for CommandError<E>
|
||||
where
|
||||
E: Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Error while running command '{}': {}",
|
||||
self.command, self.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Error for CommandError<E> where E: Error {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct StartError(io::Error);
|
||||
|
||||
impl From<io::Error> for StartError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
StartError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for StartError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "Failed to run command: {}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for StartError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ExecutionError<T> {
|
||||
StartError(StartError),
|
||||
BadExitStatus(T),
|
||||
}
|
||||
|
||||
impl<T> From<StartError> for ExecutionError<T> {
|
||||
fn from(value: StartError) -> Self {
|
||||
Self::StartError(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Display for ExecutionError<T>
|
||||
where
|
||||
T: AsRef<ExitStatus>,
|
||||
{
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ExecutionError::StartError(e) => Display::fmt(e, f),
|
||||
ExecutionError::BadExitStatus(status) => {
|
||||
write!(f, "execution failed with {}", status.as_ref())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Error for ExecutionError<T> where T: AsRef<ExitStatus> + Debug {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_commands {
|
||||
use crate::shell_interface::{ScpParam, ServerCommand, ShellCommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn test_to_string() {
|
||||
assert_eq!(
|
||||
ShellCommand::Ssh {
|
||||
address: "crea".to_string(),
|
||||
server_command: ServerCommand::Realpath {
|
||||
path: PathBuf::from("plugins/*.jar")
|
||||
}
|
||||
}
|
||||
.to_string(),
|
||||
r#"ssh crea "realpath -e plugins/*.jar""#
|
||||
);
|
||||
assert_eq!(
|
||||
ShellCommand::Ssh {
|
||||
address: "crea".to_string(),
|
||||
server_command: ServerCommand::Ls {
|
||||
dir: PathBuf::from("creative/plugins")
|
||||
}
|
||||
}
|
||||
.to_string(),
|
||||
r#"ssh crea "ls creative/plugins""#
|
||||
);
|
||||
assert_eq!(
|
||||
ShellCommand::Ssh {
|
||||
address: "crea".to_string(),
|
||||
server_command: ServerCommand::Rm {
|
||||
file: PathBuf::from("foo.txt")
|
||||
},
|
||||
}
|
||||
.to_string(),
|
||||
r#"ssh crea "rm foo.txt""#
|
||||
);
|
||||
assert_eq!(
|
||||
ShellCommand::Ssh {
|
||||
address: "crea".to_string(),
|
||||
server_command: ServerCommand::Mv {
|
||||
source: PathBuf::from("foo"),
|
||||
destination: PathBuf::from("bar")
|
||||
}
|
||||
}
|
||||
.to_string(),
|
||||
r#"ssh crea "mv foo bar""#
|
||||
);
|
||||
assert_eq!(
|
||||
ShellCommand::Ssh {
|
||||
address: "crea".to_string(),
|
||||
server_command: ServerCommand::Execute {
|
||||
working_directory: PathBuf::from(".."),
|
||||
command: "sudo rm -rf *".into(),
|
||||
}
|
||||
}
|
||||
.to_string(),
|
||||
r#"ssh crea "cd ..; sudo rm -rf *""#
|
||||
);
|
||||
assert_eq!(
|
||||
ShellCommand::Scp {
|
||||
source: ScpParam {
|
||||
server: None,
|
||||
path: PathBuf::from("target/mssh")
|
||||
},
|
||||
destination: ScpParam {
|
||||
server: Some("crea".into()),
|
||||
path: PathBuf::from("/usr/bin")
|
||||
},
|
||||
}
|
||||
.to_string(),
|
||||
r#"scp target/mssh crea:/usr/bin"#
|
||||
);
|
||||
assert_eq!(ShellCommand::SshAgent.to_string(), r#"ssh-agent -s"#);
|
||||
assert_eq!(ShellCommand::ShhAdd.to_string(), r#"ssh-add"#);
|
||||
assert_eq!(
|
||||
ShellCommand::Editor(vec!["kate".into(), "-b".into(), "test.txt".into()]).to_string(),
|
||||
r#"kate -b test.txt"#
|
||||
);
|
||||
assert_eq!(
|
||||
ShellCommand::Execute {
|
||||
working_directory: PathBuf::from("/home/me/server"),
|
||||
command: vec!["java".into(), "-jar".into(), "paper.jar".into()]
|
||||
}
|
||||
.to_string(),
|
||||
r#"java -jar paper.jar"#
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
exit(1)
|
||||
Loading…
Reference in New Issue
Block a user