Compare commits

...

9 commits

Author SHA1 Message Date
mo8it c8d1d9c51f chore: Release 2024-08-29 17:20:17 +02:00
mo8it ab2eb3442e Update changelog 2024-08-29 17:10:39 +02:00
mo8it dbbeb7d4ed Fix displaying the list message in narrow mode 2024-08-29 17:06:37 +02:00
mo8it bfa00ffbdc Update deps 2024-08-29 16:40:40 +02:00
mo8it 10eb1a3aee Fix header padding 2024-08-29 16:01:41 +02:00
mo8it fd2bf9f6f6 Simplify next_pending_exercise_ind 2024-08-29 01:59:04 +02:00
mo8it fc1f9f0124 Optimize reading and writing the state file 2024-08-29 01:56:45 +02:00
mo8it 789492d1a9 The number of exercises can't be zero, but still 2024-08-29 00:32:58 +02:00
mo8it afc320bed4 Fix error about too many open files during the final check 2024-08-29 00:17:22 +02:00
5 changed files with 211 additions and 129 deletions

View file

@ -1,3 +1,44 @@
<a name="6.3.0"></a>
## 6.3.0 (2024-08-29)
### Added
- Add the following exercise lints:
- `forbid(unsafe_code)`: You shouldn't write unsafe code in Rustlings.
- `forbid(unstable_features)`: You don't need unstable features in Rustlings and shouldn't rely on them while learning Rust.
- `forbid(todo)`: You forgot a `todo!()`.
- `forbid(empty_loop)`: This can only happen by mistake in Rustlings.
- `deny(infinite_loop)`: No infinite loops are needed in Rustlings.
- `deny(mem_forget)`: You shouldn't leak memory while still learning Rust.
- Show a link to every exercise file in the list.
- Add scroll padding in the list.
- Break the help footer of the list into two lines when the terminal width isn't big enough.
- Enable scrolling with the mouse in the list.
- `dev check`: Show the progress of checks.
- `dev check`: Check that the length of all exercise names is lower than 32.
- `dev check`: Check if exercise contains no tests and isn't marked with `test = false`.
### Changed
- The compilation time when installing Rustlings is reduced.
- Pressing `c` in the list for "continue on" now quits the list after setting the selected exercise as the current one.
- Better highlighting of the solution file after an exercise is done.
- Don't show the output of successful tests anymore. Instead, show the pretty output for tests.
- Be explicit about `q` only quitting the list and not the whole program in the list.
- Be explicit about `r` only resetting one exercise (the selected one) in the list.
- Ignore the standard output of `git init`.
- `threads3`: Remove the queue length and improve tests.
- `errors4`: Use match instead of a comparison chain in the solution.
- `functions3`: Only take `u8` to avoid using a too high number of iterations by mistake.
- `dev check`: Always check with strict Clippy (warnings to errors) when checking the solutions.
### Fixed
- Fix the error on some systems about too many open files during the final check of all exercises.
- Fix the list when the terminal height is too low.
- Restore the terminal after an error in the list.
<a name="6.2.0"></a> <a name="6.2.0"></a>
## 6.2.0 (2024-08-09) ## 6.2.0 (2024-08-09)

12
Cargo.lock generated
View file

