Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

run-make: arm command wrappers with drop bombs #125752

Merged
merged 6 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3452,7 +3452,7 @@ dependencies = [

[[package]]
name = "run_make_support"
version = "0.0.0"
version = "0.1.0"
dependencies = [
"gimli 0.28.1",
"object 0.34.0",
Expand Down
67 changes: 67 additions & 0 deletions src/tools/run-make-support/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Changelog

All notable changes to the `run_make_support` library should be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the support
library should adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) even if it's
not intended for public consumption (it's moreso to help internally, to help test writers track
changes to the support library).

This support library will probably never reach 1.0. Please bump the minor version in `Cargo.toml` if
you make any breaking changes or other significant changes, or bump the patch version for bug fixes.

## [0.1.0] - 2024-06-09

### Changed

- Use *drop bombs* to enforce that commands are executed; a command invocation will panic if it is
constructed but never executed. Execution methods `Command::{run, run_fail}` will defuse the drop
bomb.
- Added `Command` helpers that forward to `std::process::Command` counterparts.

### Removed

- The `env_var` method which was incorrectly named and is `env_clear` underneath and is a footgun
from `impl_common_helpers`. For example, removing `TMPDIR` on Unix and `TMP`/`TEMP` breaks
`std::env::temp_dir` and wrecks anything using that, such as rustc's codgen.
- Removed `Deref`/`DerefMut` for `run_make_support::Command` -> `std::process::Command` because it
causes a method chain like `htmldocck().arg().run()` to fail, because `arg()` resolves to
`std::process::Command` which also returns a `&mut std::process::Command`, causing the `run()` to
be not found.

## [0.0.0] - 2024-06-09

Consider this version to contain all changes made to the support library before we started to track
changes in this changelog.

### Added

- Custom command wrappers around `std::process::Command` (`run_make_support::Command`) and custom
wrapper around `std::process::Output` (`CompletedProcess`) to make it more convenient to work with
commands and their output, and help avoid forgetting to check for exit status.
- `Command`: `set_stdin`, `run`, `run_fail`.
- `CompletedProcess`: `std{err,out}_utf8`, `status`, `assert_std{err,out}_{equals, contains,
not_contains}`, `assert_exit_code`.
- `impl_common_helpers` macro to avoid repeating adding common convenience methods, including:
- Environment manipulation methods: `env`, `env_remove`
- Command argument providers: `arg`, `args`
- Common invocation inspection (of the command invocation up until `inspect` is called):
`inspect`
- Execution methods: `run` (for commands expected to succeed execution, exit status `0`) and
`run_fail` (for commands expected to fail execution, exit status non-zero).
- Command wrappers around: `rustc`, `clang`, `cc`, `rustc`, `rustdoc`, `llvm-readobj`.
- Thin helpers to construct `python` and `htmldocck` commands.
- `run` and `run_fail` (like `Command::{run, run_fail}`) for running binaries, which sets suitable
env vars (like `LD_LIB_PATH` or equivalent, `TARGET_RPATH_ENV`, `PATH` on Windows).
- Pseudo command `diff` which has similar functionality as the cli util but not the same API.
- Convenience panic-on-fail helpers `env_var`, `env_var_os`, `cwd` for their `std::env` conterparts.
- Convenience panic-on-fail helpers for reading respective env vars: `target`, `source_root`.
- Platform check helpers: `is_windows`, `is_msvc`, `cygpath_windows`, `uname`.
- fs helpers: `copy_dir_all`.
- `recursive_diff` helper.
- Generic `assert_not_contains` helper.
- Scoped run-with-teardown helper `run_in_tmpdir` which is designed to run commands in a temporary
directory that is cleared when closure returns.
- Helpers for constructing the name of binaries and libraries: `rust_lib_name`, `static_lib_name`,
`bin_name`, `dynamic_lib_name`.
- Re-export libraries: `gimli`, `object`, `regex`, `wasmparsmer`.
2 changes: 1 addition & 1 deletion src/tools/run-make-support/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "run_make_support"
version = "0.0.0"
version = "0.1.0"
edition = "2021"

[dependencies]
Expand Down
7 changes: 2 additions & 5 deletions src/tools/run-make-support/src/cc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{bin_name, cygpath_windows, env_var, is_msvc, is_windows, uname};
///
/// WARNING: This means that what flags are accepted by the underlying C compiler is
/// platform- AND compiler-specific. Consult the relevant docs for `gcc`, `clang` and `mvsc`.
#[track_caller]
pub fn cc() -> Cc {
Cc::new()
}
Expand All @@ -25,6 +26,7 @@ impl Cc {
///
/// WARNING: This means that what flags are accepted by the underlying C compile is
/// platform- AND compiler-specific. Consult the relevant docs for `gcc`, `clang` and `mvsc`.
#[track_caller]
pub fn new() -> Self {
let compiler = env_var("CC");

Expand Down Expand Up @@ -84,11 +86,6 @@ impl Cc {
self.cmd.arg(path.as_ref());
self
}

/// Get the [`Output`][::std::process::Output] of the finished process.
pub fn command_output(&mut self) -> ::std::process::Output {
self.cmd.output().expect("failed to get output of finished process")
}
}

/// `EXTRACFLAGS`
Expand Down
2 changes: 2 additions & 0 deletions src/tools/run-make-support/src/clang.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::command::Command;
use crate::{bin_name, env_var};

/// Construct a new `clang` invocation. `clang` is not always available for all targets.
#[track_caller]
pub fn clang() -> Clang {
Clang::new()
}
Expand All @@ -18,6 +19,7 @@ crate::impl_common_helpers!(Clang);

impl Clang {
/// Construct a new `clang` invocation. `clang` is not always available for all targets.
#[track_caller]
pub fn new() -> Self {
let clang = env_var("CLANG");
let cmd = Command::new(clang);
Expand Down
115 changes: 85 additions & 30 deletions src/tools/run-make-support/src/command.rs
Original file line number Diff line number Diff line change
@@ -1,55 +1,124 @@
use crate::{assert_not_contains, handle_failed_output};
use std::ffi;
use std::ffi::OsStr;
use std::io::Write;
use std::ops::{Deref, DerefMut};
use std::panic;
use std::path::Path;
use std::process::{Command as StdCommand, ExitStatus, Output, Stdio};

/// This is a custom command wrapper that simplifies working with commands
/// and makes it easier to ensure that we check the exit status of executed
/// processes.
use crate::drop_bomb::DropBomb;
use crate::{assert_not_contains, handle_failed_output};

/// This is a custom command wrapper that simplifies working with commands and makes it easier to
/// ensure that we check the exit status of executed processes.
///
/// # A [`Command`] must be executed
///
/// A [`Command`] is armed by a [`DropBomb`] on construction to enforce that it will be executed. If
/// a [`Command`] is constructed but never executed, the drop bomb will explode and cause the test
/// to panic. Execution methods [`run`] and [`run_fail`] will defuse the drop bomb. A test
/// containing constructed but never executed commands is dangerous because it can give a false
/// sense of confidence.
///
/// [`run`]: Self::run
/// [`run_fail`]: Self::run_fail
#[derive(Debug)]
pub struct Command {
cmd: StdCommand,
stdin: Option<Box<[u8]>>,
drop_bomb: DropBomb,
}

impl Command {
pub fn new<S: AsRef<OsStr>>(program: S) -> Self {
Self { cmd: StdCommand::new(program), stdin: None }
#[track_caller]
pub fn new<P: AsRef<OsStr>>(program: P) -> Self {
let program = program.as_ref();
Self { cmd: StdCommand::new(program), stdin: None, drop_bomb: DropBomb::arm(program) }
}

pub fn set_stdin(&mut self, stdin: Box<[u8]>) {
self.stdin = Some(stdin);
}

/// Specify an environment variable.
pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: AsRef<ffi::OsStr>,
V: AsRef<ffi::OsStr>,
{
self.cmd.env(key, value);
self
}

/// Remove an environmental variable.
pub fn env_remove<K>(&mut self, key: K) -> &mut Self
where
K: AsRef<ffi::OsStr>,
{
self.cmd.env_remove(key);
self
}

/// Generic command argument provider. Prefer specific helper methods if possible.
/// Note that for some executables, arguments might be platform specific. For C/C++
/// compilers, arguments might be platform *and* compiler specific.
pub fn arg<S>(&mut self, arg: S) -> &mut Self
where
S: AsRef<ffi::OsStr>,
{
self.cmd.arg(arg);
self
}

/// Generic command arguments provider. Prefer specific helper methods if possible.
/// Note that for some executables, arguments might be platform specific. For C/C++
/// compilers, arguments might be platform *and* compiler specific.
pub fn args<S>(&mut self, args: &[S]) -> &mut Self
where
S: AsRef<ffi::OsStr>,
{
self.cmd.args(args);
self
}

/// Inspect what the underlying [`std::process::Command`] is up to the
/// current construction.
pub fn inspect<I>(&mut self, inspector: I) -> &mut Self
where
I: FnOnce(&StdCommand),
{
inspector(&self.cmd);
self
}

/// Set the path where the command will be run.
pub fn current_dir<P: AsRef<Path>>(&mut self, path: P) -> &mut Self {
self.cmd.current_dir(path);
self
}

/// Run the constructed command and assert that it is successfully run.
#[track_caller]
pub fn run(&mut self) -> CompletedProcess {
let caller_location = std::panic::Location::caller();
let caller_line_number = caller_location.line();

let output = self.command_output();
if !output.status().success() {
handle_failed_output(&self, output, caller_line_number);
handle_failed_output(&self, output, panic::Location::caller().line());
}
output
}

/// Run the constructed command and assert that it does not successfully run.
#[track_caller]
pub fn run_fail(&mut self) -> CompletedProcess {
let caller_location = std::panic::Location::caller();
let caller_line_number = caller_location.line();

let output = self.command_output();
if output.status().success() {
handle_failed_output(&self, output, caller_line_number);
handle_failed_output(&self, output, panic::Location::caller().line());
}
output
}

#[track_caller]
pub(crate) fn command_output(&mut self) -> CompletedProcess {
fn command_output(&mut self) -> CompletedProcess {
self.drop_bomb.defuse();
// let's make sure we piped all the input and outputs
self.cmd.stdin(Stdio::piped());
self.cmd.stdout(Stdio::piped());
Expand All @@ -71,20 +140,6 @@ impl Command {
}
}

impl Deref for Command {
type Target = StdCommand;

fn deref(&self) -> &Self::Target {
&self.cmd
}
}

impl DerefMut for Command {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.cmd
}
}

/// Represents the result of an executed process.
/// The various `assert_` helper methods should preferably be used for
/// checking the contents of stdout/stderr.
Expand Down
10 changes: 8 additions & 2 deletions src/tools/run-make-support/src/diff/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ use regex::Regex;
use similar::TextDiff;
use std::path::Path;

use crate::drop_bomb::DropBomb;

#[cfg(test)]
mod tests;

#[track_caller]
pub fn diff() -> Diff {
Diff::new()
}
Expand All @@ -16,17 +19,20 @@ pub struct Diff {
actual: Option<String>,
actual_name: Option<String>,
normalizers: Vec<(String, String)>,
drop_bomb: DropBomb,
}

impl Diff {
/// Construct a bare `diff` invocation.
#[track_caller]
pub fn new() -> Self {
Self {
expected: None,
expected_name: None,
actual: None,
actual_name: None,
normalizers: Vec::new(),
drop_bomb: DropBomb::arm("diff"),
}
}

Expand Down Expand Up @@ -79,9 +85,9 @@ impl Diff {
self
}

/// Executes the diff process, prints any differences to the standard error.
#[track_caller]
pub fn run(&self) {
pub fn run(&mut self) {
self.drop_bomb.defuse();
let expected = self.expected.as_ref().expect("expected text not set");
let mut actual = self.actual.as_ref().expect("actual text not set").to_string();
let expected_name = self.expected_name.as_ref().unwrap();
Expand Down
50 changes: 50 additions & 0 deletions src/tools/run-make-support/src/drop_bomb/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! This module implements "drop bombs" intended for use by command wrappers to ensure that the
//! constructed commands are *eventually* executed. This is exactly like `rustc_errors::Diag` where
//! we force every `Diag` to be consumed or we emit a bug, but we panic instead.
//!
//! This is adapted from <https://docs.rs/drop_bomb/latest/drop_bomb/> and simplified for our
//! purposes.

use std::ffi::{OsStr, OsString};
use std::panic;

#[cfg(test)]
mod tests;

#[derive(Debug)]
pub(crate) struct DropBomb {
command: OsString,
defused: bool,
armed_line: u32,
}

impl DropBomb {
/// Arm a [`DropBomb`]. If the value is dropped without being [`defused`][Self::defused], then
/// it will panic. It is expected that the command wrapper uses `#[track_caller]` to help
/// propagate the caller info from rmake.rs.
#[track_caller]
pub(crate) fn arm<S: AsRef<OsStr>>(command: S) -> DropBomb {
DropBomb {
command: command.as_ref().into(),
defused: false,
armed_line: panic::Location::caller().line(),
}
}

/// Defuse the [`DropBomb`]. This will prevent the drop bomb from panicking when dropped.
pub(crate) fn defuse(&mut self) {
self.defused = true;
}
}

impl Drop for DropBomb {
fn drop(&mut self) {
if !self.defused && !std::thread::panicking() {
panic!(
"command constructed but not executed at line {}: `{}`",
self.armed_line,
self.command.to_string_lossy()
)
}
}
}
Loading
Loading