Skip to content

Commit 722d7c3

Browse files
committed
Make async code testable
This moves all non-trivial examples in the async part of the course to self-contained Rust files. This ensures that we can test them in CI.
1 parent 00500e7 commit 722d7c3

File tree

19 files changed

+394
-262
lines changed

19 files changed

+394
-262
lines changed

Cargo.lock

Lines changed: 60 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ members = [
2323
"src/std-traits",
2424
"src/std-types",
2525
"src/testing",
26+
"src/concurrency/async",
27+
"src/concurrency/async-control-flow",
28+
"src/concurrency/async-pitfalls",
2629
"src/tuples-and-arrays",
2730
"src/types-and-values",
2831
"src/unsafe-rust",
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "async-control-flow"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[[bin]]
8+
name = "join"
9+
path = "join.rs"
10+
11+
[[bin]]
12+
name = "select"
13+
path = "select.rs"
14+
15+
[dependencies]
16+
anyhow = "1.0.81"
17+
async-trait = "0.1.79"
18+
futures = { version = "0.3.30", default-features = false }
19+
reqwest = { version = "0.12.1", default-features = false }
20+
tokio = { version = "1.36.0", features = ["full"] }

src/concurrency/async-control-flow/channels.md

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,31 +7,7 @@ minutes: 8
77
Several crates have support for asynchronous channels. For instance `tokio`:
88

99
```rust,editable,compile_fail
10-
use tokio::sync::mpsc::{self, Receiver};
11-
12-
async fn ping_handler(mut input: Receiver<()>) {
13-
let mut count: usize = 0;
14-
15-
while let Some(_) = input.recv().await {
16-
count += 1;
17-
println!("Received {count} pings so far.");
18-
}
19-
20-
println!("ping_handler complete");
21-
}
22-
23-
#[tokio::main]
24-
async fn main() {
25-
let (sender, receiver) = mpsc::channel(32);
26-
let ping_handler_task = tokio::spawn(ping_handler(receiver));
27-
for i in 0..10 {
28-
sender.send(()).await.expect("Failed to send ping.");
29-
println!("Sent {} pings so far.", i + 1);
30-
}
31-
32-
drop(sender);
33-
ping_handler_task.await.expect("Something went wrong in ping handler task.");
34-
}
10+
{{#include channels.rs}}
3511
```
3612

3713
<details>

src/concurrency/async-control-flow/join.md

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,7 @@ collection of their results. This is similar to `Promise.all` in JavaScript or
99
`asyncio.gather` in Python.
1010

1111
```rust,editable,compile_fail
12-
use anyhow::Result;
13-
use futures::future;
14-
use reqwest;
15-
use std::collections::HashMap;
16-
17-
async fn size_of_page(url: &str) -> Result<usize> {
18-
let resp = reqwest::get(url).await?;
19-
Ok(resp.text().await?.len())
20-
}
21-
22-
#[tokio::main]
23-
async fn main() {
24-
let urls: [&str; 4] = [
25-
"https://google.com",
26-
"https://httpbin.org/ip",
27-
"https://play.rust-lang.org/",
28-
"BAD_URL",
29-
];
30-
let futures_iter = urls.into_iter().map(size_of_page);
31-
let results = future::join_all(futures_iter).await;
32-
let page_sizes_dict: HashMap<&str, Result<usize>> =
33-
urls.into_iter().zip(results.into_iter()).collect();
34-
println!("{:?}", page_sizes_dict);
35-
}
12+
{{#include join.rs}}
3613
```
3714

3815
<details>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use anyhow::Result;
2+
use futures::future;
3+
use reqwest;
4+
use std::collections::HashMap;
5+
6+
async fn size_of_page(url: &str) -> Result<usize> {
7+
let resp = reqwest::get(url).await?;
8+
Ok(resp.text().await?.len())
9+
}
10+
11+
#[tokio::main]
12+
async fn main() {
13+
let urls: [&str; 4] = [
14+
"https://google.com",
15+
"https://httpbin.org/ip",
16+
"https://play.rust-lang.org/",
17+
"BAD_URL",
18+
];
19+
let futures_iter = urls.into_iter().map(size_of_page);
20+
let results = future::join_all(futures_iter).await;
21+
let page_sizes_dict: HashMap<&str, Result<usize>> =
22+
urls.into_iter().zip(results.into_iter()).collect();
23+
println!("{:?}", page_sizes_dict);
24+
}

src/concurrency/async-control-flow/select.md

Lines changed: 1 addition & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,7 @@ the resulting variables. The `statement` result becomes the result of the
1616
`select!` macro.
1717

1818
```rust,editable,compile_fail
19-
use tokio::sync::mpsc::{self, Receiver};
20-
use tokio::time::{sleep, Duration};
21-
22-
#[derive(Debug, PartialEq)]
23-
enum Animal {
24-
Cat { name: String },
25-
Dog { name: String },
26-
}
27-
28-
async fn first_animal_to_finish_race(
29-
mut cat_rcv: Receiver<String>,
30-
mut dog_rcv: Receiver<String>,
31-
) -> Option<Animal> {
32-
tokio::select! {
33-
cat_name = cat_rcv.recv() => Some(Animal::Cat { name: cat_name? }),
34-
dog_name = dog_rcv.recv() => Some(Animal::Dog { name: dog_name? })
35-
}
36-
}
37-
38-
#[tokio::main]
39-
async fn main() {
40-
let (cat_sender, cat_receiver) = mpsc::channel(32);
41-
let (dog_sender, dog_receiver) = mpsc::channel(32);
42-
tokio::spawn(async move {
43-
sleep(Duration::from_millis(500)).await;
44-
cat_sender.send(String::from("Felix")).await.expect("Failed to send cat.");
45-
});
46-
tokio::spawn(async move {
47-
sleep(Duration::from_millis(50)).await;
48-
dog_sender.send(String::from("Rex")).await.expect("Failed to send dog.");
49-
});
50-
51-
let winner = first_animal_to_finish_race(cat_receiver, dog_receiver)
52-
.await
53-
.expect("Failed to receive winner");
54-
55-
println!("Winner is {winner:?}");
56-
}
19+
{{#include select.rs}}
5720
```
5821

5922
<details>
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
use tokio::sync::mpsc::{self, Receiver};
2+
use tokio::time::{sleep, Duration};
3+
4+
#[derive(Debug, PartialEq)]
5+
enum Animal {
6+
Cat { name: String },
7+
Dog { name: String },
8+
}
9+
10+
async fn first_animal_to_finish_race(
11+
mut cat_rcv: Receiver<String>,
12+
mut dog_rcv: Receiver<String>,
13+
) -> Option<Animal> {
14+
tokio::select! {
15+
cat_name = cat_rcv.recv() => Some(Animal::Cat { name: cat_name? }),
16+
dog_name = dog_rcv.recv() => Some(Animal::Dog { name: dog_name? })
17+
}
18+
}
19+
20+
#[tokio::main]
21+
async fn main() {
22+
let (cat_sender, cat_receiver) = mpsc::channel(32);
23+
let (dog_sender, dog_receiver) = mpsc::channel(32);
24+
tokio::spawn(async move {
25+
sleep(Duration::from_millis(500)).await;
26+
cat_sender.send(String::from("Felix")).await.expect("Failed to send cat.");
27+
});
28+
tokio::spawn(async move {
29+
sleep(Duration::from_millis(50)).await;
30+
dog_sender.send(String::from("Rex")).await.expect("Failed to send dog.");
31+
});
32+
33+
let winner = first_animal_to_finish_race(cat_receiver, dog_receiver)
34+
.await
35+
.expect("Failed to receive winner");
36+
37+
println!("Winner is {winner:?}");
38+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "async-pitfalls"
3+
version = "0.1.0"
4+
edition = "2021"
5+
publish = false
6+
7+
[[bin]]
8+
name = "async-traits"
9+
path = "async-traits.rs"
10+
11+
[[bin]]
12+
name = "cancellation"
13+
path = "cancellation.rs"
14+
15+
[[bin]]
16+
name = "pin"
17+
path = "pin.rs"
18+
19+
[dependencies]
20+
anyhow = "1.0.81"
21+
async-trait = "0.1.79"
22+
futures = { version = "0.3.30", default-features = false }
23+
reqwest = { version = "0.12.1", default-features = false }
24+
tokio = { version = "1.36.0", features = ["full"] }

src/concurrency/async-pitfalls/async-traits.md

Lines changed: 1 addition & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -20,48 +20,7 @@ The [async_trait] crate provides a workaround for `dyn` support through a macro,
2020
with some caveats:
2121

2222
```rust,editable,compile_fail
23-
use async_trait::async_trait;
24-
use std::time::Instant;
25-
use tokio::time::{sleep, Duration};
26-
27-
#[async_trait]
28-
trait Sleeper {
29-
async fn sleep(&self);
30-
}
31-
32-
struct FixedSleeper {
33-
sleep_ms: u64,
34-
}
35-
36-
#[async_trait]
37-
impl Sleeper for FixedSleeper {
38-
async fn sleep(&self) {
39-
sleep(Duration::from_millis(self.sleep_ms)).await;
40-
}
41-
}
42-
43-
async fn run_all_sleepers_multiple_times(
44-
sleepers: Vec<Box<dyn Sleeper>>,
45-
n_times: usize,
46-
) {
47-
for _ in 0..n_times {
48-
println!("Running all sleepers...");
49-
for sleeper in &sleepers {
50-
let start = Instant::now();
51-
sleeper.sleep().await;
52-
println!("Slept for {} ms", start.elapsed().as_millis());
53-
}
54-
}
55-
}
56-
57-
#[tokio::main]
58-
async fn main() {
59-
let sleepers: Vec<Box<dyn Sleeper>> = vec![
60-
Box::new(FixedSleeper { sleep_ms: 50 }),
61-
Box::new(FixedSleeper { sleep_ms: 100 }),
62-
];
63-
run_all_sleepers_multiple_times(sleepers, 5).await;
64-
}
23+
{{#include async-traits.rs}}
6524
```
6625

6726
<details>

0 commit comments

Comments
 (0)