@ -203,9 +203,9 @@ checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6"
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.24" version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf401df4a4e3872c4fe8151134cf483738e74b67fc934d6532c882b3d24a4550" checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
@ -469,9 +469,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.34" version = "0.38.35"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f"
dependencies = [ dependencies = [
"bitflags 2.6.0", "bitflags 2.6.0",
"errno", "errno",
@ -482,7 +482,7 @@ dependencies = [
[[package]] [[package]]
name = "rustlings" name = "rustlings"
version = "6.2.0" version = "6.3.0"
dependencies = [ dependencies = [
"ahash", "ahash",
"anyhow", "anyhow",
@ -499,7 +499,7 @@ dependencies = [
[[package]] [[package]]
name = "rustlings-macros" name = "rustlings-macros"
version = "6.2.0" version = "6.3.0"
dependencies = [ dependencies = [
"quote", "quote",
"serde", "serde",

View file

@ -6,7 +6,7 @@ exclude = [
] ]
[workspace.package] [workspace.package]
version = "6.2.0" version = "6.3.0"
authors = [ authors = [
"Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it "Mo Bitar <mo8it@proton.me>", # https://github.com/mo8it
"Liv <mokou@fastmail.com>", # https://github.com/shadows-withal "Liv <mokou@fastmail.com>", # https://github.com/shadows-withal
@ -52,7 +52,7 @@ clap = { version = "4.5.16", features = ["derive"] }
crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] } crossterm = { version = "0.28.1", default-features = false, features = ["windows", "events"] }
notify-debouncer-mini = { version = "0.4.1", default-features = false } notify-debouncer-mini = { version = "0.4.1", default-features = false }
os_pipe = "1.2.1" os_pipe = "1.2.1"
rustlings-macros = { path = "rustlings-macros", version = "=6.2.0" } rustlings-macros = { path = "rustlings-macros", version = "=6.3.0" }
serde_json = "1.0.127" serde_json = "1.0.127"
serde.workspace = true serde.workspace = true
toml_edit.workspace = true toml_edit.workspace = true

View file

@ -1,8 +1,8 @@
use anyhow::{bail, Context, Error, Result}; use anyhow::{bail, Context, Result};
use std::{ use std::{
env, env,
fs::{self, File}, fs::{File, OpenOptions},
io::{Read, StdoutLock, Write}, io::{self, Read, Seek, StdoutLock, Write},
path::Path, path::Path,
process::{Command, Stdio}, process::{Command, Stdio},
thread, thread,
@ -18,7 +18,6 @@ use crate::{
}; };
const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
#[must_use] #[must_use]
pub enum ExercisesProgress { pub enum ExercisesProgress {
@ -35,12 +34,19 @@ pub enum StateFileStatus {
NotRead, NotRead,
} }
enum AllExercisesCheck {
Pending(usize),
AllDone,
CheckedUntil(usize),
}
pub struct AppState { pub struct AppState {
current_exercise_ind: usize, current_exercise_ind: usize,
exercises: Vec<Exercise>, exercises: Vec<Exercise>,
// Caches the number of done exercises to avoid iterating over all exercises every time. // Caches the number of done exercises to avoid iterating over all exercises every time.
n_done: u16, n_done: u16,
final_message: String, final_message: String,
state_file: File,
// Preallocated buffer for reading and writing the state file. // Preallocated buffer for reading and writing the state file.
file_buf: Vec<u8>, file_buf: Vec<u8>,
official_exercises: bool, official_exercises: bool,
@ -50,59 +56,22 @@ pub struct AppState {
} }
impl AppState { impl AppState {
// Update the app state from the state file.
fn update_from_file(&mut self) -> StateFileStatus {
self.file_buf.clear();
self.n_done = 0;
if File::open(STATE_FILE_NAME)
.and_then(|mut file| file.read_to_end(&mut self.file_buf))
.is_err()
{
return StateFileStatus::NotRead;
}
// See `Self::write` for more information about the file format.
let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2);
let Some(current_exercise_name) = lines.next() else {
return StateFileStatus::NotRead;
};
if current_exercise_name.is_empty() || lines.next().is_none() {
return StateFileStatus::NotRead;
}
let mut done_exercises = hash_set_with_capacity(self.exercises.len());
for done_exerise_name in lines {
if done_exerise_name.is_empty() {
break;
}
done_exercises.insert(done_exerise_name);
}
for (ind, exercise) in self.exercises.iter_mut().enumerate() {
if done_exercises.contains(exercise.name.as_bytes()) {
exercise.done = true;
self.n_done += 1;
}
if exercise.name.as_bytes() == current_exercise_name {
self.current_exercise_ind = ind;
}
}
StateFileStatus::Read
}
pub fn new( pub fn new(
exercise_infos: Vec<ExerciseInfo>, exercise_infos: Vec<ExerciseInfo>,
final_message: String, final_message: String,
) -> Result<(Self, StateFileStatus)> { ) -> Result<(Self, StateFileStatus)> {
let cmd_runner = CmdRunner::build()?; let cmd_runner = CmdRunner::build()?;
let mut state_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(STATE_FILE_NAME)
.with_context(|| {
format!("Failed to open or create the state file {STATE_FILE_NAME}")
})?;
let exercises = exercise_infos let mut exercises = exercise_infos
.into_iter() .into_iter()
.map(|exercise_info| { .map(|exercise_info| {
// Leaking to be able to borrow in the watch mode `Table`. // Leaking to be able to borrow in the watch mode `Table`.
@ -120,25 +89,69 @@ impl AppState {
test: exercise_info.test, test: exercise_info.test,
strict_clippy: exercise_info.strict_clippy, strict_clippy: exercise_info.strict_clippy,
hint, hint,
// Updated in `Self::update_from_file`. // Updated below.
done: false, done: false,
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut slf = Self { let mut current_exercise_ind = 0;
current_exercise_ind: 0, let mut n_done = 0;
let mut file_buf = Vec::with_capacity(2048);
let state_file_status = 'block: {
if state_file.read_to_end(&mut file_buf).is_err() {
break 'block StateFileStatus::NotRead;
}
// See `Self::write` for more information about the file format.
let mut lines = file_buf.split(|c| *c == b'\n').skip(2);
let Some(current_exercise_name) = lines.next() else {
break 'block StateFileStatus::NotRead;
};
if current_exercise_name.is_empty() || lines.next().is_none() {
break 'block StateFileStatus::NotRead;
}
let mut done_exercises = hash_set_with_capacity(exercises.len());
for done_exerise_name in lines {
if done_exerise_name.is_empty() {
break;
}
done_exercises.insert(done_exerise_name);
}
for (ind, exercise) in exercises.iter_mut().enumerate() {
if done_exercises.contains(exercise.name.as_bytes()) {
exercise.done = true;
n_done += 1;
}
if exercise.name.as_bytes() == current_exercise_name {
current_exercise_ind = ind;
}
}
StateFileStatus::Read
};
file_buf.clear();
file_buf.extend_from_slice(STATE_FILE_HEADER);
let slf = Self {
current_exercise_ind,
exercises, exercises,
n_done: 0, n_done,
final_message, final_message,
file_buf: Vec::with_capacity(2048), state_file,
file_buf,
official_exercises: !Path::new("info.toml").exists(), official_exercises: !Path::new("info.toml").exists(),
cmd_runner, cmd_runner,
vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"), vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"),
}; };
let state_file_status = slf.update_from_file();
Ok((slf, state_file_status)) Ok((slf, state_file_status))
} }
@ -181,10 +194,8 @@ impl AppState {
// - The fourth line is an empty line. // - The fourth line is an empty line.
// - All remaining lines are the names of done exercises. // - All remaining lines are the names of done exercises.
fn write(&mut self) -> Result<()> { fn write(&mut self) -> Result<()> {
self.file_buf.clear(); self.file_buf.truncate(STATE_FILE_HEADER.len());
self.file_buf
.extend_from_slice(b"DON'T EDIT THIS FILE!\n\n");
self.file_buf self.file_buf
.extend_from_slice(self.current_exercise().name.as_bytes()); .extend_from_slice(self.current_exercise().name.as_bytes());
self.file_buf.push(b'\n'); self.file_buf.push(b'\n');
@ -196,7 +207,14 @@ impl AppState {
} }
} }
fs::write(STATE_FILE_NAME, &self.file_buf) self.state_file
.rewind()
.with_context(|| format!("Failed to rewind the state file {STATE_FILE_NAME}"))?;
self.state_file
.set_len(0)
.with_context(|| format!("Failed to truncate the state file {STATE_FILE_NAME}"))?;
self.state_file
.write_all(&self.file_buf)
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?; .with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
Ok(()) Ok(())
@ -295,25 +313,22 @@ impl AppState {
// Return the index of the next pending exercise or `None` if all exercises are done. // Return the index of the next pending exercise or `None` if all exercises are done.
fn next_pending_exercise_ind(&self) -> Option<usize> { fn next_pending_exercise_ind(&self) -> Option<usize> {
if self.current_exercise_ind == self.exercises.len() - 1 { let next_ind = self.current_exercise_ind + 1;
// The last exercise is done. self.exercises
// Search for exercises not done from the start. // If the exercise done isn't the last, search for pending exercises after it.
return self.exercises[..self.current_exercise_ind] .get(next_ind..)
.iter() .and_then(|later_exercises| {
.position(|exercise| !exercise.done); later_exercises
} .iter()
.position(|exercise| !exercise.done)
// The done exercise isn't the last one. .map(|ind| next_ind + ind)
// Search for a pending exercise after the current one and then from the start. })
match self.exercises[self.current_exercise_ind + 1..] // Search from the start.
.iter() .or_else(|| {
.position(|exercise| !exercise.done) self.exercises[..self.current_exercise_ind]
{ .iter()
Some(ind) => Some(self.current_exercise_ind + 1 + ind), .position(|exercise| !exercise.done)
None => self.exercises[..self.current_exercise_ind] })
.iter()
.position(|exercise| !exercise.done),
}
} }
/// Official exercises: Dump the solution file form the binary and return its path. /// Official exercises: Dump the solution file form the binary and return its path.
@ -340,6 +355,58 @@ impl AppState {
} }
} }
// Return the exercise index of the first pending exercise found.
fn check_all_exercises(&self, stdout: &mut StdoutLock) -> Result<Option<usize>> {
stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
let n_exercises = self.exercises.len();
let status = thread::scope(|s| {
let handles = self
.exercises
.iter()
.map(|exercise| s.spawn(|| exercise.run_exercise(None, &self.cmd_runner)))
.collect::<Vec<_>>();
for (exercise_ind, handle) in handles.into_iter().enumerate() {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
stdout.flush()?;
let Ok(success) = handle.join().unwrap() else {
return Ok(AllExercisesCheck::CheckedUntil(exercise_ind));
};
if !success {
return Ok(AllExercisesCheck::Pending(exercise_ind));
}
}
Ok::<_, io::Error>(AllExercisesCheck::AllDone)
})?;
let mut exercise_ind = match status {
AllExercisesCheck::Pending(exercise_ind) => return Ok(Some(exercise_ind)),
AllExercisesCheck::AllDone => return Ok(None),
AllExercisesCheck::CheckedUntil(ind) => ind,
};
// We got an error while checking all exercises in parallel.
// This could be because we exceeded the limit of open file descriptors.
// Therefore, try to continue the check sequentially.
for exercise in &self.exercises[exercise_ind..] {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
stdout.flush()?;
let success = exercise.run_exercise(None, &self.cmd_runner)?;
if !success {
return Ok(Some(exercise_ind));
}
exercise_ind += 1;
}
Ok(None)
}
/// Mark the current exercise as done and move on to the next pending exercise if one exists. /// Mark the current exercise as done and move on to the next pending exercise if one exists.
/// If all exercises are marked as done, run all of them to make sure that they are actually /// If all exercises are marked as done, run all of them to make sure that they are actually
/// done. If an exercise which is marked as done fails, mark it as pending and continue on it. /// done. If an exercise which is marked as done fails, mark it as pending and continue on it.
@ -355,44 +422,13 @@ impl AppState {
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
stdout.write_all(RERUNNING_ALL_EXERCISES_MSG)?; if let Some(pending_exercise_ind) = self.check_all_exercises(stdout)? {
stdout.write_all(b"\n\n")?;
let n_exercises = self.exercises.len();
let pending_exercise_ind = thread::scope(|s| {
let handles = self
.exercises
.iter_mut()
.map(|exercise| {
s.spawn(|| {
let success = exercise.run_exercise(None, &self.cmd_runner)?;
exercise.done = success;
Ok::<_, Error>(success)
})
})
.collect::<Vec<_>>();
for (exercise_ind, handle) in handles.into_iter().enumerate() {
write!(stdout, "\rProgress: {exercise_ind}/{n_exercises}")?;
stdout.flush()?;
let success = handle.join().unwrap()?;
if !success {
stdout.write_all(b"\n\n")?;
return Ok(Some(exercise_ind));
}
}
Ok::<_, Error>(None)
})?;
if let Some(pending_exercise_ind) = pending_exercise_ind {
self.current_exercise_ind = pending_exercise_ind; self.current_exercise_ind = pending_exercise_ind;
self.n_done = self self.exercises[pending_exercise_ind].done = false;
.exercises // All exercises were marked as done.
.iter() self.n_done -= 1;
.filter(|exercise| exercise.done)
.count() as u16;
self.write()?; self.write()?;
return Ok(ExercisesProgress::NewPending); return Ok(ExercisesProgress::NewPending);
} }
@ -413,11 +449,12 @@ impl AppState {
} }
} }
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
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 = "+----------------------------------------------------+
| You made it to the Fe-nish line! | | You made it to the Fe-nish line! |
+-------------------------- ------------------------+ +-------------------------- ------------------------+
@ -463,6 +500,7 @@ mod tests {
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()], exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
n_done: 0, n_done: 0,
final_message: String::new(), final_message: String::new(),
state_file: tempfile::tempfile().unwrap(),
file_buf: Vec::new(), file_buf: Vec::new(),
official_exercises: true, official_exercises: true,
cmd_runner: CmdRunner::build().unwrap(), cmd_runner: CmdRunner::build().unwrap(),

View file

@ -189,7 +189,7 @@ impl<'a> ListState<'a> {
// Header // Header
let mut writer = MaxLenWriter::new(stdout, self.term_width as usize); let mut writer = MaxLenWriter::new(stdout, self.term_width as usize);
writer.write_ascii(b" Current State Name")?; writer.write_ascii(b" Current State Name")?;
writer.write_ascii(&self.name_col_padding[2..])?; writer.write_ascii(&self.name_col_padding[4..])?;
writer.write_ascii(b"Path")?; writer.write_ascii(b"Path")?;
next_ln(stdout)?; next_ln(stdout)?;
@ -263,14 +263,17 @@ impl<'a> ListState<'a> {
} }
writer.write_ascii(b" | <q>uit list")?; writer.write_ascii(b" | <q>uit list")?;
next_ln(stdout)?;
} else { } else {
writer.stdout.queue(SetForegroundColor(Color::Magenta))?; writer.stdout.queue(SetForegroundColor(Color::Magenta))?;
writer.write_str(&self.message)?; writer.write_str(&self.message)?;
stdout.queue(ResetColor)?; stdout.queue(ResetColor)?;
next_ln(stdout)?; next_ln(stdout)?;
}
next_ln(stdout)?; if self.narrow_term {
next_ln(stdout)?;
}
}
} }
stdout.queue(EndSynchronizedUpdate)?.flush() stdout.queue(EndSynchronizedUpdate)?.flush()