Refactor distant binary to yield software exit code when oneoff operation fails

pull/38/head
Chip Senkbeil 3 years ago
parent 5d0a352414
commit 8cdc9f271d
No known key found for this signature in database
GPG Key ID: 35EF1F8EC72A4131

19
Cargo.lock generated

@ -244,6 +244,7 @@ dependencies = [
"fork", "fork",
"lazy_static", "lazy_static",
"log", "log",
"predicates",
"rand", "rand",
"rstest", "rstest",
"serde_json", "serde_json",
@ -343,6 +344,15 @@ dependencies = [
"yansi", "yansi",
] ]
[[package]]
name = "float-cmp"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -675,6 +685,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "normalize-line-endings"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]] [[package]]
name = "ntapi" name = "ntapi"
version = "0.3.6" version = "0.3.6"
@ -797,8 +813,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c143348f141cc87aab5b950021bac6145d0e5ae754b0591de23244cee42c9308" checksum = "c143348f141cc87aab5b950021bac6145d0e5ae754b0591de23244cee42c9308"
dependencies = [ dependencies = [
"difflib", "difflib",
"float-cmp",
"itertools", "itertools",
"normalize-line-endings",
"predicates-core", "predicates-core",
"regex",
] ]
[[package]] [[package]]

@ -36,4 +36,5 @@ whoami = "1.1.2"
[dev-dependencies] [dev-dependencies]
assert_cmd = "2.0.0" assert_cmd = "2.0.0"
assert_fs = "1.0.3" assert_fs = "1.0.3"
predicates = "2.0.2"
rstest = "0.11.0" rstest = "0.11.0"

@ -19,7 +19,8 @@ pub enum ExitCode {
/// EX_UNAVAILABLE (69) - being used when IO error encountered where connection is problem /// EX_UNAVAILABLE (69) - being used when IO error encountered where connection is problem
Unavailable, Unavailable,
/// EX_SOFTWARE (70) - being used for internal errors that can occur like joining a task /// EX_SOFTWARE (70) - being used for when an action fails as well as for internal errors that
/// can occur like joining a task
Software, Software,
/// EX_OSERR (71) - being used when fork failed /// EX_OSERR (71) - being used when fork failed
@ -38,24 +39,44 @@ pub enum ExitCode {
Custom(i32), Custom(i32),
} }
impl ExitCode {
/// Convert into numeric exit code
pub fn to_i32(&self) -> i32 {
match *self {
Self::Usage => 64,
Self::DataErr => 65,
Self::NoInput => 66,
Self::NoHost => 68,
Self::Unavailable => 69,
Self::Software => 70,
Self::OsErr => 71,
Self::IoError => 74,
Self::TempFail => 75,
Self::Protocol => 76,
Self::Custom(x) => x,
}
}
}
impl From<ExitCode> for i32 {
fn from(code: ExitCode) -> Self {
code.to_i32()
}
}
/// Represents an error that can be converted into an exit code /// Represents an error that can be converted into an exit code
pub trait ExitCodeError: std::error::Error { pub trait ExitCodeError: std::error::Error {
fn to_exit_code(&self) -> ExitCode; fn to_exit_code(&self) -> ExitCode;
/// Indicates if the error message associated with this exit code error
/// should be printed, or if this is just used to reflect the exit code
/// when the process exits
fn is_silent(&self) -> bool {
false
}
fn to_i32(&self) -> i32 { fn to_i32(&self) -> i32 {
match self.to_exit_code() { self.to_exit_code().to_i32()
ExitCode::Usage => 64,
ExitCode::DataErr => 65,
ExitCode::NoInput => 66,
ExitCode::NoHost => 68,
ExitCode::Unavailable => 69,
ExitCode::Software => 70,
ExitCode::OsErr => 71,
ExitCode::IoError => 74,
ExitCode::TempFail => 75,
ExitCode::Protocol => 76,
ExitCode::Custom(x) => x,
}
} }
} }

