Compare commits

...

9 commits

Author SHA1 Message Date
Mo b9a71c8ab5
Merge 9f517e4eb8 into 766f3c50ec 2024-08-01 16:00:09 +08:00
mo8it 766f3c50ec Add hint to run dev check again after dev update 2024-08-01 01:07:56 +02:00
mo8it 802b97b2ed Set stdin to null when running the binary of an exercise 2024-08-01 01:07:31 +02:00
mo8it 2ad408f2b8 Update deps 2024-07-31 18:54:24 +02:00
mo8it c8fddd8f62 Add Github profile links for every author 2024-07-31 18:53:25 +02:00
mo8it 74fab994e2 Make the output optional 2024-07-28 20:30:23 +02:00
mo8it 3a99542f73 Run the final check in parallel 2024-07-28 17:39:46 +02:00
mo8it 2ae9f3555b Update deps 2024-07-28 13:30:31 +02:00
mo8it 9f517e4eb8 Test apple target 2024-07-17 14:49:35 +02:00
10 changed files with 153 additions and 95 deletions

View file

@ -37,7 +37,10 @@ jobs:
- name: Run cargo test - name: Run cargo test
run: cargo test --workspace run: cargo test --workspace
dev-check: dev-check:
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macOS-latest]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: swatinem/rust-cache@v2 - uses: swatinem/rust-cache@v2

View file

@ -1,3 +1,10 @@
<a name="6.1.1"></a>
## 6.1.1 (UNRELEASED)
- Run the final check of all exercises in parallel.
- Small exercise improvements.
<a name="6.1.0"></a> <a name="6.1.0"></a>
## 6.1.0 (2024-07-10) ## 6.1.0 (2024-07-10)

33
Cargo.lock generated
View file

