Skip to content

feat(huntsman): Add the end-to-end integration-test driver. - #379

Merged
LinZhihao-723 merged 7 commits into
y-scope:mainfrom
LinZhihao-723:test-driver-impl
Jul 10, 2026
Merged

feat(huntsman): Add the end-to-end integration-test driver.#379
LinZhihao-723 merged 7 commits into
y-scope:mainfrom
LinZhihao-723:test-driver-impl

Conversation

@LinZhihao-723

@LinZhihao-723 LinZhihao-723 commented Jul 8, 2026

Copy link
Copy Markdown
Member

Description

This PR adds the end-to-end integration-test driver as a new e2e crate under tests/huntsman/. The driver wraps spider-client's SpiderClient in a process-wide singleton with the concurrency control that end-to-end scenarios share, and exposes a small surface for driving a job (task graph + inputs) through a live Spider deployment and asserting its terminal outcome.

This PR adds the driver library only; the concrete test scenarios that use it are added in a follow-up.

Note

This PR does not add test scenarios. The e2e crate builds and lints as a library, and its tests/ directory is reserved for the scenarios added later.

New e2e crate (tests/huntsman/e2e/)

  • Added tests/huntsman/e2e/ as a new workspace member. The crate is named e2e: its src/ is the driver library, and its tests/ directory is reserved for the scenario suite.
  • Depends on spider-client and spider-core for the gRPC and core types, tokio (sync/time for the concurrency primitives and polling), tonic (to build the endpoint), and anyhow for the high-level error surface.

Scenario types (src/types.rs)

  • TerminationResult — the terminal outcome a scenario converges to: Success(Vec<TaskOutput>) carrying the job's task outputs, Failure(String) carrying the reported error message, or Cancelled.
  • JobSubmission — a single job to drive: an external resource-group id (String), the TaskGraph, and the job-level inputs (Vec<TaskInput>).

The driver (src/test_driver.rs)

  • SpiderTestDriver is a lazily initialized, process-wide singleton (tokio::sync::OnceCell). On first access it reads the Spider endpoint from SPIDER_ENDPOINT and the concurrency from SPIDER_CONCURRENCY (defaulting to 8), then connects a SpiderClient pool sized to that concurrency. It holds the client behind a RwLock, a Semaphore sized to the concurrency, and a Mutex<HashMap<String, ResourceGroupId>> for resource-group resolution.
  • SpiderTestDriver::run is the non-exclusive entry point: it acquires the client's shared read lock and a concurrency permit from the semaphore, then drives the scenario with no failure injection. Multiple run scenarios execute concurrently up to the configured limit.
  • SpiderTestDriver::run_exclusive is the exclusive entry point: it acquires the client's write lock so no other scenario runs concurrently, and drives the scenario with a caller-supplied failure-injection coroutine, which is what a failure-recovery scenario uses to perturb a running job.
  • In both cases the acquired lock (and, for run, the permit) is held for the entire scenario; the client is cloned under the lock and passed by value into the scenario runner.
  • Resource groups are resolved outside the scenario runner: the driver maps each external resource-group id to its Spider-assigned ResourceGroupId, registering it through the client on first use (with an empty password) and caching the result under the table lock, so subsequent submissions reuse the same id.
  • run_scenario is the shared core: it submits and starts the job, then concurrently runs the failure-injection coroutine while polling the job to a terminal state, bounding the whole race by timeout, and finally hands the TerminationResult to the caller's outcome-assertion coroutine.
  • submit_and_start_job performs both the submit and the start of the job through the client.
  • poll_until_terminal polls the job state every 10ms via get_job_state. On a terminal state it builds the corresponding TerminationResult: Success retrieves the job outputs, Failure retrieves the job error message, and Cancelled carries no payload.

Checklist

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

  • Ensure all workflows pass.

Summary by CodeRabbit

  • New Features
    • Added an end-to-end testing harness for running jobs against live deployments.
    • Added shared job submission and termination result types for test scenarios.
    • Added support for concurrent and exclusive test execution.
    • Added configurable endpoint, concurrency, timeout, failure-injection, and job outcome handling.

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds a workspace-integrated Huntsman E2E crate with shared job types and a singleton SpiderTestDriver that configures concurrency, resolves resource groups, runs jobs, polls terminal states, and invokes outcome assertions.

Changes

Huntsman E2E test harness

Layer / File(s) Summary
Crate wiring and job contracts
Cargo.toml, tests/huntsman/e2e/Cargo.toml, tests/huntsman/e2e/src/lib.rs, tests/huntsman/e2e/src/types.rs
Registers the E2E crate, declares Spider and async dependencies, and exposes JobSubmission, TerminationResult, and SpiderTestDriver.
Driver initialization and resource groups
tests/huntsman/e2e/src/test_driver.rs
Initializes a process-wide driver from environment variables, configures concurrency, and caches resource-group mappings.
Job submission and terminal outcome
tests/huntsman/e2e/src/test_driver.rs
Adds shared and exclusive execution, job submission and startup, concurrent failure injection and polling, timeout handling, terminal-state mapping, and outcome assertions.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HuntsmanE2EScenario
  participant SpiderTestDriver
  participant SpiderClient
  participant SpiderDeployment
  HuntsmanE2EScenario->>SpiderTestDriver: run or run_exclusive(JobSubmission)
  SpiderTestDriver->>SpiderClient: submit and start job
  SpiderClient->>SpiderDeployment: execute job
  SpiderTestDriver->>SpiderClient: poll job state and outputs
  HuntsmanE2EScenario->>SpiderTestDriver: perform failure injection callback
  SpiderClient-->>SpiderTestDriver: terminal state
  SpiderTestDriver-->>HuntsmanE2EScenario: invoke outcome assertion
