mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
fix: move inline codex-rs/core unit tests into sibling files (#14444)
## Why PR #13783 moved the `codex.rs` unit tests into `codex_tests.rs`. This applies the same extraction pattern across the rest of `codex-rs/core` so the production modules stay focused on runtime code instead of large inline test blocks. Keeping the tests in sibling files also makes follow-up edits easier to review because product changes no longer have to share a file with hundreds or thousands of lines of test scaffolding. ## What changed - replaced each inline `mod tests { ... }` in `codex-rs/core/src/**` with a path-based module declaration - moved each extracted unit test module into a sibling `*_tests.rs` file, using `mod_tests.rs` for `mod.rs` modules - preserved the existing `cfg(...)` guards and module-local structure so the refactor remains structural rather than behavioral ## Testing - `cargo test -p codex-core --lib` (`1653 passed; 0 failed; 5 ignored`) - `just fix -p codex-core` - `cargo fmt --check` - `cargo shear`
This commit is contained in:
committed by
GitHub
Unverified
parent
7f2ca502f5
commit
0c8a36676a
@@ -251,40 +251,5 @@ async fn resolve_aggregated_output(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::split_valid_utf8_prefix_with_max;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn split_valid_utf8_prefix_respects_max_bytes_for_ascii() {
|
||||
let mut buf = b"hello word!".to_vec();
|
||||
|
||||
let first = split_valid_utf8_prefix_with_max(&mut buf, 5).expect("expected prefix");
|
||||
assert_eq!(first, b"hello".to_vec());
|
||||
assert_eq!(buf, b" word!".to_vec());
|
||||
|
||||
let second = split_valid_utf8_prefix_with_max(&mut buf, 5).expect("expected prefix");
|
||||
assert_eq!(second, b" word".to_vec());
|
||||
assert_eq!(buf, b"!".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_valid_utf8_prefix_avoids_splitting_utf8_codepoints() {
|
||||
// "é" is 2 bytes in UTF-8. With a max of 3 bytes, we should only emit 1 char (2 bytes).
|
||||
let mut buf = "ééé".as_bytes().to_vec();
|
||||
|
||||
let first = split_valid_utf8_prefix_with_max(&mut buf, 3).expect("expected prefix");
|
||||
assert_eq!(std::str::from_utf8(&first).unwrap(), "é");
|
||||
assert_eq!(buf, "éé".as_bytes().to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_valid_utf8_prefix_makes_progress_on_invalid_utf8() {
|
||||
let mut buf = vec![0xff, b'a', b'b'];
|
||||
|
||||
let first = split_valid_utf8_prefix_with_max(&mut buf, 2).expect("expected prefix");
|
||||
assert_eq!(first, vec![0xff]);
|
||||
assert_eq!(buf, b"ab".to_vec());
|
||||
}
|
||||
}
|
||||
#[path = "async_watcher_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
use super::split_valid_utf8_prefix_with_max;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn split_valid_utf8_prefix_respects_max_bytes_for_ascii() {
|
||||
let mut buf = b"hello word!".to_vec();
|
||||
|
||||
let first = split_valid_utf8_prefix_with_max(&mut buf, 5).expect("expected prefix");
|
||||
assert_eq!(first, b"hello".to_vec());
|
||||
assert_eq!(buf, b" word!".to_vec());
|
||||
|
||||
let second = split_valid_utf8_prefix_with_max(&mut buf, 5).expect("expected prefix");
|
||||
assert_eq!(second, b" word".to_vec());
|
||||
assert_eq!(buf, b"!".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_valid_utf8_prefix_avoids_splitting_utf8_codepoints() {
|
||||
// "é" is 2 bytes in UTF-8. With a max of 3 bytes, we should only emit 1 char (2 bytes).
|
||||
let mut buf = "ééé".as_bytes().to_vec();
|
||||
|
||||
let first = split_valid_utf8_prefix_with_max(&mut buf, 3).expect("expected prefix");
|
||||
assert_eq!(std::str::from_utf8(&first).unwrap(), "é");
|
||||
assert_eq!(buf, "éé".as_bytes().to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_valid_utf8_prefix_makes_progress_on_invalid_utf8() {
|
||||
let mut buf = vec![0xff, b'a', b'b'];
|
||||
|
||||
let first = split_valid_utf8_prefix_with_max(&mut buf, 2).expect("expected prefix");
|
||||
assert_eq!(first, vec![0xff]);
|
||||
assert_eq!(buf, b"ab".to_vec());
|
||||
}
|
||||
@@ -179,94 +179,5 @@ impl HeadTailBuffer {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::HeadTailBuffer;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn keeps_prefix_and_suffix_when_over_budget() {
|
||||
let mut buf = HeadTailBuffer::new(10);
|
||||
|
||||
buf.push_chunk(b"0123456789".to_vec());
|
||||
assert_eq!(buf.omitted_bytes(), 0);
|
||||
|
||||
// Exceeds max by 2; we should keep head+tail and omit the middle.
|
||||
buf.push_chunk(b"ab".to_vec());
|
||||
assert!(buf.omitted_bytes() > 0);
|
||||
|
||||
let rendered = String::from_utf8_lossy(&buf.to_bytes()).to_string();
|
||||
assert!(rendered.starts_with("01234"));
|
||||
assert!(rendered.ends_with("89ab"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_bytes_zero_drops_everything() {
|
||||
let mut buf = HeadTailBuffer::new(0);
|
||||
buf.push_chunk(b"abc".to_vec());
|
||||
|
||||
assert_eq!(buf.retained_bytes(), 0);
|
||||
assert_eq!(buf.omitted_bytes(), 3);
|
||||
assert_eq!(buf.to_bytes(), b"".to_vec());
|
||||
assert_eq!(buf.snapshot_chunks(), Vec::<Vec<u8>>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn head_budget_zero_keeps_only_last_byte_in_tail() {
|
||||
let mut buf = HeadTailBuffer::new(1);
|
||||
buf.push_chunk(b"abc".to_vec());
|
||||
|
||||
assert_eq!(buf.retained_bytes(), 1);
|
||||
assert_eq!(buf.omitted_bytes(), 2);
|
||||
assert_eq!(buf.to_bytes(), b"c".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draining_resets_state() {
|
||||
let mut buf = HeadTailBuffer::new(10);
|
||||
buf.push_chunk(b"0123456789".to_vec());
|
||||
buf.push_chunk(b"ab".to_vec());
|
||||
|
||||
let drained = buf.drain_chunks();
|
||||
assert!(!drained.is_empty());
|
||||
|
||||
assert_eq!(buf.retained_bytes(), 0);
|
||||
assert_eq!(buf.omitted_bytes(), 0);
|
||||
assert_eq!(buf.to_bytes(), b"".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_larger_than_tail_budget_keeps_only_tail_end() {
|
||||
let mut buf = HeadTailBuffer::new(10);
|
||||
buf.push_chunk(b"0123456789".to_vec());
|
||||
|
||||
// Tail budget is 5 bytes. This chunk should replace the tail and keep only its last 5 bytes.
|
||||
buf.push_chunk(b"ABCDEFGHIJK".to_vec());
|
||||
|
||||
let out = String::from_utf8_lossy(&buf.to_bytes()).to_string();
|
||||
assert!(out.starts_with("01234"));
|
||||
assert!(out.ends_with("GHIJK"));
|
||||
assert!(buf.omitted_bytes() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fills_head_then_tail_across_multiple_chunks() {
|
||||
let mut buf = HeadTailBuffer::new(10);
|
||||
|
||||
// Fill the 5-byte head budget across multiple chunks.
|
||||
buf.push_chunk(b"01".to_vec());
|
||||
buf.push_chunk(b"234".to_vec());
|
||||
assert_eq!(buf.to_bytes(), b"01234".to_vec());
|
||||
|
||||
// Then fill the 5-byte tail budget.
|
||||
buf.push_chunk(b"567".to_vec());
|
||||
buf.push_chunk(b"89".to_vec());
|
||||
assert_eq!(buf.to_bytes(), b"0123456789".to_vec());
|
||||
assert_eq!(buf.omitted_bytes(), 0);
|
||||
|
||||
// One more byte causes the tail to drop its oldest byte.
|
||||
buf.push_chunk(b"a".to_vec());
|
||||
assert_eq!(buf.to_bytes(), b"012346789a".to_vec());
|
||||
assert_eq!(buf.omitted_bytes(), 1);
|
||||
}
|
||||
}
|
||||
#[path = "head_tail_buffer_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use super::HeadTailBuffer;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn keeps_prefix_and_suffix_when_over_budget() {
|
||||
let mut buf = HeadTailBuffer::new(10);
|
||||
|
||||
buf.push_chunk(b"0123456789".to_vec());
|
||||
assert_eq!(buf.omitted_bytes(), 0);
|
||||
|
||||
// Exceeds max by 2; we should keep head+tail and omit the middle.
|
||||
buf.push_chunk(b"ab".to_vec());
|
||||
assert!(buf.omitted_bytes() > 0);
|
||||
|
||||
let rendered = String::from_utf8_lossy(&buf.to_bytes()).to_string();
|
||||
assert!(rendered.starts_with("01234"));
|
||||
assert!(rendered.ends_with("89ab"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_bytes_zero_drops_everything() {
|
||||
let mut buf = HeadTailBuffer::new(0);
|
||||
buf.push_chunk(b"abc".to_vec());
|
||||
|
||||
assert_eq!(buf.retained_bytes(), 0);
|
||||
assert_eq!(buf.omitted_bytes(), 3);
|
||||
assert_eq!(buf.to_bytes(), b"".to_vec());
|
||||
assert_eq!(buf.snapshot_chunks(), Vec::<Vec<u8>>::new());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn head_budget_zero_keeps_only_last_byte_in_tail() {
|
||||
let mut buf = HeadTailBuffer::new(1);
|
||||
buf.push_chunk(b"abc".to_vec());
|
||||
|
||||
assert_eq!(buf.retained_bytes(), 1);
|
||||
assert_eq!(buf.omitted_bytes(), 2);
|
||||
assert_eq!(buf.to_bytes(), b"c".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draining_resets_state() {
|
||||
let mut buf = HeadTailBuffer::new(10);
|
||||
buf.push_chunk(b"0123456789".to_vec());
|
||||
buf.push_chunk(b"ab".to_vec());
|
||||
|
||||
let drained = buf.drain_chunks();
|
||||
assert!(!drained.is_empty());
|
||||
|
||||
assert_eq!(buf.retained_bytes(), 0);
|
||||
assert_eq!(buf.omitted_bytes(), 0);
|
||||
assert_eq!(buf.to_bytes(), b"".to_vec());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chunk_larger_than_tail_budget_keeps_only_tail_end() {
|
||||
let mut buf = HeadTailBuffer::new(10);
|
||||
buf.push_chunk(b"0123456789".to_vec());
|
||||
|
||||
// Tail budget is 5 bytes. This chunk should replace the tail and keep only its last 5 bytes.
|
||||
buf.push_chunk(b"ABCDEFGHIJK".to_vec());
|
||||
|
||||
let out = String::from_utf8_lossy(&buf.to_bytes()).to_string();
|
||||
assert!(out.starts_with("01234"));
|
||||
assert!(out.ends_with("GHIJK"));
|
||||
assert!(buf.omitted_bytes() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fills_head_then_tail_across_multiple_chunks() {
|
||||
let mut buf = HeadTailBuffer::new(10);
|
||||
|
||||
// Fill the 5-byte head budget across multiple chunks.
|
||||
buf.push_chunk(b"01".to_vec());
|
||||
buf.push_chunk(b"234".to_vec());
|
||||
assert_eq!(buf.to_bytes(), b"01234".to_vec());
|
||||
|
||||
// Then fill the 5-byte tail budget.
|
||||
buf.push_chunk(b"567".to_vec());
|
||||
buf.push_chunk(b"89".to_vec());
|
||||
assert_eq!(buf.to_bytes(), b"0123456789".to_vec());
|
||||
assert_eq!(buf.omitted_bytes(), 0);
|
||||
|
||||
// One more byte causes the tail to drop its oldest byte.
|
||||
buf.push_chunk(b"a".to_vec());
|
||||
assert_eq!(buf.to_bytes(), b"012346789a".to_vec());
|
||||
assert_eq!(buf.omitted_bytes(), 1);
|
||||
}
|
||||
@@ -169,350 +169,5 @@ pub(crate) fn generate_chunk_id() -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(unix)]
|
||||
mod tests {
|
||||
use super::head_tail_buffer::HeadTailBuffer;
|
||||
use super::*;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::codex::make_session_and_context;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::tools::context::ExecCommandToolOutput;
|
||||
use crate::unified_exec::ExecCommandRequest;
|
||||
use crate::unified_exec::WriteStdinRequest;
|
||||
use core_test_support::skip_if_sandbox;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::Duration;
|
||||
|
||||
async fn test_session_and_turn() -> (Arc<Session>, Arc<TurnContext>) {
|
||||
let (session, mut turn) = make_session_and_context().await;
|
||||
turn.approval_policy
|
||||
.set(AskForApproval::Never)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
turn.sandbox_policy
|
||||
.set(SandboxPolicy::DangerFullAccess)
|
||||
.expect("test setup should allow updating sandbox policy");
|
||||
turn.file_system_sandbox_policy =
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy::from(turn.sandbox_policy.get());
|
||||
turn.network_sandbox_policy =
|
||||
codex_protocol::permissions::NetworkSandboxPolicy::from(turn.sandbox_policy.get());
|
||||
(Arc::new(session), Arc::new(turn))
|
||||
}
|
||||
|
||||
async fn exec_command(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
cmd: &str,
|
||||
yield_time_ms: u64,
|
||||
) -> Result<ExecCommandToolOutput, UnifiedExecError> {
|
||||
let context =
|
||||
UnifiedExecContext::new(Arc::clone(session), Arc::clone(turn), "call".to_string());
|
||||
let process_id = session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.allocate_process_id()
|
||||
.await;
|
||||
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.exec_command(
|
||||
ExecCommandRequest {
|
||||
command: vec!["bash".to_string(), "-lc".to_string(), cmd.to_string()],
|
||||
process_id,
|
||||
yield_time_ms,
|
||||
max_output_tokens: None,
|
||||
workdir: None,
|
||||
network: None,
|
||||
tty: true,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
additional_permissions_preapproved: false,
|
||||
justification: None,
|
||||
prefix_rule: None,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn write_stdin(
|
||||
session: &Arc<Session>,
|
||||
process_id: i32,
|
||||
input: &str,
|
||||
yield_time_ms: u64,
|
||||
) -> Result<ExecCommandToolOutput, UnifiedExecError> {
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.write_stdin(WriteStdinRequest {
|
||||
process_id,
|
||||
input,
|
||||
yield_time_ms,
|
||||
max_output_tokens: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_chunk_preserves_prefix_and_suffix() {
|
||||
let mut buffer = HeadTailBuffer::default();
|
||||
buffer.push_chunk(vec![b'a'; UNIFIED_EXEC_OUTPUT_MAX_BYTES]);
|
||||
buffer.push_chunk(vec![b'b']);
|
||||
buffer.push_chunk(vec![b'c']);
|
||||
|
||||
assert_eq!(buffer.retained_bytes(), UNIFIED_EXEC_OUTPUT_MAX_BYTES);
|
||||
let snapshot = buffer.snapshot_chunks();
|
||||
|
||||
let first = snapshot.first().expect("expected at least one chunk");
|
||||
assert_eq!(first.first(), Some(&b'a'));
|
||||
assert!(snapshot.iter().any(|chunk| chunk.as_slice() == b"b"));
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.last()
|
||||
.expect("expected at least one chunk")
|
||||
.as_slice(),
|
||||
b"c"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn head_tail_buffer_default_preserves_prefix_and_suffix() {
|
||||
let mut buffer = HeadTailBuffer::default();
|
||||
buffer.push_chunk(vec![b'a'; UNIFIED_EXEC_OUTPUT_MAX_BYTES]);
|
||||
buffer.push_chunk(b"bc".to_vec());
|
||||
|
||||
let rendered = buffer.to_bytes();
|
||||
assert_eq!(rendered.first(), Some(&b'a'));
|
||||
assert!(rendered.ends_with(b"bc"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unified_exec_persists_across_requests() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?;
|
||||
let process_id = open_shell.process_id.expect("expected process_id");
|
||||
|
||||
write_stdin(
|
||||
&session,
|
||||
process_id,
|
||||
"export CODEX_INTERACTIVE_SHELL_VAR=codex\n",
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let out_2 = write_stdin(
|
||||
&session,
|
||||
process_id,
|
||||
"echo $CODEX_INTERACTIVE_SHELL_VAR\n",
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
out_2.truncated_output().contains("codex"),
|
||||
"expected environment variable output"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multi_unified_exec_sessions() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let shell_a = exec_command(&session, &turn, "bash -i", 2_500).await?;
|
||||
let session_a = shell_a.process_id.expect("expected process id");
|
||||
|
||||
write_stdin(
|
||||
&session,
|
||||
session_a,
|
||||
"export CODEX_INTERACTIVE_SHELL_VAR=codex\n",
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let out_2 =
|
||||
exec_command(&session, &turn, "echo $CODEX_INTERACTIVE_SHELL_VAR", 2_500).await?;
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
assert!(
|
||||
out_2.process_id.is_none(),
|
||||
"short command should not report a process id if it exits quickly"
|
||||
);
|
||||
assert!(
|
||||
!out_2.truncated_output().contains("codex"),
|
||||
"short command should run in a fresh shell"
|
||||
);
|
||||
|
||||
let out_3 = write_stdin(
|
||||
&session,
|
||||
shell_a.process_id.expect("expected process id"),
|
||||
"echo $CODEX_INTERACTIVE_SHELL_VAR\n",
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
out_3.truncated_output().contains("codex"),
|
||||
"session should preserve state"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_exec_timeouts() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
const TEST_VAR_VALUE: &str = "unified_exec_var_123";
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?;
|
||||
let process_id = open_shell.process_id.expect("expected process id");
|
||||
|
||||
write_stdin(
|
||||
&session,
|
||||
process_id,
|
||||
format!("export CODEX_INTERACTIVE_SHELL_VAR={TEST_VAR_VALUE}\n").as_str(),
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let out_2 = write_stdin(
|
||||
&session,
|
||||
process_id,
|
||||
"sleep 5 && echo $CODEX_INTERACTIVE_SHELL_VAR\n",
|
||||
10,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
!out_2.truncated_output().contains(TEST_VAR_VALUE),
|
||||
"timeout too short should yield incomplete output"
|
||||
);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(7)).await;
|
||||
|
||||
let out_3 = write_stdin(&session, process_id, "", 100).await?;
|
||||
|
||||
assert!(
|
||||
out_3.truncated_output().contains(TEST_VAR_VALUE),
|
||||
"subsequent poll should retrieve output"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unified_exec_pause_blocks_yield_timeout() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
session.set_out_of_band_elicitation_pause_state(true);
|
||||
|
||||
let paused_session = Arc::clone(&session);
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
paused_session.set_out_of_band_elicitation_pause_state(false);
|
||||
});
|
||||
|
||||
let started = tokio::time::Instant::now();
|
||||
let response =
|
||||
exec_command(&session, &turn, "sleep 1 && echo unified-exec-done", 250).await?;
|
||||
|
||||
assert!(
|
||||
started.elapsed() >= Duration::from_secs(2),
|
||||
"pause should block the unified exec yield timeout"
|
||||
);
|
||||
assert!(
|
||||
response.truncated_output().contains("unified-exec-done"),
|
||||
"exec_command should wait for output after the pause lifts"
|
||||
);
|
||||
assert!(
|
||||
response.process_id.is_none(),
|
||||
"completed command should not leave a background process"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Ignored while we have a better way to test this.
|
||||
async fn requests_with_large_timeout_are_capped() -> anyhow::Result<()> {
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let result = exec_command(&session, &turn, "echo codex", 120_000).await?;
|
||||
|
||||
assert!(result.process_id.is_some());
|
||||
assert!(result.truncated_output().contains("codex"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Ignored while we have a better way to test this.
|
||||
async fn completed_commands_do_not_persist_sessions() -> anyhow::Result<()> {
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
let result = exec_command(&session, &turn, "echo codex", 2_500).await?;
|
||||
|
||||
assert!(
|
||||
result.process_id.is_some(),
|
||||
"completed command should report a process id"
|
||||
);
|
||||
assert!(result.truncated_output().contains("codex"));
|
||||
|
||||
assert!(
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.process_store
|
||||
.lock()
|
||||
.await
|
||||
.processes
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn reusing_completed_process_returns_unknown_process() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?;
|
||||
let process_id = open_shell.process_id.expect("expected process id");
|
||||
|
||||
write_stdin(&session, process_id, "exit\n", 2_500).await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let err = write_stdin(&session, process_id, "", 100)
|
||||
.await
|
||||
.expect_err("expected unknown process error");
|
||||
|
||||
match err {
|
||||
UnifiedExecError::UnknownProcessId { process_id: err_id } => {
|
||||
assert_eq!(err_id, process_id, "process id should match request");
|
||||
}
|
||||
other => panic!("expected UnknownProcessId, got {other:?}"),
|
||||
}
|
||||
|
||||
assert!(
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.process_store
|
||||
.lock()
|
||||
.await
|
||||
.processes
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
#[path = "mod_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
use super::head_tail_buffer::HeadTailBuffer;
|
||||
use super::*;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::codex::make_session_and_context;
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::tools::context::ExecCommandToolOutput;
|
||||
use crate::unified_exec::ExecCommandRequest;
|
||||
use crate::unified_exec::WriteStdinRequest;
|
||||
use core_test_support::skip_if_sandbox;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::Duration;
|
||||
|
||||
async fn test_session_and_turn() -> (Arc<Session>, Arc<TurnContext>) {
|
||||
let (session, mut turn) = make_session_and_context().await;
|
||||
turn.approval_policy
|
||||
.set(AskForApproval::Never)
|
||||
.expect("test setup should allow updating approval policy");
|
||||
turn.sandbox_policy
|
||||
.set(SandboxPolicy::DangerFullAccess)
|
||||
.expect("test setup should allow updating sandbox policy");
|
||||
turn.file_system_sandbox_policy =
|
||||
codex_protocol::permissions::FileSystemSandboxPolicy::from(turn.sandbox_policy.get());
|
||||
turn.network_sandbox_policy =
|
||||
codex_protocol::permissions::NetworkSandboxPolicy::from(turn.sandbox_policy.get());
|
||||
(Arc::new(session), Arc::new(turn))
|
||||
}
|
||||
|
||||
async fn exec_command(
|
||||
session: &Arc<Session>,
|
||||
turn: &Arc<TurnContext>,
|
||||
cmd: &str,
|
||||
yield_time_ms: u64,
|
||||
) -> Result<ExecCommandToolOutput, UnifiedExecError> {
|
||||
let context =
|
||||
UnifiedExecContext::new(Arc::clone(session), Arc::clone(turn), "call".to_string());
|
||||
let process_id = session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.allocate_process_id()
|
||||
.await;
|
||||
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.exec_command(
|
||||
ExecCommandRequest {
|
||||
command: vec!["bash".to_string(), "-lc".to_string(), cmd.to_string()],
|
||||
process_id,
|
||||
yield_time_ms,
|
||||
max_output_tokens: None,
|
||||
workdir: None,
|
||||
network: None,
|
||||
tty: true,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
additional_permissions_preapproved: false,
|
||||
justification: None,
|
||||
prefix_rule: None,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn write_stdin(
|
||||
session: &Arc<Session>,
|
||||
process_id: i32,
|
||||
input: &str,
|
||||
yield_time_ms: u64,
|
||||
) -> Result<ExecCommandToolOutput, UnifiedExecError> {
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.write_stdin(WriteStdinRequest {
|
||||
process_id,
|
||||
input,
|
||||
yield_time_ms,
|
||||
max_output_tokens: None,
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn push_chunk_preserves_prefix_and_suffix() {
|
||||
let mut buffer = HeadTailBuffer::default();
|
||||
buffer.push_chunk(vec![b'a'; UNIFIED_EXEC_OUTPUT_MAX_BYTES]);
|
||||
buffer.push_chunk(vec![b'b']);
|
||||
buffer.push_chunk(vec![b'c']);
|
||||
|
||||
assert_eq!(buffer.retained_bytes(), UNIFIED_EXEC_OUTPUT_MAX_BYTES);
|
||||
let snapshot = buffer.snapshot_chunks();
|
||||
|
||||
let first = snapshot.first().expect("expected at least one chunk");
|
||||
assert_eq!(first.first(), Some(&b'a'));
|
||||
assert!(snapshot.iter().any(|chunk| chunk.as_slice() == b"b"));
|
||||
assert_eq!(
|
||||
snapshot
|
||||
.last()
|
||||
.expect("expected at least one chunk")
|
||||
.as_slice(),
|
||||
b"c"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn head_tail_buffer_default_preserves_prefix_and_suffix() {
|
||||
let mut buffer = HeadTailBuffer::default();
|
||||
buffer.push_chunk(vec![b'a'; UNIFIED_EXEC_OUTPUT_MAX_BYTES]);
|
||||
buffer.push_chunk(b"bc".to_vec());
|
||||
|
||||
let rendered = buffer.to_bytes();
|
||||
assert_eq!(rendered.first(), Some(&b'a'));
|
||||
assert!(rendered.ends_with(b"bc"));
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unified_exec_persists_across_requests() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?;
|
||||
let process_id = open_shell.process_id.expect("expected process_id");
|
||||
|
||||
write_stdin(
|
||||
&session,
|
||||
process_id,
|
||||
"export CODEX_INTERACTIVE_SHELL_VAR=codex\n",
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let out_2 = write_stdin(
|
||||
&session,
|
||||
process_id,
|
||||
"echo $CODEX_INTERACTIVE_SHELL_VAR\n",
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
out_2.truncated_output().contains("codex"),
|
||||
"expected environment variable output"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn multi_unified_exec_sessions() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let shell_a = exec_command(&session, &turn, "bash -i", 2_500).await?;
|
||||
let session_a = shell_a.process_id.expect("expected process id");
|
||||
|
||||
write_stdin(
|
||||
&session,
|
||||
session_a,
|
||||
"export CODEX_INTERACTIVE_SHELL_VAR=codex\n",
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let out_2 = exec_command(&session, &turn, "echo $CODEX_INTERACTIVE_SHELL_VAR", 2_500).await?;
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
assert!(
|
||||
out_2.process_id.is_none(),
|
||||
"short command should not report a process id if it exits quickly"
|
||||
);
|
||||
assert!(
|
||||
!out_2.truncated_output().contains("codex"),
|
||||
"short command should run in a fresh shell"
|
||||
);
|
||||
|
||||
let out_3 = write_stdin(
|
||||
&session,
|
||||
shell_a.process_id.expect("expected process id"),
|
||||
"echo $CODEX_INTERACTIVE_SHELL_VAR\n",
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
out_3.truncated_output().contains("codex"),
|
||||
"session should preserve state"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unified_exec_timeouts() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
const TEST_VAR_VALUE: &str = "unified_exec_var_123";
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?;
|
||||
let process_id = open_shell.process_id.expect("expected process id");
|
||||
|
||||
write_stdin(
|
||||
&session,
|
||||
process_id,
|
||||
format!("export CODEX_INTERACTIVE_SHELL_VAR={TEST_VAR_VALUE}\n").as_str(),
|
||||
2_500,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let out_2 = write_stdin(
|
||||
&session,
|
||||
process_id,
|
||||
"sleep 5 && echo $CODEX_INTERACTIVE_SHELL_VAR\n",
|
||||
10,
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
!out_2.truncated_output().contains(TEST_VAR_VALUE),
|
||||
"timeout too short should yield incomplete output"
|
||||
);
|
||||
|
||||
tokio::time::sleep(Duration::from_secs(7)).await;
|
||||
|
||||
let out_3 = write_stdin(&session, process_id, "", 100).await?;
|
||||
|
||||
assert!(
|
||||
out_3.truncated_output().contains(TEST_VAR_VALUE),
|
||||
"subsequent poll should retrieve output"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn unified_exec_pause_blocks_yield_timeout() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
session.set_out_of_band_elicitation_pause_state(true);
|
||||
|
||||
let paused_session = Arc::clone(&session);
|
||||
tokio::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(2)).await;
|
||||
paused_session.set_out_of_band_elicitation_pause_state(false);
|
||||
});
|
||||
|
||||
let started = tokio::time::Instant::now();
|
||||
let response = exec_command(&session, &turn, "sleep 1 && echo unified-exec-done", 250).await?;
|
||||
|
||||
assert!(
|
||||
started.elapsed() >= Duration::from_secs(2),
|
||||
"pause should block the unified exec yield timeout"
|
||||
);
|
||||
assert!(
|
||||
response.truncated_output().contains("unified-exec-done"),
|
||||
"exec_command should wait for output after the pause lifts"
|
||||
);
|
||||
assert!(
|
||||
response.process_id.is_none(),
|
||||
"completed command should not leave a background process"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Ignored while we have a better way to test this.
|
||||
async fn requests_with_large_timeout_are_capped() -> anyhow::Result<()> {
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let result = exec_command(&session, &turn, "echo codex", 120_000).await?;
|
||||
|
||||
assert!(result.process_id.is_some());
|
||||
assert!(result.truncated_output().contains("codex"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // Ignored while we have a better way to test this.
|
||||
async fn completed_commands_do_not_persist_sessions() -> anyhow::Result<()> {
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
let result = exec_command(&session, &turn, "echo codex", 2_500).await?;
|
||||
|
||||
assert!(
|
||||
result.process_id.is_some(),
|
||||
"completed command should report a process id"
|
||||
);
|
||||
assert!(result.truncated_output().contains("codex"));
|
||||
|
||||
assert!(
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.process_store
|
||||
.lock()
|
||||
.await
|
||||
.processes
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn reusing_completed_process_returns_unknown_process() -> anyhow::Result<()> {
|
||||
skip_if_sandbox!(Ok(()));
|
||||
|
||||
let (session, turn) = test_session_and_turn().await;
|
||||
|
||||
let open_shell = exec_command(&session, &turn, "bash -i", 2_500).await?;
|
||||
let process_id = open_shell.process_id.expect("expected process id");
|
||||
|
||||
write_stdin(&session, process_id, "exit\n", 2_500).await?;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
|
||||
let err = write_stdin(&session, process_id, "", 100)
|
||||
.await
|
||||
.expect_err("expected unknown process error");
|
||||
|
||||
match err {
|
||||
UnifiedExecError::UnknownProcessId { process_id: err_id } => {
|
||||
assert_eq!(err_id, process_id, "process id should match request");
|
||||
}
|
||||
other => panic!("expected UnknownProcessId, got {other:?}"),
|
||||
}
|
||||
|
||||
assert!(
|
||||
session
|
||||
.services
|
||||
.unified_exec_manager
|
||||
.process_store
|
||||
.lock()
|
||||
.await
|
||||
.processes
|
||||
.is_empty()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -832,104 +832,5 @@ enum ProcessStatus {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn unified_exec_env_injects_defaults() {
|
||||
let env = apply_unified_exec_env(HashMap::new());
|
||||
let expected = HashMap::from([
|
||||
("NO_COLOR".to_string(), "1".to_string()),
|
||||
("TERM".to_string(), "dumb".to_string()),
|
||||
("LANG".to_string(), "C.UTF-8".to_string()),
|
||||
("LC_CTYPE".to_string(), "C.UTF-8".to_string()),
|
||||
("LC_ALL".to_string(), "C.UTF-8".to_string()),
|
||||
("COLORTERM".to_string(), String::new()),
|
||||
("PAGER".to_string(), "cat".to_string()),
|
||||
("GIT_PAGER".to_string(), "cat".to_string()),
|
||||
("GH_PAGER".to_string(), "cat".to_string()),
|
||||
("CODEX_CI".to_string(), "1".to_string()),
|
||||
]);
|
||||
|
||||
assert_eq!(env, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_exec_env_overrides_existing_values() {
|
||||
let mut base = HashMap::new();
|
||||
base.insert("NO_COLOR".to_string(), "0".to_string());
|
||||
base.insert("PATH".to_string(), "/usr/bin".to_string());
|
||||
|
||||
let env = apply_unified_exec_env(base);
|
||||
|
||||
assert_eq!(env.get("NO_COLOR"), Some(&"1".to_string()));
|
||||
assert_eq!(env.get("PATH"), Some(&"/usr/bin".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_prefers_exited_processes_outside_recently_used() {
|
||||
let now = Instant::now();
|
||||
let meta = vec![
|
||||
(1, now - Duration::from_secs(40), false),
|
||||
(2, now - Duration::from_secs(30), true),
|
||||
(3, now - Duration::from_secs(20), false),
|
||||
(4, now - Duration::from_secs(19), false),
|
||||
(5, now - Duration::from_secs(18), false),
|
||||
(6, now - Duration::from_secs(17), false),
|
||||
(7, now - Duration::from_secs(16), false),
|
||||
(8, now - Duration::from_secs(15), false),
|
||||
(9, now - Duration::from_secs(14), false),
|
||||
(10, now - Duration::from_secs(13), false),
|
||||
];
|
||||
|
||||
let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);
|
||||
|
||||
assert_eq!(candidate, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_falls_back_to_lru_when_no_exited() {
|
||||
let now = Instant::now();
|
||||
let meta = vec![
|
||||
(1, now - Duration::from_secs(40), false),
|
||||
(2, now - Duration::from_secs(30), false),
|
||||
(3, now - Duration::from_secs(20), false),
|
||||
(4, now - Duration::from_secs(19), false),
|
||||
(5, now - Duration::from_secs(18), false),
|
||||
(6, now - Duration::from_secs(17), false),
|
||||
(7, now - Duration::from_secs(16), false),
|
||||
(8, now - Duration::from_secs(15), false),
|
||||
(9, now - Duration::from_secs(14), false),
|
||||
(10, now - Duration::from_secs(13), false),
|
||||
];
|
||||
|
||||
let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);
|
||||
|
||||
assert_eq!(candidate, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_protects_recent_processes_even_if_exited() {
|
||||
let now = Instant::now();
|
||||
let meta = vec![
|
||||
(1, now - Duration::from_secs(40), false),
|
||||
(2, now - Duration::from_secs(30), false),
|
||||
(3, now - Duration::from_secs(20), true),
|
||||
(4, now - Duration::from_secs(19), false),
|
||||
(5, now - Duration::from_secs(18), false),
|
||||
(6, now - Duration::from_secs(17), false),
|
||||
(7, now - Duration::from_secs(16), false),
|
||||
(8, now - Duration::from_secs(15), false),
|
||||
(9, now - Duration::from_secs(14), false),
|
||||
(10, now - Duration::from_secs(13), true),
|
||||
];
|
||||
|
||||
let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);
|
||||
|
||||
// (10) is exited but among the last 8; we should drop the LRU outside that set.
|
||||
assert_eq!(candidate, Some(1));
|
||||
}
|
||||
}
|
||||
#[path = "process_manager_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tokio::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
|
||||
#[test]
|
||||
fn unified_exec_env_injects_defaults() {
|
||||
let env = apply_unified_exec_env(HashMap::new());
|
||||
let expected = HashMap::from([
|
||||
("NO_COLOR".to_string(), "1".to_string()),
|
||||
("TERM".to_string(), "dumb".to_string()),
|
||||
("LANG".to_string(), "C.UTF-8".to_string()),
|
||||
("LC_CTYPE".to_string(), "C.UTF-8".to_string()),
|
||||
("LC_ALL".to_string(), "C.UTF-8".to_string()),
|
||||
("COLORTERM".to_string(), String::new()),
|
||||
("PAGER".to_string(), "cat".to_string()),
|
||||
("GIT_PAGER".to_string(), "cat".to_string()),
|
||||
("GH_PAGER".to_string(), "cat".to_string()),
|
||||
("CODEX_CI".to_string(), "1".to_string()),
|
||||
]);
|
||||
|
||||
assert_eq!(env, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unified_exec_env_overrides_existing_values() {
|
||||
let mut base = HashMap::new();
|
||||
base.insert("NO_COLOR".to_string(), "0".to_string());
|
||||
base.insert("PATH".to_string(), "/usr/bin".to_string());
|
||||
|
||||
let env = apply_unified_exec_env(base);
|
||||
|
||||
assert_eq!(env.get("NO_COLOR"), Some(&"1".to_string()));
|
||||
assert_eq!(env.get("PATH"), Some(&"/usr/bin".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_prefers_exited_processes_outside_recently_used() {
|
||||
let now = Instant::now();
|
||||
let meta = vec![
|
||||
(1, now - Duration::from_secs(40), false),
|
||||
(2, now - Duration::from_secs(30), true),
|
||||
(3, now - Duration::from_secs(20), false),
|
||||
(4, now - Duration::from_secs(19), false),
|
||||
(5, now - Duration::from_secs(18), false),
|
||||
(6, now - Duration::from_secs(17), false),
|
||||
(7, now - Duration::from_secs(16), false),
|
||||
(8, now - Duration::from_secs(15), false),
|
||||
(9, now - Duration::from_secs(14), false),
|
||||
(10, now - Duration::from_secs(13), false),
|
||||
];
|
||||
|
||||
let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);
|
||||
|
||||
assert_eq!(candidate, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_falls_back_to_lru_when_no_exited() {
|
||||
let now = Instant::now();
|
||||
let meta = vec![
|
||||
(1, now - Duration::from_secs(40), false),
|
||||
(2, now - Duration::from_secs(30), false),
|
||||
(3, now - Duration::from_secs(20), false),
|
||||
(4, now - Duration::from_secs(19), false),
|
||||
(5, now - Duration::from_secs(18), false),
|
||||
(6, now - Duration::from_secs(17), false),
|
||||
(7, now - Duration::from_secs(16), false),
|
||||
(8, now - Duration::from_secs(15), false),
|
||||
(9, now - Duration::from_secs(14), false),
|
||||
(10, now - Duration::from_secs(13), false),
|
||||
];
|
||||
|
||||
let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);
|
||||
|
||||
assert_eq!(candidate, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pruning_protects_recent_processes_even_if_exited() {
|
||||
let now = Instant::now();
|
||||
let meta = vec![
|
||||
(1, now - Duration::from_secs(40), false),
|
||||
(2, now - Duration::from_secs(30), false),
|
||||
(3, now - Duration::from_secs(20), true),
|
||||
(4, now - Duration::from_secs(19), false),
|
||||
(5, now - Duration::from_secs(18), false),
|
||||
(6, now - Duration::from_secs(17), false),
|
||||
(7, now - Duration::from_secs(16), false),
|
||||
(8, now - Duration::from_secs(15), false),
|
||||
(9, now - Duration::from_secs(14), false),
|
||||
(10, now - Duration::from_secs(13), true),
|
||||
];
|
||||
|
||||
let candidate = UnifiedExecProcessManager::process_id_to_prune_from_meta(&meta);
|
||||
|
||||
// (10) is exited but among the last 8; we should drop the LRU outside that set.
|
||||
assert_eq!(candidate, Some(1));
|
||||
}
|
||||
Reference in New Issue
Block a user