mirror of
https://github.com/rust-lang/rustlings.git
synced 2026-08-14 14:26:56 +00:00
Merge 2d58dced266140639476e00ad17f4e857d9d660d into 02b22b49e7349b36305dbe97cd5d6e198d2f0537
This commit is contained in:
commit
46a7822978
@ -188,6 +188,8 @@ bin = [
|
|||||||
{ name = "conversions4_sol", path = "../solutions/23_conversions/conversions4.rs" },
|
{ name = "conversions4_sol", path = "../solutions/23_conversions/conversions4.rs" },
|
||||||
{ name = "conversions5", path = "../exercises/23_conversions/conversions5.rs" },
|
{ name = "conversions5", path = "../exercises/23_conversions/conversions5.rs" },
|
||||||
{ name = "conversions5_sol", path = "../solutions/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]
|
[package]
|
||||||
@ -196,6 +198,9 @@ edition = "2024"
|
|||||||
# Don't publish the exercises on crates.io!
|
# Don't publish the exercises on crates.io!
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
||||||
|
|||||||
13
exercises/24_async/README.md
Normal file
13
exercises/24_async/README.md
Normal 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/)
|
||||||
40
exercises/24_async/async1.rs
Normal file
40
exercises/24_async/async1.rs
Normal 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
|
||||||
|
}
|
||||||
3
exercises/24_async/scores_class_a.txt
Normal file
3
exercises/24_async/scores_class_a.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
83
|
||||||
|
77
|
||||||
|
92
|
||||||
3
exercises/24_async/scores_class_b.txt
Normal file
3
exercises/24_async/scores_class_b.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
84
|
||||||
|
88
|
||||||
|
96
|
||||||
3
exercises/24_async/scores_class_c.txt
Normal file
3
exercises/24_async/scores_class_c.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
71
|
||||||
|
83
|
||||||
|
76
|
||||||
@ -1208,3 +1208,22 @@ name = "conversions5"
|
|||||||
dir = "23_conversions"
|
dir = "23_conversions"
|
||||||
hint = """
|
hint = """
|
||||||
Add `AsRef<str>` or `AsMut<u32>` as a trait bound to the functions."""
|
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."""
|
||||||
|
|||||||
@ -6,6 +6,8 @@ use serde::Deserialize;
|
|||||||
struct ExerciseInfo<'a> {
|
struct ExerciseInfo<'a> {
|
||||||
name: &'a str,
|
name: &'a str,
|
||||||
dir: &'a str,
|
dir: &'a str,
|
||||||
|
#[serde(default)]
|
||||||
|
input_files: Vec<&'a str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -17,9 +19,8 @@ struct InfoFile<'a> {
|
|||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn include_files(_: TokenStream) -> TokenStream {
|
pub fn include_files(_: TokenStream) -> TokenStream {
|
||||||
let info_file = include_str!("../info.toml");
|
let info_file = include_str!("../info.toml");
|
||||||
let exercises = toml::de::from_str::<InfoFile>(info_file)
|
let info = toml::de::from_str::<InfoFile>(info_file).expect("Failed to parse `info.toml`");
|
||||||
.expect("Failed to parse `info.toml`")
|
let exercises = info.exercises;
|
||||||
.exercises;
|
|
||||||
|
|
||||||
let exercise_files = exercises
|
let exercise_files = exercises
|
||||||
.iter()
|
.iter()
|
||||||
@ -42,6 +43,20 @@ pub fn include_files(_: TokenStream) -> TokenStream {
|
|||||||
*dir_ind = dirs.len() - 1;
|
*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
|
let readmes = dirs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|dir| format!("../exercises/{dir}/README.md"));
|
.map(|dir| format!("../exercises/{dir}/README.md"));
|
||||||
@ -49,8 +64,13 @@ pub fn include_files(_: TokenStream) -> TokenStream {
|
|||||||
quote! {
|
quote! {
|
||||||
EmbeddedFiles {
|
EmbeddedFiles {
|
||||||
info_file: #info_file,
|
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_dirs: &[#(ExerciseDir { name: #dirs, readme: include_bytes!(#readmes) }),*]
|
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()
|
.into()
|
||||||
|
|||||||
38
solutions/24_async/async1.rs
Normal file
38
solutions/24_async/async1.rs
Normal 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
|
||||||
|
}
|
||||||
@ -81,9 +81,11 @@ impl AppState {
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let dir_canonical_path = term::canonicalize("exercises");
|
let dir_canonical_path = term::canonicalize("exercises");
|
||||||
|
let official_exercises = !Path::new("info.toml").exists();
|
||||||
let mut exercises = exercise_infos
|
let mut exercises = exercise_infos
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|exercise_info| {
|
.enumerate()
|
||||||
|
.map(|(i, exercise_info)| {
|
||||||
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
|
let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| {
|
||||||
let mut canonical_path;
|
let mut canonical_path;
|
||||||
if let Some(dir) = exercise_info.dir {
|
if let Some(dir) = exercise_info.dir {
|
||||||
@ -105,10 +107,16 @@ impl AppState {
|
|||||||
canonical_path.push_str(".rs");
|
canonical_path.push_str(".rs");
|
||||||
canonical_path
|
canonical_path
|
||||||
});
|
});
|
||||||
|
let embedded_input_files = if official_exercises {
|
||||||
|
EMBEDDED_FILES.exercise_files[i].input_files
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
};
|
||||||
|
|
||||||
Exercise {
|
Exercise {
|
||||||
name: exercise_info.name,
|
name: exercise_info.name,
|
||||||
dir: exercise_info.dir,
|
dir: exercise_info.dir,
|
||||||
|
embedded_input_files,
|
||||||
// LEAKING: For `Editor::open`. The app state is used until the end of the program.
|
// LEAKING: For `Editor::open`. The app state is used until the end of the program.
|
||||||
path: exercise_info.path().leak(),
|
path: exercise_info.path().leak(),
|
||||||
canonical_path,
|
canonical_path,
|
||||||
@ -173,7 +181,7 @@ impl AppState {
|
|||||||
final_message,
|
final_message,
|
||||||
state_file,
|
state_file,
|
||||||
file_buf,
|
file_buf,
|
||||||
official_exercises: !Path::new("info.toml").exists(),
|
official_exercises,
|
||||||
cmd_runner,
|
cmd_runner,
|
||||||
// VS Code has its own file link handling
|
// VS Code has its own file link handling
|
||||||
emit_file_links: !vs_code_term,
|
emit_file_links: !vs_code_term,
|
||||||
@ -597,6 +605,7 @@ mod tests {
|
|||||||
Exercise {
|
Exercise {
|
||||||
name: "0",
|
name: "0",
|
||||||
dir: None,
|
dir: None,
|
||||||
|
embedded_input_files: &[],
|
||||||
path: "exercises/0.rs",
|
path: "exercises/0.rs",
|
||||||
canonical_path: None,
|
canonical_path: None,
|
||||||
test: false,
|
test: false,
|
||||||
|
|||||||
@ -110,6 +110,7 @@ mod tests {
|
|||||||
dir: None,
|
dir: None,
|
||||||
test: true,
|
test: true,
|
||||||
strict_clippy: true,
|
strict_clippy: true,
|
||||||
|
input_files: vec![],
|
||||||
hint: String::new(),
|
hint: String::new(),
|
||||||
skip_check_unsolved: false,
|
skip_check_unsolved: false,
|
||||||
},
|
},
|
||||||
@ -118,6 +119,7 @@ mod tests {
|
|||||||
dir: Some("d"),
|
dir: Some("d"),
|
||||||
test: false,
|
test: false,
|
||||||
strict_clippy: false,
|
strict_clippy: false,
|
||||||
|
input_files: vec![],
|
||||||
hint: String::new(),
|
hint: String::new(),
|
||||||
skip_check_unsolved: false,
|
skip_check_unsolved: false,
|
||||||
},
|
},
|
||||||
|
|||||||
23
src/cmd.rs
23
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.
|
/// 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.
|
/// 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| {
|
let spawn = |mut cmd: Command| {
|
||||||
// The closure drops `cmd` which prevents a pipe deadlock.
|
// The closure drops `cmd` which prevents a pipe deadlock.
|
||||||
cmd.stdin(Stdio::null())
|
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))
|
.wait_timeout(Duration::from_secs(TIMEOUT_SECS))
|
||||||
.with_context(|| format!("Failed to wait on `{description}` to exit"))
|
.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 handle = if let Some(output) = output {
|
||||||
let (mut reader, writer) =
|
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.
|
/// 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()
|
// 7 = "/debug/".len()
|
||||||
let mut bin_path =
|
let mut bin_path =
|
||||||
PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len());
|
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("debug");
|
||||||
bin_path.push(bin_name);
|
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.
|
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||||
pub fn run(self, description: &str) -> Result<bool> {
|
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");
|
cmd.arg("Hello");
|
||||||
|
|
||||||
let mut output = Vec::with_capacity(8);
|
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");
|
assert_eq!(output, b"Hello\n\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,19 +133,26 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
|
|||||||
|
|
||||||
file_buf.clear();
|
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)
|
Ok(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check `dir` for unexpected files.
|
// 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.
|
// 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| {
|
let unexpected_file = |path: &Path| {
|
||||||
anyhow!(
|
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()
|
path.display()
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
@ -160,7 +167,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allowed_rust_files.contains(&path) {
|
if !allowed_files.contains(&path) {
|
||||||
return Err(unexpected_file(&path));
|
return Err(unexpected_file(&path));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +194,7 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allowed_rust_files.contains(&path) {
|
if !allowed_files.contains(&path) {
|
||||||
return Err(unexpected_file(&path));
|
return Err(unexpected_file(&path));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,13 +10,21 @@ use crate::info_file::ExerciseInfo;
|
|||||||
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!();
|
||||||
|
|
||||||
// Files related to one exercise.
|
// Files related to one exercise.
|
||||||
struct ExerciseFiles {
|
pub struct ExerciseFiles {
|
||||||
// The content of the exercise file.
|
// The content of the exercise file.
|
||||||
exercise: &'static [u8],
|
exercise: &'static [u8],
|
||||||
// The content of the solution file.
|
// The content of the solution file.
|
||||||
solution: &'static [u8],
|
solution: &'static [u8],
|
||||||
// Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`.
|
// Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`.
|
||||||
dir_ind: usize,
|
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<()> {
|
fn create_dir_if_not_exists(path: &str) -> Result<()> {
|
||||||
@ -56,7 +64,7 @@ impl ExerciseDir {
|
|||||||
pub struct EmbeddedFiles {
|
pub struct EmbeddedFiles {
|
||||||
/// The content of the `info.toml` file.
|
/// The content of the `info.toml` file.
|
||||||
pub info_file: &'static str,
|
pub info_file: &'static str,
|
||||||
exercise_files: &'static [ExerciseFiles],
|
pub exercise_files: &'static [ExerciseFiles],
|
||||||
pub exercise_dirs: &'static [ExerciseDir],
|
pub exercise_dirs: &'static [ExerciseDir],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +98,12 @@ impl EmbeddedFiles {
|
|||||||
|
|
||||||
fs::write(&exercise_path, exercise_files.exercise)
|
fs::write(&exercise_path, exercise_files.exercise)
|
||||||
.with_context(|| format!("Failed to write the exercise file {exercise_path}"))?;
|
.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(())
|
Ok(())
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
QueueableCommand,
|
QueueableCommand,
|
||||||
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor},
|
||||||
@ -7,6 +7,7 @@ use std::io::{self, StdoutLock, Write};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
cmd::CmdRunner,
|
cmd::CmdRunner,
|
||||||
|
embedded::InputFile,
|
||||||
term::{self, CountedWrite, file_path, terminal_file_link, write_ansi},
|
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.
|
// Compilation must be done before calling this method.
|
||||||
fn run_bin(
|
fn run_bin(
|
||||||
bin_name: &str,
|
bin_name: &str,
|
||||||
|
cwd: &str,
|
||||||
mut output: Option<&mut Vec<u8>>,
|
mut output: Option<&mut Vec<u8>>,
|
||||||
cmd_runner: &CmdRunner,
|
cmd_runner: &CmdRunner,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
@ -46,7 +48,7 @@ fn run_bin(
|
|||||||
output.push(b'\n');
|
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
|
if let Some(output) = output
|
||||||
&& !success
|
&& !success
|
||||||
@ -68,6 +70,7 @@ fn run_bin(
|
|||||||
pub struct Exercise {
|
pub struct Exercise {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub dir: Option<&'static str>,
|
pub dir: Option<&'static str>,
|
||||||
|
pub embedded_input_files: &'static [InputFile],
|
||||||
/// Path of the exercise file starting with the `exercises/` directory.
|
/// Path of the exercise file starting with the `exercises/` directory.
|
||||||
pub path: &'static str,
|
pub path: &'static str,
|
||||||
pub canonical_path: Option<String>,
|
pub canonical_path: Option<String>,
|
||||||
@ -98,6 +101,7 @@ pub trait RunnableExercise {
|
|||||||
fn dir(&self) -> Option<&str>;
|
fn dir(&self) -> Option<&str>;
|
||||||
fn strict_clippy(&self) -> bool;
|
fn strict_clippy(&self) -> bool;
|
||||||
fn test(&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´).
|
// Compile, check and run the exercise or its solution (depending on `bin_name´).
|
||||||
// The output is written to the `output` buffer after clearing it.
|
// The output is written to the `output` buffer after clearing it.
|
||||||
@ -111,6 +115,19 @@ pub trait RunnableExercise {
|
|||||||
output.clear();
|
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
|
let build_success = cmd_runner
|
||||||
.cargo("build", bin_name, output.as_deref_mut())
|
.cargo("build", bin_name, output.as_deref_mut())
|
||||||
.run("cargo build …")?;
|
.run("cargo build …")?;
|
||||||
@ -123,6 +140,14 @@ pub trait RunnableExercise {
|
|||||||
output.clear();
|
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() {
|
if self.test() {
|
||||||
let output_is_some = output.is_some();
|
let output_is_some = output.is_some();
|
||||||
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
|
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 …")?;
|
let test_success = test_cmd.run("cargo test …")?;
|
||||||
if !test_success {
|
if !test_success {
|
||||||
run_bin(bin_name, output, cmd_runner)?;
|
run_bin(bin_name, cwd, output, cmd_runner)?;
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -151,7 +176,7 @@ pub trait RunnableExercise {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let clippy_success = clippy_cmd.run("cargo clippy …")?;
|
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)
|
Ok(clippy_success && run_success)
|
||||||
}
|
}
|
||||||
@ -215,4 +240,8 @@ impl RunnableExercise for Exercise {
|
|||||||
fn test(&self) -> bool {
|
fn test(&self) -> bool {
|
||||||
self.test
|
self.test
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn embedded_input_files(&self) -> &[InputFile] {
|
||||||
|
self.embedded_input_files
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,10 @@ use anyhow::{Context, Error, Result, bail};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{fs, io::ErrorKind};
|
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.
|
/// Deserialized from the `info.toml` file.
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
@ -17,6 +20,9 @@ pub struct ExerciseInfo<'a> {
|
|||||||
/// Deny all Clippy warnings.
|
/// Deny all Clippy warnings.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub strict_clippy: bool,
|
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.
|
/// The exercise's hint to be shown to the user on request.
|
||||||
pub hint: String,
|
pub hint: String,
|
||||||
/// The exercise is already solved. Ignore it when checking that all exercises are unsolved.
|
/// 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 {
|
fn test(&self) -> bool {
|
||||||
self.test
|
self.test
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn embedded_input_files(&self) -> &[InputFile] {
|
||||||
|
// We don't have the input files embedded for communtiy exercises.
|
||||||
|
&[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The deserialized `info.toml` file.
|
/// The deserialized `info.toml` file.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user