Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dev/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ bin = [
{ name = "conversions4_sol", path = "../solutions/23_conversions/conversions4.rs" },
{ name = "conversions5", path = "../exercises/23_conversions/conversions5.rs" },
{ name = "conversions5_sol", path = "../solutions/23_conversions/conversions5.rs" },
{ name = "async1", path = "../exercises/24_async/async1.rs" },
{ name = "async1_sol", path = "../solutions/24_async/async1.rs" },
]

[package]
Expand All @@ -196,6 +198,9 @@ edition = "2024"
# Don't publish the exercises on crates.io!
publish = false

[dependencies]
tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread"] }

[profile.release]
panic = "abort"

Expand Down
13 changes: 13 additions & 0 deletions exercises/24_async/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Async

Asynchronous programming is a model where tasks are delegated to a runtime that executes them concurrently.
It is particularly efficient for applications where many independent IO-operations are performed, e.g. web servers.

Rust provides the necessary primitives to do asynchronous programming in the language.
However, Rust's standard library does not include a runtime.
For these exercises, we will use the mainstream runtime called `tokio`.

## Further information

- [Fundamentals of Asynchronous Programming](https://doc.rust-lang.org/book/ch17-00-async-await.html)
- [Tokio documentation](https://docs.rs/tokio/latest/tokio/)
40 changes: 40 additions & 0 deletions exercises/24_async/async1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Alice is an elementary school teacher who needs to calculate the mean test
// score for three classes she teaches. Instead of calculating them one after
// the other, she decides to ask her friends Bob and Catherine for help. Working
// together, they can finish the job much faster.
Comment thread
mo8it marked this conversation as resolved.
//
// Let's simulate this using asynchronous programming. Each person is
// represented as an asynchronous task, which can be executed concurrently.

// Async tasks need to be executed by a "runtime", which is not provided by
// Rust's standard library. Here, we use the mainstream runtime `tokio`.
// The macro `tokio::main` wraps the entire main function in a runtime.
#[tokio::main]
async fn main() {
let mean_score_a = tokio::spawn(calculate_mean_score("scores_class_a.txt"));
let mean_score_b = tokio::spawn(calculate_mean_score("scores_class_b.txt"));
let mean_score_c = tokio::spawn(calculate_mean_score("scores_class_c.txt"));

// TODO: Await the spawned tasks to check their results.
assert_eq!(mean_score_a, 84); // alice
assert_eq!(mean_score_b, 89); // bob
assert_eq!(mean_score_c, 76); // catherine
}

// TODO: Fix the compiler errors by making the spawned function async.
fn calculate_mean_score(scores_file: &str) -> usize {
// Read the file asynchronously
let file = tokio::fs::read_to_string(scores_file).await.unwrap();

// Initialize the sum and the number of scores
let mut sum = 0;
let mut n = 0;
for line in file.lines() {
// Parse every line as a score
let score = line.parse::<usize>().unwrap();
sum += score;
n += 1;
}

sum / n
}
3 changes: 3 additions & 0 deletions exercises/24_async/scores_class_a.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
83
77
92
3 changes: 3 additions & 0 deletions exercises/24_async/scores_class_b.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
84
88
96
3 changes: 3 additions & 0 deletions exercises/24_async/scores_class_c.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
71
83
76
19 changes: 19 additions & 0 deletions rustlings-macros/info.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1211,3 +1211,22 @@ name = "conversions5"
dir = "23_conversions"
hint = """
Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions."""

# ASYNC

[[exercises]]
name = "async1"
dir = "24_async"
test = false
input_files = [
"scores_class_a.txt",
"scores_class_b.txt",
"scores_class_c.txt",
]
hint = """
Asynchronous runtimes like tokio can only spawn tasks that are defined as async
functions, not regular ones. Add the "async" keyword before the "fn" keyword of
the functions "tim", "carl" and "nick".

An async task can wait for another one to complete by "awaiting" it. Add
".await" after the three "task_name" variables in the "block_on" call."""
30 changes: 25 additions & 5 deletions rustlings-macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ use serde::Deserialize;
struct ExerciseInfo<'a> {
name: &'a str,
dir: &'a str,
#[serde(default)]
input_files: Vec<&'a str>,
}

#[derive(Deserialize)]
Expand All @@ -17,9 +19,8 @@ struct InfoFile<'a> {
#[proc_macro]
pub fn include_files(_: TokenStream) -> TokenStream {
let info_file = include_str!("../info.toml");
let exercises = toml::de::from_str::<InfoFile>(info_file)
.expect("Failed to parse `info.toml`")
.exercises;
let info = toml::de::from_str::<InfoFile>(info_file).expect("Failed to parse `info.toml`");
let exercises = info.exercises;

let exercise_files = exercises
.iter()
Expand All @@ -42,15 +43,34 @@ pub fn include_files(_: TokenStream) -> TokenStream {
*dir_ind = dirs.len() - 1;
}

let input_files = exercises.iter().map(|exercise| {
let names = exercise.input_files.iter();
let paths = exercise
.input_files
.iter()
.map(|f| format!("../exercises/{}/{}", exercise.dir, f));
quote! {
&[#(InputFile {
name: #names,
content: include_str!(#paths),
}),*]
}
});

let readmes = dirs
.iter()
.map(|dir| format!("../exercises/{dir}/README.md"));

quote! {
EmbeddedFiles {
info_file: #info_file,
exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*],
exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*]
exercise_files: &[#(ExerciseFiles {
exercise: include_bytes!(#exercise_files),
solution: include_bytes!(#solution_files),
dir_ind: #dir_inds,
input_files: #input_files,
}),*],
exercise_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*],
}
}
.into()
Expand Down
38 changes: 38 additions & 0 deletions solutions/24_async/async1.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Alice is an elementary school teacher who needs to calculate the mean test
// score for three classes she teaches. Instead of calculating them one after
// the other, she decides to ask her friends Bob and Catherine for help. Working
// together, they can finish the job much faster.
//
// Let's simulate this using asynchronous programming. Each person is
// represented as an asynchronous task, which can be executed concurrently.

