From b3e80316bb69170ba342429ac041dff569e58b15 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Fri, 17 Apr 2026 15:14:01 +0200 Subject: [PATCH 01/13] Add exercise async1 The goal here was to get the first bit of "muscle memory" for using the async and await keywords. The little story should make it more intuitive for users why asynchronous programming is needed in the first place. This exercise will be moved to the location corresponding to the book in a later commit, to keep the diff of this one clean. --- dev/Cargo.toml | 5 ++++ exercises/24_async/README.md | 13 +++++++++ exercises/24_async/async1.rs | 55 ++++++++++++++++++++++++++++++++++++ rustlings-macros/info.toml | 14 +++++++++ solutions/24_async/async1.rs | 53 ++++++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+) create mode 100644 exercises/24_async/README.md create mode 100644 exercises/24_async/async1.rs create mode 100644 solutions/24_async/async1.rs diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 66bc1dfe..cfc874fb 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -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] @@ -196,6 +198,9 @@ edition = "2024" # Don't publish the exercises on crates.io! publish = false +[dependencies] +tokio = { version = "1.52.1", features = ["rt"] } + [profile.release] panic = "abort" diff --git a/exercises/24_async/README.md b/exercises/24_async/README.md new file mode 100644 index 00000000..7928650e --- /dev/null +++ b/exercises/24_async/README.md @@ -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 popular 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/) diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs new file mode 100644 index 00000000..5810ced3 --- /dev/null +++ b/exercises/24_async/async1.rs @@ -0,0 +1,55 @@ +// Tim has to complete a few chores today, before he's allowed to play soccer +// with his friends. His friends decide to help him. Working together, they +// finish the chores earlier and have more time left to play soccer. +// +// Let's simulate this using asynchronous programming. Each boy is represented +// as an asynchronous task, which can be executed concurrently (they can be +// working at the same time). + +use std::sync::atomic::{AtomicU8, Ordering}; + +// Used by "mom" to check that all chores are done before Tim plays soccer :-) +static CHORES_DONE: AtomicU8 = AtomicU8::new(0); + +fn main() { + // Async tasks need to be executed by a "runtime", which is not provided by + // Rust's standard library. We use the popular "tokio" runtime here. + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + + // TODO: Fix the compiler errors by making the spawned function async. + let task_tim = rt.spawn(tim()); + let task_carl = rt.spawn(carl()); + let task_nick = rt.spawn(nick()); + + // Block the runtime on a task that waits for all boys to finish the chores. + // TODO: "await" all three tasks to fix the compiler errors. + rt.block_on(async { + task_tim; + task_carl; + task_nick; + }); + + assert_eq!( + CHORES_DONE.load(Ordering::SeqCst), + 3, + "Did you (a)wait for all the boys to finish the chores?" + ); + println!("Ready to play soccer!"); +} + +fn tim() { + println!("Cleaning my room..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} + +fn carl() { + println!("Washing the dishes..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} + +fn nick() { + println!("Mowing the lawn..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 3a1cac35..533b1d50 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1211,3 +1211,17 @@ name = "conversions5" dir = "23_conversions" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" + +# ASYNC + +[[exercises]] +name = "async1" +dir = "24_async" +test = false +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.""" diff --git a/solutions/24_async/async1.rs b/solutions/24_async/async1.rs new file mode 100644 index 00000000..a897067a --- /dev/null +++ b/solutions/24_async/async1.rs @@ -0,0 +1,53 @@ +// Tim has to complete a few chores today, before he's allowed to play soccer +// with his friends. His friends decide to help him. Working together, they +// finish the chores earlier and have more time left to play soccer. +// +// Let's simulate this using asynchronous programming. Each boy is represented +// as an asynchronous task, which can be executed concurrently (they can be +// working at the same time). + +use std::sync::atomic::{AtomicU8, Ordering}; + +// Used by "mom" to check that all chores are done before Tim plays soccer :-) +static CHORES_DONE: AtomicU8 = AtomicU8::new(0); + +fn main() { + // Async tasks need to be executed by a "runtime", which is not provided by + // Rust's standard library. We use the popular "tokio" runtime here. + let rt = tokio::runtime::Builder::new_current_thread() + .build() + .unwrap(); + + let task_tim = rt.spawn(tim()); + let task_carl = rt.spawn(carl()); + let task_nick = rt.spawn(nick()); + + // Block the runtime on a task that waits for all boys to finish the chores. + rt.block_on(async { + task_tim.await.unwrap(); + task_carl.await.unwrap(); + task_nick.await.unwrap(); + }); + + assert_eq!( + CHORES_DONE.load(Ordering::SeqCst), + 3, + "Did you (a)wait for all the boys to finish the chores?" + ); + println!("Ready to play soccer!"); +} + +async fn tim() { + println!("Cleaning my room..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} + +async fn carl() { + println!("Washing the dishes..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} + +async fn nick() { + println!("Mowing the lawn..."); + CHORES_DONE.fetch_add(1, Ordering::SeqCst); +} From 21d4d77f4feed48c26bd904264ed9e43079d2ec8 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 25 Apr 2026 15:05:52 +0200 Subject: [PATCH 02/13] Relax version requirement for tokio --- dev/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index cfc874fb..168fc10a 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -199,7 +199,7 @@ edition = "2024" publish = false [dependencies] -tokio = { version = "1.52.1", features = ["rt"] } +tokio = { version = "1", features = ["rt"] } [profile.release] panic = "abort" From d5599507b02f757eac2de8a10c1bdd6618134e88 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 25 Apr 2026 15:06:27 +0200 Subject: [PATCH 03/13] Refer to tokio as "mainstream" instead of "popular" --- exercises/24_async/README.md | 2 +- exercises/24_async/async1.rs | 2 +- solutions/24_async/async1.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/exercises/24_async/README.md b/exercises/24_async/README.md index 7928650e..4e61bc32 100644 --- a/exercises/24_async/README.md +++ b/exercises/24_async/README.md @@ -5,7 +5,7 @@ It is particularly efficient for applications where many independent IO-operatio 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 popular runtime called `tokio`. +For these exercises, we will use the mainstream runtime called `tokio`. ## Further information diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs index 5810ced3..40d71bc0 100644 --- a/exercises/24_async/async1.rs +++ b/exercises/24_async/async1.rs @@ -13,7 +13,7 @@ static CHORES_DONE: AtomicU8 = AtomicU8::new(0); fn main() { // Async tasks need to be executed by a "runtime", which is not provided by - // Rust's standard library. We use the popular "tokio" runtime here. + // Rust's standard library. Here, we use the mainstream runtime `tokio`. let rt = tokio::runtime::Builder::new_current_thread() .build() .unwrap(); diff --git a/solutions/24_async/async1.rs b/solutions/24_async/async1.rs index a897067a..ae9e0c37 100644 --- a/solutions/24_async/async1.rs +++ b/solutions/24_async/async1.rs @@ -13,7 +13,7 @@ static CHORES_DONE: AtomicU8 = AtomicU8::new(0); fn main() { // Async tasks need to be executed by a "runtime", which is not provided by - // Rust's standard library. We use the popular "tokio" runtime here. + // Rust's standard library. Here, we use the mainstream runtime `tokio`. let rt = tokio::runtime::Builder::new_current_thread() .build() .unwrap(); From 1dd31a8fddb0e39aa7b152a44d8862e34e7fde6e Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sat, 25 Apr 2026 15:07:13 +0200 Subject: [PATCH 04/13] Change story of exercise async1 - Remove confusing use of atomics. Use return values of async tasks instead, to ensure all tasks are awaited. - Remove use of `println!()`, which uses a global lock and cannot be executed in parallel. --- exercises/24_async/async1.rs | 67 +++++++++++++++--------------------- solutions/24_async/async1.rs | 65 +++++++++++++++------------------- 2 files changed, 54 insertions(+), 78 deletions(-) diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs index 40d71bc0..8c2067ed 100644 --- a/exercises/24_async/async1.rs +++ b/exercises/24_async/async1.rs @@ -1,15 +1,11 @@ -// Tim has to complete a few chores today, before he's allowed to play soccer -// with his friends. His friends decide to help him. Working together, they -// finish the chores earlier and have more time left to play soccer. +// 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 boy is represented -// as an asynchronous task, which can be executed concurrently (they can be -// working at the same time). - -use std::sync::atomic::{AtomicU8, Ordering}; - -// Used by "mom" to check that all chores are done before Tim plays soccer :-) -static CHORES_DONE: AtomicU8 = AtomicU8::new(0); +// Let's simulate this using asynchronous programming. Each person is +// represented as an asynchronous task, which can be executed concurrently (i.e. +// they can be doing the calculations at the same time). fn main() { // Async tasks need to be executed by a "runtime", which is not provided by @@ -18,38 +14,29 @@ fn main() { .build() .unwrap(); - // TODO: Fix the compiler errors by making the spawned function async. - let task_tim = rt.spawn(tim()); - let task_carl = rt.spawn(carl()); - let task_nick = rt.spawn(nick()); + let scores_class_a = &[83, 77, 92]; + let scores_class_b = &[84, 88, 96]; + let scores_class_c = &[71, 83, 76]; - // Block the runtime on a task that waits for all boys to finish the chores. - // TODO: "await" all three tasks to fix the compiler errors. - rt.block_on(async { - task_tim; - task_carl; - task_nick; + // TODO: Fix the compiler errors by making the spawned function async. + let alice = rt.spawn(calculate_mean_score(scores_class_a)); + let bob = rt.spawn(calculate_mean_score(scores_class_b)); + let catherine = rt.spawn(calculate_mean_score(scores_class_c)); + + // Block the runtime on a task that awaits all three calculations. + let [mean_score_a, mean_score_b, mean_score_c]: [usize; _] = rt.block_on(async { + [ + // TODO: "await" all three tasks to fix the compiler error. + alice, bob, catherine, + ] }); - assert_eq!( - CHORES_DONE.load(Ordering::SeqCst), - 3, - "Did you (a)wait for all the boys to finish the chores?" - ); - println!("Ready to play soccer!"); + assert_eq!(mean_score_a, 84); + assert_eq!(mean_score_b, 89); + assert_eq!(mean_score_c, 76); } -fn tim() { - println!("Cleaning my room..."); - CHORES_DONE.fetch_add(1, Ordering::SeqCst); -} - -fn carl() { - println!("Washing the dishes..."); - CHORES_DONE.fetch_add(1, Ordering::SeqCst); -} - -fn nick() { - println!("Mowing the lawn..."); - CHORES_DONE.fetch_add(1, Ordering::SeqCst); +fn calculate_mean_score(score_list: &[usize]) -> usize { + let score_sum: usize = score_list.iter().sum(); + score_sum / score_list.len() } diff --git a/solutions/24_async/async1.rs b/solutions/24_async/async1.rs index ae9e0c37..925be766 100644 --- a/solutions/24_async/async1.rs +++ b/solutions/24_async/async1.rs @@ -1,15 +1,11 @@ -// Tim has to complete a few chores today, before he's allowed to play soccer -// with his friends. His friends decide to help him. Working together, they -// finish the chores earlier and have more time left to play soccer. +// 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 boy is represented -// as an asynchronous task, which can be executed concurrently (they can be -// working at the same time). - -use std::sync::atomic::{AtomicU8, Ordering}; - -// Used by "mom" to check that all chores are done before Tim plays soccer :-) -static CHORES_DONE: AtomicU8 = AtomicU8::new(0); +// Let's simulate this using asynchronous programming. Each person is +// represented as an asynchronous task, which can be executed concurrently (i.e. +// they can be doing the calculations at the same time). fn main() { // Async tasks need to be executed by a "runtime", which is not provided by @@ -18,36 +14,29 @@ fn main() { .build() .unwrap(); - let task_tim = rt.spawn(tim()); - let task_carl = rt.spawn(carl()); - let task_nick = rt.spawn(nick()); + let scores_class_a = &[83, 77, 92]; + let scores_class_b = &[84, 88, 96]; + let scores_class_c = &[71, 83, 76]; - // Block the runtime on a task that waits for all boys to finish the chores. - rt.block_on(async { - task_tim.await.unwrap(); - task_carl.await.unwrap(); - task_nick.await.unwrap(); + let alice = rt.spawn(calculate_mean_score(scores_class_a)); + let bob = rt.spawn(calculate_mean_score(scores_class_b)); + let catherine = rt.spawn(calculate_mean_score(scores_class_c)); + + // Block the runtime on a task that awaits all three calculations. + let [mean_score_a, mean_score_b, mean_score_c]: [usize; _] = rt.block_on(async { + [ + alice.await.unwrap(), + bob.await.unwrap(), + catherine.await.unwrap(), + ] }); - assert_eq!( - CHORES_DONE.load(Ordering::SeqCst), - 3, - "Did you (a)wait for all the boys to finish the chores?" - ); - println!("Ready to play soccer!"); + assert_eq!(mean_score_a, 84); + assert_eq!(mean_score_b, 89); + assert_eq!(mean_score_c, 76); } -async fn tim() { - println!("Cleaning my room..."); - CHORES_DONE.fetch_add(1, Ordering::SeqCst); -} - -async fn carl() { - println!("Washing the dishes..."); - CHORES_DONE.fetch_add(1, Ordering::SeqCst); -} - -async fn nick() { - println!("Mowing the lawn..."); - CHORES_DONE.fetch_add(1, Ordering::SeqCst); +async fn calculate_mean_score(score_list: &[usize]) -> usize { + let score_sum: usize = score_list.iter().sum(); + score_sum / score_list.len() } From 9e76c96ef869a204f3b2cc16bfb490d72f662f5f Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Tue, 4 Aug 2026 23:06:38 +0200 Subject: [PATCH 05/13] async1: Read input from files --- dev/Cargo.toml | 2 +- exercises/24_async/async1.rs | 59 +++++++++++++++++----------------- input_files/scores_class_a.txt | 3 ++ input_files/scores_class_b.txt | 3 ++ input_files/scores_class_c.txt | 3 ++ rustlings-macros/info.toml | 9 ++++++ rustlings-macros/src/lib.rs | 20 +++++++++--- solutions/24_async/async1.rs | 58 ++++++++++++++++----------------- src/embedded.rs | 7 ++++ src/init.rs | 9 ++++++ 10 files changed, 107 insertions(+), 66 deletions(-) create mode 100644 input_files/scores_class_a.txt create mode 100644 input_files/scores_class_b.txt create mode 100644 input_files/scores_class_c.txt diff --git a/dev/Cargo.toml b/dev/Cargo.toml index 168fc10a..fe334b16 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -199,7 +199,7 @@ edition = "2024" publish = false [dependencies] -tokio = { version = "1", features = ["rt"] } +tokio = { version = "1", features = ["fs", "macros", "rt", "rt-multi-thread"] } [profile.release] panic = "abort" diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs index 8c2067ed..01183920 100644 --- a/exercises/24_async/async1.rs +++ b/exercises/24_async/async1.rs @@ -4,39 +4,38 @@ // 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 (i.e. -// they can be doing the calculations at the same time). - -fn main() { - // 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`. - let rt = tokio::runtime::Builder::new_current_thread() - .build() - .unwrap(); - - let scores_class_a = &[83, 77, 92]; - let scores_class_b = &[84, 88, 96]; - let scores_class_c = &[71, 83, 76]; +// 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() { // TODO: Fix the compiler errors by making the spawned function async. - let alice = rt.spawn(calculate_mean_score(scores_class_a)); - let bob = rt.spawn(calculate_mean_score(scores_class_b)); - let catherine = rt.spawn(calculate_mean_score(scores_class_c)); + let mean_score_a = tokio::spawn(calculate_mean_score("input_files/scores_class_a.txt")); + let mean_score_b = tokio::spawn(calculate_mean_score("input_files/scores_class_b.txt")); + let mean_score_c = tokio::spawn(calculate_mean_score("input_files/scores_class_c.txt")); - // Block the runtime on a task that awaits all three calculations. - let [mean_score_a, mean_score_b, mean_score_c]: [usize; _] = rt.block_on(async { - [ - // TODO: "await" all three tasks to fix the compiler error. - alice, bob, catherine, - ] - }); - - assert_eq!(mean_score_a, 84); - assert_eq!(mean_score_b, 89); - assert_eq!(mean_score_c, 76); + // 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 } -fn calculate_mean_score(score_list: &[usize]) -> usize { - let score_sum: usize = score_list.iter().sum(); - score_sum / score_list.len() +// 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::().unwrap(); + sum += score; + n += 1; + } + + sum / n } diff --git a/input_files/scores_class_a.txt b/input_files/scores_class_a.txt new file mode 100644 index 00000000..29fc7d8f --- /dev/null +++ b/input_files/scores_class_a.txt @@ -0,0 +1,3 @@ +83 +77 +92 diff --git a/input_files/scores_class_b.txt b/input_files/scores_class_b.txt new file mode 100644 index 00000000..7bbc2e8e --- /dev/null +++ b/input_files/scores_class_b.txt @@ -0,0 +1,3 @@ +84 +88 +96 diff --git a/input_files/scores_class_c.txt b/input_files/scores_class_c.txt new file mode 100644 index 00000000..cda6e810 --- /dev/null +++ b/input_files/scores_class_c.txt @@ -0,0 +1,3 @@ +71 +83 +76 diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index 533b1d50..d31fb4c7 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1225,3 +1225,12 @@ 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.""" + +[[input_files]] +name = "scores_class_a.txt" + +[[input_files]] +name = "scores_class_b.txt" + +[[input_files]] +name = "scores_class_c.txt" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index db758d59..a4104851 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -8,18 +8,23 @@ struct ExerciseInfo<'a> { dir: &'a str, } +#[derive(Deserialize)] +struct InputFileInfo<'a> { + name: &'a str, +} + #[derive(Deserialize)] struct InfoFile<'a> { #[serde(borrow)] exercises: Vec>, + input_files: Vec>, } #[proc_macro] pub fn include_files(_: TokenStream) -> TokenStream { let info_file = include_str!("../info.toml"); - let exercises = toml::de::from_str::(info_file) - .expect("Failed to parse `info.toml`") - .exercises; + let info = toml::de::from_str::(info_file).expect("Failed to parse `info.toml`"); + let exercises = info.exercises; let exercise_files = exercises .iter() @@ -46,11 +51,18 @@ pub fn include_files(_: TokenStream) -> TokenStream { .iter() .map(|dir| format!("../exercises/{dir}/README.md")); + let input_file_names = info.input_files.iter().map(|f| f.name); + let input_file_paths = info + .input_files + .iter() + .map(|f| format!("../input_files/{}", f.name)); + 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_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*], + input_files: &[#(InputFile { name: #input_file_names, content: include_str!(#input_file_paths) }),*], } } .into() diff --git a/solutions/24_async/async1.rs b/solutions/24_async/async1.rs index 925be766..9321447a 100644 --- a/solutions/24_async/async1.rs +++ b/solutions/24_async/async1.rs @@ -4,39 +4,35 @@ // 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 (i.e. -// they can be doing the calculations at the same time). +// represented as an asynchronous task, which can be executed concurrently. -fn main() { - // 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`. - let rt = tokio::runtime::Builder::new_current_thread() - .build() - .unwrap(); +// 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("input_files/scores_class_a.txt")); + let mean_score_b = tokio::spawn(calculate_mean_score("input_files/scores_class_b.txt")); + let mean_score_c = tokio::spawn(calculate_mean_score("input_files/scores_class_c.txt")); - let scores_class_a = &[83, 77, 92]; - let scores_class_b = &[84, 88, 96]; - let scores_class_c = &[71, 83, 76]; - - let alice = rt.spawn(calculate_mean_score(scores_class_a)); - let bob = rt.spawn(calculate_mean_score(scores_class_b)); - let catherine = rt.spawn(calculate_mean_score(scores_class_c)); - - // Block the runtime on a task that awaits all three calculations. - let [mean_score_a, mean_score_b, mean_score_c]: [usize; _] = rt.block_on(async { - [ - alice.await.unwrap(), - bob.await.unwrap(), - catherine.await.unwrap(), - ] - }); - - assert_eq!(mean_score_a, 84); - assert_eq!(mean_score_b, 89); - assert_eq!(mean_score_c, 76); + 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(score_list: &[usize]) -> usize { - let score_sum: usize = score_list.iter().sum(); - score_sum / score_list.len() +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::().unwrap(); + sum += score; + n += 1; + } + + sum / n } diff --git a/src/embedded.rs b/src/embedded.rs index bee4119c..3e0c0087 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -19,6 +19,12 @@ struct ExerciseFiles { dir_ind: usize, } +// Input files that may be read by exercises. +pub struct InputFile { + pub name: &'static str, + pub content: &'static str, +} + fn create_dir_if_not_exists(path: &str) -> Result<()> { if let Err(e) = create_dir(path) && e.kind() != io::ErrorKind::AlreadyExists @@ -58,6 +64,7 @@ pub struct EmbeddedFiles { pub info_file: &'static str, exercise_files: &'static [ExerciseFiles], pub exercise_dirs: &'static [ExerciseDir], + pub input_files: &'static [InputFile], } impl EmbeddedFiles { diff --git a/src/init.rs b/src/init.rs index 3ccef756..fe4332fb 100644 --- a/src/init.rs +++ b/src/init.rs @@ -144,6 +144,15 @@ pub fn init() -> Result<()> { .with_context(|| format!("Failed to create the file {solution_path}"))?; } + // init input files + create_dir("input_files").context("Failed to create the directory `input_files`")?; + for input_file in EMBEDDED_FILES.input_files { + fs::write( + format!("input_files/{}", input_file.name), + input_file.content, + )?; + } + let current_cargo_toml = include_str!("../dev-Cargo.toml"); // Skip the first line (comment). let newline_ind = current_cargo_toml From 09aabdea6521dbcbe7df5337f15d5a0e095e28a8 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 9 Aug 2026 14:04:16 +0200 Subject: [PATCH 06/13] async1: remove redundant TODO comment --- exercises/24_async/async1.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs index 01183920..c57efbed 100644 --- a/exercises/24_async/async1.rs +++ b/exercises/24_async/async1.rs @@ -11,7 +11,6 @@ // The macro `tokio::main` wraps the entire main function in a runtime. #[tokio::main] async fn main() { - // TODO: Fix the compiler errors by making the spawned function async. let mean_score_a = tokio::spawn(calculate_mean_score("input_files/scores_class_a.txt")); let mean_score_b = tokio::spawn(calculate_mean_score("input_files/scores_class_b.txt")); let mean_score_c = tokio::spawn(calculate_mean_score("input_files/scores_class_c.txt")); From 1c106e5f1e81c59a47826b178a4c8d09b7959f16 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 9 Aug 2026 14:04:35 +0200 Subject: [PATCH 07/13] omit tokio feature "rt", "rt-multi-thread" activates it --- dev/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/Cargo.toml b/dev/Cargo.toml index fe334b16..c57bc95d 100644 --- a/dev/Cargo.toml +++ b/dev/Cargo.toml @@ -199,7 +199,7 @@ edition = "2024" publish = false [dependencies] -tokio = { version = "1", features = ["fs", "macros", "rt", "rt-multi-thread"] } +tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread"] } [profile.release] panic = "abort" From f63e11aeb6d87bb13a8419726206ddbb7b129fce Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 9 Aug 2026 14:06:43 +0200 Subject: [PATCH 08/13] move input files into exercise directory --- exercises/24_async/async1.rs | 10 ++++-- .../24_async}/scores_class_a.txt | 0 .../24_async}/scores_class_b.txt | 0 .../24_async}/scores_class_c.txt | 0 rustlings-macros/info.toml | 14 +++----- rustlings-macros/src/lib.rs | 36 +++++++++++-------- solutions/24_async/async1.rs | 10 ++++-- src/embedded.rs | 9 ++++- src/init.rs | 9 ----- 9 files changed, 49 insertions(+), 39 deletions(-) rename {input_files => exercises/24_async}/scores_class_a.txt (100%) rename {input_files => exercises/24_async}/scores_class_b.txt (100%) rename {input_files => exercises/24_async}/scores_class_c.txt (100%) diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs index c57efbed..aa61ea70 100644 --- a/exercises/24_async/async1.rs +++ b/exercises/24_async/async1.rs @@ -6,14 +6,18 @@ // Let's simulate this using asynchronous programming. Each person is // represented as an asynchronous task, which can be executed concurrently. +const SCORES_CLASS_A: &str = "exercises/24_async/scores_class_a.txt"; +const SCORES_CLASS_B: &str = "exercises/24_async/scores_class_b.txt"; +const SCORES_CLASS_C: &str = "exercises/24_async/scores_class_c.txt"; + // 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("input_files/scores_class_a.txt")); - let mean_score_b = tokio::spawn(calculate_mean_score("input_files/scores_class_b.txt")); - let mean_score_c = tokio::spawn(calculate_mean_score("input_files/scores_class_c.txt")); + let mean_score_a = tokio::spawn(calculate_mean_score(SCORES_CLASS_A)); + let mean_score_b = tokio::spawn(calculate_mean_score(SCORES_CLASS_B)); + let mean_score_c = tokio::spawn(calculate_mean_score(SCORES_CLASS_C)); // TODO: Await the spawned tasks to check their results. assert_eq!(mean_score_a, 84); // alice diff --git a/input_files/scores_class_a.txt b/exercises/24_async/scores_class_a.txt similarity index 100% rename from input_files/scores_class_a.txt rename to exercises/24_async/scores_class_a.txt diff --git a/input_files/scores_class_b.txt b/exercises/24_async/scores_class_b.txt similarity index 100% rename from input_files/scores_class_b.txt rename to exercises/24_async/scores_class_b.txt diff --git a/input_files/scores_class_c.txt b/exercises/24_async/scores_class_c.txt similarity index 100% rename from input_files/scores_class_c.txt rename to exercises/24_async/scores_class_c.txt diff --git a/rustlings-macros/info.toml b/rustlings-macros/info.toml index d31fb4c7..01c47c4f 100644 --- a/rustlings-macros/info.toml +++ b/rustlings-macros/info.toml @@ -1218,6 +1218,11 @@ Add `AsRef` or `AsMut` as a trait bound to the functions.""" 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 @@ -1225,12 +1230,3 @@ 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.""" - -[[input_files]] -name = "scores_class_a.txt" - -[[input_files]] -name = "scores_class_b.txt" - -[[input_files]] -name = "scores_class_c.txt" diff --git a/rustlings-macros/src/lib.rs b/rustlings-macros/src/lib.rs index a4104851..b672b850 100644 --- a/rustlings-macros/src/lib.rs +++ b/rustlings-macros/src/lib.rs @@ -6,18 +6,14 @@ use serde::Deserialize; struct ExerciseInfo<'a> { name: &'a str, dir: &'a str, -} - -#[derive(Deserialize)] -struct InputFileInfo<'a> { - name: &'a str, + #[serde(default)] + input_files: Vec<&'a str>, } #[derive(Deserialize)] struct InfoFile<'a> { #[serde(borrow)] exercises: Vec>, - input_files: Vec>, } #[proc_macro] @@ -47,22 +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")); - let input_file_names = info.input_files.iter().map(|f| f.name); - let input_file_paths = info - .input_files - .iter() - .map(|f| format!("../input_files/{}", f.name)); - quote! { EmbeddedFiles { info_file: #info_file, - exercise_files: &[#(ExerciseFiles { exercise: include_bytes!(#exercise_files), solution: include_bytes!(#solution_files), dir_ind: #dir_inds }),*], + 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) }),*], - input_files: &[#(InputFile { name: #input_file_names, content: include_str!(#input_file_paths) }),*], } } .into() diff --git a/solutions/24_async/async1.rs b/solutions/24_async/async1.rs index 9321447a..97121c77 100644 --- a/solutions/24_async/async1.rs +++ b/solutions/24_async/async1.rs @@ -6,14 +6,18 @@ // Let's simulate this using asynchronous programming. Each person is // represented as an asynchronous task, which can be executed concurrently. +const SCORES_CLASS_A: &str = "exercises/24_async/scores_class_a.txt"; +const SCORES_CLASS_B: &str = "exercises/24_async/scores_class_b.txt"; +const SCORES_CLASS_C: &str = "exercises/24_async/scores_class_c.txt"; + // 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("input_files/scores_class_a.txt")); - let mean_score_b = tokio::spawn(calculate_mean_score("input_files/scores_class_b.txt")); - let mean_score_c = tokio::spawn(calculate_mean_score("input_files/scores_class_c.txt")); + let mean_score_a = tokio::spawn(calculate_mean_score(SCORES_CLASS_A)); + let mean_score_b = tokio::spawn(calculate_mean_score(SCORES_CLASS_B)); + let mean_score_c = tokio::spawn(calculate_mean_score(SCORES_CLASS_C)); assert_eq!(mean_score_a.await.unwrap(), 84); // alice assert_eq!(mean_score_b.await.unwrap(), 89); // bob diff --git a/src/embedded.rs b/src/embedded.rs index 3e0c0087..36263a9c 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -17,6 +17,8 @@ struct ExerciseFiles { solution: &'static [u8], // Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`. dir_ind: usize, + // Files that are read by the exercise. + input_files: &'static [InputFile], } // Input files that may be read by exercises. @@ -64,7 +66,6 @@ pub struct EmbeddedFiles { pub info_file: &'static str, exercise_files: &'static [ExerciseFiles], pub exercise_dirs: &'static [ExerciseDir], - pub input_files: &'static [InputFile], } impl EmbeddedFiles { @@ -97,6 +98,12 @@ impl EmbeddedFiles { fs::write(&exercise_path, exercise_files.exercise) .with_context(|| format!("Failed to write the exercise file {exercise_path}"))?; + + for InputFile { name, content } in exercise_files.input_files { + let path = format!("{prefix}/{dir_name}/{name}", dir_name = dir.name); + fs::write(&path, content) + .with_context(|| format!("Failed to write the input file {path}"))?; + } } Ok(()) diff --git a/src/init.rs b/src/init.rs index fe4332fb..3ccef756 100644 --- a/src/init.rs +++ b/src/init.rs @@ -144,15 +144,6 @@ pub fn init() -> Result<()> { .with_context(|| format!("Failed to create the file {solution_path}"))?; } - // init input files - create_dir("input_files").context("Failed to create the directory `input_files`")?; - for input_file in EMBEDDED_FILES.input_files { - fs::write( - format!("input_files/{}", input_file.name), - input_file.content, - )?; - } - let current_cargo_toml = include_str!("../dev-Cargo.toml"); // Skip the first line (comment). let newline_ind = current_cargo_toml From b054d8759e3827d293eee07f880d8bf50e388407 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 9 Aug 2026 14:53:55 +0200 Subject: [PATCH 09/13] dev check: allow input files --- src/dev/check.rs | 18 ++++++++++++------ src/info_file.rs | 3 +++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 58c2a174..925f1d86 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -133,19 +133,25 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { file_buf.clear(); - paths.insert(PathBuf::from(path)); + let path = PathBuf::from(path); + + for input_file in &exercise_info.input_files { + paths.insert(path.parent().unwrap().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) -> Result<()> { +fn check_unexpected_files(dir: &str, allowed_files: &HashSet) -> 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() ) }; @@ -160,7 +166,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> R continue; } - if !allowed_rust_files.contains(&path) { + if !allowed_files.contains(&path) { return Err(unexpected_file(&path)); } @@ -187,7 +193,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet) -> R continue; } - if !allowed_rust_files.contains(&path) { + if !allowed_files.contains(&path) { return Err(unexpected_file(&path)); } } diff --git a/src/info_file.rs b/src/info_file.rs index da4086aa..ece40afb 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -17,6 +17,9 @@ pub struct ExerciseInfo<'a> { /// Deny all Clippy warnings. #[serde(default)] pub strict_clippy: bool, + // Files that are read by the exercise. + #[serde(default)] + pub input_files: Vec<&'a str>, /// The exercise's hint to be shown to the user on request. pub hint: String, /// The exercise is already solved. Ignore it when checking that all exercises are unsolved. From 0f0d3b78ca7e35a7c3a3b6d16eb22812af069a12 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Sun, 9 Aug 2026 14:56:48 +0200 Subject: [PATCH 10/13] fix tests --- src/cargo_toml.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cargo_toml.rs b/src/cargo_toml.rs index 24077452..fcbf0588 100644 --- a/src/cargo_toml.rs +++ b/src/cargo_toml.rs @@ -110,6 +110,7 @@ mod tests { dir: None, test: true, strict_clippy: true, + input_files: vec![], hint: String::new(), skip_check_unsolved: false, }, @@ -118,6 +119,7 @@ mod tests { dir: Some("d"), test: false, strict_clippy: false, + input_files: vec![], hint: String::new(), skip_check_unsolved: false, }, From 343e21b3bd8f479ef7d9d74a21cd01c960197472 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Mon, 10 Aug 2026 21:50:15 +0200 Subject: [PATCH 11/13] run exercises with CWD set to their directory --- exercises/24_async/async1.rs | 10 +++------- solutions/24_async/async1.rs | 10 +++------- src/cmd.rs | 23 ++++++++++++++++++----- src/exercise.rs | 15 ++++++++++++--- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/exercises/24_async/async1.rs b/exercises/24_async/async1.rs index aa61ea70..4549759e 100644 --- a/exercises/24_async/async1.rs +++ b/exercises/24_async/async1.rs @@ -6,18 +6,14 @@ // Let's simulate this using asynchronous programming. Each person is // represented as an asynchronous task, which can be executed concurrently. -const SCORES_CLASS_A: &str = "exercises/24_async/scores_class_a.txt"; -const SCORES_CLASS_B: &str = "exercises/24_async/scores_class_b.txt"; -const SCORES_CLASS_C: &str = "exercises/24_async/scores_class_c.txt"; - // 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)); - let mean_score_b = tokio::spawn(calculate_mean_score(SCORES_CLASS_B)); - let mean_score_c = tokio::spawn(calculate_mean_score(SCORES_CLASS_C)); + 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 diff --git a/solutions/24_async/async1.rs b/solutions/24_async/async1.rs index 97121c77..45f0ab8e 100644 --- a/solutions/24_async/async1.rs +++ b/solutions/24_async/async1.rs @@ -6,18 +6,14 @@ // Let's simulate this using asynchronous programming. Each person is // represented as an asynchronous task, which can be executed concurrently. -const SCORES_CLASS_A: &str = "exercises/24_async/scores_class_a.txt"; -const SCORES_CLASS_B: &str = "exercises/24_async/scores_class_b.txt"; -const SCORES_CLASS_C: &str = "exercises/24_async/scores_class_c.txt"; - // 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)); - let mean_score_b = tokio::spawn(calculate_mean_score(SCORES_CLASS_B)); - let mean_score_c = tokio::spawn(calculate_mean_score(SCORES_CLASS_C)); + 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 diff --git a/src/cmd.rs b/src/cmd.rs index 094db5a0..0717ee0b 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -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>) -> Result { +fn run_cmd( + mut cmd: Command, + description: &str, + cwd: Option<&str>, + output: Option<&mut Vec>, +) -> Result { let spawn = |mut cmd: Command| { // The closure drops `cmd` which prevents a pipe deadlock. cmd.stdin(Stdio::null()) @@ -25,6 +30,9 @@ fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec>) -> .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) = @@ -133,7 +141,12 @@ 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>) -> Result { + pub fn run_debug_bin( + &self, + bin_name: &str, + cwd: &str, + output: Option<&mut Vec>, + ) -> Result { // 7 = "/debug/".len() let mut bin_path = PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len()); @@ -141,7 +154,7 @@ impl CmdRunner { 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) } } @@ -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 { - run_cmd(self.cmd, description, self.output) + run_cmd(self.cmd, description, None, self.output) } } @@ -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"); } diff --git a/src/exercise.rs b/src/exercise.rs index 7732e1a2..e31b3174 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -36,6 +36,7 @@ pub fn solution_link_line( // Compilation must be done before calling this method. fn run_bin( bin_name: &str, + cwd: &str, mut output: Option<&mut Vec>, cmd_runner: &CmdRunner, ) -> Result { @@ -46,7 +47,7 @@ fn run_bin( output.push(b'\n'); } - let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?; + let success = cmd_runner.run_debug_bin(bin_name, cwd, output.as_deref_mut())?; if let Some(output) = output && !success @@ -123,6 +124,14 @@ pub trait RunnableExercise { output.clear(); } + let cwd_buf; + let cwd = if let Some(dir) = self.dir() { + cwd_buf = format!("exercises/{dir}"); + cwd_buf.as_str() + } else { + "exercises" + }; + if self.test() { let output_is_some = output.is_some(); let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut()); @@ -131,7 +140,7 @@ pub trait RunnableExercise { } let test_success = test_cmd.run("cargo test …")?; if !test_success { - run_bin(bin_name, output, cmd_runner)?; + run_bin(bin_name, cwd, output, cmd_runner)?; return Ok(false); } @@ -151,7 +160,7 @@ pub trait RunnableExercise { } let clippy_success = clippy_cmd.run("cargo clippy …")?; - let run_success = run_bin(bin_name, output, cmd_runner)?; + let run_success = run_bin(bin_name, cwd, output, cmd_runner)?; Ok(clippy_success && run_success) } From c114fb3bbb0430125795d34d1a03b0ced29f23e7 Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Mon, 10 Aug 2026 22:10:20 +0200 Subject: [PATCH 12/13] write input files to disk before running exercise --- src/app_state.rs | 13 +++++++++++-- src/embedded.rs | 6 +++--- src/exercise.rs | 22 +++++++++++++++++++++- src/info_file.rs | 10 +++++++++- 4 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/app_state.rs b/src/app_state.rs index 541534a2..c29fe07f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -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 { @@ -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, @@ -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, @@ -597,6 +605,7 @@ mod tests { Exercise { name: "0", dir: None, + embedded_input_files: &[], path: "exercises/0.rs", canonical_path: None, test: false, diff --git a/src/embedded.rs b/src/embedded.rs index 36263a9c..9e2527d6 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -10,7 +10,7 @@ use crate::info_file::ExerciseInfo; pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); // Files related to one exercise. -struct ExerciseFiles { +pub struct ExerciseFiles { // The content of the exercise file. exercise: &'static [u8], // The content of the solution file. @@ -18,7 +18,7 @@ struct ExerciseFiles { // Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`. dir_ind: usize, // Files that are read by the exercise. - input_files: &'static [InputFile], + pub input_files: &'static [InputFile], } // Input files that may be read by exercises. @@ -64,7 +64,7 @@ impl ExerciseDir { pub struct EmbeddedFiles { /// The content of the `info.toml` file. pub info_file: &'static str, - exercise_files: &'static [ExerciseFiles], + pub exercise_files: &'static [ExerciseFiles], pub exercise_dirs: &'static [ExerciseDir], } diff --git a/src/exercise.rs b/src/exercise.rs index e31b3174..180e3b12 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use crossterm::{ QueueableCommand, style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, @@ -7,6 +7,7 @@ use std::io::{self, StdoutLock, Write}; use crate::{ cmd::CmdRunner, + embedded::InputFile, term::{self, CountedWrite, file_path, terminal_file_link, write_ansi}, }; @@ -69,6 +70,7 @@ fn run_bin( pub struct Exercise { pub name: &'static str, pub dir: Option<&'static str>, + pub embedded_input_files: &'static [InputFile], /// Path of the exercise file starting with the `exercises/` directory. pub path: &'static str, pub canonical_path: Option, @@ -99,6 +101,7 @@ pub trait RunnableExercise { fn dir(&self) -> Option<&str>; fn strict_clippy(&self) -> bool; fn test(&self) -> bool; + fn embedded_input_files(&self) -> &[InputFile]; // Compile, check and run the exercise or its solution (depending on `bin_name´). // The output is written to the `output` buffer after clearing it. @@ -112,6 +115,19 @@ pub trait RunnableExercise { output.clear(); } + // Input files are already written to disk during `rustlings init`. + // We repeat it here to ensure an exercise doesn't fail because the + // input files were deleted or modified in the meantime. Note that + // `self.embedded_input_files()` is empty for community exercises. + for input_file in self.embedded_input_files() { + let name = input_file.name; + let path = match self.dir() { + Some(dir) => format!("exercises/{dir}/{name}"), + None => format!("exercises/{name}"), + }; + std::fs::write(path, input_file.content).context("failed to write input file")?; + } + let build_success = cmd_runner .cargo("build", bin_name, output.as_deref_mut()) .run("cargo build …")?; @@ -224,4 +240,8 @@ impl RunnableExercise for Exercise { fn test(&self) -> bool { self.test } + + fn embedded_input_files(&self) -> &[InputFile] { + self.embedded_input_files + } } diff --git a/src/info_file.rs b/src/info_file.rs index ece40afb..047e0c18 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -2,7 +2,10 @@ use anyhow::{Context, Error, Result, bail}; use serde::Deserialize; use std::{fs, io::ErrorKind}; -use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; +use crate::{ + embedded::{EMBEDDED_FILES, InputFile}, + exercise::RunnableExercise, +}; /// Deserialized from the `info.toml` file. #[derive(Deserialize)] @@ -72,6 +75,11 @@ impl RunnableExercise for ExerciseInfo<'_> { fn test(&self) -> bool { self.test } + + fn embedded_input_files(&self) -> &[InputFile] { + // We don't have the input files embedded for communtiy exercises. + &[] + } } /// The deserialized `info.toml` file. From 2d58dced266140639476e00ad17f4e857d9d660d Mon Sep 17 00:00:00 2001 From: Remo Senekowitsch Date: Mon, 10 Aug 2026 22:30:06 +0200 Subject: [PATCH 13/13] optimize loop --- src/dev/check.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dev/check.rs b/src/dev/check.rs index 925f1d86..a9b64316 100644 --- a/src/dev/check.rs +++ b/src/dev/check.rs @@ -134,9 +134,10 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result> { file_buf.clear(); let path = PathBuf::from(path); + let parent = path.parent().unwrap(); for input_file in &exercise_info.input_files { - paths.insert(path.parent().unwrap().join(input_file)); + paths.insert(parent.join(input_file)); } paths.insert(path);