Skip to content

Commit fc1f9f0

Browse files
committed
Optimize reading and writing the state file
1 parent 789492d commit fc1f9f0

File tree

1 file changed

+77
-63
lines changed

1 file changed

+77
-63
lines changed

src/app_state.rs

Lines changed: 77 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use anyhow::{bail, Context, Result};
22
use std::{
33
env,
4-
fs::{self, File},
5-
io::{self, Read, StdoutLock, Write},
4+
fs::{File, OpenOptions},
5+
io::{self, Read, Seek, StdoutLock, Write},
66
path::Path,
77
process::{Command, Stdio},
88
thread,
@@ -18,7 +18,6 @@ use crate::{
1818
};
1919

2020
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
21-
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
2221

2322
#[must_use]
2423
pub enum ExercisesProgress {
@@ -47,6 +46,7 @@ pub struct AppState {
4746
// Caches the number of done exercises to avoid iterating over all exercises every time.
4847
n_done: u16,
4948
final_message: String,
49+
state_file: File,
5050
// Preallocated buffer for reading and writing the state file.
5151
file_buf: Vec<u8>,
5252
official_exercises: bool,
@@ -56,59 +56,22 @@ pub struct AppState {
5656
}
5757

5858
impl AppState {
59-
// Update the app state from the state file.
60-
fn update_from_file(&mut self) -> StateFileStatus {
61-
self.file_buf.clear();
62-
self.n_done = 0;
63-
64-
if File::open(STATE_FILE_NAME)
65-
.and_then(|mut file| file.read_to_end(&mut self.file_buf))
66-
.is_err()
67-
{
68-
return StateFileStatus::NotRead;
69-
}
70-
71-
// See `Self::write` for more information about the file format.
72-
let mut lines = self.file_buf.split(|c| *c == b'\n').skip(2);
73-
74-
let Some(current_exercise_name) = lines.next() else {
75-
return StateFileStatus::NotRead;
76-
};
77-
78-
if current_exercise_name.is_empty() || lines.next().is_none() {
79-
return StateFileStatus::NotRead;
80-
}
81-
82-
let mut done_exercises = hash_set_with_capacity(self.exercises.len());
83-
84-
for done_exerise_name in lines {
85-
if done_exerise_name.is_empty() {
86-
break;
87-
}
88-
done_exercises.insert(done_exerise_name);
89-
}
90-
91-
for (ind, exercise) in self.exercises.iter_mut().enumerate() {
92-
if done_exercises.contains(exercise.name.as_bytes()) {
93-
exercise.done = true;
94-
self.n_done += 1;
95-
}
96-
97-
if exercise.name.as_bytes() == current_exercise_name {
98-
self.current_exercise_ind = ind;
99-
}
100-
}
101-
102-
StateFileStatus::Read
103-
}
104-
10559
pub fn new(
10660
exercise_infos: Vec<ExerciseInfo>,
10761
final_message: String,
10862
) -> Result<(Self, StateFileStatus)> {
10963
let cmd_runner = CmdRunner::build()?;
110-
111-
let exercises = exercise_infos
64+
let mut state_file = OpenOptions::new()
65+
.create(true)
66+
.read(true)
67+
.write(true)
68+
.truncate(false)
69+
.open(STATE_FILE_NAME)
70+
.with_context(|| {
71+
format!("Failed to open or create the state file {STATE_FILE_NAME}")
72+
})?;
73+
74+
let mut exercises = exercise_infos
11275
.into_iter()
11376
.map(|exercise_info| {
11477
// Leaking to be able to borrow in the watch mode `Table`.
@@ -126,25 +89,69 @@ impl AppState {
12689
test: exercise_info.test,
12790
strict_clippy: exercise_info.strict_clippy,
12891
hint,
129-
// Updated in `Self::update_from_file`.
92+
// Updated below.
13093
done: false,
13194
}
13295
})
13396
.collect::<Vec<_>>();
13497

135-
let mut slf = Self {
136-
current_exercise_ind: 0,
98+
let mut current_exercise_ind = 0;
99+
let mut n_done = 0;
100+
let mut file_buf = Vec::with_capacity(2048);
101+
let state_file_status = 'block: {
102+
if state_file.read_to_end(&mut file_buf).is_err() {
103+
break 'block StateFileStatus::NotRead;
104+
}
105+
106+
// See `Self::write` for more information about the file format.
107+
let mut lines = file_buf.split(|c| *c == b'\n').skip(2);
108+
109+
let Some(current_exercise_name) = lines.next() else {
110+
break 'block StateFileStatus::NotRead;
111+
};
112+
113+
if current_exercise_name.is_empty() || lines.next().is_none() {
114+
break 'block StateFileStatus::NotRead;
115+
}
116+
117+
let mut done_exercises = hash_set_with_capacity(exercises.len());
118+
119+
for done_exerise_name in lines {
120+
if done_exerise_name.is_empty() {
121+
break;
122+
}
123+
done_exercises.insert(done_exerise_name);
124+
}
125+
126+
for (ind, exercise) in exercises.iter_mut().enumerate() {
127+
if done_exercises.contains(exercise.name.as_bytes()) {
128+
exercise.done = true;
129+
n_done += 1;
130+
}
131+
132+
if exercise.name.as_bytes() == current_exercise_name {
133+
current_exercise_ind = ind;
134+
}
135+
}
136+
137+
StateFileStatus::Read
138+
};
139+
140+
file_buf.clear();
141+
file_buf.extend_from_slice(STATE_FILE_HEADER);
142+
143+
let slf = Self {
144+
current_exercise_ind,
137145
exercises,
138-
n_done: 0,
146+
n_done,
139147
final_message,
140-
file_buf: Vec::with_capacity(2048),
148+
state_file,
149+
file_buf,
141150
official_exercises: !Path::new("info.toml").exists(),
142151
cmd_runner,
143152
vs_code: env::var_os("TERM_PROGRAM").is_some_and(|v| v == "vscode"),
144153
};
145154

146-
let state_file_status = slf.update_from_file();
147-
148155
Ok((slf, state_file_status))
149156
}
150157

@@ -187,10 +194,8 @@ impl AppState {
187194
// - The fourth line is an empty line.
188195
// - All remaining lines are the names of done exercises.
189196
fn write(&mut self) -> Result<()> {
190-
self.file_buf.clear();
197+
self.file_buf.truncate(STATE_FILE_HEADER.len());
191198

192-
self.file_buf
193-
.extend_from_slice(b"DON'T EDIT THIS FILE!\n\n");
194199
self.file_buf
195200
.extend_from_slice(self.current_exercise().name.as_bytes());
196201
self.file_buf.push(b'\n');
@@ -202,7 +207,14 @@ impl AppState {
202207
}
203208
}
204209

205-
fs::write(STATE_FILE_NAME, &self.file_buf)
210+
self.state_file
211+
.rewind()
212+
.with_context(|| format!("Failed to rewind the state file {STATE_FILE_NAME}"))?;
213+
self.state_file
214+
.set_len(0)
215+
.with_context(|| format!("Failed to truncate the state file {STATE_FILE_NAME}"))?;
216+
self.state_file
217+
.write_all(&self.file_buf)
206218
.with_context(|| format!("Failed to write the state file {STATE_FILE_NAME}"))?;
207219

208220
Ok(())
@@ -440,11 +452,12 @@ impl AppState {
440452
}
441453
}
442454

455+
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
456+
const STATE_FILE_HEADER: &[u8] = b"DON'T EDIT THIS FILE!\n\n";
443457
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
444458
All exercises seem to be done.
445459
Recompiling and running all exercises to make sure that all of them are actually done.
446460
";
447-
448461
const FENISH_LINE: &str = "+----------------------------------------------------+
449462
| You made it to the Fe-nish line! |
450463
+-------------------------- ------------------------+
@@ -490,6 +503,7 @@ mod tests {
490503
exercises: vec![dummy_exercise(), dummy_exercise(), dummy_exercise()],
491504
n_done: 0,
492505
final_message: String::new(),
506+
state_file: tempfile::tempfile().unwrap(),
493507
file_buf: Vec::new(),
494508
official_exercises: true,
495509
cmd_runner: CmdRunner::build().unwrap(),

0 commit comments

Comments
 (0)