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 = "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"
|
||||
|
||||
|
||||
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"
|
||||
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."""
|
||||
|
||||
@ -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()
|
||||
|
||||
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 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,
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
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.
|
||||
/// 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");
|
||||
}
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(())
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user