Skip to content

Commit 7b621fb

Browse files
ai-and-iclaude
andcommitted
Add extended browser test coverage for streams, datagrams, and error handling
Tests cover half-close semantics, stream priorities, rapid stream creation, concurrent bidi/uni streams, session stats, datagram edge cases (max size, oversized, empty, high water marks), and post-close/reset/stop error behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a5e1efe commit 7b621fb

5 files changed

Lines changed: 1130 additions & 4 deletions

File tree

web-transport-browser-tests/tests/bidi_stream.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,3 +386,132 @@ async fn read_from_cancelled_stream() {
386386
let result = result.unwrap();
387387
assert!(result.success, "{}", result.message);
388388
}
389+
390+
// ---------------------------------------------------------------------------
391+
// Half-Close & Priority
392+
// ---------------------------------------------------------------------------
393+
394+
#[tokio::test]
395+
async fn bidi_stream_half_close_write_then_read() {
396+
init_tracing();
397+
398+
let handler: ServerHandler = Box::new(|session| {
399+
Box::pin(async move {
400+
let (mut send, mut recv) = session.accept_bi().await.expect("accept_bi failed");
401+
let data = recv
402+
.read_to_end(64 * 1024)
403+
.await
404+
.expect("read_to_end failed");
405+
// Double the data to make the half-close assertion unambiguous
406+
let mut doubled = data.clone();
407+
doubled.extend_from_slice(&data);
408+
send.write_all(&doubled).await.expect("write_all failed");
409+
send.finish().expect("finish failed");
410+
let err = session.closed().await;
411+
assert!(
412+
matches!(
413+
err,
414+
SessionError::WebTransportError(WebTransportError::Closed(_, _))
415+
),
416+
"expected WebTransportError::Closed, got {err}"
417+
);
418+
})
419+
});
420+
421+
let harness = harness::setup(handler).await.unwrap();
422+
423+
let result = harness
424+
.run_js(
425+
r#"
426+
const wt = await connectWebTransport();
427+
const stream = await wt.createBidirectionalStream();
428+
const writer = stream.writable.getWriter();
429+
const reader = stream.readable.getReader();
430+
431+
await writer.write(new TextEncoder().encode("abc"));
432+
await writer.close();
433+
434+
let received = "";
435+
while (true) {
436+
const { value, done } = await reader.read();
437+
if (done) break;
438+
received += new TextDecoder().decode(value);
439+
}
440+
wt.close();
441+
return {
442+
success: received === "abcabc",
443+
message: "received: " + received
444+
};
445+
"#,
446+
TIMEOUT,
447+
)
448+
.await;
449+
450+
harness.teardown().await;
451+
let result = result.unwrap();
452+
assert!(result.success, "{}", result.message);
453+
}
454+
455+
#[tokio::test]
456+
async fn bidi_stream_server_priority() {
457+
init_tracing();
458+
459+
let handler: ServerHandler = Box::new(|session| {
460+
Box::pin(async move {
461+
for i in 0..3i32 {
462+
let (mut send, _recv) = session.open_bi().await.expect("open_bi failed");
463+
send.set_priority(i).expect("set_priority failed");
464+
let msg = format!("prio{i}");
465+
send.write_all(msg.as_bytes())
466+
.await
467+
.expect("write_all failed");
468+
send.finish().expect("finish failed");
469+
}
470+
let err = session.closed().await;
471+
assert!(
472+
matches!(
473+
err,
474+
SessionError::WebTransportError(WebTransportError::Closed(_, _))
475+
),
476+
"expected WebTransportError::Closed, got {err}"
477+
);
478+
})
479+
});
480+
481+
let harness = harness::setup(handler).await.unwrap();
482+
483+
let result = harness
484+
.run_js(
485+
r#"
486+
const wt = await connectWebTransport();
487+
const reader = wt.incomingBidirectionalStreams.getReader();
488+
const messages = [];
489+
for (let i = 0; i < 3; i++) {
490+
const { value: stream, done } = await reader.read();
491+
if (done) break;
492+
const sr = stream.readable.getReader();
493+
let msg = "";
494+
while (true) {
495+
const { value, done } = await sr.read();
496+
if (done) break;
497+
msg += new TextDecoder().decode(value);
498+
}
499+
messages.push(msg);
500+
}
501+
messages.sort();
502+
const expected = ["prio0", "prio1", "prio2"];
503+
const ok = JSON.stringify(messages) === JSON.stringify(expected);
504+
wt.close();
505+
return {
506+
success: ok,
507+
message: "messages: " + JSON.stringify(messages)
508+
};
509+
"#,
510+
TIMEOUT,
511+
)
512+
.await;
513+
514+
harness.teardown().await;
515+
let result = result.unwrap();
516+
assert!(result.success, "{}", result.message);
517+
}

web-transport-browser-tests/tests/concurrent.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use std::time::Duration;
2+
13
use web_transport_browser_tests::harness;
24
use web_transport_browser_tests::server::ServerHandler;
35
use web_transport_quinn::{SessionError, WebTransportError};
@@ -62,6 +64,185 @@ async fn multiple_bidi_streams_concurrent() {
6264
assert!(result.success, "{}", result.message);
6365
}
6466

