From 82b563f1654860ba3590d91ec3c0f321e3130ae2 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sun, 31 Mar 2024 16:55:33 +0200 Subject: Use Cargo instead of rustc --- src/exercise.rs | 260 ++++++++++++-------------------------------------------- 1 file changed, 56 insertions(+), 204 deletions(-) (limited to 'src/exercise.rs') diff --git a/src/exercise.rs b/src/exercise.rs index 1125916..83d444f 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,21 +1,21 @@ +use anyhow::{Context, Result}; use serde::Deserialize; -use std::fmt::{self, Display, Formatter}; -use std::fs::{self, remove_file, File}; +use std::fmt::{self, Debug, Display, Formatter}; +use std::fs::{self, File}; use std::io::{self, BufRead, BufReader}; use std::path::PathBuf; -use std::process::{self, exit, Command, Stdio}; -use std::{array, env, mem}; +use std::process::{exit, Command, Output}; +use std::{array, mem}; use winnow::ascii::{space0, Caseless}; use winnow::combinator::opt; use winnow::Parser; -const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; -const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"]; -const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"]; +use crate::embedded::EMBEDDED_FILES; + +// The number of context lines above and below a highlighted line. const CONTEXT: usize = 2; -const CLIPPY_CARGO_TOML_PATH: &str = "exercises/22_clippy/Cargo.toml"; -// Checks if the line contains the "I AM NOT DONE" comment. +// Check if the line contains the "I AM NOT DONE" comment. fn contains_not_done_comment(input: &str) -> bool { ( space0::<_, ()>, @@ -28,26 +28,15 @@ fn contains_not_done_comment(input: &str) -> bool { .is_ok() } -// Get a temporary file name that is hopefully unique -#[inline] -fn temp_file() -> String { - let thread_id: String = format!("{:?}", std::thread::current().id()) - .chars() - .filter(|c| c.is_alphanumeric()) - .collect(); - - format!("./temp_{}_{thread_id}", process::id()) -} - // The mode of the exercise. -#[derive(Deserialize, Copy, Clone, Debug)] +#[derive(Deserialize, Copy, Clone)] #[serde(rename_all = "lowercase")] pub enum Mode { - // Indicates that the exercise should be compiled as a binary + // The exercise should be compiled as a binary Compile, - // Indicates that the exercise should be compiled as a test harness + // The exercise should be compiled as a test harness Test, - // Indicates that the exercise should be linted with clippy + // The exercise should be linted with clippy Clippy, } @@ -56,171 +45,72 @@ pub struct ExerciseList { pub exercises: Vec, } -// A representation of a rustlings exercise. -// This is deserialized from the accompanying info.toml file -#[derive(Deserialize, Debug)] +impl ExerciseList { + pub fn parse() -> Result { + // Read a local `info.toml` if it exists. + // Mainly to let the tests work for now. + if let Ok(file_content) = fs::read_to_string("info.toml") { + toml_edit::de::from_str(&file_content) + } else { + toml_edit::de::from_str(EMBEDDED_FILES.info_toml_content) + } + .context("Failed to parse `info.toml`") + } +} + +// Deserialized from the `info.toml` file. +#[derive(Deserialize)] pub struct Exercise { // Name of the exercise pub name: String, // The path to the file containing the exercise's source code pub path: PathBuf, - // The mode of the exercise (Test, Compile, or Clippy) + // The mode of the exercise pub mode: Mode, // The hint text associated with the exercise pub hint: String, } -// An enum to track of the state of an Exercise. -// An Exercise can be either Done or Pending +// The state of an Exercise. #[derive(PartialEq, Eq, Debug)] pub enum State { - // The state of the exercise once it's been completed Done, - // The state of the exercise while it's not completed yet Pending(Vec), } -// The context information of a pending exercise +// The context information of a pending exercise. #[derive(PartialEq, Eq, Debug)] pub struct ContextLine { - // The source code that is still pending completion + // The source code line pub line: String, - // The line number of the source code still pending completion + // The line number pub number: usize, - // Whether or not this is important + // Whether this is important and should be highlighted pub important: bool, } -// The result of compiling an exercise -pub struct CompiledExercise<'a> { - exercise: &'a Exercise, - _handle: FileHandle, -} - -impl<'a> CompiledExercise<'a> { - // Run the compiled exercise - pub fn run(&self) -> Result { - self.exercise.run() - } -} - -// A representation of an already executed binary -#[derive(Debug)] -pub struct ExerciseOutput { - // The textual contents of the standard output of the binary - pub stdout: String, - // The textual contents of the standard error of the binary - pub stderr: String, -} - -struct FileHandle; - -impl Drop for FileHandle { - fn drop(&mut self) { - clean(); - } -} - impl Exercise { - pub fn compile(&self) -> Result { - let cmd = match self.mode { - Mode::Compile => Command::new("rustc") - .args([self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .output(), - Mode::Test => Command::new("rustc") - .args(["--test", self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .output(), - Mode::Clippy => { - let cargo_toml = format!( - r#"[package] -name = "{}" -version = "0.0.1" -edition = "2021" -[[bin]] -name = "{}" -path = "{}.rs""#, - self.name, self.name, self.name - ); - let cargo_toml_error_msg = if env::var("NO_EMOJI").is_ok() { - "Failed to write Clippy Cargo.toml file." - } else { - "Failed to write 📎 Clippy 📎 Cargo.toml file." - }; - fs::write(CLIPPY_CARGO_TOML_PATH, cargo_toml).expect(cargo_toml_error_msg); - // To support the ability to run the clippy exercises, build - // an executable, in addition to running clippy. With a - // compilation failure, this would silently fail. But we expect - // clippy to reflect the same failure while compiling later. - Command::new("rustc") - .args([self.path.to_str().unwrap(), "-o", &temp_file()]) - .args(RUSTC_COLOR_ARGS) - .args(RUSTC_EDITION_ARGS) - .args(RUSTC_NO_DEBUG_ARGS) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("Failed to compile!"); - // Due to an issue with Clippy, a cargo clean is required to catch all lints. - // See https://github.com/rust-lang/rust-clippy/issues/2604 - // This is already fixed on Clippy's master branch. See this issue to track merging into Cargo: - // https://github.com/rust-lang/rust-clippy/issues/3837 - Command::new("cargo") - .args(["clean", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) - .args(RUSTC_COLOR_ARGS) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .status() - .expect("Failed to run 'cargo clean'"); - Command::new("cargo") - .args(["clippy", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) - .args(RUSTC_COLOR_ARGS) - .args(["--", "-D", "warnings", "-D", "clippy::float_cmp"]) - .output() - } - } - .expect("Failed to run 'compile' command."); - - if cmd.status.success() { - Ok(CompiledExercise { - exercise: self, - _handle: FileHandle, - }) - } else { - clean(); - Err(ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), - }) - } - } - - fn run(&self) -> Result { - let arg = match self.mode { - Mode::Test => "--show-output", - _ => "", - }; - let cmd = Command::new(temp_file()) - .arg(arg) + fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result { + Command::new("cargo") + .arg(command) + .arg("--color") + .arg("always") + .arg("-q") + .arg("--bin") + .arg(&self.name) + .args(args) .output() - .expect("Failed to run 'run' command"); - - let output = ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), - }; + .context("Failed to run Cargo") + } - if cmd.status.success() { - Ok(output) - } else { - Err(output) + pub fn run(&self) -> Result { + match self.mode { + Mode::Compile => self.cargo_cmd("run", &[]), + Mode::Test => self.cargo_cmd("test", &["--", "--nocapture"]), + Mode::Clippy => self.cargo_cmd( + "clippy", + &["--", "-D", "warnings", "-D", "clippy::float_cmp"], + ), } } @@ -335,51 +225,13 @@ path = "{}.rs""#, impl Display for Exercise { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "{}", self.path.to_str().unwrap()) + self.path.fmt(f) } } -#[inline] -fn clean() { - let _ignored = remove_file(temp_file()); -} - #[cfg(test)] mod test { use super::*; - use std::path::Path; - - #[test] - fn test_clean() { - File::create(temp_file()).unwrap(); - let exercise = Exercise { - name: String::from("example"), - path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), - mode: Mode::Compile, - hint: String::from(""), - }; - let compiled = exercise.compile().unwrap(); - drop(compiled); - assert!(!Path::new(&temp_file()).exists()); - } - - #[test] - #[cfg(target_os = "windows")] - fn test_no_pdb_file() { - [Mode::Compile, Mode::Test] // Clippy doesn't like to test - .iter() - .for_each(|mode| { - let exercise = Exercise { - name: String::from("example"), - // We want a file that does actually compile - path: PathBuf::from("tests/fixture/state/exercises/pending_exercise.rs"), - mode: *mode, - hint: String::from(""), - }; - let _ = exercise.compile().unwrap(); - assert!(!Path::new(&format!("{}.pdb", temp_file())).exists()); - }); - } #[test] fn test_pending_state() { @@ -442,8 +294,8 @@ mod test { mode: Mode::Test, hint: String::new(), }; - let out = exercise.compile().unwrap().run().unwrap(); - assert!(out.stdout.contains("THIS TEST TOO SHALL PASS")); + let out = exercise.run().unwrap(); + assert_eq!(out.stdout, b"THIS TEST TOO SHALL PASS"); } #[test] -- cgit v1.2.3