|
1 |
| -use regex::Regex; |
2 | 1 | use serde::Deserialize;
|
3 |
| -use std::env; |
4 | 2 | use std::fmt::{self, Display, Formatter};
|
5 | 3 | use std::fs::{self, remove_file, File};
|
6 |
| -use std::io::Read; |
| 4 | +use std::io::{self, BufRead, BufReader}; |
7 | 5 | use std::path::PathBuf;
|
8 |
| -use std::process::{self, Command, Stdio}; |
| 6 | +use std::process::{self, exit, Command, Stdio}; |
| 7 | +use std::{array, env, mem}; |
| 8 | +use winnow::ascii::{space0, Caseless}; |
| 9 | +use winnow::combinator::opt; |
| 10 | +use winnow::Parser; |
9 | 11 |
|
10 | 12 | const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];
|
11 | 13 | const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"];
|
12 | 14 | const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"];
|
13 |
| -const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE"; |
14 | 15 | const CONTEXT: usize = 2;
|
15 | 16 | const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/22_clippy/Cargo.toml";
|
16 | 17 |
|
| 18 | +// Checks if the line contains the "I AM NOT DONE" comment. |
| 19 | +fn contains_not_done_comment(input: &str) -> bool { |
| 20 | + ( |
| 21 | + space0::<_, ()>, |
| 22 | + "//", |
| 23 | + opt('/'), |
| 24 | + space0, |
| 25 | + Caseless("I AM NOT DONE"), |
| 26 | + ) |
| 27 | + .parse_next(&mut &*input) |
| 28 | + .is_ok() |
| 29 | +} |
| 30 | + |
17 | 31 | // Get a temporary file name that is hopefully unique
|
18 | 32 | #[inline]
|
19 | 33 | fn temp_file() -> String {
|
@@ -211,51 +225,101 @@ path = "{}.rs""#,
|
211 | 225 | }
|
212 | 226 |
|
213 | 227 | pub fn state(&self) -> State {
|
214 |
| - let mut source_file = File::open(&self.path).unwrap_or_else(|e| { |
215 |
| - panic!( |
216 |
| - "We were unable to open the exercise file {}! {e}", |
217 |
| - self.path.display() |
218 |
| - ) |
| 228 | + let source_file = File::open(&self.path).unwrap_or_else(|e| { |
| 229 | + println!( |
| 230 | + "Failed to open the exercise file {}: {e}", |
| 231 | + self.path.display(), |
| 232 | + ); |
| 233 | + exit(1); |
219 | 234 | });
|
220 |
| - |
221 |
| - let source = { |
222 |
| - let mut s = String::new(); |
223 |
| - source_file.read_to_string(&mut s).unwrap_or_else(|e| { |
224 |
| - panic!( |
225 |
| - "We were unable to read the exercise file {}! {e}", |
226 |
| - self.path.display() |
227 |
| - ) |
228 |
| - }); |
229 |
| - s |
| 235 | + let mut source_reader = BufReader::new(source_file); |
| 236 | + |
| 237 | + // Read the next line into `buf` without the newline at the end. |
| 238 | + let mut read_line = |buf: &mut String| -> io::Result<_> { |
| 239 | + let n = source_reader.read_line(buf)?; |
| 240 | + if buf.ends_with('\n') { |
| 241 | + buf.pop(); |
| 242 | + if buf.ends_with('\r') { |
| 243 | + buf.pop(); |
| 244 | + } |
| 245 | + } |
| 246 | + Ok(n) |
230 | 247 | };
|
231 | 248 |
|
232 |
| - let re = Regex::new(I_AM_DONE_REGEX).unwrap(); |
| 249 | + let mut current_line_number: usize = 1; |
| 250 | + // Keep the last `CONTEXT` lines while iterating over the file lines. |
| 251 | + let mut prev_lines: [_; CONTEXT] = array::from_fn(|_| String::with_capacity(256)); |
| 252 | + let mut line = String::with_capacity(256); |
233 | 253 |
|
234 |
| - if !re.is_match(&source) { |
235 |
| - return State::Done; |
236 |
| - } |
| 254 | + loop { |
| 255 | + let n = read_line(&mut line).unwrap_or_else(|e| { |
| 256 | + println!( |
| 257 | + "Failed to read the exercise file {}: {e}", |
| 258 | + self.path.display(), |
| 259 | + ); |
| 260 | + exit(1); |
| 261 | + }); |
237 | 262 |
|
238 |
| - let matched_line_index = source |
239 |
| - .lines() |
240 |
| - .enumerate() |
241 |
| - .find_map(|(i, line)| if re.is_match(line) { Some(i) } else { None }) |
242 |
| - .expect("This should not happen at all"); |
243 |
| - |
244 |
| - let min_line = ((matched_line_index as i32) - (CONTEXT as i32)).max(0) as usize; |
245 |
| - let max_line = matched_line_index + CONTEXT; |
246 |
| - |
247 |
| - let context = source |
248 |
| - .lines() |
249 |
| - .enumerate() |
250 |
| - .filter(|&(i, _)| i >= min_line && i <= max_line) |
251 |
| - .map(|(i, line)| ContextLine { |
252 |
| - line: line.to_string(), |
253 |
| - number: i + 1, |
254 |
| - important: i == matched_line_index, |
255 |
| - }) |
256 |
| - .collect(); |
| 263 | + // Reached the end of the file and didn't find the comment. |
| 264 | + if n == 0 { |
| 265 | + return State::Done; |
| 266 | + } |
| 267 | + |
| 268 | + if contains_not_done_comment(&line) { |
| 269 | + let mut context = Vec::with_capacity(2 * CONTEXT + 1); |
| 270 | + // Previous lines. |
| 271 | + for (ind, prev_line) in prev_lines |
| 272 | + .into_iter() |
| 273 | + .take(current_line_number - 1) |
| 274 | + .enumerate() |
| 275 | + .rev() |
| 276 | + { |
| 277 | + context.push(ContextLine { |
| 278 | + line: prev_line, |
| 279 | + number: current_line_number - 1 - ind, |
| 280 | + important: false, |
| 281 | + }); |
| 282 | + } |
| 283 | + |
| 284 | + // Current line. |
| 285 | + context.push(ContextLine { |
| 286 | + line, |
| 287 | + number: current_line_number, |
| 288 | + important: true, |
| 289 | + }); |
| 290 | + |
| 291 | + // Next lines. |
| 292 | + for ind in 0..CONTEXT { |
| 293 | + let mut next_line = String::with_capacity(256); |
| 294 | + let Ok(n) = read_line(&mut next_line) else { |
| 295 | + // If an error occurs, just ignore the next lines. |
| 296 | + break; |
| 297 | + }; |
| 298 | + |
| 299 | + // Reached the end of the file. |
| 300 | + if n == 0 { |
| 301 | + break; |
| 302 | + } |
| 303 | + |
| 304 | + context.push(ContextLine { |
| 305 | + line: next_line, |
| 306 | + number: current_line_number + 1 + ind, |
| 307 | + important: false, |
| 308 | + }); |
| 309 | + } |
| 310 | + |
| 311 | + return State::Pending(context); |
| 312 | + } |
257 | 313 |
|
258 |
| - State::Pending(context) |
| 314 | + current_line_number += 1; |
| 315 | + // Add the current line as a previous line and shift the older lines by one. |
| 316 | + for prev_line in &mut prev_lines { |
| 317 | + mem::swap(&mut line, prev_line); |
| 318 | + } |
| 319 | + // The current line now contains the oldest previous line. |
| 320 | + // Recycle it for reading the next line. |
| 321 | + line.clear(); |
| 322 | + } |
259 | 323 | }
|
260 | 324 |
|
261 | 325 | // Check that the exercise looks to be solved using self.state()
|
@@ -381,4 +445,20 @@ mod test {
|
381 | 445 | let out = exercise.compile().unwrap().run().unwrap();
|
382 | 446 | assert!(out.stdout.contains("THIS TEST TOO SHALL PASS"));
|
383 | 447 | }
|
| 448 | + |
| 449 | + #[test] |
| 450 | + fn test_not_done() { |
| 451 | + assert!(contains_not_done_comment("// I AM NOT DONE")); |
| 452 | + assert!(contains_not_done_comment("/// I AM NOT DONE")); |
| 453 | + assert!(contains_not_done_comment("// I AM NOT DONE")); |
| 454 | + assert!(contains_not_done_comment("/// I AM NOT DONE")); |
| 455 | + assert!(contains_not_done_comment("// I AM NOT DONE ")); |
| 456 | + assert!(contains_not_done_comment("// I AM NOT DONE!")); |
| 457 | + assert!(contains_not_done_comment("// I am not done")); |
| 458 | + assert!(contains_not_done_comment("// i am NOT done")); |
| 459 | + |
| 460 | + assert!(!contains_not_done_comment("I AM NOT DONE")); |
| 461 | + assert!(!contains_not_done_comment("// NOT DONE")); |
| 462 | + assert!(!contains_not_done_comment("DONE")); |
| 463 | + } |
384 | 464 | }
|
0 commit comments