diff options
author | Matt Mastracci <matthew@mastracci.com> | 2024-02-09 13:33:05 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-09 13:33:05 -0700 |
commit | dcbbcd23f5dd8601e2851aded4cabc6557164363 (patch) | |
tree | f9c3842b8c80028c6ab8e298a4d08ac828eff11d /test_util/src/spawn.rs | |
parent | 24bdc1de33494bc1619bfebea826ab08ffb74a01 (diff) |
refactor: split integration tests from CLI (part 1) (#22308)
This PR separates integration tests from CLI tests into a new project
named `cli_tests`. This is a prerequisite for an integration test runner
that can work with either the CLI binary in the current project, or one
that is built ahead of time.
## Background
Rust does not have the concept of artifact dependencies yet
(https://github.com/rust-lang/cargo/issues/9096). Because of this, the
only way we can ensure a binary is built before running associated tests
is by hanging tests off the crate with the binary itself.
Unfortunately this means that to run those tests, you _must_ build the
binary and in the case of the deno executable that might be a 10 minute
wait in release mode.
## Implementation
To allow for tests to run with and without the requirement that the
binary is up-to-date, we split the integration tests into a project of
their own. As these tests would not require the binary to build itself
before being run as-is, we add a stub integration `[[test]]` target in
the `cli` project that invokes these tests using `cargo test`.
The stub test runner we add has `harness = false` so that we can get
access to a `main` function. This `main` function's sole job is to
`execvp` the command `cargo test -p deno_cli`, effectively "calling"
another cargo target.
This ensures that the deno executable is always correctly rebuilt before
running the stub test runner from `cli`, and gets us closer to be able
to run the entire integration test suite on arbitrary deno executables
(and therefore split the build into multiple phases).
The new `cli_tests` project lives within `cli` to avoid a large PR. In
later PRs, the test data will be split from the `cli` project. As there
are a few thousand files, it'll be better to do this as a completely
separate PR to avoid noise.
Diffstat (limited to 'test_util/src/spawn.rs')
-rw-r--r-- | test_util/src/spawn.rs | 71 |
1 files changed, 71 insertions, 0 deletions
diff --git a/test_util/src/spawn.rs b/test_util/src/spawn.rs new file mode 100644 index 000000000..bfd83e9b2 --- /dev/null +++ b/test_util/src/spawn.rs @@ -0,0 +1,71 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. +use anyhow::Error; +use std::convert::Infallible; + +/// For unix targets, we just replace our current process with the desired cargo process. +#[cfg(unix)] +pub fn exec_replace_inner( + cmd: &str, + args: &[&str], +) -> Result<Infallible, Error> { + use std::ffi::CStr; + use std::ffi::CString; + + let args = args + .iter() + .map(|arg| CString::new(*arg).unwrap()) + .collect::<Vec<_>>(); + let args: Vec<&CStr> = + args.iter().map(|arg| arg.as_ref()).collect::<Vec<_>>(); + + let err = nix::unistd::execvp(&CString::new(cmd).unwrap(), &args) + .expect_err("Impossible"); + Err(err.into()) +} + +#[cfg(windows)] +pub fn exec_replace_inner( + cmd: &str, + args: &[&str], +) -> Result<Infallible, Error> { + use std::os::windows::io::AsRawHandle; + use std::process::Command; + use win32job::ExtendedLimitInfo; + use win32job::Job; + + // Use a job to ensure the child process's lifetime does not exceed the current process's lifetime. + // This ensures that if the current process is terminated (e.g., via ctrl+c or task manager), + // the child process is automatically reaped. + + // For more information about this technique, see Raymond Chen's blog post: + // https://devblogs.microsoft.com/oldnewthing/20131209-00/?p=2433 + // Note: While our implementation is not perfect, it serves its purpose for test code. + + // In the future, we may directly obtain the main thread's handle from Rust code and use it + // to create a suspended process that we can then resume: + // https://github.com/rust-lang/rust/issues/96723 + + // Creates a child process and assigns it to our current job. + // A more reliable approach would be to create the child suspended and then assign it to the job. + // For now, we create the child, create the job, and then assign both us and the child to the job. + let mut child = Command::new(cmd).args(&args[1..]).spawn()?; + + let mut info = ExtendedLimitInfo::default(); + info.limit_kill_on_job_close(); + let job = Job::create_with_limit_info(&info)?; + job.assign_current_process()?; + let handle = child.as_raw_handle(); + job.assign_process(handle as _)?; + + let exit = child.wait()?; + std::process::exit(exit.code().unwrap_or(1)); +} + +/// Runs a command, replacing the current process on Unix. On Windows, this function blocks and +/// exits. +/// +/// In either case, the only way this function returns is if it fails to launch the child +/// process. +pub fn exec_replace(command: &str, args: &[&str]) -> Result<Infallible, Error> { + exec_replace_inner(command, args) +} |