67+
// ---------------------------------------------------------------------------
68+
// Race conditions, rapid creation, mixed server streams
69+
// ---------------------------------------------------------------------------
70+
71+
#[tokio::test]
72+
async fn rapid_stream_creation() {
73+
init_tracing();
74+
let harness = harness::setup(harness::echo_handler()).await.unwrap();
75+
76+
let result = harness
77+
.run_js(
78+
r#"
79+
const wt = await connectWebTransport();
80+
const N = 50;
81+
const promises = [];
82+
for (let i = 0; i < N; i++) {
83+
promises.push((async () => {
84+
const stream = await wt.createBidirectionalStream();
85+
const writer = stream.writable.getWriter();
86+
const reader = stream.readable.getReader();
87+
await writer.write(new Uint8Array([i % 256]));
88+
await writer.close();
89+
const { value, done } = await reader.read();
90+
if (done) return false;
91+
return value.length === 1 && value[0] === (i % 256);
92+
})());
93+
}
94+
const results = await Promise.all(promises);
95+
const allOk = results.every(r => r === true);
96+
wt.close();
97+
return {
98+
success: allOk,
99+
message: results.filter(r => !r).length + " of " + N + " failed"
100+
};
101+
"#,
102+
LONG_TIMEOUT,
103+
)
104+
.await;
105+
106+
harness.teardown().await;
107+
let result = result.unwrap();
108+
assert!(result.success, "{}", result.message);
109+
}
110+
111+
#[tokio::test]
112+
async fn server_close_while_client_creating_streams() {
113+
init_tracing();
114+
115+
let handler: ServerHandler = Box::new(|session| {
116+
Box::pin(async move {
117+
// Accept the first stream
118+
let _s1 = session.accept_bi().await.expect("accept_bi failed");
119+
tokio::time::sleep(Duration::from_millis(100)).await;
120+
session.close(99, b"closing");
121+
session.closed().await;
122+
})
123+
});
124+
125+
let harness = harness::setup(handler).await.unwrap();
126+
127+
let result = harness
128+
.run_js(
129+
r#"
130+
const wt = await connectWebTransport();
131+
const N = 20;
132+
const promises = [];
133+
for (let i = 0; i < N; i++) {
134+
promises.push((async () => {
135+
try {
136+
const stream = await wt.createBidirectionalStream();
137+
const writer = stream.writable.getWriter();
138+
await writer.write(new Uint8Array([i]));
139+
await writer.close();
140+
return true;
141+
} catch (e) {
142+
return false;
143+
}
144+
})());
145+
}
146+
const results = await Promise.all(promises);
147+
const succeeded = results.filter(r => r).length;
148+
const failed = results.filter(r => !r).length;
149+
try { await wt.closed; } catch (e) {}
150+
return {
151+
success: succeeded >= 1,
152+
message: "succeeded=" + succeeded + " failed=" + failed
153+
};
154+
"#,
155+
TIMEOUT,
156+
)
157+
.await;
158+
159+
harness.teardown().await;
160+
let result = result.unwrap();
161+
assert!(result.success, "{}", result.message);
162+
}
163+
164+
#[tokio::test]
165+
async fn server_opens_bidi_and_uni_simultaneously() {
166+
init_tracing();
167+
168+
let handler: ServerHandler = Box::new(|session| {
169+
Box::pin(async move {
170+
let (mut bi_send, _bi_recv) = session.open_bi().await.expect("open_bi failed");
171+
bi_send
172+
.write_all(b"bidi-data")
173+
.await
174+
.expect("write_all failed");
175+
bi_send.finish().expect("finish failed");
176+
177+
let mut uni_send = session.open_uni().await.expect("open_uni failed");
178+
uni_send
179+
.write_all(b"uni-data")
180+
.await
181+
.expect("write_all failed");
182+
uni_send.finish().expect("finish failed");
183+
184+
let err = session.closed().await;
185+
assert!(
186+
matches!(
187+
err,
188+
SessionError::WebTransportError(WebTransportError::Closed(_, _))
189+
),
190+
"expected WebTransportError::Closed, got {err}"
191+
);
192+
})
193+
});
194+
195+
let harness = harness::setup(handler).await.unwrap();
196+
197+
let result = harness
198+
.run_js(
199+
r#"
200+
const wt = await connectWebTransport();
201+
const [bidiResult, uniResult] = await Promise.all([
202+
(async () => {
203+
const reader = wt.incomingBidirectionalStreams.getReader();
204+
const { value: stream, done } = await reader.read();
205+
if (done) return "";
206+
const sr = stream.readable.getReader();
207+
let msg = "";
208+
while (true) {
209+
const { value, done } = await sr.read();
210+
if (done) break;
211+
msg += new TextDecoder().decode(value);
212+
}
213+
return msg;
214+
})(),
215+
(async () => {
216+
const reader = wt.incomingUnidirectionalStreams.getReader();
217+
const { value: stream, done } = await reader.read();
218+
if (done) return "";
219+
const sr = stream.getReader();
220+
let msg = "";
221+
while (true) {
222+
const { value, done } = await sr.read();
223+
if (done) break;
224+
msg += new TextDecoder().decode(value);
225+
}
226+
return msg;
227+
})()
228+
]);
229+
230+
wt.close();
231+
const ok = bidiResult === "bidi-data" && uniResult === "uni-data";
232+
return {
233+
success: ok,
234+
message: "bidi=" + bidiResult + " uni=" + uniResult
235+
};
236+
"#,
237+
TIMEOUT,
238+
)
239+
.await;
240+
241+
harness.teardown().await;
242+
let result = result.unwrap();
243+
assert!(result.success, "{}", result.message);
244+
}
245+
65246
#[tokio::test]
66247
async fn multiple_streams_mixed_types() {
67248
init_tracing();

0 commit comments

Comments
 (0)