Skip to content

Commit 9a3443d

Browse files
authored
test: reduce test execution time (#726)
#### Overview Reduce normal hermetic test execution time by building dynamic-plugin fixtures once per test command and reusing them across Rust, Python, FFI, and Go tests. The updated suites keep all observed individual tests below five seconds while preserving package-build coverage outside normal test cases. - [x] I confirm this contribution is my own work, or I have the right to submit it under this project's license. - [x] I searched existing issues and open pull requests, and this does not duplicate existing work. #### Details - Add `just build-test-plugin-fixtures` and a stable, platform-specific fixture path contract under `target/test-plugin-fixtures`. - Prepare fixtures automatically in the Rust, Python, and Go test recipes; focused raw tests now fail quickly with preparation instructions when fixtures are missing. - Move the Python wheel/sdist/rebuild/import regression from pytest into `package-python-plugin`. - Make every Redis integration test honor `NEMO_RELAY_RUN_REDIS_TESTS` before connecting. - Reduce test-only real-time guards to five seconds, add pytest slow-duration reporting, and lower reusable Rust, Python, Node, and Go test-job limits from 120 to 30 minutes. - Document the fixture workflow and five-second test design expectation in contributor guidance and affected maintainer skills. - No public runtime, binding, ABI, or package API changes. Validation: - `just ci=true test-rust`: 3,850 passed in 33.1s; slowest test 3.687s. - `just ci=true test-python`: 639 passed in 14.94s; slowest test 2.01s. - `just test-python-plugin`: 124 passed in 2.36s; Python worker round trip passed in 1.03s. - `just ci=true test-go`: passed; main package completed in 3.52s. - `just test-node`: 353 passed; slowest individual test 1.115s. - Focused native, worker, and FFI plugin suites completed in 0.95s, 1.53s, and 0.16s respectively; non-opted-in Redis tests skipped in effectively 0.00s. - `just package-python-plugin` - `cargo fmt --all` - `cargo clippy --workspace --all-targets -- -D warnings` - `uv run pre-commit run --all-files` - `just docs` `just ci=true test-node` could not run locally because the installed executable reports Node 26.5.0, outside the repository's supported Node 24 toolchain, and the pinned `c8`/`yargs` coverage wrapper fails before starting tests. The underlying Node suite passes as reported above. #### Where should the reviewer start? Start with the shared fixture preparation in `justfile`, then review the native and worker consumers in `crates/core/tests/integration/` and the package validation moved to `scripts/validate_python_plugin_package.py`. #### Related Issues: (use one of the action keywords Closes / Fixes / Resolves / Relates to) - Relates to: none ## Summary by CodeRabbit - **Tests** - Dynamic plugin tests now use prebuilt fixtures, improving consistency and reducing test setup time. - Redis integration tests skip cleanly unless explicitly enabled. - Test and process timeouts were standardized at five seconds, helping failures surface sooner. - Added comprehensive validation for Python plugin packages, including wheels, source archives, rebuilding, installation, and generated bindings. - **Documentation** - Updated testing guidance and validation commands for plugin fixtures and package checks. - **Chores** - Reduced CI job limits from 120 minutes to 30 minutes. Authors: - Will Killian (https://github.com/willkill07) Approvers: - Eric Evans II (https://github.com/ericevans-nv) URL: #726
1 parent 5c807f3 commit 9a3443d

25 files changed

Lines changed: 708 additions & 433 deletions

File tree

.agents/skills/maintain-dynamic-plugins/SKILL.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ Use this skill for `plugin.kind = "rust_dynamic"`, `plugin.kind = "worker"`,
5959
## Validation
6060

6161
```bash
62+
just build-test-plugin-fixtures
6263
cargo test -p nemo-relay-types
6364
cargo test -p nemo-relay-plugin
6465
cargo test -p nemo-relay-worker-proto
@@ -70,6 +71,11 @@ just test-python
7071
just docs
7172
```
7273

74+
The canonical `just test-rust`, `just test-python`, and `just test-go` recipes
75+
prepare plugin fixtures automatically. Run `just build-test-plugin-fixtures`
76+
before raw focused native or worker plugin tests; fixture compilation must not
77+
happen inside an individual test case.
78+
7379
For broad runtime or public API changes, run the full `validate-change` matrix.
7480

7581
## References

.agents/skills/test-python-binding/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ docs/examples.
5757
# Focused test loop
5858
uv run pytest -k "<pattern>"
5959

60+
# Required first for focused dynamic-plugin host tests
61+
just build-test-plugin-fixtures
62+
uv run pytest python/tests/test_dynamic_plugin_host.py
63+
6064
# Focused Python worker plugin SDK suite
6165
just test-python-plugin
6266

.agents/skills/test-rust-core/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ cargo test -p nemo-relay
4747
cargo test -p nemo-relay-adaptive
4848

4949
# Dynamic plugin crates when touched
50+
just build-test-plugin-fixtures
5051
cargo test -p nemo-relay-types
5152
cargo test -p nemo-relay-plugin
5253
cargo test -p nemo-relay-worker-proto

.github/workflows/ci_go.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
Test:
2525
name: Test (${{ matrix.platform }})
2626
runs-on: ${{ matrix.runner }}
27-
timeout-minutes: 120
27+
timeout-minutes: 30
2828
continue-on-error: ${{ startsWith(matrix.platform, 'windows') }}
2929
permissions:
3030
contents: read

.github/workflows/ci_node.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
Test:
4444
name: Test (${{ matrix.platform }})
4545
runs-on: ${{ matrix.runner }}
46-
timeout-minutes: 120
46+
timeout-minutes: 30
4747
continue-on-error: ${{ startsWith(matrix.platform, 'windows') }}
4848
permissions:
4949
contents: read

.github/workflows/ci_python.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
Test:
4444
name: Test (${{ matrix.platform }})
4545
runs-on: ${{ matrix.runner }}
46-
timeout-minutes: 120
46+
timeout-minutes: 30
4747
continue-on-error: ${{ startsWith(matrix.platform, 'windows') }}
4848
permissions:
4949
contents: read

.github/workflows/ci_rust.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
Test:
3131
name: Test (${{ matrix.platform }})
3232
runs-on: ${{ matrix.runner }}
33-
timeout-minutes: 120
33+
timeout-minutes: 30
3434
continue-on-error: ${{ startsWith(matrix.platform, 'windows') }}
3535
permissions:
3636
contents: read

crates/adaptive/tests/integration/redis_tests.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,7 @@ fn enable_operational_logs() {
5555
/// Redis tests were not explicitly enabled or Redis is unavailable.
5656
async fn get_test_redis() -> Option<RedisBackend> {
5757
enable_operational_logs();
58-
let redis_test_env =
59-
std::env::var_os(REDIS_TEST_ENV).map(|value| value.to_string_lossy().into_owned());
60-
if !env_value_is_truthy(redis_test_env.as_deref()) {
61-
eprintln!(
62-
"SKIP: set {REDIS_TEST_ENV} to a truthy value (for example, {REDIS_TEST_ENV}=1) to run Redis-backed tests"
63-
);
58+
if !redis_tests_enabled() {
6459
return None;
6560
}
6661

@@ -75,8 +70,23 @@ async fn get_test_redis() -> Option<RedisBackend> {
7570
}
7671
}
7772

73+
fn redis_tests_enabled() -> bool {
74+
let redis_test_env =
75+
std::env::var_os(REDIS_TEST_ENV).map(|value| value.to_string_lossy().into_owned());
76+
if !env_value_is_truthy(redis_test_env.as_deref()) {
77+
eprintln!(
78+
"SKIP: set {REDIS_TEST_ENV} to a truthy value (for example, {REDIS_TEST_ENV}=1) to run Redis-backed tests"
79+
);
80+
return false;
81+
}
82+
true
83+
}
84+
7885
async fn get_test_redis_with_prefix() -> Option<(RedisBackend, String)> {
7986
enable_operational_logs();
87+
if !redis_tests_enabled() {
88+
return None;
89+
}
8090
let prefix = format!("test:{}:", Uuid::now_v7());
8191
match RedisBackend::new("redis://127.0.0.1/", prefix.clone()).await {
8292
Ok(backend) => Some((backend, prefix)),

crates/cli/tests/cli_tests.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ fn gateway_bin() -> &'static str {
2525

2626
const ACTIVE_GENERATION_TOKEN: &str = "active-generation";
2727
const BOOTSTRAP_PROTOCOL_VERSION: u64 = 3;
28-
const SIDECAR_PUBLICATION_TIMEOUT: Duration = Duration::from_secs(30);
28+
const CHILD_PROCESS_TIMEOUT_SECONDS: u64 = 5;
29+
const SIDECAR_PUBLICATION_TIMEOUT: Duration = Duration::from_secs(5);
2930

3031
fn write_active_generation(temp: &std::path::Path) -> std::path::PathBuf {
3132
let generation = temp.join("plugin/.nemo-relay-generation");
@@ -1466,15 +1467,15 @@ fn find_runtime_files_matching(
14661467
}
14671468

14681469
fn wait_child(child: &mut Child) -> ExitStatus {
1469-
let deadline = Instant::now() + Duration::from_secs(10);
1470+
let deadline = Instant::now() + Duration::from_secs(CHILD_PROCESS_TIMEOUT_SECONDS);
14701471
loop {
14711472
if let Some(status) = child.try_wait().unwrap() {
14721473
return status;
14731474
}
14741475
if Instant::now() >= deadline {
14751476
let _ = child.kill();
14761477
let _ = child.wait();
1477-
panic!("child process did not exit within 10 seconds");
1478+
panic!("child process did not exit within {CHILD_PROCESS_TIMEOUT_SECONDS} seconds");
14781479
}
14791480
thread::sleep(Duration::from_millis(20));
14801481
}
@@ -1523,15 +1524,15 @@ fn wait_child_with_output(mut child: Child) -> Output {
15231524

15241525
let stdout = read_pipe(child.stdout.take());
15251526
let stderr = read_pipe(child.stderr.take());
1526-
let deadline = Instant::now() + Duration::from_secs(10);
1527+
let deadline = Instant::now() + Duration::from_secs(CHILD_PROCESS_TIMEOUT_SECONDS);
15271528
let status = loop {
15281529
if let Some(status) = child.try_wait().unwrap() {
15291530
break status;
15301531
}
15311532
if Instant::now() >= deadline {
15321533
let _ = child.kill();
15331534
let _ = child.wait();
1534-
panic!("child process did not exit within 10 seconds");
1535+
panic!("child process did not exit within {CHILD_PROCESS_TIMEOUT_SECONDS} seconds");
15351536
}
15361537
thread::sleep(Duration::from_millis(20));
15371538
};
@@ -1607,7 +1608,7 @@ fn run_persistent_hook_with_token(
16071608
}
16081609

16091610
fn wait_for_port_closed(address: SocketAddr) {
1610-
let deadline = Instant::now() + Duration::from_secs(10);
1611+
let deadline = Instant::now() + Duration::from_secs(5);
16111612
loop {
16121613
if TcpStream::connect_timeout(&address, Duration::from_millis(100)).is_err() {
16131614
return;
@@ -4341,7 +4342,7 @@ fn assert_non_tty_signal_forwarding(
43414342

43424343
#[cfg(unix)]
43434344
fn wait_for_agent_pid_file(relay: &mut std::process::Child, pids: &Path, signal_name: &str) {
4344-
let deadline = Instant::now() + Duration::from_secs(10);
4345+
let deadline = Instant::now() + Duration::from_secs(5);
43454346
while !pids.is_file() {
43464347
if Instant::now() >= deadline {
43474348
// SAFETY: Relay's PID is live and owned by this test; SIGTERM exercises its registered
@@ -4593,7 +4594,7 @@ fn collect_replacement_requests(
45934594
stopped: &AtomicBool,
45944595
requests: &Mutex<Vec<String>>,
45954596
) {
4596-
let deadline = Instant::now() + Duration::from_secs(12);
4597+
let deadline = Instant::now() + Duration::from_secs(5);
45974598
while !stopped.load(Ordering::Relaxed) && Instant::now() < deadline {
45984599
match listener.accept() {
45994600
Ok((mut stream, _)) => {

crates/cli/tests/coverage/agents/plugin_install_tests.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1168,7 +1168,7 @@ fn cross_process_lock_holder() {
11681168
return;
11691169
}
11701170
std::fs::write(ready, b"ready").unwrap();
1171-
let deadline = Instant::now() + Duration::from_secs(10);
1171+
let deadline = Instant::now() + Duration::from_secs(5);
11721172
while !release.exists() {
11731173
assert!(Instant::now() < deadline, "lock holder release timed out");
11741174
thread::sleep(Duration::from_millis(10));

0 commit comments

Comments
 (0)