@ -11,12 +11,16 @@ mod utils;
use log::error; use log::error;
pub use exit::{ExitCode, ExitCodeError};
/// Main entrypoint into the program /// Main entrypoint into the program
pub fn run() { pub fn run() {
let opt = opt::Opt::load(); let opt = opt::Opt::load();
let logger = init_logging(&opt.common); let logger = init_logging(&opt.common);
if let Err(x) = opt.subcommand.run(opt.common) { if let Err(x) = opt.subcommand.run(opt.common) {
error!("Exiting due to error: {}", x); if !x.is_silent() {
error!("Exiting due to error: {}", x);
}
logger.flush(); logger.flush();
logger.shutdown(); logger.shutdown();

@ -20,16 +20,22 @@ pub enum Error {
IoError(io::Error), IoError(io::Error),
#[display(fmt = "Non-interactive but no operation supplied")] #[display(fmt = "Non-interactive but no operation supplied")]
MissingOperation, MissingOperation,
OperationFailed,
RemoteProcessError(RemoteProcessError), RemoteProcessError(RemoteProcessError),
TransportError(TransportError), TransportError(TransportError),
} }
impl ExitCodeError for Error { impl ExitCodeError for Error {
fn is_silent(&self) -> bool {
matches!(self, Self::OperationFailed)
}
fn to_exit_code(&self) -> ExitCode { fn to_exit_code(&self) -> ExitCode {
match self { match self {
Self::BadProcessExit(x) => ExitCode::Custom(*x), Self::BadProcessExit(x) => ExitCode::Custom(*x),
Self::IoError(x) => x.to_exit_code(), Self::IoError(x) => x.to_exit_code(),
Self::MissingOperation => ExitCode::Usage, Self::MissingOperation => ExitCode::Usage,
Self::OperationFailed => ExitCode::Software,
Self::RemoteProcessError(x) => x.to_exit_code(), Self::RemoteProcessError(x) => x.to_exit_code(),
Self::TransportError(x) => x.to_exit_code(), Self::TransportError(x) => x.to_exit_code(),
} }
@ -155,8 +161,18 @@ where
let res = session let res = session
.send_timeout(Request::new(utils::new_tenant(), vec![data]), timeout) .send_timeout(Request::new(utils::new_tenant(), vec![data]), timeout)
.await?; .await?;
// If we have an error as our payload, then we want to reflect that in our
// exit code
let is_err = res.payload.iter().any(|d| d.is_error());
ResponseOut::new(cmd.format, res)?.print(); ResponseOut::new(cmd.format, res)?.print();
Ok(())
if is_err {
Err(Error::OperationFailed)
} else {
Ok(())
}
} }
// Interactive mode will send an optional first request and then continue // Interactive mode will send an optional first request and then continue

@ -1,5 +1,7 @@
use crate::fixtures::*; use crate::{fixtures::*, utils::FAILURE_LINE};
use assert_cmd::Command;
use assert_fs::prelude::*; use assert_fs::prelude::*;
use distant::ExitCode;
use distant_core::{ use distant_core::{
data::{Error, ErrorKind}, data::{Error, ErrorKind},
Response, ResponseData, Response, ResponseData,
@ -7,13 +9,13 @@ use distant_core::{
use rstest::*; use rstest::*;
#[rstest] #[rstest]
fn should_print_out_file_contents(ctx: &'_ DistantServerCtx) { fn should_print_out_file_contents(mut action_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap(); let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file"); let file = temp.child("test-file");
file.write_str("some\ntext\ncontent").unwrap(); file.write_str("some\ntext\ncontent").unwrap();
// distant action file-read {path} // distant action file-read {path}
ctx.new_cmd("action") action_cmd
.args(&["file-read", file.to_str().unwrap()]) .args(&["file-read", file.to_str().unwrap()])
.assert() .assert()
.success() .success()
@ -22,14 +24,13 @@ fn should_print_out_file_contents(ctx: &'_ DistantServerCtx) {
} }
#[rstest] #[rstest]
fn should_support_json_output(ctx: &'_ DistantServerCtx) { fn should_support_json_output(mut action_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap(); let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("test-file"); let file = temp.child("test-file");
file.write_str("some\ntext\ncontent").unwrap(); file.write_str("some\ntext\ncontent").unwrap();
// distant action --format json file-read {path} // distant action --format json file-read {path}
let cmd = ctx let cmd = action_cmd
.new_cmd("action")
.args(&["--format", "json"]) .args(&["--format", "json"])
.args(&["file-read", file.to_str().unwrap()]) .args(&["file-read", file.to_str().unwrap()])
.assert() .assert()
@ -46,17 +47,30 @@ fn should_support_json_output(ctx: &'_ DistantServerCtx) {
} }
#[rstest] #[rstest]
fn yield_an_error_when_fails(ctx: &'_ DistantServerCtx) { fn yield_an_error_when_fails(mut action_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("missing-file");
// distant action file-read {path}
action_cmd
.args(&["file-read", file.to_str().unwrap()])
.assert()
.code(ExitCode::Software.to_i32())
.stdout("")
.stderr(FAILURE_LINE.clone());
}
#[rstest]
fn should_support_json_output_for_error(mut action_cmd: Command) {
let temp = assert_fs::TempDir::new().unwrap(); let temp = assert_fs::TempDir::new().unwrap();
let file = temp.child("missing-file"); let file = temp.child("missing-file");
// distant action --format json file-read {path} // distant action --format json file-read {path}
let cmd = ctx let cmd = action_cmd
.new_cmd("action")
.args(&["--format", "json"]) .args(&["--format", "json"])
.args(&["file-read", file.to_str().unwrap()]) .args(&["file-read", file.to_str().unwrap()])
.assert() .assert()
.success() .code(ExitCode::Software.to_i32())
.stderr(""); .stderr("");
let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap(); let res: Response = serde_json::from_slice(&cmd.get_output().stdout).unwrap();

@ -1,2 +1,3 @@
mod action; mod action;
mod fixtures; mod fixtures;
mod utils;

@ -1,12 +1,9 @@
use assert_cmd::Command; use assert_cmd::Command;
use distant_core::*; use distant_core::*;
use rstest::*; use rstest::*;
use std::{ffi::OsStr, net::SocketAddr, thread, time::Duration}; use std::{ffi::OsStr, net::SocketAddr, thread};
use tokio::{runtime::Runtime, sync::mpsc}; use tokio::{runtime::Runtime, sync::mpsc};
/// Timeout to wait for a command to complete
const TIMEOUT_SECS: u64 = 10;
/// Context for some listening distant server /// Context for some listening distant server
pub struct DistantServerCtx { pub struct DistantServerCtx {
pub addr: SocketAddr, pub addr: SocketAddr,
@ -60,19 +57,11 @@ impl DistantServerCtx {
/// configured with an environment that can talk to a remote distant server /// configured with an environment that can talk to a remote distant server
pub fn new_cmd(&self, subcommand: impl AsRef<OsStr>) -> Command { pub fn new_cmd(&self, subcommand: impl AsRef<OsStr>) -> Command {
let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); let mut cmd = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap();
println!("DISTANT_HOST = {}", self.addr.ip());
println!("DISTANT_PORT = {}", self.addr.port());
println!("DISTANT_AUTH_KEY = {}", self.auth_key);
// NOTE: We define a command that has a timeout of 10s because the handshake
// involved in a non-release test can take several seconds
cmd.arg(subcommand) cmd.arg(subcommand)
.args(&["--session", "environment"]) .args(&["--session", "environment"])
.env("DISTANT_HOST", self.addr.ip().to_string()) .env("DISTANT_HOST", self.addr.ip().to_string())
.env("DISTANT_PORT", self.addr.port().to_string()) .env("DISTANT_PORT", self.addr.port().to_string())
.env("DISTANT_AUTH_KEY", self.auth_key.as_str()) .env("DISTANT_AUTH_KEY", self.auth_key.as_str());
.timeout(Duration::from_secs(TIMEOUT_SECS));
cmd cmd
} }
} }
@ -86,9 +75,19 @@ impl Drop for DistantServerCtx {
#[fixture] #[fixture]
pub fn ctx() -> &'static DistantServerCtx { pub fn ctx() -> &'static DistantServerCtx {
&DISTANT_SERVER_CTX lazy_static::lazy_static! {
static ref CTX: DistantServerCtx = DistantServerCtx::initialize();
}
&CTX
} }
lazy_static::lazy_static! { #[fixture]
static ref DISTANT_SERVER_CTX: DistantServerCtx = DistantServerCtx::initialize(); pub fn action_cmd(ctx: &'_ DistantServerCtx) -> Command {
ctx.new_cmd("action")
}
#[fixture]
pub fn lsp_cmd(ctx: &'_ DistantServerCtx) -> Command {
ctx.new_cmd("lsp")
} }

@ -0,0 +1,7 @@
use predicates::prelude::*;
lazy_static::lazy_static! {
/// Predicate that checks for a single line that is a failure
pub static ref FAILURE_LINE: predicates::str::RegexPredicate =
predicate::str::is_match(r"^Failed \(.*\): '.*'\.\n$").unwrap();
}
Loading…
Cancel
Save