Skip to content

Commit 4405f45

Browse files
Speed up Rust E2E tests with shared clients (#2250)
Reuse file-scoped Copilot clients for eligible Rust E2E tests while retaining dedicated clients for startup and lifecycle-sensitive cases. Keep shared router tasks on a persistent runtime and bound harness cleanup.
1 parent a76c968 commit 4405f45

51 files changed

Lines changed: 2652 additions & 1643 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

rust/src/lib.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2196,6 +2196,60 @@ impl Client {
21962196
Ok(())
21972197
}
21982198

2199+
/// Start this client's notification and request router on the current runtime.
2200+
/// This is test-harness plumbing, not part of the supported SDK API.
2201+
#[cfg(feature = "test-support")]
2202+
#[doc(hidden)]
2203+
pub fn start_router_for_test(&self) {
2204+
self.inner.router.ensure_started(
2205+
&self.inner.notification_tx,
2206+
&self.inner.request_rx,
2207+
self.inner.llm_inference.get().cloned(),
2208+
self.inner.on_github_telemetry.clone(),
2209+
);
2210+
}
2211+
2212+
#[cfg(feature = "test-support")]
2213+
#[doc(hidden)]
2214+
/// Disconnect and delete every session owned by this test client's isolated
2215+
/// runtime. This is test-harness plumbing, not part of the supported SDK API.
2216+
pub async fn cleanup_sessions_for_test(&self) -> Result<()> {
2217+
let mut first_error = None;
2218+
2219+
for session_id in self.inner.router.session_ids() {
2220+
if let Err(error) = self
2221+
.call(
2222+
"session.destroy",
2223+
Some(serde_json::json!({ "sessionId": session_id })),
2224+
)
2225+
.await
2226+
&& first_error.is_none()
2227+
{
2228+
first_error = Some(error);
2229+
}
2230+
self.inner.router.unregister(&session_id);
2231+
}
2232+
2233+
match self.list_sessions(None).await {
2234+
Ok(sessions) => {
2235+
for session in sessions {
2236+
if let Err(error) = self.delete_session(&session.session_id).await
2237+
&& first_error.is_none()
2238+
{
2239+
first_error = Some(error);
2240+
}
2241+
}
2242+
}
2243+
Err(error) if first_error.is_none() => first_error = Some(error),
2244+
Err(_) => {}
2245+
}
2246+
2247+
match first_error {
2248+
Some(error) => Err(error),
2249+
None => Ok(()),
2250+
}
2251+
}
2252+
21992253
/// Return the ID of the most recently updated session, if any.
22002254
///
22012255
/// Useful for resuming the last conversation when the session ID was

rust/tests/e2e/abort.rs

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -10,77 +10,80 @@ use tokio::sync::{Mutex, mpsc, oneshot};
1010

1111
use super::support::{
1212
DEFAULT_TEST_TOKEN, assistant_message_content, recv_with_timeout, wait_for_event,
13-
with_e2e_context,
1413
};
1514

1615
#[tokio::test]
1716
async fn should_abort_during_active_streaming() {
18-
with_e2e_context("abort", "should_abort_during_active_streaming", |ctx| {
19-
Box::pin(async move {
20-
ctx.set_default_copilot_user();
21-
let client = ctx.start_client().await;
22-
let session = client
23-
.create_session(ctx.approve_all_session_config().with_streaming(true))
24-
.await
25-
.expect("create session");
26-
let events = session.subscribe();
17+
super::support::with_dedicated_e2e_context(
18+
"abort",
19+
"should_abort_during_active_streaming",
20+
|ctx| {
21+
Box::pin(async move {
22+
ctx.set_default_copilot_user();
23+
let client = ctx.start_client().await;
24+
let session = client
25+
.create_session(ctx.approve_all_session_config().with_streaming(true))
26+
.await
27+
.expect("create session");
28+
let events = session.subscribe();
2729

28-
session
30+
session
2931
.send(
3032
"Write a very long essay about the history of computing, covering every decade \
3133
from the 1940s to the 2020s in great detail.",
3234
)
3335
.await
3436
.expect("send long streaming turn");
3537

36-
let delta = wait_for_event(events, "assistant.message_delta", |event| {
37-
event.parsed_type() == SessionEventType::AssistantMessageDelta
38+
let delta = wait_for_event(events, "assistant.message_delta", |event| {
39+
event.parsed_type() == SessionEventType::AssistantMessageDelta
40+
})
41+
.await;
42+
assert!(
43+
!delta
44+
.typed_data::<AssistantMessageDeltaData>()
45+
.expect("assistant.message_delta data")
46+
.delta_content
47+
.is_empty()
48+
);
49+
50+
session.abort().await.expect("abort session");
51+
52+
// Session should be usable after abort. Wait for the specific recovery
53+
// message rather than racing against a late idle from the aborted turn.
54+
let recovery_events = session.subscribe();
55+
session
56+
.send("Say 'abort_recovery_ok'.")
57+
.await
58+
.expect("send recovery");
59+
let recovery = wait_for_event(
60+
recovery_events,
61+
"assistant.message containing abort_recovery_ok",
62+
|event| {
63+
event.parsed_type() == SessionEventType::AssistantMessage
64+
&& assistant_message_content(event)
65+
.to_lowercase()
66+
.contains("abort_recovery_ok")
67+
},
68+
)
69+
.await;
70+
assert!(
71+
assistant_message_content(&recovery)
72+
.to_lowercase()
73+
.contains("abort_recovery_ok")
74+
);
75+
76+
session.disconnect().await.expect("disconnect session");
77+
client.stop().await.expect("stop client");
3878
})
39-
.await;
40-
assert!(
41-
!delta
42-
.typed_data::<AssistantMessageDeltaData>()
43-
.expect("assistant.message_delta data")
44-
.delta_content
45-
.is_empty()
46-
);
47-
48-
session.abort().await.expect("abort session");
49-
50-
// Session should be usable after abort. Wait for the specific recovery
51-
// message rather than racing against a late idle from the aborted turn.
52-
let recovery_events = session.subscribe();
53-
session
54-
.send("Say 'abort_recovery_ok'.")
55-
.await
56-
.expect("send recovery");
57-
let recovery = wait_for_event(
58-
recovery_events,
59-
"assistant.message containing abort_recovery_ok",
60-
|event| {
61-
event.parsed_type() == SessionEventType::AssistantMessage
62-
&& assistant_message_content(event)
63-
.to_lowercase()
64-
.contains("abort_recovery_ok")
65-
},
66-
)
67-
.await;
68-
assert!(
69-
assistant_message_content(&recovery)
70-
.to_lowercase()
71-
.contains("abort_recovery_ok")
72-
);
73-
74-
session.disconnect().await.expect("disconnect session");
75-
client.stop().await.expect("stop client");
76-
})
77-
})
79+
},
80+
)
7881
.await;
7982
}
8083

8184
#[tokio::test]
8285
async fn should_abort_during_active_tool_execution() {
83-
with_e2e_context(
86+
super::support::with_dedicated_e2e_context(
8487
"abort",
8588
"should_abort_during_active_tool_execution",
8689
|ctx| {

rust/tests/e2e/ask_user.rs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ use github_copilot_sdk::{
1212
use serde_json::json;
1313
use tokio::sync::{Notify, mpsc};
1414

15-
use super::support::{
16-
DEFAULT_TEST_TOKEN, assistant_message_content, recv_with_timeout, with_e2e_context,
17-
};
15+
use super::support::{DEFAULT_TEST_TOKEN, assistant_message_content, recv_with_timeout};
1816

1917
#[tokio::test]
2018
async fn should_invoke_user_input_handler_when_model_uses_ask_user_tool() {
21-
with_e2e_context(
19+
super::support::with_shared_e2e_context(&E2E,
2220
"ask_user",
2321
"should_invoke_user_input_handler_when_model_uses_ask_user_tool",
2422
|ctx| {
@@ -62,7 +60,7 @@ async fn should_invoke_user_input_handler_when_model_uses_ask_user_tool() {
6260

6361
#[tokio::test]
6462
async fn should_receive_choices_in_user_input_request() {
65-
with_e2e_context(
63+
super::support::with_shared_e2e_context(&E2E,
6664
"ask_user",
6765
"should_receive_choices_in_user_input_request",
6866
|ctx| {
@@ -107,7 +105,7 @@ async fn should_receive_choices_in_user_input_request() {
107105

108106
#[tokio::test]
109107
async fn should_handle_freeform_user_input_response() {
110-
with_e2e_context(
108+
super::support::with_shared_e2e_context(&E2E,
111109
"ask_user",
112110
"should_handle_freeform_user_input_response",
113111
|ctx| {
@@ -164,7 +162,8 @@ async fn should_handle_freeform_user_input_response() {
164162
/// the handler observes the sibling tool while its own request is still pending.
165163
#[tokio::test]
166164
async fn ask_user_does_not_block_sibling_tool_call_in_same_turn() {
167-
with_e2e_context(
165+
super::support::with_shared_e2e_context(
166+
&E2E,
168167
"ask_user",
169168
"ask_user_does_not_block_sibling_tool_call_in_same_turn",
170169
|ctx| {
@@ -346,3 +345,5 @@ impl ToolHandler for SetMarkerTool {
346345
Ok(ToolResult::Text(format!("MARKER_{}", value.to_uppercase())))
347346
}
348347
}
348+
static E2E: super::support::SharedE2eGroup =
349+
super::support::SharedE2eGroup::standard("ask_user", 4);

rust/tests/e2e/builtin_tools.rs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::time::Duration;
22

33
use github_copilot_sdk::MessageOptions;
44

5-
use super::support::{assistant_message_content, with_e2e_context};
5+
use super::support::assistant_message_content;
66

77
/// Built-in tool tests spawn a real CLI subprocess and execute actual shell /
88
/// file tools. Under concurrent Windows CI load (e2e runs 4-wide on a 4-vCPU
@@ -16,7 +16,8 @@ fn message(prompt: &str) -> MessageOptions {
1616

1717
#[tokio::test]
1818
async fn should_capture_exit_code_in_output() {
19-
with_e2e_context(
19+
super::support::with_shared_e2e_context(
20+
&E2E,
2021
"builtin_tools",
2122
"should_capture_exit_code_in_output",
2223
|ctx| {
@@ -49,7 +50,7 @@ async fn should_capture_exit_code_in_output() {
4950

5051
#[tokio::test]
5152
async fn should_capture_stderr_output() {
52-
with_e2e_context("builtin_tools", "should_capture_stderr_output", |ctx| {
53+
super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_capture_stderr_output", |ctx| {
5354
Box::pin(async move {
5455
if cfg!(windows) {
5556
return;
@@ -77,7 +78,7 @@ async fn should_capture_stderr_output() {
7778

7879
#[tokio::test]
7980
async fn should_read_file_with_line_range() {
80-
with_e2e_context("builtin_tools", "should_read_file_with_line_range", |ctx| {
81+
super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_read_file_with_line_range", |ctx| {
8182
Box::pin(async move {
8283
ctx.set_default_copilot_user();
8384
std::fs::write(ctx.work_dir().join("lines.txt"), "line1\nline2\nline3\nline4\nline5\n")
@@ -106,7 +107,7 @@ async fn should_read_file_with_line_range() {
106107

107108
#[tokio::test]
108109
async fn should_handle_nonexistent_file_gracefully() {
109-
with_e2e_context(
110+
super::support::with_shared_e2e_context(&E2E,
110111
"builtin_tools",
111112
"should_handle_nonexistent_file_gracefully",
112113
|ctx| {
@@ -144,7 +145,7 @@ async fn should_handle_nonexistent_file_gracefully() {
144145

145146
#[tokio::test]
146147
async fn should_edit_a_file_successfully() {
147-
with_e2e_context("builtin_tools", "should_edit_a_file_successfully", |ctx| {
148+
super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_edit_a_file_successfully", |ctx| {
148149
Box::pin(async move {
149150
ctx.set_default_copilot_user();
150151
std::fs::write(ctx.work_dir().join("edit_me.txt"), "Hello World\nGoodbye World\n")
@@ -171,7 +172,7 @@ async fn should_edit_a_file_successfully() {
171172

172173
#[tokio::test]
173174
async fn should_create_a_new_file() {
174-
with_e2e_context("builtin_tools", "should_create_a_new_file", |ctx| {
175+
super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_create_a_new_file", |ctx| {
175176
Box::pin(async move {
176177
ctx.set_default_copilot_user();
177178
let client = ctx.start_client().await;
@@ -196,7 +197,7 @@ async fn should_create_a_new_file() {
196197

197198
#[tokio::test]
198199
async fn should_search_for_patterns_in_files() {
199-
with_e2e_context(
200+
super::support::with_shared_e2e_context(&E2E,
200201
"builtin_tools",
201202
"should_search_for_patterns_in_files",
202203
|ctx| {
@@ -229,7 +230,7 @@ async fn should_search_for_patterns_in_files() {
229230

230231
#[tokio::test]
231232
async fn should_find_files_by_pattern() {
232-
with_e2e_context("builtin_tools", "should_find_files_by_pattern", |ctx| {
233+
super::support::with_shared_e2e_context(&E2E, "builtin_tools", "should_find_files_by_pattern", |ctx| {
233234
Box::pin(async move {
234235
ctx.set_default_copilot_user();
235236
let src = ctx.work_dir().join("src");
@@ -256,3 +257,5 @@ async fn should_find_files_by_pattern() {
256257
})
257258
.await;
258259
}
260+
static E2E: super::support::SharedE2eGroup =
261+
super::support::SharedE2eGroup::standard("builtin_tools", 8);

0 commit comments

Comments
 (0)