Skip to content

Commit

Permalink
Auto merge of #125752 - jieyouxu:kaboom, r=Kobzol
Browse files Browse the repository at this point in the history
run-make: arm command wrappers with drop bombs

This PR is one in a series of cleanups to run-make tests and the run-make-support library.

### Summary

It's easy to forget to actually executed constructed command wrappers, e.g. `rustc().input("foo.rs")` but forget the `run()`, so to help catch these mistakes, we arm command wrappers with drop bombs on construction to force them to be executed by test code.

This PR also removes the `Deref`/`DerefMut` impl for our custom `Command` which derefs to `std::process::Command` because it can cause issues when trying to use a custom command:

```rs
htmldocck().arg().run()
```

fails to compile because the `arg()` is resolved to `std::process::Command::arg`, which returns `&mut std::process::Command` that doesn't have a `run()` command.

This PR also:

- Removes `env_var` on the `impl_common_helper` macro that was wrongly named and is a footgun (no users).
- Bumps the run-make-support library to version `0.1.0`.
- Adds a changelog to the support library.

### Details

Especially for command wrappers like `Rustc`, it's very easy to build up
a command invocation but forget to actually execute it, e.g. by using
`run()`. This commit adds "drop bombs" to command wrappers, which are
armed on command wrapper construction, and only defused if the command
is executed (through `run`, `run_fail`).

If the test writer forgets to execute the command, the drop bomb will
"explode" and panic with an error message. This is so that tests don't
silently pass with constructed-but-not-executed command wrappers.

This PR is best reviewed commit-by-commit.

try-job: x86_64-msvc
  • Loading branch information
bors committed Jun 11, 2024
2 parents 6a207f4 + 5ec3eef commit 20ba13c
Show file tree
Hide file tree
Showing 22 changed files with 280 additions and 80 deletions.
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

0 comments on commit 20ba13c

Please sign in to comment.