Merge 2d58dced266140639476e00ad17f4e857d9d660d into 02b22b49e7349b36305dbe97cd5d6e198d2f0537

This commit is contained in:
Remo Senekowitsch 2026-08-11 14:46:32 +03:00 committed by GitHub
commit 46a7822978
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 254 additions and 25 deletions

View File

@ -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", features = ["fs", "macros", "rt-multi-thread"] }
[profile.release]
panic = "abort"

View File

@ -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/)

View File

@ -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.
//
// 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
}

View File

@ -0,0 +1,3 @@
83
77
92

View File

@ -0,0 +1,3 @@
84
88
96

View File

@ -0,0 +1,3 @@
71
83
76

View File

@ -1208,3 +1208,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."""

View File

@ -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)]
@ -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()
@ -42,6 +43,20 @@ 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"));
@ -49,8 +64,13 @@ pub fn include_files(_: TokenStream) -> TokenStream {
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()

View File

@ -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
}

View File

@ -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,

View File

@ -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,
},

View File

@ -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())
@ -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) =
@ -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<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());
@ -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<bool> {
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");
}

View File

@ -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()
)
};
@ -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));
}
@ -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));
}
}

View File

@ -10,13 +10,21 @@ 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.
solution: &'static [u8],
// Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`.
dir_ind: usize,
// Files that are read by the exercise.
pub input_files: &'static [InputFile],
}
// 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<()> {
@ -56,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],
}
@ -90,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(())

View File

@ -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},
};
@ -36,6 +37,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<u8>>,
cmd_runner: &CmdRunner,
) -> Result<bool> {
@ -46,7 +48,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
@ -68,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<String>,
@ -98,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.
@ -111,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 …")?;
@ -123,6 +140,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 +156,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 +176,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)
}
@ -215,4 +240,8 @@ impl RunnableExercise for Exercise {
fn test(&self) -> bool {
self.test
}
fn embedded_input_files(&self) -> &[InputFile] {
self.embedded_input_files
}
}

View File

@ -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)]
@ -17,6 +20,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.
@ -69,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.