mirror of
https://github.com/rust-lang/rustlings.git
synced 2024-12-26 00:00:03 +03:00
Compare commits
6 commits
2a14bfc9c3
...
45c07fcb84
Author | SHA1 | Date | |
---|---|---|---|
45c07fcb84 | |||
175294fa5d | |||
5016c7cf7c | |||
1468206052 | |||
e65ae09789 | |||
dacdce1ea2 |
|
@ -2,6 +2,7 @@
|
|||
|
||||
## 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.
|
||||
- Run the final check of all exercises in parallel.
|
||||
- Small exercise improvements.
|
||||
- `dev check`: Check that all solutions are formatted with `rustfmt`.
|
||||
|
|
|
@ -16,6 +16,7 @@ authors = [
|
|||
repository = "https://github.com/rust-lang/rustlings"
|
||||
license = "MIT"
|
||||
edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions.
|
||||
rust-version = "1.80"
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
|
@ -29,6 +30,7 @@ authors.workspace = true
|
|||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
keywords = [
|
||||
"exercise",
|
||||
"learning",
|
||||
|
|
|
@ -17,7 +17,7 @@ It contains code examples and exercises similar to Rustlings, but online.
|
|||
|
||||
### Installing Rust
|
||||
|
||||
Before installing Rustlings, you need to have _Rust installed_.
|
||||
Before installing Rustlings, you need to have the **latest version of Rust** installed.
|
||||
Visit [www.rust-lang.org/tools/install](https://www.rust-lang.org/tools/install) for further instructions on installing Rust.
|
||||
This will also install _Cargo_, Rust's package/project manager.
|
||||
|
||||
|
|
|
@ -10,12 +10,12 @@ use std::collections::HashMap;
|
|||
|
||||
// A structure to store the goal details of a team.
|
||||
#[derive(Default)]
|
||||
struct Team {
|
||||
struct TeamScores {
|
||||
goals_scored: u8,
|
||||
goals_conceded: u8,
|
||||
}
|
||||
|
||||
fn build_scores_table(results: &str) -> HashMap<&str, Team> {
|
||||
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||
// The name of the team is the key and its associated struct is the value.
|
||||
let mut scores = HashMap::new();
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ authors.workspace = true
|
|||
repository.workspace = true
|
||||
license.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
include = [
|
||||
"/src/",
|
||||
"/info.toml",
|
||||
|
|
|
@ -10,12 +10,12 @@ use std::collections::HashMap;
|
|||
|
||||
// A structure to store the goal details of a team.
|
||||
#[derive(Default)]
|
||||
struct Team {
|
||||
struct TeamScores {
|
||||
goals_scored: u8,
|
||||
goals_conceded: u8,
|
||||
}
|
||||
|
||||
fn build_scores_table(results: &str) -> HashMap<&str, Team> {
|
||||
fn build_scores_table(results: &str) -> HashMap<&str, TeamScores> {
|
||||
// The name of the team is the key and its associated struct is the value.
|
||||
let mut scores = HashMap::new();
|
||||
|
||||
|
@ -28,13 +28,17 @@ fn build_scores_table(results: &str) -> HashMap<&str, Team> {
|
|||
let team_2_score: u8 = split_iterator.next().unwrap().parse().unwrap();
|
||||
|
||||
// Insert the default with zeros if a team doesn't exist yet.
|
||||
let team_1 = scores.entry(team_1_name).or_insert_with(Team::default);
|
||||
let team_1 = scores
|
||||
.entry(team_1_name)
|
||||
.or_insert_with(TeamScores::default);
|
||||
// Update the values.
|
||||
team_1.goals_scored += team_1_score;
|
||||
team_1.goals_conceded += team_2_score;
|
||||
|
||||
// Similarely for the second team.
|
||||
let team_2 = scores.entry(team_2_name).or_insert_with(Team::default);
|
||||
let team_2 = scores
|
||||
.entry(team_2_name)
|
||||
.or_insert_with(TeamScores::default);
|
||||
team_2.goals_scored += team_2_score;
|
||||
team_2.goals_conceded += team_1_score;
|
||||
}
|
||||
|
|
|
@ -107,8 +107,7 @@ impl AppState {
|
|||
let path = exercise_info.path().leak();
|
||||
let name = exercise_info.name.leak();
|
||||
let dir = exercise_info.dir.map(|dir| &*dir.leak());
|
||||
|
||||
let hint = exercise_info.hint.trim().to_owned();
|
||||
let hint = exercise_info.hint.leak().trim_ascii();
|
||||
|
||||
Exercise {
|
||||
dir,
|
||||
|
@ -397,7 +396,7 @@ impl AppState {
|
|||
clear_terminal(writer)?;
|
||||
writer.write_all(FENISH_LINE.as_bytes())?;
|
||||
|
||||
let final_message = self.final_message.trim();
|
||||
let final_message = self.final_message.trim_ascii();
|
||||
if !final_message.is_empty() {
|
||||
writer.write_all(final_message.as_bytes())?;
|
||||
writer.write_all(b"\n")?;
|
||||
|
@ -445,7 +444,7 @@ mod tests {
|
|||
path: "exercises/0.rs",
|
||||
test: false,
|
||||
strict_clippy: false,
|
||||
hint: String::new(),
|
||||
hint: "",
|
||||
done: false,
|
||||
}
|
||||
}
|
||||
|
|
112
src/dev/check.rs
112
src/dev/check.rs
|
@ -5,7 +5,6 @@ use std::{
|
|||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, Stdio},
|
||||
sync::atomic::{self, AtomicBool},
|
||||
thread,
|
||||
};
|
||||
|
||||
|
@ -22,7 +21,7 @@ fn forbidden_char(input: &str) -> Option<char> {
|
|||
input.chars().find(|c| !c.is_alphanumeric() && *c != '_')
|
||||
}
|
||||
|
||||
// Check that the Cargo.toml file is up-to-date.
|
||||
// Check that the `Cargo.toml` file is up-to-date.
|
||||
fn check_cargo_toml(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
cargo_toml_path: &str,
|
||||
|
@ -72,7 +71,7 @@ fn check_info_file_exercises(info_file: &InfoFile) -> Result<hashbrown::HashSet<
|
|||
}
|
||||
}
|
||||
|
||||
if exercise_info.hint.trim().is_empty() {
|
||||
if exercise_info.hint.trim_ascii().is_empty() {
|
||||
bail!("The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise");
|
||||
}
|
||||
|
||||
|
@ -164,41 +163,42 @@ fn check_unexpected_files(
|
|||
}
|
||||
|
||||
fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
|
||||
let error_occurred = AtomicBool::new(false);
|
||||
|
||||
println!(
|
||||
"Running all exercises to check that they aren't already solved. This may take a while…\n",
|
||||
);
|
||||
thread::scope(|s| {
|
||||
for exercise_info in &info_file.exercises {
|
||||
if exercise_info.skip_check_unsolved {
|
||||
continue;
|
||||
}
|
||||
|
||||
s.spawn(|| {
|
||||
let error = |e| {
|
||||
let mut stderr = io::stderr().lock();
|
||||
stderr.write_all(e).unwrap();
|
||||
stderr.write_all(b"\nProblem with the exercise ").unwrap();
|
||||
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
|
||||
stderr.write_all(SEPARATOR).unwrap();
|
||||
error_occurred.store(true, atomic::Ordering::Relaxed);
|
||||
};
|
||||
|
||||
match exercise_info.run_exercise(None, cmd_runner) {
|
||||
Ok(true) => error(b"Already solved!"),
|
||||
Ok(false) => (),
|
||||
Err(e) => error(e.to_string().as_bytes()),
|
||||
let handles = info_file
|
||||
.exercises
|
||||
.iter()
|
||||
.filter_map(|exercise_info| {
|
||||
if exercise_info.skip_check_unsolved {
|
||||
return None;
|
||||
}
|
||||
});
|
||||
|
||||
Some(s.spawn(|| exercise_info.run_exercise(None, cmd_runner)))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
|
||||
let Ok(result) = handle.join() else {
|
||||
bail!(
|
||||
"Panic while trying to run the exericse {}",
|
||||
exercise_info.name,
|
||||
);
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(true) => bail!(
|
||||
"The exercise {} is already solved.\n{SKIP_CHECK_UNSOLVED_HINT}",
|
||||
exercise_info.name,
|
||||
),
|
||||
Ok(false) => (),
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if error_occurred.load(atomic::Ordering::Relaxed) {
|
||||
bail!(CHECK_EXERCISES_UNSOLVED_ERR);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
|
||||
|
@ -209,9 +209,10 @@ fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
|
|||
}
|
||||
|
||||
let info_file_paths = check_info_file_exercises(info_file)?;
|
||||
check_unexpected_files("exercises", &info_file_paths)?;
|
||||
let handle = thread::spawn(move || check_unexpected_files("exercises", &info_file_paths));
|
||||
|
||||
check_exercises_unsolved(info_file, cmd_runner)
|
||||
check_exercises_unsolved(info_file, cmd_runner)?;
|
||||
handle.join().unwrap()
|
||||
}
|
||||
|
||||
enum SolutionCheck {
|
||||
|
@ -263,29 +264,34 @@ fn check_solutions(
|
|||
.arg("always")
|
||||
.stdin(Stdio::null());
|
||||
|
||||
for (exercise_name, handle) in info_file
|
||||
.exercises
|
||||
.iter()
|
||||
.map(|exercise_info| &exercise_info.name)
|
||||
.zip(handles)
|
||||
{
|
||||
match handle.join() {
|
||||
Ok(SolutionCheck::Success { sol_path }) => {
|
||||
for (exercise_info, handle) in info_file.exercises.iter().zip(handles) {
|
||||
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));
|
||||
}
|
||||
Ok(SolutionCheck::MissingRequired) => {
|
||||
bail!("The solution of the exercise {exercise_name} is missing");
|
||||
SolutionCheck::MissingRequired => {
|
||||
bail!(
|
||||
"The solution of the exercise {} is missing",
|
||||
exercise_info.name,
|
||||
);
|
||||
}
|
||||
Ok(SolutionCheck::MissingOptional) => (),
|
||||
Ok(SolutionCheck::RunFailure { output }) => {
|
||||
SolutionCheck::MissingOptional => (),
|
||||
SolutionCheck::RunFailure { output } => {
|
||||
io::stderr().lock().write_all(&output)?;
|
||||
bail!("Running the solution of the exercise {exercise_name} failed with the error above");
|
||||
}
|
||||
Ok(SolutionCheck::Err(e)) => return Err(e),
|
||||
Err(_) => {
|
||||
bail!("Panic while trying to run the solution of the exericse {exercise_name}");
|
||||
bail!(
|
||||
"Running the solution of the exercise {} failed with the error above",
|
||||
exercise_info.name,
|
||||
);
|
||||
}
|
||||
SolutionCheck::Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -322,8 +328,4 @@ pub fn check(require_solutions: bool) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
const SEPARATOR: &[u8] =
|
||||
b"\n========================================================================================\n";
|
||||
|
||||
const CHECK_EXERCISES_UNSOLVED_ERR: &str = "At least one exercise is already solved or failed to run. See the output above.
|
||||
If this is an intro exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file.";
|
||||
const SKIP_CHECK_UNSOLVED_HINT: &str = "If this is an introduction exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file";
|
||||
|
|
|
@ -49,7 +49,7 @@ pub struct Exercise {
|
|||
pub path: &'static str,
|
||||
pub test: bool,
|
||||
pub strict_clippy: bool,
|
||||
pub hint: String,
|
||||
pub hint: &'static str,
|
||||
pub done: bool,
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ fn main() -> Result<()> {
|
|||
let mut stdout = io::stdout().lock();
|
||||
clear_terminal(&mut stdout)?;
|
||||
|
||||
let welcome_message = welcome_message.trim();
|
||||
let welcome_message = welcome_message.trim_ascii();
|
||||
write!(stdout, "{welcome_message}\n\nPress ENTER to continue ")?;
|
||||
stdout.flush()?;
|
||||
press_enter_prompt()?;
|
||||
|
|
Loading…
Reference in a new issue