Loading

Possibly related PRs

  • y-scope/spider#363: Adds SpiderClient APIs used by the new E2E driver for job submission, state, outputs, errors, and resource groups.

Suggested reviewers: sitaowang1998

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a Huntsman end-to-end integration-test driver.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@LinZhihao-723 LinZhihao-723 left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's merge #377 first and integrate NN-testing into this crate after that.

@LinZhihao-723
LinZhihao-723 marked this pull request as ready for review July 8, 2026 21:41
@LinZhihao-723
LinZhihao-723 requested review from a team and sitaowang1998 as code owners July 8, 2026 21:41
Comment thread tests/huntsman/e2e/src/test_driver.rs Outdated
///
/// # Panics
///
/// Panics if the default value is zero.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For most libraries/software I have used, 0 for concurrency setting usually means using the number of CPUs as the concurrency level.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should document this clearly: the default value is a compile-time constant which should never be 0.

const DEFAULT_CONCURRENCY: usize = 8;

match std::env::var(CONCURRENCY_ENV_VAR) {
Ok(value) => value.parse::<NonZeroUsize>().with_context(|| {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should guard against negative values and a crazily-large value.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Parsing a negative number should fail 'cuz the given type is unsigned.
  • I don't think it's necessary for a tester.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
tests/huntsman/e2e/src/types.rs (2)

8-17: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider deriving Debug for TerminationResult.

Without Debug, assertion failures that include a TerminationResult value will produce unhelpful error messages. Deriving Debug is low-cost and improves test diagnostics.

♻️ Proposed refactor
+#[derive(Debug)]
 pub enum TerminationResult {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/huntsman/e2e/src/types.rs` around lines 8 - 17, Derive the standard
Debug trait for the TerminationResult enum so assertion failures can display its
values; add Debug to the enum’s derives and ensure all contained types,
including TaskOutput, support Debug.

20-30: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider deriving Debug for JobSubmission.

Same rationale — deriving Debug aids diagnostics when submissions are logged or included in assertion failures.

♻️ Proposed refactor
+#[derive(Debug)]
 pub struct JobSubmission {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/huntsman/e2e/src/types.rs` around lines 20 - 30, Derive the Rust Debug
trait for the JobSubmission struct so job submissions can be formatted in
diagnostics, logs, and assertion failures; update its derive attributes while
preserving the existing fields and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/huntsman/e2e/src/test_driver.rs`:
- Line 160: Configure an explicit connection timeout before creating the client
in the test initialization flow around SpiderClient::connect, preferably in the
shared ConnectionPool::connect helper or by setting connect_timeout on the
Endpoint. Ensure unreachable Spider endpoints fail within a bounded duration
instead of allowing init to hang indefinitely.
- Around line 58-63: Acquire the concurrency permit before obtaining the client
read lock in the shared-scenario path. In the relevant test driver method, move
the concurrency_limiter.acquire() call and its context handling ahead of
driver.client.read().await so scenarios waiting for capacity do not hold the
read lock and can allow run_exclusive to acquire the write lock.

---

Nitpick comments:
In `@tests/huntsman/e2e/src/types.rs`:
- Around line 8-17: Derive the standard Debug trait for the TerminationResult
enum so assertion failures can display its values; add Debug to the enum’s
derives and ensure all contained types, including TaskOutput, support Debug.
- Around line 20-30: Derive the Rust Debug trait for the JobSubmission struct so
job submissions can be formatted in diagnostics, logs, and assertion failures;
update its derive attributes while preserving the existing fields and behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a1b39dbc-4a85-4e80-86eb-36d2c35f045e

📥 Commits

Reviewing files that changed from the base of the PR and between a1d05e8 and 3567013.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (5)
  • Cargo.toml
  • tests/huntsman/e2e/Cargo.toml
  • tests/huntsman/e2e/src/lib.rs
  • tests/huntsman/e2e/src/test_driver.rs
  • tests/huntsman/e2e/src/types.rs

Comment on lines +58 to +63
let client_guard = driver.client.read().await;
let _permit = driver
.concurrency_limiter
.acquire()
.await
.context("concurrency limiter closed")?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Acquire the concurrency permit before the read lock to prevent exclusive-scenario starvation.

The read lock on client is acquired at Line 58 before the semaphore permit at Line 59. If all permits are exhausted, a new shared scenario holds a read lock while waiting for a permit. tokio's RwLock does not guarantee write-lock fairness, so exclusive scenarios (which need a write lock via run_exclusive) can be starved indefinitely as long as new shared scenarios keep arriving and acquiring read locks before blocking on the semaphore.

Swapping the order ensures that waiting for a permit does not block exclusive scenarios.

🔒 Proposed fix
     let driver = Self::instance().await?;
-    let client_guard = driver.client.read().await;
     let _permit = driver
         .concurrency_limiter
         .acquire()
         .await
         .context("concurrency limiter closed")?;
+    let client_guard = driver.client.read().await;
     let client = client_guard.clone();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let client_guard = driver.client.read().await;
let _permit = driver
.concurrency_limiter
.acquire()
.await
.context("concurrency limiter closed")?;
let _permit = driver
.concurrency_limiter
.acquire()
.await
.context("concurrency limiter closed")?;
let client_guard = driver.client.read().await;
let client = client_guard.clone();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/huntsman/e2e/src/test_driver.rs` around lines 58 - 63, Acquire the
concurrency permit before obtaining the client read lock in the shared-scenario
path. In the relevant test driver method, move the concurrency_limiter.acquire()
call and its context handling ahead of driver.client.read().await so scenarios
waiting for capacity do not hold the read lock and can allow run_exclusive to
acquire the write lock.

Comment thread tests/huntsman/e2e/src/test_driver.rs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants