Compare commits

...

16 commits

Author SHA1 Message Date
Kacper Poneta af1410cd46
Merge 59e8f70e55 into a2d1cb3b22 2024-08-21 02:42:18 +08:00
mo8it a2d1cb3b22 Push newline after running an exercise instead on each rendering 2024-08-20 16:05:52 +02:00
mo8it e7ba88f905 Highlight the solution file 2024-08-20 16:04:29 +02:00
mo8it 50f6e5232e Leak info_file and cmd_runner in dev check 2024-08-20 14:47:08 +02:00
mo8it 8854f0a5ed Use anyhow! 2024-08-20 14:32:47 +02:00
mo8it 13cc3acdfd Improve readability 2024-08-20 13:56:52 +02:00
mo8it 5b7368c46d Improve error message if no exercise exists 2024-08-20 13:54:20 +02:00
mo8it 27999f2d26 Check if exercise doesn't contain tests 2024-08-20 13:49:48 +02:00
mo8it e74f2a4274 Check for #[test] with newline at the end 2024-08-20 13:39:14 +02:00
mo8it d141a73493 threads3: Improve the test 2024-08-20 13:35:07 +02:00
mo8it 631f44331e Remove --show-output for tests and use --format pretty 2024-08-20 13:08:15 +02:00
mo8it 59e8f70e55 Format code 2024-07-12 18:31:23 +02:00
mo8it 4c8365fe88 Update dev/Cargo.toml 2024-07-12 18:25:01 +02:00
Kacper Poneta 52af0674c1 changed the task to make it more appropriate 2024-07-12 18:14:40 +02:00
Kacper Poneta 938b90e5f2 very small solution update 2024-07-11 22:55:48 +02:00
Kacper Poneta 55cc8584bd added exercise 2024-07-11 22:53:38 +02:00
12 changed files with 291 additions and 171 deletions

View file