@ -357,9 +357,9 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]] [[package]]
name = "lru" name = "lru"
version = "0.12.3" version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc" checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
dependencies = [ dependencies = [
"hashbrown", "hashbrown",
] ]
@ -587,20 +587,21 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.120" version = "1.0.121"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" checksum = "4ab380d7d9f22ef3f21ad3e6c1ebe8e4fc7a2000ccba2e4d71fc96f15b2cb609"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr",
"ryu", "ryu",
"serde", "serde",
] ]
[[package]] [[package]]
name = "serde_spanned" name = "serde_spanned"
version = "0.6.6" version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0" checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d"
dependencies = [ dependencies = [
"serde", "serde",
] ]
@ -617,9 +618,9 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook-mio" name = "signal-hook-mio"
version = "0.2.3" version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd"
dependencies = [ dependencies = [
"libc", "libc",
"mio", "mio",
@ -698,18 +699,18 @@ dependencies = [
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.6.6" version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
dependencies = [ dependencies = [
"serde", "serde",
] ]
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.16" version = "0.22.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" checksum = "1490595c74d930da779e944f5ba2ecdf538af67df1a9848cbd156af43c1b7cf0"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"serde", "serde",
@ -755,9 +756,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.4" version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]] [[package]]
name = "walkdir" name = "walkdir"
@ -947,9 +948,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.15" version = "0.6.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" checksum = "b480ae9340fc261e6be3e95a1ba86d54ae3f9171132a73ce8d4bbaf68339507c"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]

View file

@ -8,10 +8,10 @@ exclude = [
[workspace.package] [workspace.package]
version = "6.1.0" version = "6.1.0"
authors = [ authors = [
"Liv <mokou@fastmail.com>", "Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
"Mo Bitar <mo8it@proton.me>", "Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
# Alumni # Alumni
"Carol (Nichols || Goulding) <carol.nichols@gmail.com>", "Carol (Nichols || Goulding) <carol.nichols@gmail.com>", # https://github.com/carols10cents
] ]
repository = "https://github.com/rust-lang/rustlings" repository = "https://github.com/rust-lang/rustlings"
license = "MIT" license = "MIT"
@ -19,7 +19,7 @@ edition = "2021"
[workspace.dependencies] [workspace.dependencies]
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
toml_edit = { version = "0.22.16", default-features = false, features = ["parse", "serde"] } toml_edit = { version = "0.22.18", default-features = false, features = ["parse", "serde"] }
[package] [package]
name = "rustlings" name = "rustlings"
@ -51,7 +51,7 @@ notify-debouncer-mini = { version = "0.4.1", default-features = false }
os_pipe = "1.2.0" os_pipe = "1.2.0"
ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] } ratatui = { version = "0.27.0", default-features = false, features = ["crossterm"] }
rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" } rustlings-macros = { path = "rustlings-macros", version = "=6.1.0" }
serde_json = "1.0.120" serde_json = "1.0.121"
serde.workspace = true serde.workspace = true
toml_edit.workspace = true toml_edit.workspace = true
@ -63,3 +63,7 @@ panic = "abort"
[package.metadata.release] [package.metadata.release]
pre-release-hook = ["./release-hook.sh"] pre-release-hook = ["./release-hook.sh"]
# TODO: Remove after the following fix is released: https://github.com/rust-lang/rust-clippy/pull/13102
[lints.clippy]
needless_option_as_deref = "allow"

View file

@ -1,17 +1,17 @@
use anyhow::{bail, Context, Result}; use anyhow::{bail, Context, Error, Result};
use ratatui::crossterm::style::Stylize;
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
fs::{self, File}, fs::{self, File},
io::{Read, StdoutLock, Write}, io::{Read, StdoutLock, Write},
path::{Path, PathBuf}, path::{Path, PathBuf},
process::{Command, Stdio}, process::{Command, Stdio},
thread,
}; };
use crate::{ use crate::{
clear_terminal, clear_terminal,
embedded::EMBEDDED_FILES, embedded::EMBEDDED_FILES,
exercise::{Exercise, RunnableExercise, OUTPUT_CAPACITY}, exercise::{Exercise, RunnableExercise},
info_file::ExerciseInfo, info_file::ExerciseInfo,
DEBUG_PROFILE, DEBUG_PROFILE,
}; };
@ -373,34 +373,49 @@ impl AppState {
if let Some(ind) = self.next_pending_exercise_ind() { if let Some(ind) = self.next_pending_exercise_ind() {
self.set_current_exercise_ind(ind)?; self.set_current_exercise_ind(ind)?;
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
let mut output = Vec::with_capacity(OUTPUT_CAPACITY); let n_exercises = self.exercises.len();
for (exercise_ind, exercise) in self.exercises().iter().enumerate() {
write!(writer, "Running {exercise} ... ")?;
writer.flush()?;
let success = exercise.run_exercise(&mut output, &self.target_dir)?; let pending_exercise_ind = thread::scope(|s| {
if !success { let handles = self
writeln!(writer, "{}\n", "FAILED".red())?; .exercises
.iter_mut()
.map(|exercise| {
s.spawn(|| {
let success = exercise.run_exercise(None, &self.target_dir)?;
exercise.done = success;
Ok::<_, Error>(success)
})
})
.collect::<Vec<_>>();
self.current_exercise_ind = exercise_ind; for (exercise_ind, handle) in handles.into_iter().enumerate() {
write!(writer, "\rProgress: {exercise_ind}/{n_exercises}")?;
writer.flush()?;
// No check if the exercise is done before setting it to pending let success = handle.join().unwrap()?;
// because no pending exercise was found. if !success {
self.exercises[exercise_ind].done = false; writer.write_all(b"\n\n")?;
self.n_done -= 1; return Ok(Some(exercise_ind));
}
self.write()?;
return Ok(ExercisesProgress::NewPending);
} }
writeln!(writer, "{}", "ok".green())?; Ok::<_, Error>(None)
})?;
if let Some(pending_exercise_ind) = pending_exercise_ind {
self.current_exercise_ind = pending_exercise_ind;
self.n_done = self
.exercises
.iter()
.filter(|exercise| exercise.done)
.count() as u16;
self.write()?;
return Ok(ExercisesProgress::NewPending);
} }
// Write that the last exercise is done. // Write that the last exercise is done.
@ -426,7 +441,6 @@ Try running `cargo --version` to diagnose the problem.";
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b" const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
All exercises seem to be done. All exercises seem to be done.
Recompiling and running all exercises to make sure that all of them are actually done. Recompiling and running all exercises to make sure that all of them are actually done.
"; ";
const FENISH_LINE: &str = "+----------------------------------------------------+ const FENISH_LINE: &str = "+----------------------------------------------------+

View file

@ -1,30 +1,43 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use std::{io::Read, path::Path, process::Command}; use std::{
io::Read,
path::Path,
process::{Command, Stdio},
};
/// 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.
pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec<u8>) -> Result<bool> { pub fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
let (mut reader, writer) = os_pipe::pipe() let spawn = |mut cmd: Command| {
.with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; // NOTE: The closure drops `cmd` which prevents a pipe deadlock.
cmd.stdin(Stdio::null())
.spawn()
.with_context(|| format!("Failed to run the command `{description}`"))
};
let writer_clone = writer.try_clone().with_context(|| { let mut handle = if let Some(output) = output {
format!("Failed to clone the pipe writer for the command `{description}`") let (mut reader, writer) = os_pipe::pipe().with_context(|| {
})?; format!("Failed to create a pipe to run the command `{description}``")
})?;
let mut handle = cmd let writer_clone = writer.try_clone().with_context(|| {
.stdout(writer_clone) format!("Failed to clone the pipe writer for the command `{description}`")
.stderr(writer) })?;
.spawn()
.with_context(|| format!("Failed to run the command `{description}`"))?;
// Prevent pipe deadlock. cmd.stdout(writer_clone).stderr(writer);
drop(cmd); let handle = spawn(cmd)?;
reader reader
.read_to_end(output) .read_to_end(output)
.with_context(|| format!("Failed to read the output of the command `{description}`"))?; .with_context(|| format!("Failed to read the output of the command `{description}`"))?;
output.push(b'\n'); output.push(b'\n');
handle
} else {
cmd.stdout(Stdio::null()).stderr(Stdio::null());
spawn(cmd)?
};
handle handle
.wait() .wait()
@ -42,14 +55,14 @@ pub struct CargoCmd<'a> {
/// Added as `--target-dir` if `Self::dev` is true. /// Added as `--target-dir` if `Self::dev` is true.
pub target_dir: &'a Path, pub target_dir: &'a Path,
/// The output buffer to append the merged stdout and stderr. /// The output buffer to append the merged stdout and stderr.
pub output: &'a mut Vec<u8>, pub output: Option<&'a mut Vec<u8>>,
/// true while developing Rustlings. /// true while developing Rustlings.
pub dev: bool, pub dev: bool,
} }
impl<'a> CargoCmd<'a> { impl<'a> CargoCmd<'a> {
/// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`. /// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
pub fn run(&mut self) -> Result<bool> { pub fn run(self) -> Result<bool> {
let mut cmd = Command::new("cargo"); let mut cmd = Command::new("cargo");
cmd.arg(self.subcommand); cmd.arg(self.subcommand);
@ -86,7 +99,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 …", &mut output).unwrap(); run_cmd(cmd, "echo …", Some(&mut output)).unwrap();
assert_eq!(output, b"Hello\n\n"); assert_eq!(output, b"Hello\n\n");
} }

View file

@ -41,7 +41,7 @@ fn check_cargo_toml(
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it"); bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it");
} }
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it"); bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
} }
Ok(()) Ok(())
@ -184,8 +184,7 @@ fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<(
error_occurred.store(true, atomic::Ordering::Relaxed); error_occurred.store(true, atomic::Ordering::Relaxed);
}; };
let mut output = Vec::with_capacity(OUTPUT_CAPACITY); match exercise_info.run_exercise(None, target_dir) {
match exercise_info.run_exercise(&mut output, target_dir) {
Ok(true) => error(b"Already solved!"), Ok(true) => error(b"Already solved!"),
Ok(false) => (), Ok(false) => (),
Err(e) => error(e.to_string().as_bytes()), Err(e) => error(e.to_string().as_bytes()),
@ -244,7 +243,7 @@ fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &P
} }
let mut output = Vec::with_capacity(OUTPUT_CAPACITY); let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
match exercise_info.run_solution(&mut output, target_dir) { match exercise_info.run_solution(Some(&mut output), target_dir) {
Ok(true) => { Ok(true) => {
paths.lock().unwrap().insert(PathBuf::from(path)); paths.lock().unwrap().insert(PathBuf::from(path));
} }

View file

@ -19,8 +19,10 @@ pub const OUTPUT_CAPACITY: usize = 1 << 14;
// Run an exercise binary and append its output to the `output` buffer. // Run an exercise binary and append its output to the `output` buffer.
// Compilation must be done before calling this method. // Compilation must be done before calling this method.
fn run_bin(bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> { fn run_bin(bin_name: &str, mut output: Option<&mut Vec<u8>>, target_dir: &Path) -> Result<bool> {
writeln!(output, "{}", "Output".underlined())?; if let Some(output) = output.as_deref_mut() {
writeln!(output, "{}", "Output".underlined())?;
}
// 7 = "/debug/".len() // 7 = "/debug/".len()
let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len()); let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len());
@ -28,19 +30,25 @@ fn run_bin(bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bo
bin_path.push("debug"); bin_path.push("debug");
bin_path.push(bin_name); bin_path.push(bin_name);
let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; let success = run_cmd(
Command::new(&bin_path),
&bin_path.to_string_lossy(),
output.as_deref_mut(),
)?;
if !success { if let Some(output) = output {
// This output is important to show the user that something went wrong. if !success {
// Otherwise, calling something like `exit(1)` in an exercise without further output // This output is important to show the user that something went wrong.
// leaves the user confused about why the exercise isn't done yet. // Otherwise, calling something like `exit(1)` in an exercise without further output
writeln!( // leaves the user confused about why the exercise isn't done yet.
output, writeln!(
"{}", output,
"The exercise didn't run successfully (nonzero exit code)" "{}",
.bold() "The exercise didn't run successfully (nonzero exit code)"
.red(), .bold()
)?; .red(),
)?;
}
} }
Ok(success) Ok(success)
@ -77,8 +85,15 @@ pub trait RunnableExercise {
// 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.
fn run(&self, bin_name: &str, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> { fn run(
output.clear(); &self,
bin_name: &str,
mut output: Option<&mut Vec<u8>>,
target_dir: &Path,
) -> Result<bool> {
if let Some(output) = output.as_deref_mut() {
output.clear();
}
// Developing the official Rustlings. // Developing the official Rustlings.
let dev = DEBUG_PROFILE && in_official_repo(); let dev = DEBUG_PROFILE && in_official_repo();
@ -90,7 +105,7 @@ pub trait RunnableExercise {
description: "cargo build …", description: "cargo build …",
hide_warnings: false, hide_warnings: false,
target_dir, target_dir,
output, output: output.as_deref_mut(),
dev, dev,
} }
.run()?; .run()?;
@ -99,7 +114,9 @@ pub trait RunnableExercise {
} }
// Discard the output of `cargo build` because it will be shown again by Clippy. // Discard the output of `cargo build` because it will be shown again by Clippy.
output.clear(); if let Some(output) = output.as_deref_mut() {
output.clear();
}
// `--profile test` is required to also check code with `[cfg(test)]`. // `--profile test` is required to also check code with `[cfg(test)]`.
let clippy_args: &[&str] = if self.strict_clippy() { let clippy_args: &[&str] = if self.strict_clippy() {
@ -114,7 +131,7 @@ pub trait RunnableExercise {
description: "cargo clippy …", description: "cargo clippy …",
hide_warnings: false, hide_warnings: false,
target_dir, target_dir,
output, output: output.as_deref_mut(),
dev, dev,
} }
.run()?; .run()?;
@ -123,7 +140,7 @@ pub trait RunnableExercise {
} }
if !self.test() { if !self.test() {
return run_bin(bin_name, output, target_dir); return run_bin(bin_name, output.as_deref_mut(), target_dir);
} }
let test_success = CargoCmd { let test_success = CargoCmd {
@ -134,12 +151,12 @@ pub trait RunnableExercise {
// Hide warnings because they are shown by Clippy. // Hide warnings because they are shown by Clippy.
hide_warnings: true, hide_warnings: true,
target_dir, target_dir,
output, output: output.as_deref_mut(),
dev, dev,
} }
.run()?; .run()?;
let run_success = run_bin(bin_name, output, target_dir)?; let run_success = run_bin(bin_name, output.as_deref_mut(), target_dir)?;
Ok(test_success && run_success) Ok(test_success && run_success)
} }
@ -147,13 +164,13 @@ pub trait RunnableExercise {
/// Compile, check and run the exercise. /// Compile, check and run the exercise.
/// The output is written to the `output` buffer after clearing it. /// The output is written to the `output` buffer after clearing it.
#[inline] #[inline]
fn run_exercise(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> { fn run_exercise(&self, output: Option<&mut Vec<u8>>, target_dir: &Path) -> Result<bool> {
self.run(self.name(), output, target_dir) self.run(self.name(), output, target_dir)
} }
/// Compile, check and run the exercise's solution. /// Compile, check and run the exercise's solution.
/// The output is written to the `output` buffer after clearing it. /// The output is written to the `output` buffer after clearing it.
fn run_solution(&self, output: &mut Vec<u8>, target_dir: &Path) -> Result<bool> { fn run_solution(&self, output: Option<&mut Vec<u8>>, target_dir: &Path) -> Result<bool> {
let name = self.name(); let name = self.name();
let mut bin_name = String::with_capacity(name.len()); let mut bin_name = String::with_capacity(name.len());
bin_name.push_str(name); bin_name.push_str(name);

View file

@ -11,7 +11,7 @@ use crate::{
pub fn run(app_state: &mut AppState) -> Result<()> { pub fn run(app_state: &mut AppState) -> Result<()> {
let exercise = app_state.current_exercise(); let exercise = app_state.current_exercise();
let mut output = Vec::with_capacity(OUTPUT_CAPACITY); let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
let success = exercise.run_exercise(&mut output, app_state.target_dir())?; let success = exercise.run_exercise(Some(&mut output), app_state.target_dir())?;
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
stdout.write_all(&output)?; stdout.write_all(&output)?;

View file

@ -54,7 +54,7 @@ impl<'a> WatchState<'a> {
let success = self let success = self
.app_state .app_state
.current_exercise() .current_exercise()
.run_exercise(&mut self.output, self.app_state.target_dir())?; .run_exercise(Some(&mut self.output), self.app_state.target_dir())?;
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()? {