Skip to content
This repository was archived by the owner on Feb 22, 2026. It is now read-only.

Commit 357f52b

Browse files
committed
test: add some tests of recent stop & drop additions
1 parent 78c9be6 commit 357f52b

2 files changed

Lines changed: 183 additions & 59 deletions

File tree

tests/cynic-tests.rs

Lines changed: 110 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
use std::{future::IntoFuture, time::Duration};
22

33
use assert_matches::assert_matches;
4+
use cynic::StreamingOperation;
45
use futures_lite::{StreamExt, future};
6+
use graphql_ws_client::Subscription;
57
use subscription_server::SubscriptionServer;
68
use tokio::time::sleep;
79

@@ -54,29 +56,14 @@ struct BooksChangedSubscription {
5456

5557
#[tokio::test]
5658
async fn main_test() {
57-
use async_tungstenite::tungstenite::{client::IntoClientRequest, http::HeaderValue};
58-
5959
let server = SubscriptionServer::start().await;
6060

61-
sleep(Duration::from_millis(20)).await;
62-
63-
let mut request = server.websocket_url().into_client_request().unwrap();
64-
request.headers_mut().insert(
65-
"Sec-WebSocket-Protocol",
66-
HeaderValue::from_str("graphql-transport-ws").unwrap(),
67-
);
68-
69-
let (connection, _) = async_tungstenite::tokio::connect_async(request)
70-
.await
71-
.unwrap();
72-
73-
println!("Connected");
74-
75-
let (client, actor) = graphql_ws_client::Client::build(connection).await.unwrap();
61+
let client_builder = server.client_builder().await;
62+
let (client, actor) = client_builder.await.unwrap();
7663

7764
tokio::spawn(actor.into_future());
7865

79-
let stream = client.subscribe(build_query()).await.unwrap();
66+
let mut stream = client.subscribe(build_query()).await.unwrap();
8067

8168
sleep(Duration::from_millis(100)).await;
8269

@@ -95,50 +82,16 @@ async fn main_test() {
9582
},
9683
];
9784

98-
future::zip(
99-
async {
100-
for update in &updates {
101-
server.send(update.to_owned()).unwrap();
102-
}
103-
},
104-
async {
105-
let received_updates = stream.take(updates.len()).collect::<Vec<_>>().await;
106-
107-
for (expected, update) in updates.iter().zip(received_updates) {
108-
let update = update.unwrap();
109-
assert_matches!(update.errors, None);
110-
let data = update.data.unwrap();
111-
assert_eq!(data.books.id.inner(), expected.id.0);
112-
}
113-
},
114-
)
115-
.await;
85+
send_and_verify_updates(&server, &updates, &mut stream).await;
11686
}
11787