// Async tasks need to be executed by a "runtime", which is not provided by
// Rust's standard library. Here, we use the mainstream runtime `tokio`.
// The macro `tokio::main` wraps the entire main function in a runtime.
#[tokio::main]
async fn main() {
let mean_score_a = tokio::spawn(calculate_mean_score("scores_class_a.txt"));
let mean_score_b = tokio::spawn(calculate_mean_score("scores_class_b.txt"));
let mean_score_c = tokio::spawn(calculate_mean_score("scores_class_c.txt"));

assert_eq!(mean_score_a.await.unwrap(), 84); // alice
assert_eq!(mean_score_b.await.unwrap(), 89); // bob
assert_eq!(mean_score_c.await.unwrap(), 76); // catherine
}

async fn calculate_mean_score(scores_file: &str) -> usize {
// Read the file asynchronously
let file = tokio::fs::read_to_string(scores_file).await.unwrap();

// Initialize the sum and the number of scores
let mut sum = 0;
let mut n = 0;
for line in file.lines() {
// Parse every line as a score
let score = line.parse::<usize>().unwrap();
sum += score;
n += 1;
}

sum / n
}
13 changes: 11 additions & 2 deletions src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,11 @@ impl AppState {
})?;

let dir_canonical_path = term::canonicalize("exercises");
let official_exercises = !Path::new("info.toml").exists();
let mut exercises = exercise_infos
.into_iter()
.map(|exercise_info| {
.enumerate()
.map(|(i, exercise_info)| {
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
let mut canonical_path;
if let Some(dir) = exercise_info.dir {
Expand All @@ -105,10 +107,16 @@ impl AppState {
canonical_path.push_str(".rs");
canonical_path
});
let embedded_input_files = if official_exercises {
EMBEDDED_FILES.exercise_files[i].input_files
} else {
&[]
};

Exercise {
name: exercise_info.name,
dir: exercise_info.dir,
embedded_input_files,
// LEAKING: For `Editor::open`. The app state is used until the end of the program.
path: exercise_info.path().leak(),
canonical_path,
Expand Down Expand Up @@ -173,7 +181,7 @@ impl AppState {
final_message,
state_file,
file_buf,
official_exercises: !Path::new("info.toml").exists(),
official_exercises,
cmd_runner,
// VS Code has its own file link handling
emit_file_links: !vs_code_term,
Expand Down Expand Up @@ -597,6 +605,7 @@ mod tests {
Exercise {
name: "0",
dir: None,
embedded_input_files: &[],
path: "exercises/0.rs",
canonical_path: None,
test: false,
Expand Down
2 changes: 2 additions & 0 deletions src/cargo_toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ mod tests {
dir: None,
test: true,
strict_clippy: true,
input_files: vec![],
hint: String::new(),
skip_check_unsolved: false,
},
Expand All @@ -118,6 +119,7 @@ mod tests {
dir: Some("d"),
test: false,
strict_clippy: false,
input_files: vec![],
hint: String::new(),
skip_check_unsolved: false,
},
Expand Down
23 changes: 18 additions & 5 deletions src/cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ const TIMEOUT_SECS: u64 = 30;

/// Run a command with a description for a possible error and append the merged stdout and stderr.
/// The boolean in the returned `Result` is true if the command's exit status is success.
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
fn run_cmd(
mut cmd: Command,
description: &str,
cwd: Option<&str>,
output: Option<&mut Vec<u8>>,
) -> Result<bool> {
let spawn = |mut cmd: Command| {
// The closure drops `cmd` which prevents a pipe deadlock.
cmd.stdin(Stdio::null())
Expand All @@ -25,6 +30,9 @@ fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) ->
.wait_timeout(Duration::from_secs(TIMEOUT_SECS))
.with_context(|| format!("Failed to wait on `{description}` to exit"))
};
if let Some(cwd) = cwd {
cmd.current_dir(cwd);
}

let mut handle = if let Some(output) = output {
let (mut reader, writer) =
Expand Down Expand Up @@ -133,15 +141,20 @@ impl CmdRunner {
}

/// The boolean in the returned `Result` is true if the command's exit status is success.
pub fn run_debug_bin(&self, bin_name: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
pub fn run_debug_bin(
&self,
bin_name: &str,
cwd: &str,
output: Option<&mut Vec<u8>>,
) -> Result<bool> {
// 7 = "/debug/".len()
let mut bin_path =
PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len());
bin_path.push(&self.target_dir);
bin_path.push("debug");
bin_path.push(bin_name);

run_cmd(Command::new(&bin_path), bin_name, output)
run_cmd(Command::new(&bin_path), bin_name, Some(cwd), output)
}
}

Expand All @@ -161,7 +174,7 @@ impl CargoSubcommand<'_> {

/// The boolean in the returned `Result` is true if the command's exit status is success.
pub fn run(self, description: &str) -> Result<bool> {
run_cmd(self.cmd, description, self.output)
run_cmd(self.cmd, description, None, self.output)
}
}

Expand All @@ -179,7 +192,7 @@ mod tests {
cmd.arg("Hello");

let mut output = Vec::with_capacity(8);
run_cmd(cmd, "echo …", Some(&mut output)).unwrap();
run_cmd(cmd, "echo …", None, Some(&mut output)).unwrap();

assert_eq!(output, b"Hello\n\n");
}
Expand Down
19 changes: 13 additions & 6 deletions src/dev/check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,19 +133,26 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {

file_buf.clear();

paths.insert(PathBuf::from(path));
let path = PathBuf::from(path);
let parent = path.parent().unwrap();

for input_file in &exercise_info.input_files {
paths.insert(parent.join(input_file));
}

paths.insert(path);
}

Ok(paths)
}

// Check `dir` for unexpected files.
// Only Rust files in `allowed_rust_files` and `README.md` files are allowed.
// Only files in `allowed_files` and `README.md` files are allowed.
// Only one level of directory nesting is allowed.
fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> Result<()> {
fn check_unexpected_files(dir: &str, allowed_files: &HashSet<PathBuf>) -> Result<()> {
let unexpected_file = |path: &Path| {
anyhow!(
"Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory",
"Found the file `{}`. Only `README.md`, Rust files and input files related to an exercise in `info.toml` are allowed in the `{dir}` directory",
path.display()
)
};
Expand All @@ -160,7 +167,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
continue;
}

if !allowed_rust_files.contains(&path) {
if !allowed_files.contains(&path) {
return Err(unexpected_file(&path));
}

Expand All @@ -187,7 +194,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
continue;
}

if !allowed_rust_files.contains(&path) {
if !allowed_files.contains(&path) {
return Err(unexpected_file(&path));
}
}
Expand Down
Loading