Compare commits

..

No commits in common. "4ce8667b9d878dc48fafb665699a5fc71c190972" and "13124aafe3fd0fcd5efad12419ea5cc5a3b8ceef" have entirely different histories.

19 changed files with 110 additions and 146 deletions

View file

@ -1,10 +1,8 @@
<a name="6.2.0"></a> <a name="6.1.1"></a>
## 6.2.0 (2024-08-08) ## 6.1.1 (UNRELEASED)
- Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports. - Show a helpful error message when trying to install Rustlings with a Rust version lower than the minimum one that Rustlings supports.
- Remove the state file and the solutions directory from the generated `.gitignore` file.
- Add a `README.md` file to the `solutions/` directory.
- Run the final check of all exercises in parallel. - Run the final check of all exercises in parallel.
- Small exercise improvements. - Small exercise improvements.
- `dev check`: Check that all solutions are formatted with `rustfmt`. - `dev check`: Check that all solutions are formatted with `rustfmt`.

View file

@ -88,6 +88,8 @@ While working with Rustlings, please use a modern terminal for the best user exp
The default terminal on Linux and Mac should be sufficient. The default terminal on Linux and Mac should be sufficient.
On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal). On Windows, we recommend the [Windows Terminal](https://aka.ms/terminal).
If you use VS Code, the builtin terminal should also be fine.
## Doing exercises ## Doing exercises
The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`. The exercises are sorted by topic and can be found in the subdirectory `exercises/<topic>`.

View file

@ -195,9 +195,3 @@ name = "exercises"
edition = "2021" edition = "2021"
# Don't publish the exercises on crates.io! # Don't publish the exercises on crates.io!
publish = false publish = false
[profile.release]
panic = "abort"
[profile.dev]
panic = "abort"

View file

@ -10,12 +10,12 @@ use std::collections::HashMap;
// A structure to store the goal details of a team. // A structure to store the goal details of a team.
#[derive(Default)] #[derive(Default)]
struct TeamScores { struct Team {
goals_scored: u8, goals_scored: u8,
goals_conceded: u8, goals_conceded: u8,
} }
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { fn build_scores_table(results: &str) -> HashMap<&str, Team> {
// The name of the team is the key and its associated struct is the value. // The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new(); let mut scores = HashMap::new();

View file

@ -25,7 +25,7 @@ impl ParsePosNonzeroError {
} }
// TODO: Add another error conversion function here. // TODO: Add another error conversion function here.
// fn from_parse_int(???) -> Self { ??? } // fn from_parseint(???) -> Self { ??? }
} }
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]

View file

@ -9,5 +9,6 @@ cargo upgrades
# Similar to CI # Similar to CI
cargo clippy -- --deny warnings cargo clippy -- --deny warnings
cargo fmt --all --check cargo fmt --all --check
rustfmt --check --edition 2021 solutions/**/*.rs
cargo test --workspace --all-targets cargo test --workspace --all-targets
cargo run -- dev check --require-solutions cargo run -- dev check --require-solutions

View file

@ -571,7 +571,7 @@ name = "hashmaps3"
dir = "11_hashmaps" dir = "11_hashmaps"
hint = """ hint = """
Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of Hint 1: Use the `entry()` and `or_insert()` (or `or_insert_with()`) methods of
`HashMap` to insert the default value of `TeamScores` if a team doesn't `HashMap` to insert the default value of `Team` if a team doesn't
exist in the table yet. exist in the table yet.
Learn more in The Book: Learn more in The Book:

View file

@ -10,12 +10,12 @@ use std::collections::HashMap;
// A structure to store the goal details of a team. // A structure to store the goal details of a team.
#[derive(Default)] #[derive(Default)]
struct TeamScores { struct Team {
goals_scored: u8, goals_scored: u8,
goals_conceded: u8, goals_conceded: u8,
} }
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> { fn build_scores_table(results: &str) -> HashMap<&str, Team> {
// The name of the team is the key and its associated struct is the value. // The name of the team is the key and its associated struct is the value.
let mut scores = HashMap::new(); let mut scores = HashMap::new();
@ -28,17 +28,13 @@ fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap(); let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
// Insert the default with zeros if a team doesn't exist yet. // Insert the default with zeros if a team doesn't exist yet.
let team_1 = scores let team_1 = scores.entry(team_1_name).or_insert_with(Team::default);
.entry(team_1_name)
.or_insert_with(TeamScores::default);
// Update the values. // Update the values.
team_1.goals_scored += team_1_score; team_1.goals_scored += team_1_score;
team_1.goals_conceded += team_2_score; team_1.goals_conceded += team_2_score;
// Similarely for the second team. // Similarely for the second team.
let team_2 = scores let team_2 = scores.entry(team_2_name).or_insert_with(Team::default);
.entry(team_2_name)
.or_insert_with(TeamScores::default);
team_2.goals_scored += team_2_score; team_2.goals_scored += team_2_score;
team_2.goals_conceded += team_1_score; team_2.goals_conceded += team_1_score;
} }

View file

@ -24,7 +24,7 @@ impl ParsePosNonzeroError {
Self::Creation(err) Self::Creation(err)
} }
fn from_parse_int(err: ParseIntError) -> Self { fn from_parseint(err: ParseIntError) -> Self {
Self::ParseInt(err) Self::ParseInt(err)
} }
} }
@ -44,7 +44,7 @@ impl PositiveNonzeroInteger {
fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> { fn parse(s: &str) -> Result<Self, ParsePosNonzeroError> {
// Return an appropriate error instead of panicking when `parse()` // Return an appropriate error instead of panicking when `parse()`
// returns an error. // returns an error.
let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parse_int)?; let x: i64 = s.parse().map_err(ParsePosNonzeroError::from_parseint)?;
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Self::new(x).map_err(ParsePosNonzeroError::from_creation) Self::new(x).map_err(ParsePosNonzeroError::from_creation)
} }

View file

@ -1,6 +0,0 @@
# Official Rustlings solutions
Before you finish an exercise, its solution file will only contain an empty `main` function.
The content of this file will be automatically replaced by the actual solution once you finish the exercise.
Note that these solution are often only _one possibility_ to solve an exercise.

View file

@ -129,6 +129,13 @@ impl<'out> CargoSubcommand<'out> {
self self
} }
/// RUSTFLAGS="-A warnings"
#[inline]
pub fn hide_warnings(&mut self) -> &mut Self {
self.cmd.env("RUSTFLAGS", "-A warnings");
self
}
/// 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.
#[inline] #[inline]
pub fn run(self, description: &str) -> Result<bool> { pub fn run(self, description: &str) -> Result<bool> {

View file

@ -175,21 +175,22 @@ fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Res
return None; return None;
} }
Some(( Some(s.spawn(|| exercise_info.run_exercise(None, cmd_runner)))
exercise_info.name.as_str(),
s.spawn(|| exercise_info.run_exercise(None, cmd_runner)),
))
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
for (exercise_name, handle) in handles { for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
let Ok(result) = handle.join() else { let Ok(result) = handle.join() else {
bail!("Panic while trying to run the exericse {exercise_name}"); bail!(
"Panic while trying to run the exericse {}",
exercise_info.name,
);
}; };
match result { match result {
Ok(true) => bail!( Ok(true) => bail!(
"The exercise {exercise_name} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}", "The exercise {} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",
exercise_info.name,
), ),
Ok(false) => (), Ok(false) => (),
Err(e) => return Err(e), Err(e) => return Err(e),

View file

@ -76,8 +76,8 @@ pub fn new(path: &Path, no_git: bool) -> Result<()> {
pub const GITIGNORE: &[u8] = b".rustlings-state.txt pub const GITIGNORE: &[u8] = b".rustlings-state.txt
Cargo.lock Cargo.lock
target/ target
.vscode/ .vscode
!.vscode/extensions.json !.vscode/extensions.json
"; ";

View file

@ -78,40 +78,27 @@ pub trait RunnableExercise {
mut output: Option<&mut Vec<u8>>, mut output: Option<&mut Vec<u8>>,
cmd_runner: &CmdRunner, cmd_runner: &CmdRunner,
) -> Result<bool> { ) -> Result<bool> {
if let Some(output) = output.as_deref_mut() { let output_is_none = if let Some(output) = output.as_deref_mut() {
output.clear(); output.clear();
} false
} else {
true
};
let build_success = cmd_runner let mut build_cmd = cmd_runner.cargo("build", bin_name, output.as_deref_mut());
.cargo("build", bin_name, output.as_deref_mut()) if output_is_none {
.run("cargo build …")?; build_cmd.hide_warnings();
}
let build_success = build_cmd.run("cargo build …")?;
if !build_success { if !build_success {
return Ok(false); return Ok(false);
} }
// Discard the compiler output because it will be shown again by `cargo test` or Clippy. // Discard the output of `cargo build` because it will be shown again by Clippy.
if let Some(output) = output.as_deref_mut() { if let Some(output) = output.as_deref_mut() {
output.clear(); output.clear();
} }
if self.test() {
let output_is_some = output.is_some();
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
if output_is_some {
test_cmd.args(["--", "--color", "always", "--show-output"]);
}
let test_success = test_cmd.run("cargo test …")?;
if !test_success {
run_bin(bin_name, output, cmd_runner)?;
return Ok(false);
}
// Discard the compiler output because it will be shown again by Clippy.
if let Some(output) = output.as_deref_mut() {
output.clear();
}
}
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut()); let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
// `--profile test` is required to also check code with `[cfg(test)]`. // `--profile test` is required to also check code with `[cfg(test)]`.
@ -122,9 +109,25 @@ pub trait RunnableExercise {
} }
let clippy_success = clippy_cmd.run("cargo clippy …")?; let clippy_success = clippy_cmd.run("cargo clippy …")?;
if !clippy_success {
return Ok(false);
}
if !self.test() {
return run_bin(bin_name, output.as_deref_mut(), cmd_runner);
}
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
if !output_is_none {
test_cmd.args(["--", "--color", "always", "--show-output"]);
}
// Hide warnings because they are shown by Clippy.
test_cmd.hide_warnings();
let test_success = test_cmd.run("cargo test …")?;
let run_success = run_bin(bin_name, output, cmd_runner)?; let run_success = run_bin(bin_name, output, cmd_runner)?;
Ok(clippy_success && run_success) Ok(test_success && run_success)
} }
/// Compile, check and run the exercise. /// Compile, check and run the exercise.

View file

@ -3,40 +3,30 @@ use ratatui::crossterm::style::Stylize;
use std::{ use std::{
env::set_current_dir, env::set_current_dir,
fs::{self, create_dir}, fs::{self, create_dir},
io::{self, Write}, io::ErrorKind,
path::Path, path::Path,
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use crate::{ use crate::{cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile};
cargo_toml::updated_cargo_toml, embedded::EMBEDDED_FILES, info_file::InfoFile,
term::press_enter_prompt,
};
pub fn init() -> Result<()> { pub fn init() -> Result<()> {
let rustlings_dir = Path::new("rustlings"); // Prevent initialization in a directory that contains the file `Cargo.toml`.
if rustlings_dir.exists() { // This can mean that Rustlings was already initialized in this directory.
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR); // Otherwise, this can cause problems with Cargo workspaces.
}
let mut stdout = io::stdout().lock();
let mut init_git = true;
if Path::new("Cargo.toml").exists() { if Path::new("Cargo.toml").exists() {
if Path::new("exercises").exists() && Path::new("solutions").exists() { bail!(CARGO_TOML_EXISTS_ERR);
bail!(IN_INITIALIZED_DIR_ERR);
}
stdout.write_all(CARGO_TOML_EXISTS_PROMPT_MSG)?;
press_enter_prompt(&mut stdout)?;
init_git = false;
} }
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?; let rustlings_path = Path::new("rustlings");
press_enter_prompt(&mut stdout)?; if let Err(e) = create_dir(rustlings_path) {
if e.kind() == ErrorKind::AlreadyExists {
bail!(RUSTLINGS_DIR_ALREADY_EXISTS_ERR);
}
return Err(e.into());
}
create_dir(rustlings_dir).context("Failed to create the `rustlings/` directory")?; set_current_dir("rustlings")
set_current_dir(rustlings_dir)
.context("Failed to change the current directory to `rustlings/`")?; .context("Failed to change the current directory to `rustlings/`")?;
let info_file = InfoFile::parse()?; let info_file = InfoFile::parse()?;
@ -45,11 +35,6 @@ pub fn init() -> Result<()> {
.context("Failed to initialize the `rustlings/exercises` directory")?; .context("Failed to initialize the `rustlings/exercises` directory")?;
create_dir("solutions").context("Failed to create the `solutions/` directory")?; create_dir("solutions").context("Failed to create the `solutions/` directory")?;
fs::write(
"solutions/README.md",
include_bytes!("../solutions/README.md"),
)
.context("Failed to create the file rustlings/solutions/README.md")?;
for dir in EMBEDDED_FILES.exercise_dirs { for dir in EMBEDDED_FILES.exercise_dirs {
let mut dir_path = String::with_capacity(10 + dir.name.len()); let mut dir_path = String::with_capacity(10 + dir.name.len());
dir_path.push_str("solutions/"); dir_path.push_str("solutions/");
@ -85,21 +70,18 @@ pub fn init() -> Result<()> {
fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON) fs::write(".vscode/extensions.json", VS_CODE_EXTENSIONS_JSON)
.context("Failed to create the file `rustlings/.vscode/extensions.json`")?; .context("Failed to create the file `rustlings/.vscode/extensions.json`")?;
if init_git { // Ignore any Git error because Git initialization is not required.
// Ignore any Git error because Git initialization is not required. let _ = Command::new("git")
let _ = Command::new("git") .arg("init")
.arg("init") .stdin(Stdio::null())
.stdin(Stdio::null()) .stderr(Stdio::null())
.stderr(Stdio::null()) .status();
.status();
}
writeln!( println!(
stdout,
"\n{}\n\n{}", "\n{}\n\n{}",
"Initialization done ✓".green(), "Initialization done ✓".green(),
POST_INIT_MSG.bold(), POST_INIT_MSG.bold(),
)?; );
Ok(()) Ok(())
} }
@ -110,14 +92,16 @@ const INIT_SOLUTION_FILE: &[u8] = b"fn main() {
} }
"; ";
const GITIGNORE: &[u8] = b"Cargo.lock const GITIGNORE: &[u8] = b".rustlings-state.txt
target/ solutions
.vscode/ Cargo.lock
target
.vscode
"; ";
pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#; pub const VS_CODE_EXTENSIONS_JSON: &[u8] = br#"{"recommendations":["rust-lang.rust-analyzer"]}"#;
const IN_INITIALIZED_DIR_ERR: &str = "It looks like Rustlings is already initialized in this directory. const CARGO_TOML_EXISTS_ERR: &str = "The current directory contains the file `Cargo.toml`.
If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises. If you already initialized Rustlings, run the command `rustlings` for instructions on getting started with the exercises.
Otherwise, please run `rustlings init` again in another directory."; Otherwise, please run `rustlings init` again in another directory.";
@ -128,19 +112,5 @@ You probably already initialized Rustlings.
Run `cd rustlings` Run `cd rustlings`
Then run `rustlings` again"; Then run `rustlings` again";
const CARGO_TOML_EXISTS_PROMPT_MSG: &[u8] = br#"You are about to initialize Rustlings in a directory that already contains a `Cargo.toml` file!
=> It is recommended to abort with CTRL+C and initialize Rustlings in another directory <=
If you know what you are doing and want to initialize Rustlings in a Cargo workspace,
then you need to add its directory to `members` in the `workspace` section of the `Cargo.toml` file:
```toml
[workspace]
members = ["rustlings"]
```
Press ENTER if you are sure that you want to continue after reading the warning above "#;
const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory. const POST_INIT_MSG: &str = "Run `cd rustlings` to go into the generated directory.
Then run `rustlings` to get started."; Then run `rustlings` to get started.";

View file

@ -2,11 +2,10 @@ use anyhow::{bail, Context, Result};
use app_state::StateFileStatus; use app_state::StateFileStatus;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::{ use std::{
io::{self, IsTerminal, Write}, io::{self, BufRead, IsTerminal, StdoutLock, Write},
path::Path, path::Path,
process::exit, process::exit,
}; };
use term::{clear_terminal, press_enter_prompt};
use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit};
@ -21,12 +20,20 @@ mod init;
mod list; mod list;
mod progress_bar; mod progress_bar;
mod run; mod run;
mod term;
mod terminal_link; mod terminal_link;
mod watch; mod watch;
const CURRENT_FORMAT_VERSION: u8 = 1; const CURRENT_FORMAT_VERSION: u8 = 1;
fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
}
fn press_enter_prompt() -> io::Result<()> {
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
Ok(())
}
/// Rustlings is a collection of small exercises to get you used to writing and reading Rust code /// Rustlings is a collection of small exercises to get you used to writing and reading Rust code
#[derive(Parser)] #[derive(Parser)]
#[command(version)] #[command(version)]
@ -72,6 +79,14 @@ fn main() -> Result<()> {
match args.command { match args.command {
Some(Subcommands::Init) => { Some(Subcommands::Init) => {
{
let mut stdout = io::stdout().lock();
stdout.write_all(b"This command will create the directory `rustlings/` which will contain the exercises.\nPress ENTER to continue ")?;
stdout.flush()?;
press_enter_prompt()?;
stdout.write_all(b"\n")?;
}
return init::init().context("Initialization failed"); return init::init().context("Initialization failed");
} }
Some(Subcommands::Dev(dev_command)) => return dev_command.run(), Some(Subcommands::Dev(dev_command)) => return dev_command.run(),
@ -103,10 +118,9 @@ fn main() -> Result<()> {
let welcome_message = welcome_message.trim_ascii(); let welcome_message = welcome_message.trim_ascii();
write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?; write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
press_enter_prompt(&mut stdout)?;
clear_terminal(&mut stdout)?;
// Flush to be able to show errors occuring before printing a newline to stdout.
stdout.flush()?; stdout.flush()?;
press_enter_prompt()?;
clear_terminal(&mut stdout)?;
} }
StateFileStatus::Read => (), StateFileStatus::Read => (),
} }

View file

@ -1,12 +0,0 @@
use std::io::{self, BufRead, StdoutLock, Write};
pub fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
}
pub fn press_enter_prompt(stdout: &mut StdoutLock) -> io::Result<()> {
stdout.flush()?;
io::stdin().lock().read_until(b'\n', &mut Vec::new())?;
stdout.write_all(b"\n")?;
Ok(())
}

View file

@ -102,7 +102,8 @@ pub fn watch(
watch_state.render()?; watch_state.render()?;
} }
WatchEvent::NotifyErr(e) => { WatchEvent::NotifyErr(e) => {
return Err(Error::from(e).context(NOTIFY_ERR)); watch_state.into_writer().write_all(NOTIFY_ERR.as_bytes())?;
return Err(Error::from(e));
} }
WatchEvent::TerminalEventErr(e) => { WatchEvent::TerminalEventErr(e) => {
return Err(Error::from(e).context("Terminal event listener failed")); return Err(Error::from(e).context("Terminal event listener failed"));

View file

@ -51,11 +51,6 @@ impl<'a> WatchState<'a> {
pub fn run_current_exercise(&mut self) -> Result<()> { pub fn run_current_exercise(&mut self) -> Result<()> {
self.show_hint = false; self.show_hint = false;
writeln!(
self.writer,
"\nChecking the exercise `{}`. Please wait…",
self.app_state.current_exercise().name,
)?;
let success = self let success = self
.app_state .app_state
.current_exercise() .current_exercise()