11888
#[tokio::test]
11989
async fn oneshot_operation_test() {
120-
use async_tungstenite::tungstenite::{client::IntoClientRequest, http::HeaderValue};
121-
12290
let server = SubscriptionServer::start().await;
12391

124-
sleep(Duration::from_millis(20)).await;
125-
126-
let mut request = server.websocket_url().into_client_request().unwrap();
127-
request.headers_mut().insert(
128-
"Sec-WebSocket-Protocol",
129-
HeaderValue::from_str("graphql-transport-ws").unwrap(),
130-
);
131-
132-
let (connection, _) = async_tungstenite::tokio::connect_async(request)
133-
.await
134-
.unwrap();
92+
let client_builder = server.client_builder().await;
13593

136-
println!("Connected");
137-
138-
let stream = graphql_ws_client::Client::build(connection)
139-
.subscribe(build_query())
140-
.await
141-
.unwrap();
94+
let mut stream = client_builder.subscribe(build_query()).await.unwrap();
14295

14396
let updates = [
14497
subscription_server::BookChanged {
@@ -155,15 +108,115 @@ async fn oneshot_operation_test() {
155108
},
156109
];
157110

111+
send_and_verify_updates(&server, &updates, &mut stream).await;
112+
}
113+
114+
#[tokio::test]
115+
async fn test_client_stop() {
116+
let server = SubscriptionServer::start().await;
117+
118+
let client_builder = server.client_builder().await;
119+
let (client, actor) = client_builder.await.unwrap();
120+
121+
tokio::spawn(actor.into_future());
122+
123+
let mut stream = client.subscribe(build_query()).await.unwrap();
124+
125+
sleep(Duration::from_millis(10)).await;
126+
127+
let updates = [subscription_server::BookChanged {
128+
id: "123".into(),
129+
book: None,
130+
}];
131+
132+
send_and_verify_updates(&server, &updates, &mut stream).await;
133+
134+
assert_eq!(server.subscriber_count(), 1);
135+
136+
client.stop(stream.id()).await.unwrap();
137+
138+
sleep(Duration::from_millis(10)).await;
139+
140+
assert_eq!(server.subscriber_count(), 0);
141+
142+
assert!(stream.next().await.is_none());
143+
}
144+
145+
#[tokio::test]
146+
async fn test_subscription_stop() {
147+
let server = SubscriptionServer::start().await;
148+
149+
let client_builder = server.client_builder().await;
150+
let (client, actor) = client_builder.await.unwrap();
151+
152+
tokio::spawn(actor.into_future());
153+
154+
let mut stream = client.subscribe(build_query()).await.unwrap();
155+
156+
sleep(Duration::from_millis(10)).await;
157+
158+
let updates = [subscription_server::BookChanged {
159+
id: "123".into(),
160+
book: None,
161+
}];
162+
163+
send_and_verify_updates(&server, &updates, &mut stream).await;
164+
165+
assert_eq!(server.subscriber_count(), 1);
166+
167+
stream.stop().await.unwrap();
168+
169+
sleep(Duration::from_millis(10)).await;
170+
171+
assert_eq!(server.subscriber_count(), 0);
172+
}
173+
174+
#[tokio::test]
175+
async fn test_subscription_stops_on_drop() {
176+
let server = SubscriptionServer::start().await;
177+
178+
let client_builder = server.client_builder().await;
179+
let (client, actor) = client_builder.await.unwrap();
180+
181+
tokio::spawn(actor.into_future());
182+
183+
let mut stream = client.subscribe(build_query()).await.unwrap();
184+
185+
sleep(Duration::from_millis(10)).await;
186+
187+
let updates = [subscription_server::BookChanged {
188+
id: "123".into(),
189+
book: None,
190+
}];
191+
192+
send_and_verify_updates(&server, &updates, &mut stream).await;
193+
194+
assert_eq!(server.subscriber_count(), 1);
195+
196+
drop(stream);
197+
198+
sleep(Duration::from_millis(10)).await;
199+
200+
assert_eq!(server.subscriber_count(), 0);
201+
}
202+
203+
async fn send_and_verify_updates(
204+
server: &SubscriptionServer,
205+
updates: &[subscription_server::BookChanged],
206+
stream: &mut Subscription<StreamingOperation<BooksChangedSubscription, BooksChangedVariables>>,
207+
) {
158208
future::zip(
159209
async {
160210
sleep(Duration::from_millis(10)).await;
161-
for update in &updates {
211+
for update in updates {
162212
server.send(update.to_owned()).unwrap();
163213
}
164214
},
165215
async {
166-
let received_updates = stream.take(updates.len()).collect::<Vec<_>>().await;
216+
let mut received_updates = Vec::new();
217+
for _ in updates {
218+
received_updates.push(stream.next().await.unwrap());
219+
}
167220

168221
for (expected, update) in updates.iter().zip(received_updates) {
169222
let update = update.unwrap();

tests/subscription_server/mod.rs

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,30 @@
11
// TODO: Start a server w/ async-graphql.
22
// Query that server...
33

4+
use std::{
5+
pin::Pin,
6+
sync::{
7+
Arc,
8+
atomic::{AtomicUsize, Ordering},
9+
},
10+
time::Duration,
11+
};
12+
413
use async_graphql::{EmptyMutation, ID, Object, Schema, SimpleObject, Subscription};
514
use async_graphql_axum::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
6-
use axum::{Router, extract::Extension, routing::post};
15+
use axum::{Router, extract::Extension, http::HeaderValue, routing::post};
716
use futures_lite::{Stream, StreamExt};
817
use tokio::sync::broadcast::Sender;
918
use tokio_stream::wrappers::BroadcastStream;
19+
use tungstenite::client::IntoClientRequest;
1020

1121
pub type BooksSchema = Schema<QueryRoot, EmptyMutation, SubscriptionRoot>;
1222

1323
pub struct SubscriptionServer {
1424
shutdown: Option<tokio::sync::oneshot::Sender<()>>,
1525
port: u16,
1626
sender: Sender<BookChanged>,
27+
subscriber_count: Arc<AtomicUsize>,
1728
}
1829

1930
impl Drop for SubscriptionServer {
@@ -27,12 +38,14 @@ impl Drop for SubscriptionServer {
2738
impl SubscriptionServer {
2839
pub async fn start() -> SubscriptionServer {
2940
let (channel, _) = tokio::sync::broadcast::channel(16);
41+
let subscriber_count = Arc::new(AtomicUsize::new(0));
3042

3143
let schema = Schema::build(
3244
QueryRoot,
3345
EmptyMutation,
3446
SubscriptionRoot {
3547
channel: channel.clone(),
48+
subscriber_count: Arc::clone(&subscriber_count),
3649
},
3750
)
3851
.finish();
@@ -56,23 +69,48 @@ impl SubscriptionServer {
5669
.unwrap();
5770
});
5871

72+
tokio::time::sleep(Duration::from_millis(20)).await;
73+
5974
SubscriptionServer {
6075
port,
6176
shutdown: Some(shutdown_sender),
6277
sender: channel,
78+
subscriber_count,
6379
}
6480
}
6581

6682
pub fn websocket_url(&self) -> String {
6783
format!("ws://localhost:{}/ws", self.port)
6884
}
6985

86+
#[allow(unused)]
87+
pub fn subscriber_count(&self) -> usize {
88+
self.subscriber_count.load(Ordering::Relaxed)
89+
}
90+
7091
pub fn send(
7192
&self,
7293
change: BookChanged,
7394
) -> Result<(), tokio::sync::broadcast::error::SendError<BookChanged>> {
7495
self.sender.send(change).map(|_| ())
7596
}
97+
98+
#[allow(unused)]
99+
pub async fn client_builder(&self) -> graphql_ws_client::ClientBuilder {
100+
let mut request = self.websocket_url().into_client_request().unwrap();
101+
request.headers_mut().insert(
102+
"Sec-WebSocket-Protocol",
103+
HeaderValue::from_str("graphql-transport-ws").unwrap(),
104+
);
105+
106+
let (connection, _) = async_tungstenite::tokio::connect_async(request)
107+
.await
108+
.unwrap();
109+
110+
println!("Connected");
111+
112+
graphql_ws_client::Client::build(connection)
113+
}
76114
}
77115

78116
#[axum_macros::debug_handler]
@@ -110,12 +148,45 @@ enum MutationType {
110148

111149
pub struct SubscriptionRoot {
112150
channel: Sender<BookChanged>,
151+
subscriber_count: Arc<AtomicUsize>,
113152
}
114153

115154
#[Subscription]
116155
impl SubscriptionRoot {
117156
async fn books(&self, _mutation_type: MutationType) -> impl Stream<Item = BookChanged> {
118157
println!("Subscription received");
119-
BroadcastStream::new(self.channel.subscribe()).filter_map(Result::ok)
158+
self.subscriber_count.fetch_add(1, Ordering::Relaxed);
159+
TrackedBroadcastStream {
160+
inner: BroadcastStream::new(self.channel.subscribe()).filter_map(Result::ok),
161+
count: Arc::clone(&self.subscriber_count),
162+
}
163+
}
164+
}
165+
166+
#[pin_project::pin_project(PinnedDrop)]
167+
pub struct TrackedBroadcastStream<T> {
168+
#[pin]
169+
inner: T,
170+
count: Arc<AtomicUsize>,
171+
}
172+
173+
impl<T> Stream for TrackedBroadcastStream<T>
174+
where
175+
T: Stream,
176+
{
177+
type Item = <T as Stream>::Item;
178+
179+
fn poll_next(
180+
self: Pin<&mut Self>,
181+
cx: &mut std::task::Context<'_>,
182+
) -> std::task::Poll<Option<Self::Item>> {
183+
self.project().inner.poll_next(cx)
184+
}
185+
}
186+
187+
#[pin_project::pinned_drop]
188+
impl<T> PinnedDrop for TrackedBroadcastStream<T> {
189+
fn drop(self: Pin<&mut Self>) {
190+
self.project().count.fetch_sub(1, Ordering::Relaxed);
120191
}
121192
}

0 commit comments

Comments
 (0)