Skip to content

Commit

Permalink
Merge branch 'master' into pub-fn-to-command
Browse files Browse the repository at this point in the history
matklad authored Dec 23, 2024
2 parents 30fdd04 + 93fcf0e commit 790df7a
Showing 14 changed files with 782 additions and 625 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -2,7 +2,10 @@

## Unreleased

## 0.2.7

- MSRV is raised to 1.63.0
- Avoid using non-existant cfg in macros

## 0.2.6

12 changes: 9 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@
name = "xshell"
description = "Utilities for quick shell scripting in Rust"
categories = ["development-tools::build-utils", "filesystem"]
version = "0.2.6" # also update xshell-macros/Cargo.toml and CHANGELOG.md
version = "0.2.7" # also update xshell-macros/Cargo.toml and CHANGELOG.md
license = "MIT OR Apache-2.0"
repository = "https://github.com/matklad/xshell"
authors = ["Aleksey Kladov <aleksey.kladov@gmail.com>"]
authors = ["Alex Kladov <aleksey.kladov@gmail.com>"]
edition = "2021"
rust-version = "1.63"

@@ -14,7 +14,13 @@ exclude = [".github/", "bors.toml", "rustfmt.toml", "cbench", "mock_bin/"]
[workspace]

[dependencies]
xshell-macros = { version = "=0.2.6", path = "./xshell-macros" }
xshell-macros = { version = "=0.2.7", path = "./xshell-macros" }

[target.'cfg(unix)'.dependencies]
libc = "0.2.155"

[target.'cfg(windows)'.dependencies]
miow = "0.6.0"

