mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
This PR introduces a `codex-utils-cargo-bin` utility crate that wraps/replaces our use of `assert_cmd::Command` and `escargot::CargoBuild`. As you can infer from the introduction of `buck_project_root()` in this PR, I am attempting to make it possible to build Codex under [Buck2](https://buck2.build) as well as `cargo`. With Buck2, I hope to achieve faster incremental local builds (largely due to Buck2's [dice](https://buck2.build/docs/insights_and_knowledge/modern_dice/) build strategy, as well as benefits from its local build daemon) as well as faster CI builds if we invest in remote execution and caching. See https://buck2.build/docs/getting_started/what_is_buck2/#why-use-buck2-key-advantages for more details about the performance advantages of Buck2. Buck2 enforces stronger requirements in terms of build and test isolation. It discourages assumptions about absolute paths (which is key to enabling remote execution). Because the `CARGO_BIN_EXE_*` environment variables that Cargo provides are absolute paths (which `assert_cmd::Command` reads), this is a problem for Buck2, which is why we need this `codex-utils-cargo-bin` utility. My WIP-Buck2 setup sets the `CARGO_BIN_EXE_*` environment variables passed to a `rust_test()` build rule as relative paths. `codex-utils-cargo-bin` will resolve these values to absolute paths, when necessary. --- [//]: # (BEGIN SAPLING FOOTER) Stack created with [Sapling](https://sapling-scm.com). Best reviewed with [ReviewStack](https://reviewstack.dev/openai/codex/pull/8496). * #8498 * __->__ #8496
149 lines
5.0 KiB
Rust
149 lines
5.0 KiB
Rust
#![expect(clippy::expect_used)]
|
||
|
||
//! Optional smoke tests that hit the real OpenAI /v1/responses endpoint. They are `#[ignore]` by
|
||
//! default so CI stays deterministic and free. Developers can run them locally with
|
||
//! `cargo test --test live_cli -- --ignored` provided they set a valid `OPENAI_API_KEY`.
|
||
|
||
use assert_cmd::prelude::*;
|
||
use predicates::prelude::*;
|
||
use std::process::Command;
|
||
use std::process::Stdio;
|
||
use tempfile::TempDir;
|
||
|
||
fn require_api_key() -> String {
|
||
std::env::var("OPENAI_API_KEY")
|
||
.expect("OPENAI_API_KEY env var not set — skip running live tests")
|
||
}
|
||
|
||
/// Helper that spawns the binary inside a TempDir with minimal flags. Returns (Assert, TempDir).
|
||
fn run_live(prompt: &str) -> (assert_cmd::assert::Assert, TempDir) {
|
||
#![expect(clippy::unwrap_used)]
|
||
use std::io::Read;
|
||
use std::io::Write;
|
||
use std::thread;
|
||
|
||
let dir = TempDir::new().unwrap();
|
||
|
||
// Build a plain `std::process::Command` so we have full control over the underlying stdio
|
||
// handles. `assert_cmd`’s own `Command` wrapper always forces stdout/stderr to be piped
|
||
// internally which prevents us from streaming them live to the terminal (see its `spawn`
|
||
// implementation). Instead we configure the std `Command` ourselves, then later hand the
|
||
// resulting `Output` to `assert_cmd` for the familiar assertions.
|
||
|
||
let mut cmd = Command::new(codex_utils_cargo_bin::cargo_bin("codex-rs").unwrap());
|
||
cmd.current_dir(dir.path());
|
||
cmd.env("OPENAI_API_KEY", require_api_key());
|
||
|
||
// We want three things at once:
|
||
// 1. live streaming of the child’s stdout/stderr while the test is running
|
||
// 2. captured output so we can keep using assert_cmd’s `Assert` helpers
|
||
// 3. cross‑platform behavior (best effort)
|
||
//
|
||
// To get that we:
|
||
// • set both stdout and stderr to `piped()` so we can read them programmatically
|
||
// • spawn a thread for each stream that copies bytes into two sinks:
|
||
// – the parent process’ stdout/stderr for live visibility
|
||
// – an in‑memory buffer so we can pass it to `assert_cmd` later
|
||
|
||
// Pass the prompt through the `--` separator so the CLI knows when user input ends.
|
||
cmd.arg("--allow-no-git-exec")
|
||
.arg("-v")
|
||
.arg("--")
|
||
.arg(prompt);
|
||
|
||
cmd.stdin(Stdio::piped());
|
||
cmd.stdout(Stdio::piped());
|
||
cmd.stderr(Stdio::piped());
|
||
|
||
let mut child = cmd.spawn().expect("failed to spawn codex-rs");
|
||
|
||
// Send the terminating newline so Session::run exits after the first turn.
|
||
child
|
||
.stdin
|
||
.as_mut()
|
||
.expect("child stdin unavailable")
|
||
.write_all(b"\n")
|
||
.expect("failed to write to child stdin");
|
||
|
||
// Helper that tees a ChildStdout/ChildStderr into both the parent’s stdio and a Vec<u8>.
|
||
fn tee<R: Read + Send + 'static>(
|
||
mut reader: R,
|
||
mut writer: impl Write + Send + 'static,
|
||
) -> thread::JoinHandle<Vec<u8>> {
|
||
thread::spawn(move || {
|
||
let mut buf = Vec::new();
|
||
let mut chunk = [0u8; 4096];
|
||
loop {
|
||
match reader.read(&mut chunk) {
|
||
Ok(0) => break,
|
||
Ok(n) => {
|
||
writer.write_all(&chunk[..n]).ok();
|
||
writer.flush().ok();
|
||
buf.extend_from_slice(&chunk[..n]);
|
||
}
|
||
Err(_) => break,
|
||
}
|
||
}
|
||
buf
|
||
})
|
||
}
|
||
|
||
let stdout_handle = tee(
|
||
child.stdout.take().expect("child stdout"),
|
||
std::io::stdout(),
|
||
);
|
||
let stderr_handle = tee(
|
||
child.stderr.take().expect("child stderr"),
|
||
std::io::stderr(),
|
||
);
|
||
|
||
let status = child.wait().expect("failed to wait on child");
|
||
let stdout = stdout_handle.join().expect("stdout thread panicked");
|
||
let stderr = stderr_handle.join().expect("stderr thread panicked");
|
||
|
||
let output = std::process::Output {
|
||
status,
|
||
stdout,
|
||
stderr,
|
||
};
|
||
|
||
(output.assert(), dir)
|
||
}
|
||
|
||
#[ignore]
|
||
#[test]
|
||
fn live_create_file_hello_txt() {
|
||
if std::env::var("OPENAI_API_KEY").is_err() {
|
||
eprintln!("skipping live_create_file_hello_txt – OPENAI_API_KEY not set");
|
||
return;
|
||
}
|
||
|
||
let (assert, dir) = run_live(
|
||
"Use the shell tool with the apply_patch command to create a file named hello.txt containing the text 'hello'.",
|
||
);
|
||
|
||
assert.success();
|
||
|
||
let path = dir.path().join("hello.txt");
|
||
assert!(path.exists(), "hello.txt was not created by the model");
|
||
|
||
let contents = std::fs::read_to_string(path).unwrap();
|
||
|
||
assert_eq!(contents.trim(), "hello");
|
||
}
|
||
|
||
#[ignore]
|
||
#[test]
|
||
fn live_print_working_directory() {
|
||
if std::env::var("OPENAI_API_KEY").is_err() {
|
||
eprintln!("skipping live_print_working_directory – OPENAI_API_KEY not set");
|
||
return;
|
||
}
|
||
|
||
let (assert, dir) = run_live("Print the current working directory using the shell function.");
|
||
|
||
assert
|
||
.success()
|
||
.stdout(predicate::str::contains(dir.path().to_string_lossy()));
|
||
}
|