@ -116,6 +116,8 @@ bin = [
{ name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" }, { name = "generics1_sol", path = "../solutions/14_generics/generics1.rs" },
{ name = "generics2", path = "../exercises/14_generics/generics2.rs" }, { name = "generics2", path = "../exercises/14_generics/generics2.rs" },
{ name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" }, { name = "generics2_sol", path = "../solutions/14_generics/generics2.rs" },
{ name = "generics3", path = "../exercises/14_generics/generics3.rs" },
{ name = "generics3_sol", path = "../solutions/14_generics/generics3.rs" },
{ name = "traits1", path = "../exercises/15_traits/traits1.rs" }, { name = "traits1", path = "../exercises/15_traits/traits1.rs" },
{ name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" }, { name = "traits1_sol", path = "../solutions/15_traits/traits1.rs" },
{ name = "traits2", path = "../exercises/15_traits/traits2.rs" }, { name = "traits2", path = "../exercises/15_traits/traits2.rs" },

View file

@ -0,0 +1,54 @@
// generics3.rs
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
// This function should take an array of `Option` elements and returns array of not None elements
// TODO fix this function signature
fn into_dispose_nulls(list: Vec<Option<&str>>) -> Vec<&str> {
list.into_iter().flatten().collect()
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_str_on_list() {
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
let only_values = into_dispose_nulls(names_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_numbers_on_list() {
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
let only_values = into_dispose_nulls(numbers_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_custom_type_on_list() {
#[allow(dead_code)]
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Self {
Self { width, height }
}
}
let custom_list = vec![
Some(Rectangle::new(1, 2)),
None,
None,
Some(Rectangle::new(3, 4)),
];
let only_values = into_dispose_nulls(custom_list);
assert_eq!(only_values.len(), 2);
}
}

View file

@ -1,7 +1,6 @@
use std::{sync::mpsc, thread, time::Duration}; use std::{sync::mpsc, thread, time::Duration};
struct Queue { struct Queue {
length: u32,
first_half: Vec<u32>, first_half: Vec<u32>,
second_half: Vec<u32>, second_half: Vec<u32>,
} }
@ -9,7 +8,6 @@ struct Queue {
impl Queue { impl Queue {
fn new() -> Self { fn new() -> Self {
Self { Self {
length: 10,
first_half: vec![1, 2, 3, 4, 5], first_half: vec![1, 2, 3, 4, 5],
second_half: vec![6, 7, 8, 9, 10], second_half: vec![6, 7, 8, 9, 10],
} }
@ -48,17 +46,15 @@ mod tests {
fn threads3() { fn threads3() {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let queue = Queue::new(); let queue = Queue::new();
let queue_length = queue.length;
send_tx(queue, tx); send_tx(queue, tx);
let mut total_received: u32 = 0; let mut received = Vec::with_capacity(10);
for received in rx { for value in rx {
println!("Got: {received}"); received.push(value);
total_received += 1;
} }
println!("Number of received values: {total_received}"); received.sort();
assert_eq!(total_received, queue_length); assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
} }
} }

View file

@ -744,6 +744,17 @@ hint = """
Related section in The Book: Related section in The Book:
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions""" https://doc.rust-lang.org/book/ch10-01-syntax.html#in-method-definitions"""
[[exercises]]
name = "generics3"
dir = "14_generics"
hint = """
Vectors in Rust use generics to create dynamically-sized arrays of any type.
The `into_dispose_nulls` function takes a vector as an argument, but only accepts vectors that store the &str type.
To allow the function to accept vectors that store any type, you can leverage your knowledge about generics.
If you're unsure how to proceed, please refer to the Rust Book at:
https://doc.rust-lang.org/book/ch10-01-syntax.html#in-function-definitions.
"""
# TRAITS # TRAITS
[[exercises]] [[exercises]]

View file

@ -0,0 +1,53 @@
// generics3.rs
// Execute `rustlings hint generics3` or use the `hint` watch subcommand for a hint.
// Here we added generic type `T` to function signature
// Now this function can be used with vector of any
fn into_dispose_nulls<T>(list: Vec<Option<T>>) -> Vec<T> {
list.into_iter().flatten().collect()
}
fn main() {
// You can optionally experiment here.
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn store_str_on_list() {
let names_list = vec![Some("maria"), Some("jacob"), None, Some("kacper"), None];
let only_values = into_dispose_nulls(names_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_numbers_on_list() {
let numbers_list = vec![Some(1), Some(2), None, Some(3)];
let only_values = into_dispose_nulls(numbers_list);
assert_eq!(only_values.len(), 3);
}
#[test]
fn store_custom_type_on_list() {
struct Rectangle {
width: i32,
height: i32,
}
impl Rectangle {
fn new(width: i32, height: i32) -> Self {
Self { width, height }
}
}
let custom_list = vec![
Some(Rectangle::new(1, 2)),
None,
None,
Some(Rectangle::new(3, 4)),
];
let only_values = into_dispose_nulls(custom_list);
assert_eq!(only_values.len(), 2);
}
}

View file

@ -1,7 +1,6 @@
use std::{sync::mpsc, thread, time::Duration}; use std::{sync::mpsc, thread, time::Duration};
struct Queue { struct Queue {
length: u32,
first_half: Vec<u32>, first_half: Vec<u32>,
second_half: Vec<u32>, second_half: Vec<u32>,
} }
@ -9,7 +8,6 @@ struct Queue {
impl Queue { impl Queue {
fn new() -> Self { fn new() -> Self {
Self { Self {
length: 10,
first_half: vec![1, 2, 3, 4, 5], first_half: vec![1, 2, 3, 4, 5],
second_half: vec![6, 7, 8, 9, 10], second_half: vec![6, 7, 8, 9, 10],
} }
@ -50,17 +48,15 @@ mod tests {
fn threads3() { fn threads3() {
let (tx, rx) = mpsc::channel(); let (tx, rx) = mpsc::channel();
let queue = Queue::new(); let queue = Queue::new();
let queue_length = queue.length;
send_tx(queue, tx); send_tx(queue, tx);
let mut total_received: u32 = 0; let mut received = Vec::with_capacity(10);
for received in rx { for value in rx {
println!("Got: {received}"); received.push(value);
total_received += 1;
} }
println!("Number of received values: {total_received}"); received.sort();
assert_eq!(total_received, queue_length); assert_eq!(received, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
} }
} }

View file

@ -74,12 +74,14 @@ impl CmdRunner {
bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?"); bail!("The command `cargo metadata …` failed. Are you in the `rustlings/` directory?");
} }
let target_dir = serde_json::de::from_slice::<CargoMetadata>(&metadata_output.stdout) let metadata: CargoMetadata = serde_json::de::from_slice(&metadata_output.stdout)
.context( .context(
"Failed to read the field `target_directory` from the output of the command `cargo metadata …`", "Failed to read the field `target_directory` from the output of the command `cargo metadata …`",
)?.target_directory; )?;
Ok(Self { target_dir }) Ok(Self {
target_dir: metadata.target_directory,
})
} }
pub fn cargo<'out>( pub fn cargo<'out>(

View file

@ -97,7 +97,12 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<HashSet<PathBuf>> {
bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user."); bail!("Didn't find any `// TODO` comment in the file `{path}`.\nYou need to have at least one such comment to guide the user.");
} }
if !exercise_info.test && file_buf.contains("#[test]") { let contains_tests = file_buf.contains("#[test]\n");
if exercise_info.test {
if !contains_tests {
bail!("The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file");
}
} else if contains_tests {
bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file"); bail!("The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file");
} }
@ -160,71 +165,72 @@ fn check_unexpected_files(dir: &str, allowed_rust_files: &HashSet<PathBuf>) -> R
Ok(()) Ok(())
} }
fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { fn check_exercises_unsolved(
info_file: &'static InfoFile,
cmd_runner: &'static CmdRunner,
) -> Result<()> {
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?; stdout.write_all(b"Running all exercises to check that they aren't already solved...\n")?;
thread::scope(|s| { let handles = info_file
let handles = info_file .exercises
.exercises .iter()
.iter() .filter_map(|exercise_info| {
.filter_map(|exercise_info| { if exercise_info.skip_check_unsolved {
if exercise_info.skip_check_unsolved { return None;
return None;
}
Some((
exercise_info.name.as_str(),
s.spawn(|| exercise_info.run_exercise(None, cmd_runner)),
))
})
.collect::<Vec<_>>();
let n_handles = handles.len();
write!(stdout, "Progress: 0/{n_handles}")?;
stdout.flush()?;
let mut handle_num = 1;
for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}");
};
match result {
Ok(true) => bail!(
"The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",
),
Ok(false) => (),
Err(e) => return Err(e),
} }
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?; Some((
stdout.flush()?; exercise_info.name.as_str(),
handle_num += 1; thread::spawn(|| exercise_info.run_exercise(None, cmd_runner)),
} ))
stdout.write_all(b"\n")?; })
.collect::<Vec<_>>();
Ok(()) let n_handles = handles.len();
}) write!(stdout, "Progress: 0/{n_handles}")?;
stdout.flush()?;
let mut handle_num = 1;
for (exercise_name, handle) in handles {
let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}");
};
match result {
Ok(true) => {
bail!("The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",)
}
Ok(false) => (),
Err(e) => return Err(e),
}
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
stdout.flush()?;
handle_num += 1;
}
stdout.write_all(b"\n")?;
Ok(())
} }
fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> { fn check_exercises(info_file: &'static InfoFile, cmd_runner: &'static CmdRunner) -> Result<()> {
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) { match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"), Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"), Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
Ordering::Equal => (), Ordering::Equal => (),
} }
let info_file_paths = check_info_file_exercises(info_file)?; let handle = thread::spawn(move || check_exercises_unsolved(info_file, cmd_runner));
let handle = thread::spawn(move || check_unexpected_files("exercises", &info_file_paths));
let info_file_paths = check_info_file_exercises(info_file)?;
check_unexpected_files("exercises", &info_file_paths)?;
check_exercises_unsolved(info_file, cmd_runner)?;
handle.join().unwrap() handle.join().unwrap()
} }
enum SolutionCheck { enum SolutionCheck {
Success { sol_path: String }, Success { sol_path: String },
MissingRequired,
MissingOptional, MissingOptional,
RunFailure { output: Vec<u8> }, RunFailure { output: Vec<u8> },
Err(Error), Err(Error),
@ -232,101 +238,96 @@ enum SolutionCheck {
fn check_solutions( fn check_solutions(
require_solutions: bool, require_solutions: bool,
info_file: &InfoFile, info_file: &'static InfoFile,
cmd_runner: &CmdRunner, cmd_runner: &'static CmdRunner,
) -> Result<()> { ) -> Result<()> {
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
stdout.write_all(b"Running all solutions...\n")?; stdout.write_all(b"Running all solutions...\n")?;
thread::scope(|s| { let handles = info_file
let handles = info_file .exercises
.exercises .iter()
.iter() .map(|exercise_info| {
.map(|exercise_info| { thread::spawn(move || {
s.spawn(|| { let sol_path = exercise_info.sol_path();
let sol_path = exercise_info.sol_path(); if !Path::new(&sol_path).exists() {
if !Path::new(&sol_path).exists() { if require_solutions {
if require_solutions { return SolutionCheck::Err(anyhow!(
return SolutionCheck::MissingRequired; "The solution of the exercise {} is missing",
} exercise_info.name,
));
return SolutionCheck::MissingOptional;
} }
let mut output = Vec::with_capacity(OUTPUT_CAPACITY); return SolutionCheck::MissingOptional;
match exercise_info.run_solution(Some(&mut output), cmd_runner) { }
Ok(true) => SolutionCheck::Success { sol_path },
Ok(false) => SolutionCheck::RunFailure { output }, let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
Err(e) => SolutionCheck::Err(e), match exercise_info.run_solution(Some(&mut output), cmd_runner) {
} Ok(true) => SolutionCheck::Success { sol_path },
}) Ok(false) => SolutionCheck::RunFailure { output },
Err(e) => SolutionCheck::Err(e),
}
}) })
.collect::<Vec<_>>(); })
.collect::<Vec<_>>();
let mut sol_paths = hash_set_with_capacity(info_file.exercises.len()); let mut sol_paths = hash_set_with_capacity(info_file.exercises.len());
let mut fmt_cmd = Command::new("rustfmt"); let mut fmt_cmd = Command::new("rustfmt");
fmt_cmd fmt_cmd
.arg("--check") .arg("--check")
.arg("--edition") .arg("--edition")
.arg("2021") .arg("2021")
.arg("--color") .arg("--color")
.arg("always") .arg("always")
.stdin(Stdio::null()); .stdin(Stdio::null());
let n_handles = handles.len(); let n_handles = handles.len();
write!(stdout, "Progress: 0/{n_handles}")?; write!(stdout, "Progress: 0/{n_handles}")?;
stdout.flush()?; stdout.flush()?;
let mut handle_num = 1; let mut handle_num = 1;
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) { for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
let Ok(check_result) = handle.join() else { let Ok(check_result) = handle.join() else {
bail!(
"Panic while trying to run the solution of the exericse {}",
exercise_info.name,
);
};
match check_result {
SolutionCheck::Success { sol_path } => {
fmt_cmd.arg(&sol_path);
sol_paths.insert(PathBuf::from(sol_path));
}
SolutionCheck::MissingOptional => (),
SolutionCheck::RunFailure { output } => {
stdout.write_all(b"\n\n")?;
stdout.write_all(&output)?;
bail!( bail!(
"Panic while trying to run the solution of the exericse {}", "Running the solution of the exercise {} failed with the error above",
exercise_info.name, exercise_info.name,
); );
};
match check_result {
SolutionCheck::Success { sol_path } => {
fmt_cmd.arg(&sol_path);
sol_paths.insert(PathBuf::from(sol_path));
}
SolutionCheck::MissingRequired => {
bail!(
"The solution of the exercise {} is missing",
exercise_info.name,
);
}
SolutionCheck::MissingOptional => (),
SolutionCheck::RunFailure { output } => {
stdout.write_all(b"\n\n")?;
stdout.write_all(&output)?;
bail!(
"Running the solution of the exercise {} failed with the error above",
exercise_info.name,
);
}
SolutionCheck::Err(e) => return Err(e),
} }
SolutionCheck::Err(e) => return Err(e),
write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
stdout.flush()?;
handle_num += 1;
}
stdout.write_all(b"\n")?;
let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths));
if !fmt_cmd
.status()
.context("Failed to run `rustfmt` on all solution files")?
.success()
{
bail!("Some solutions aren't formatted. Run `rustfmt` on them");
} }
handle.join().unwrap() write!(stdout, "\rProgress: {handle_num}/{n_handles}")?;
}) stdout.flush()?;
handle_num += 1;
}
stdout.write_all(b"\n")?;
let handle = thread::spawn(move || check_unexpected_files("solutions", &sol_paths));
if !fmt_cmd
.status()
.context("Failed to run `rustfmt` on all solution files")?
.success()
{
bail!("Some solutions aren't formatted. Run `rustfmt` on them");
}
handle.join().unwrap()
} }
pub fn check(require_solutions: bool) -> Result<()> { pub fn check(require_solutions: bool) -> Result<()> {
@ -339,9 +340,12 @@ pub fn check(require_solutions: bool) -> Result<()> {
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?; check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
} }
let cmd_runner = CmdRunner::build()?; // Leaking is fine since they are used until the end of the program.
check_exercises(&info_file, &cmd_runner)?; let cmd_runner = Box::leak(Box::new(CmdRunner::build()?));
check_solutions(require_solutions, &info_file, &cmd_runner)?; let info_file = Box::leak(Box::new(info_file));
check_exercises(info_file, cmd_runner)?;
check_solutions(require_solutions, info_file, cmd_runner)?;
println!("Everything looks fine!"); println!("Everything looks fine!");

View file

@ -98,7 +98,7 @@ pub trait RunnableExercise {
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());
if output_is_some { if output_is_some {
test_cmd.args(["--", "--color", "always", "--show-output"]); test_cmd.args(["--", "--color", "always", "--format", "pretty"]);
} }
let test_success = test_cmd.run("cargo test …")?; let test_success = test_cmd.run("cargo test …")?;
if !test_success { if !test_success {

View file

@ -135,4 +135,4 @@ impl InfoFile {
} }
const NO_EXERCISES_ERR: &str = "There are no exercises yet! const NO_EXERCISES_ERR: &str = "There are no exercises yet!
If you are developing third-party exercises, add at least one exercise before testing."; Add at least one exercise before testing.";

View file

@ -33,18 +33,21 @@ pub fn run(app_state: &mut AppState) -> Result<()> {
)?; )?;
if let Some(solution_path) = app_state.current_solution_path()? { if let Some(solution_path) = app_state.current_solution_path()? {
println!( writeln!(
"\nA solution file can be found at {}\n", stdout,
style(TerminalFileLink(&solution_path)).underlined().green(), "\n{} for comparison: {}\n",
); "Solution".bold(),
style(TerminalFileLink(&solution_path)).underlined().cyan(),
)?;
} }
match app_state.done_current_exercise(&mut stdout)? { match app_state.done_current_exercise(&mut stdout)? {
ExercisesProgress::AllDone => (), ExercisesProgress::AllDone => (),
ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => println!( ExercisesProgress::CurrentPending | ExercisesProgress::NewPending => writeln!(
stdout,
"Next exercise: {}", "Next exercise: {}",
app_state.current_exercise().terminal_link(), app_state.current_exercise().terminal_link(),
), )?,
} }
Ok(()) Ok(())

View file

@ -56,10 +56,12 @@ impl<'a> WatchState<'a> {
"\nChecking the exercise `{}`. Please wait…", "\nChecking the exercise `{}`. Please wait…",
self.app_state.current_exercise().name, self.app_state.current_exercise().name,
)?; )?;
let success = self let success = self
.app_state .app_state
.current_exercise() .current_exercise()
.run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?; .run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?;
self.output.push(b'\n');
if success { if success {
self.done_status = self.done_status =
if let Some(solution_path) = self.app_state.current_solution_path()? { if let Some(solution_path) = self.app_state.current_solution_path()? {
@ -121,11 +123,9 @@ impl<'a> WatchState<'a> {
pub fn render(&mut self) -> Result<()> { pub fn render(&mut self) -> Result<()> {
// Prevent having the first line shifted if clearing wasn't successful. // Prevent having the first line shifted if clearing wasn't successful.
self.writer.write_all(b"\n")?; self.writer.write_all(b"\n")?;
clear_terminal(&mut self.writer)?; clear_terminal(&mut self.writer)?;
self.writer.write_all(&self.output)?; self.writer.write_all(&self.output)?;
self.writer.write_all(b"\n")?;
if self.show_hint { if self.show_hint {
writeln!( writeln!(
@ -137,21 +137,20 @@ impl<'a> WatchState<'a> {
} }
if self.done_status != DoneStatus::Pending { if self.done_status != DoneStatus::Pending {
writeln!( writeln!(self.writer, "{}", "Exercise done ✓".bold().green())?;
self.writer,
"{}\n", if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status {
"Exercise done ✓ writeln!(
When you are done experimenting, enter `n` to move on to the next exercise 🦀" self.writer,
.bold() "{} for comparison: {}",
.green(), "Solution".bold(),
)?; style(TerminalFileLink(solution_path)).underlined().cyan(),
} )?;
}
if let DoneStatus::DoneWithSolution(solution_path) = &self.done_status {
writeln!( writeln!(
self.writer, self.writer,
"A solution file can be found at {}\n", "When done experimenting, enter `n` to move on to the next exercise 🦀\n",
style(TerminalFileLink(solution_path)).underlined().green(),
)?; )?;
} }