[dev-dependencies]
anyhow = "1.0.56"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ fn main() -> anyhow::Result<()> {
let user = "matklad";
let repo = "xshell";
cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?;
sh.change_dir(repo);
sh.set_current_dir(repo);

let test_args = ["-Zunstable-options", "--report-time"];
cmd!(sh, "cargo test -- {test_args...}").run()?;
@@ -29,7 +29,7 @@ fn main() -> anyhow::Result<()> {

cmd!(sh, "git tag {version}").run()?;

let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") };
let dry_run = if sh.env_var("CI").is_ok() { None } else { Some("--dry-run") };
cmd!(sh, "cargo publish {dry_run...}").run()?;

Ok(())
16 changes: 8 additions & 8 deletions examples/ci.rs
Original file line number Diff line number Diff line change
@@ -27,18 +27,18 @@ fn test(sh: &Shell) -> Result<()> {
// running this, we've already compiled a bunch of stuff. Originally we tried to `rm -rf
// .target`, but we also observed weird SIGKILL: 9 errors on mac. Perhaps its our self-removal?
// Let's scope it only to linux (windows won't work, bc one can not remove oneself there).
if cfg!(linux) {
if cfg!(unix) {
sh.remove_path("./target")?;
}

{
let _s = Section::new("BUILD");
cmd!(sh, "cargo test --workspace --no-run").run()?;
cmd!(sh, "cargo test --workspace --no-run").run_echo()?;
}

{
let _s = Section::new("TEST");
cmd!(sh, "cargo test --workspace").run()?;
cmd!(sh, "cargo test --workspace").run_echo()?;
}
Ok(())
}
@@ -57,11 +57,11 @@ fn publish(sh: &Shell) -> Result<()> {

if current_branch == "master" && !tag_exists {
// Could also just use `CARGO_REGISTRY_TOKEN` environmental variable.
let token = sh.var("CRATES_IO_TOKEN").unwrap_or("DUMMY_TOKEN".to_string());
cmd!(sh, "git tag v{version}").run()?;
cmd!(sh, "cargo publish --token {token} --package xshell-macros").run()?;
cmd!(sh, "cargo publish --token {token} --package xshell").run()?;
cmd!(sh, "git push --tags").run()?;
let token = sh.env_var("CRATES_IO_TOKEN").unwrap_or("DUMMY_TOKEN".to_string());
cmd!(sh, "git tag v{version}").run_echo()?;
cmd!(sh, "cargo publish --token {token} --package xshell-macros").run_echo()?;
cmd!(sh, "cargo publish --token {token} --package xshell").run_echo()?;
cmd!(sh, "git push --tags").run_echo()?;
}
Ok(())
}
6 changes: 3 additions & 3 deletions examples/clone_and_publish.rs
Original file line number Diff line number Diff line change
@@ -2,12 +2,12 @@
use xshell::{cmd, Shell};

fn main() -> anyhow::Result<()> {
let sh = Shell::new()?;
let mut sh = Shell::new()?;

let user = "matklad";
let repo = "xshell";
cmd!(sh, "git clone https://github.com/{user}/{repo}.git").run()?;
sh.change_dir(repo);
sh.set_current_dir(repo);

let test_args = ["-Zunstable-options", "--report-time"];
cmd!(sh, "cargo test -- {test_args...}").run()?;
@@ -21,7 +21,7 @@ fn main() -> anyhow::Result<()> {

cmd!(sh, "git tag {version}").run()?;

let dry_run = if sh.var("CI").is_ok() { None } else { Some("--dry-run") };
let dry_run = if sh.env_var("CI").is_ok() { None } else { Some("--dry-run") };
cmd!(sh, "cargo publish {dry_run...}").run()?;

Ok(())
148 changes: 101 additions & 47 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
use std::{env, ffi::OsString, fmt, io, path::PathBuf, process::ExitStatus, string::FromUtf8Error};

use crate::{Cmd, CmdData};
use std::{
env,
ffi::OsString,
fmt, io,
path::{Path, PathBuf},
process::ExitStatus,
string::FromUtf8Error,
sync::Arc,
};

use crate::{Cmd, STREAM_SUFFIX_SIZE};

/// `Result` from std, with the error type defaulting to xshell's [`Error`].
pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -12,7 +20,7 @@ pub struct Error {

/// Note: this is intentionally not public.
enum ErrorKind {
CurrentDir { err: io::Error, path: Option<PathBuf> },
CurrentDir { err: io::Error, path: Option<Arc<Path>> },
Var { err: env::VarError, var: OsString },
ReadFile { err: io::Error, path: PathBuf },
ReadDir { err: io::Error, path: PathBuf },
@@ -21,10 +29,7 @@ enum ErrorKind {
HardLink { err: io::Error, src: PathBuf, dst: PathBuf },
CreateDir { err: io::Error, path: PathBuf },
RemovePath { err: io::Error, path: PathBuf },
CmdStatus { cmd: CmdData, status: ExitStatus },
CmdIo { err: io::Error, cmd: CmdData },
CmdUtf8 { err: FromUtf8Error, cmd: CmdData },
CmdStdin { err: io::Error, cmd: CmdData },
Cmd(CmdError),
}

impl From<ErrorKind> for Error {
@@ -34,6 +39,20 @@ impl From<ErrorKind> for Error {
}
}

struct CmdError {
cmd: Cmd,
kind: CmdErrorKind,
stdout: Vec<u8>,
stderr: Vec<u8>,
}

pub(crate) enum CmdErrorKind {
Io(io::Error),
Utf8(FromUtf8Error),
Status(ExitStatus),
Timeout,
}

impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &*self.kind {
@@ -76,48 +95,74 @@ impl fmt::Display for Error {
let path = path.display();
write!(f, "failed to remove path `{path}`: {err}")
}
ErrorKind::CmdStatus { cmd, status } => match status.code() {
Some(code) => write!(f, "command exited with non-zero code `{cmd}`: {code}"),
ErrorKind::Cmd(cmd) => fmt::Display::fmt(cmd, f),
}?;
Ok(())
}
}

impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl std::error::Error for Error {}

impl fmt::Display for CmdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let nl = if (self.stdout.len() > 0 || self.stderr.len() > 0)
&& !matches!(self.kind, CmdErrorKind::Utf8(_))
{
"\n"
} else {
""
};
let cmd = &self.cmd;
match &self.kind {
CmdErrorKind::Status(status) => match status.code() {
Some(code) => write!(f, "command exited with non-zero code `{cmd}`: {code}{nl}")?,
#[cfg(unix)]
None => {
use std::os::unix::process::ExitStatusExt;
match status.signal() {
Some(sig) => write!(f, "command was terminated by a signal `{cmd}`: {sig}"),
None => write!(f, "command was terminated by a signal `{cmd}`"),
Some(sig) => {
write!(f, "command was terminated by a signal `{cmd}`: {sig}{nl}")?
}
None => write!(f, "command was terminated by a signal `{cmd}`{nl}")?,
}
}
#[cfg(not(unix))]
None => write!(f, "command was terminated by a signal `{cmd}`"),
None => write!(f, "command was terminated by a signal `{cmd}`{nl}"),
},
ErrorKind::CmdIo { err, cmd } => {
CmdErrorKind::Utf8(err) => {
write!(f, "command produced invalid utf-8 `{cmd}`: {err}")?;
return Ok(());
}
CmdErrorKind::Io(err) => {
if err.kind() == io::ErrorKind::NotFound {
let prog = cmd.prog.display();
write!(f, "command not found: `{prog}`")
let prog = self.cmd.prog.as_path().display();
write!(f, "command not found: `{prog}`{nl}")?;
} else {
write!(f, "io error when running command `{cmd}`: {err}")
write!(f, "io error when running command `{cmd}`: {err}{nl}")?;
}
}
ErrorKind::CmdUtf8 { err, cmd } => {
write!(f, "failed to decode output of command `{cmd}`: {err}")
}
ErrorKind::CmdStdin { err, cmd } => {
write!(f, "failed to write to stdin of command `{cmd}`: {err}")
CmdErrorKind::Timeout => {
write!(f, "command timed out `{cmd}`{nl}")?;
}
}?;
}
if self.stdout.len() > 0 {
write!(f, "stdout suffix\n:{}\n", String::from_utf8_lossy(&self.stdout))?;
}
if self.stderr.len() > 0 {
write!(f, "stderr suffix:\n:{}\n", String::from_utf8_lossy(&self.stderr))?;
}
Ok(())
}
}

impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(self, f)
}
}
impl std::error::Error for Error {}

/// `pub(crate)` constructors, visible only in this crate.
impl Error {
pub(crate) fn new_current_dir(err: io::Error, path: Option<PathBuf>) -> Error {
pub(crate) fn new_current_dir(err: io::Error, path: Option<Arc<Path>>) -> Error {
ErrorKind::CurrentDir { err, path }.into()
}

@@ -153,24 +198,33 @@ impl Error {
ErrorKind::RemovePath { err, path }.into()
}

pub(crate) fn new_cmd_status(cmd: &Cmd<'_>, status: ExitStatus) -> Error {
let cmd = cmd.data.clone();
ErrorKind::CmdStatus { cmd, status }.into()
}

pub(crate) fn new_cmd_io(cmd: &Cmd<'_>, err: io::Error) -> Error {
let cmd = cmd.data.clone();
ErrorKind::CmdIo { err, cmd }.into()
}
pub(crate) fn new_cmd(
cmd: &Cmd,
kind: CmdErrorKind,
mut stdout: Vec<u8>,
mut stderr: Vec<u8>,
) -> Error {
// Try to determine whether the command failed because the current
// directory does not exist. Return an appropriate error in such a
// case.
if let CmdErrorKind::Io(err) = &kind {
if err.kind() == io::ErrorKind::NotFound {
if let Err(err) = cmd.sh.cwd.metadata() {
return Error::new_current_dir(err, Some(cmd.sh.cwd.clone()));
}
}
}

pub(crate) fn new_cmd_utf8(cmd: &Cmd<'_>, err: FromUtf8Error) -> Error {
let cmd = cmd.data.clone();
ErrorKind::CmdUtf8 { err, cmd }.into()
}
fn trim(xs: &mut Vec<u8>, size: usize) {
if xs.len() > size {
xs.drain(..xs.len() - size);
}
}

pub(crate) fn new_cmd_stdin(cmd: &Cmd<'_>, err: io::Error) -> Error {
let cmd = cmd.data.clone();
ErrorKind::CmdStdin { err, cmd }.into()
let cmd = cmd.clone();
trim(&mut stdout, STREAM_SUFFIX_SIZE);
trim(&mut stderr, STREAM_SUFFIX_SIZE);
ErrorKind::Cmd(CmdError { cmd, kind, stdout, stderr }).into()
}
}

160 changes: 160 additions & 0 deletions src/exec.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//! Executes the process, feeding it stdin, reading stdout/stderr (up to the specified limit), and
//! imposing a deadline.
//!
//! This really is quite unhappy code, wasting whopping four threads for the task _and_ including a
//! sleepy loop! This is not system programming, just a pile of work-around. What is my excuse?
//!
//! The _right_ way to do this is of course by using evened syscalls --- concurrently await stream
//! io, timeout, and process termination. The _first_ two kinda-sorta solvable, see the `read2`
//! module in Cargo. For unix, we through fds into a epoll via libc, for windows we use completion
//! ports via miow. That's some ugly platform-specific code and two dependencies, but doable.
//!
//! Both poll and completion ports naturally have a timeout, so that's doable as well. However,
//! tying process termination into the same epoll is not really possible. One can use pidfd's on
//! Linux, but that's even _more_ platform specific code, and there are other UNIXes.
//!
//! Given that, if I were to use evented IO, I'd have to pull dependencies, write a bunch of
//! platform-specific glue code _and_ write some from scratch things for waiting, I decided to stick
//! to blocking APIs.
//!
//! This should be easy, right? Just burn a thread per asynchronous operation! Well, the `wait`
//! strikes again! Both `.kill` and `.wait` require `&mut Child`, so you can't wait on the main
//! thread, and `.kill` from the timeout thread. One can think that that's just deficiency of Rust
//! API, but, now, this is again just UNIX. Both kill and wait operate on pids, and a pid can be
//! re-used immediately after wait. As far as I understand, this is a race condition you can't lock
//! your way out of. Hence the sleepy loop in wait_deadline.
use std::{
collections::VecDeque,
io::{self, Read, Write},
process::{Child, ExitStatus, Stdio},
time::{Duration, Instant},
};

#[derive(Default)]
pub(crate) struct ExecResult {
pub(crate) stdout: Vec<u8>,
pub(crate) stderr: Vec<u8>,
pub(crate) status: Option<ExitStatus>,
pub(crate) error: Option<io::Error>,
}

pub(crate) fn wait_deadline(
child: &mut Child,
deadline: Option<Instant>,
) -> io::Result<ExitStatus> {
let Some(deadline) = deadline else {
return child.wait();
};

let mut sleep_ms = 1;
let sleep_ms_max = 64;
loop {
match child.try_wait()? {
Some(status) => return Ok(status),
None => {}
}
if Instant::now() > deadline {
let _ = child.kill();
let _ = child.wait();
return Err(io::ErrorKind::TimedOut.into());
}
std::thread::sleep(Duration::from_millis(sleep_ms));
sleep_ms = std::cmp::min(sleep_ms * 2, sleep_ms_max);
}
}

pub(crate) fn exec(
mut command: std::process::Command,
stdin_contents: Option<&[u8]>,
stdout_limit: Option<usize>,
stderr_limit: Option<usize>,
deadline: Option<Instant>,
) -> ExecResult {
let mut result = ExecResult::default();
command.stdin(if stdin_contents.is_some() { Stdio::inherit() } else { Stdio::null() });
command.stdout(Stdio::piped());
command.stdout(Stdio::piped());
let mut child = match command.spawn() {
Ok(it) => it,
Err(err) => {
result.error = Some(err);
return result;
}
};

let stdin = child.stdin.take();
let mut in_error = Ok(());

let mut stdout = child.stdout.take().unwrap();
let mut out_deque = VecDeque::new();
let mut out_error = Ok(());

let mut stderr = child.stderr.take().unwrap();
let mut err_deque = VecDeque::new();
let mut err_error = Ok(());

let status = std::thread::scope(|scope| {
if let Some(stdin_contents) = stdin_contents {
scope.spawn(|| in_error = stdin.unwrap().write_all(stdin_contents));
}
scope.spawn(|| {
out_error = (|| {
let mut buffer = [0u8; 4096];
loop {
let n = stdout.read(&mut buffer)?;
if n == 0 {
return Ok(());
}
out_deque.extend(buffer[0..n].iter().copied());
let excess = out_deque.len().saturating_sub(stdout_limit.unwrap_or(usize::MAX));
if excess > 0 {
out_deque.drain(..excess);
}
}
})()
});
scope.spawn(|| {
err_error = (|| {
let mut buffer = [0u8; 4096];
loop {
let n = stderr.read(&mut buffer)?;
if n == 0 {
return Ok(());
}
err_deque.extend(buffer[0..n].iter().copied());
let excess = err_deque.len().saturating_sub(stderr_limit.unwrap_or(usize::MAX));
if excess > 0 {
err_deque.drain(..excess);
}
}
})()
});

wait_deadline(&mut child, deadline)
});

if let Err(err) = err_error {
result.error = err;
}

if let Err(err) = out_error {
result.error = err;
}

if let Err(err) = in_error {
if err.kind() != io::ErrorKind::BrokenPipe {
result.error = Some(err);
}
}

match status {
Ok(status) => result.status = Some(status),
Err(err) => result.error = Some(err),
}

result.stdout = out_deque.into();
result.stderr = err_deque.into();

result
}
891 changes: 435 additions & 456 deletions src/lib.rs

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions tests/compile_time.rs
Original file line number Diff line number Diff line change
@@ -4,26 +4,26 @@ use xshell::{cmd, Shell};

#[test]
fn fixed_cost_compile_times() {
let sh = Shell::new().unwrap();
let mut sh = Shell::new().unwrap();

let _p = sh.push_dir("tests/data");
let baseline = compile_bench(&sh, "baseline");
let _p = sh.set_current_dir("tests/data");
let baseline = compile_bench(&mut sh, "baseline");
let _ducted = compile_bench(&sh, "ducted");
let xshelled = compile_bench(&sh, "xshelled");
let xshelled = compile_bench(&mut sh, "xshelled");
let ratio = (xshelled.as_millis() as f64) / (baseline.as_millis() as f64);
assert!(1.0 < ratio && ratio < 10.0);

fn compile_bench(sh: &Shell, name: &str) -> Duration {
let _p = sh.push_dir(name);
let sh = sh.with_current_dir(name);
let cargo_build = cmd!(sh, "cargo build -q");
cargo_build.read().unwrap();
cargo_build.run().unwrap();

let n = 5;
let mut times = Vec::new();
for _ in 0..n {
sh.remove_path("./target").unwrap();
let start = Instant::now();
cargo_build.read().unwrap();
cargo_build.run().unwrap();
let elapsed = start.elapsed();
times.push(elapsed);
}
6 changes: 3 additions & 3 deletions tests/it/compile_failures.rs
Original file line number Diff line number Diff line change
@@ -2,10 +2,10 @@ use xshell::{cmd, Shell};

#[track_caller]
fn check(code: &str, err_msg: &str) {
let sh = Shell::new().unwrap();
let xshell_dir = sh.current_dir();
let mut sh = Shell::new().unwrap();
let xshell_dir = sh.current_dir().to_owned();
let temp_dir = sh.create_temp_dir().unwrap();
sh.change_dir(temp_dir.path());
sh.set_current_dir(temp_dir.path());

let manifest = format!(
r#"
12 changes: 6 additions & 6 deletions tests/it/env.rs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ use crate::setup;

#[test]
fn test_env() {
let sh = setup();
let mut sh = setup();

let v1 = "xshell_test_123";
let v2 = "xshell_test_456";
@@ -34,8 +34,8 @@ fn test_env() {
);
}

let _g1 = sh.push_env(v1, "foobar");
let _g2 = sh.push_env(v2, "quark");
let _g1 = sh.set_env_var(v1, "foobar");
let _g2 = sh.set_env_var(v2, "quark");

assert_env(cmd!(sh, "xecho -$ {v1} {v2}"), &[(v1, Some("foobar")), (v2, Some("quark"))]);
assert_env(cmd!(cloned_sh, "xecho -$ {v1} {v2}"), &[(v1, None), (v2, None)]);
@@ -58,7 +58,7 @@ fn test_env() {

#[test]
fn test_env_clear() {
let sh = setup();
let mut sh = setup();

let v1 = "xshell_test_123";
let v2 = "xshell_test_456";
@@ -79,8 +79,8 @@ fn test_env_clear() {
&[(v1, Some("789")), (v2, None)],
);

let _g1 = sh.push_env(v1, "foobar");
let _g2 = sh.push_env(v2, "quark");
let _g1 = sh.set_env_var(v1, "foobar");
let _g2 = sh.set_env_var(v2, "quark");

assert_env(cmd!(sh, "{xecho} -$ {v1} {v2}").env_clear(), &[(v1, None), (v2, None)]);
assert_env(
129 changes: 42 additions & 87 deletions tests/it/main.rs
Original file line number Diff line number Diff line change
@@ -9,18 +9,17 @@ use xshell::{cmd, Shell};
fn setup() -> Shell {
static ONCE: std::sync::Once = std::sync::Once::new();

let sh = Shell::new().unwrap();
let mut sh = Shell::new().unwrap();
let xecho_src = sh.current_dir().join("./tests/data/xecho.rs");
let target_dir = sh.current_dir().join("./target/");

ONCE.call_once(|| {
cmd!(sh, "rustc {xecho_src} --out-dir {target_dir}")
.quiet()
.run()
.unwrap_or_else(|err| panic!("failed to install binaries from mock_bin: {}", err))
});

sh.set_var("PATH", target_dir);
sh.set_env_var("PATH", target_dir);
sh
}

@@ -89,7 +88,7 @@ fn program_concatenation() {
let sh = setup();

let ho = "ho";
let output = cmd!(sh, "xec{ho} hello").read().unwrap();
let output = dbg!(cmd!(sh, "xec{ho} hello")).read().unwrap();
assert_eq!(output, "hello");
}

@@ -247,11 +246,11 @@ fn test_push_dir() {

let d1 = sh.current_dir();
{
let _p = sh.push_dir("xshell-macros");
let sh = sh.with_current_dir("xshell-macros");
let d2 = sh.current_dir();
assert_eq!(d2, d1.join("xshell-macros"));
{
let _p = sh.push_dir("src");
let sh = sh.with_current_dir("src");
let d3 = sh.current_dir();
assert_eq!(d3, d1.join("xshell-macros/src"));
}
@@ -268,10 +267,10 @@ fn test_push_and_change_dir() {

let d1 = sh.current_dir();
{
let _p = sh.push_dir("xshell-macros");
let mut sh = sh.with_current_dir("xshell-macros");
let d2 = sh.current_dir();
assert_eq!(d2, d1.join("xshell-macros"));
sh.change_dir("src");
sh.set_current_dir("src");
let d3 = sh.current_dir();
assert_eq!(d3, d1.join("xshell-macros/src"));
}
@@ -285,98 +284,54 @@ fn push_dir_parent_dir() {

let current = sh.current_dir();
let dirname = current.file_name().unwrap();
let _d = sh.push_dir("..");
let _d = sh.push_dir(dirname);
let sh = sh.with_current_dir("..");
let sh = sh.with_current_dir(dirname);
assert_eq!(sh.current_dir().canonicalize().unwrap(), current.canonicalize().unwrap());
}

const VAR: &str = "SPICA";

#[test]
fn test_push_env() {
fn test_subshells_env() {
let sh = setup();

let e1 = sh.var_os(VAR);
let e1 = sh.env_var_os(VAR);
{
let _e = sh.push_env(VAR, "1");
let e2 = sh.var_os(VAR);
assert_eq!(e2, Some("1".into()));
let mut sh = sh.clone();
sh.set_env_var(VAR, "1");
let e2 = sh.env_var_os(VAR);
assert_eq!(e2.as_deref(), Some("1".as_ref()));
{
let _e = sh.push_env(VAR, "2");
let e3 = sh.var_os(VAR);
assert_eq!(e3, Some("2".into()));
let mut sh = sh.clone();
let _e = sh.set_env_var(VAR, "2");
let e3 = sh.env_var_os(VAR);
assert_eq!(e3.as_deref(), Some("2".as_ref()));
}
let e4 = sh.var_os(VAR);
let e4 = sh.env_var_os(VAR);
assert_eq!(e4, e2);
}
let e5 = sh.var_os(VAR);
let e5 = sh.env_var_os(VAR);
assert_eq!(e5, e1);
}

#[test]
fn test_push_env_clone() {
fn test_push_env_and_set_env_var() {
let sh = setup();

assert!(sh.var_os(VAR).is_none());
let guard = sh.push_env(VAR, "1");
let cloned = sh.clone();
drop(guard);
assert_eq!(sh.var_os(VAR), None);
assert_eq!(cloned.var_os(VAR), Some("1".into()));
}

#[test]
fn test_push_env_and_set_var() {
let sh = setup();

let e1 = sh.var_os(VAR);
let e1 = sh.env_var_os(VAR);
{
let _e = sh.push_env(VAR, "1");
let e2 = sh.var_os(VAR);
assert_eq!(e2, Some("1".into()));
let _e = sh.set_var(VAR, "2");
let e3 = sh.var_os(VAR);
assert_eq!(e3, Some("2".into()));
let mut sh = sh.clone();
sh.set_env_var(VAR, "1");
let e2 = sh.env_var_os(VAR);
assert_eq!(e2.as_deref(), Some("1".as_ref()));
sh.set_env_var(VAR, "2");
let e3 = sh.env_var_os(VAR);
assert_eq!(e3.as_deref(), Some("2".as_ref()));
}
let e5 = sh.var_os(VAR);
let e5 = sh.env_var_os(VAR);
assert_eq!(e5, e1);
}

#[test]
fn output_with_ignore() {
let sh = setup();

let output = cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().output().unwrap();
assert_eq!(output.stderr, b"hello world!\n");
assert_eq!(output.stdout, b"");

let output = cmd!(sh, "xecho -e 'hello world!'").ignore_stderr().output().unwrap();
assert_eq!(output.stdout, b"hello world!\n");
assert_eq!(output.stderr, b"");

let output =
cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().ignore_stderr().output().unwrap();
assert_eq!(output.stdout, b"");
assert_eq!(output.stderr, b"");
}

#[test]
fn test_read_with_ignore() {
let sh = setup();

let stdout = cmd!(sh, "xecho -e 'hello world'").ignore_stdout().read().unwrap();
assert!(stdout.is_empty());

let stderr = cmd!(sh, "xecho -e 'hello world'").ignore_stderr().read_stderr().unwrap();
assert!(stderr.is_empty());

let stdout = cmd!(sh, "xecho -e 'hello world!'").ignore_stderr().read().unwrap();
assert_eq!(stdout, "hello world!");

let stderr = cmd!(sh, "xecho -e 'hello world!'").ignore_stdout().read_stderr().unwrap();
assert_eq!(stderr, "hello world!");
}

#[test]
fn test_copy_file() {
let sh = setup();
@@ -391,10 +346,10 @@ fn test_copy_file() {
sh.write_file(&foo, "hello world").unwrap();
sh.create_dir(&dir).unwrap();

sh.copy_file(&foo, &bar).unwrap();
sh.copy_file_to_path(&foo, &bar).unwrap();
assert_eq!(sh.read_file(&bar).unwrap(), "hello world");

sh.copy_file(&foo, &dir).unwrap();
sh.copy_file_to_dir(&foo, &dir).unwrap();
assert_eq!(sh.read_file(&dir.join("foo.txt")).unwrap(), "hello world");
assert!(path.exists());
}
@@ -403,16 +358,16 @@ fn test_copy_file() {

#[test]
fn test_exists() {
let sh = setup();
let mut sh = setup();
let tmp = sh.create_temp_dir().unwrap();
let _d = sh.change_dir(tmp.path());
let _d = sh.set_current_dir(tmp.path());
assert!(!sh.path_exists("foo.txt"));
sh.write_file("foo.txt", "foo").unwrap();
assert!(sh.path_exists("foo.txt"));
assert!(!sh.path_exists("bar"));
sh.create_dir("bar").unwrap();
assert!(sh.path_exists("bar"));
let _d = sh.change_dir("bar");
let _d = sh.set_current_dir("bar");
assert!(!sh.path_exists("quz.rs"));
sh.write_file("quz.rs", "fn main () {}").unwrap();
assert!(sh.path_exists("quz.rs"));
@@ -432,10 +387,10 @@ fn write_makes_directory() {

#[test]
fn test_remove_path() {
let sh = setup();
let mut sh = setup();

let tempdir = sh.create_temp_dir().unwrap();
sh.change_dir(tempdir.path());
sh.set_current_dir(tempdir.path());
sh.write_file(Path::new("a/b/c.rs"), "fn main() {}").unwrap();
assert!(tempdir.path().join("a/b/c.rs").exists());
sh.remove_path("./a").unwrap();
@@ -453,15 +408,15 @@ fn recovers_from_panics() {
let orig = sh.current_dir();

std::panic::catch_unwind(|| {
let _p = sh.push_dir(&tempdir);
let sh = sh.with_current_dir(&tempdir);
assert_eq!(sh.current_dir(), tempdir);
std::panic::resume_unwind(Box::new(()));
})
.unwrap_err();

assert_eq!(sh.current_dir(), orig);
{
let _p = sh.push_dir(&tempdir);
let sh = sh.with_current_dir(&tempdir);
assert_eq!(sh.current_dir(), tempdir);
}
}
@@ -477,8 +432,8 @@ fn string_escapes() {

#[test]
fn nonexistent_current_directory() {
let sh = setup();
sh.change_dir("nonexistent");
let mut sh = setup();
sh.set_current_dir("nonexistent");
let err = cmd!(sh, "ls").run().unwrap_err();
let message = err.to_string();
if cfg!(unix) {
4 changes: 2 additions & 2 deletions tests/it/tidy.rs
Original file line number Diff line number Diff line change
@@ -27,8 +27,8 @@ fn formatting() {

#[test]
fn current_version_in_changelog() {
let sh = Shell::new().unwrap();
let _p = sh.push_dir(env!("CARGO_MANIFEST_DIR"));
let mut sh = Shell::new().unwrap();
sh.set_current_dir(env!("CARGO_MANIFEST_DIR"));
let changelog = sh.read_file("CHANGELOG.md").unwrap();
let current_version_header = format!("## {}", env!("CARGO_PKG_VERSION"));
assert_eq!(changelog.lines().filter(|&line| line == current_version_header).count(), 1);
2 changes: 1 addition & 1 deletion xshell-macros/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "xshell-macros"
description = "Private implementation detail of xshell crate"
version = "0.2.6"
version = "0.2.7"
license = "MIT OR Apache-2.0"
repository = "https://github.com/matklad/xshell"
authors = ["Aleksey Kladov <aleksey.kladov@gmail.com>"]

0 comments on commit 790df7a

Please sign in to comment.