Use Cargo instead of rustc

This commit is contained in:
mo8it 2024-03-31 16:55:33 +02:00
parent b711dd692a
commit 82b563f165
4 changed files with 132 additions and 327 deletions

View file

@ -1,21 +1,21 @@
use anyhow::{Context, Result};
use serde::Deserialize; use serde::Deserialize;
use std::fmt::{self, Display, Formatter}; use std::fmt::{self, Debug, Display, Formatter};
use std::fs::{self, remove_file, File}; use std::fs::{self, File};
use std::io::{self, BufRead, BufReader}; use std::io::{self, BufRead, BufReader};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{self, exit, Command, Stdio}; use std::process::{exit, Command, Output};
use std::{array, env, mem}; use std::{array, mem};
use winnow::ascii::{space0, Caseless}; use winnow::ascii::{space0, Caseless};
use winnow::combinator::opt; use winnow::combinator::opt;
use winnow::Parser; use winnow::Parser;
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"]; use crate::embedded::EMBEDDED_FILES;
const RUSTC_EDITION_ARGS: &[&str] = &["--edition", "2021"];
const RUSTC_NO_DEBUG_ARGS: &[&str] = &["-C", "strip=debuginfo"];
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. // The number of context lines above and below a highlighted line.
const CONTEXT: usize = 2;
// Check if the line contains the "I AM NOT DONE" comment.
fn contains_not_done_comment(input: &str) -> bool { fn contains_not_done_comment(input: &str) -> bool {
( (
space0::<_, ()>, space0::<_, ()>,
@ -28,26 +28,15 @@ fn contains_not_done_comment(input: &str) -> bool {
.is_ok() .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. // The mode of the exercise.
#[derive(Deserialize, Copy, Clone, Debug)] #[derive(Deserialize, Copy, Clone)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Mode { pub enum Mode {
// Indicates that the exercise should be compiled as a binary // The exercise should be compiled as a binary
Compile, Compile,
// Indicates that the exercise should be compiled as a test harness // The exercise should be compiled as a test harness
Test, Test,
// Indicates that the exercise should be linted with clippy // The exercise should be linted with clippy
Clippy, Clippy,
} }
@ -56,171 +45,72 @@ pub struct ExerciseList {
pub exercises: Vec<Exercise>, pub exercises: Vec<Exercise>,
} }
// A representation of a rustlings exercise. impl ExerciseList {
// This is deserialized from the accompanying info.toml file pub fn parse() -> Result<Self> {
#[derive(Deserialize, Debug)] // 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 { pub struct Exercise {
// Name of the exercise // Name of the exercise
pub name: String, pub name: String,
// The path to the file containing the exercise's source code // The path to the file containing the exercise's source code
pub path: PathBuf, pub path: PathBuf,
// The mode of the exercise (Test, Compile, or Clippy) // The mode of the exercise
pub mode: Mode, pub mode: Mode,
// The hint text associated with the exercise // The hint text associated with the exercise
pub hint: String, pub hint: String,
} }
// An enum to track of the state of an Exercise. // The state of an Exercise.
// An Exercise can be either Done or Pending
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub enum State { pub enum State {
// The state of the exercise once it's been completed
Done, Done,
// The state of the exercise while it's not completed yet
Pending(Vec<ContextLine>), Pending(Vec<ContextLine>),
} }
// The context information of a pending exercise // The context information of a pending exercise.
#[derive(PartialEq, Eq, Debug)] #[derive(PartialEq, Eq, Debug)]
pub struct ContextLine { pub struct ContextLine {
// The source code that is still pending completion // The source code line
pub line: String, pub line: String,
// The line number of the source code still pending completion // The line number
pub number: usize, pub number: usize,
// Whether or not this is important // Whether this is important and should be highlighted
pub important: bool, 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<ExerciseOutput, ExerciseOutput> {
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 { impl Exercise {
pub fn compile(&self) -> Result<CompiledExercise, ExerciseOutput> { fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result<Output> {
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") Command::new("cargo")
.args(["clean", "--manifest-path", CLIPPY_CARGO_TOML_PATH]) .arg(command)
.args(RUSTC_COLOR_ARGS) .arg("--color")
.stdin(Stdio::null()) .arg("always")
.stdout(Stdio::null()) .arg("-q")
.stderr(Stdio::null()) .arg("--bin")
.status() .arg(&self.name)
.expect("Failed to run 'cargo clean'"); .args(args)
Command::new("cargo")
.args(["clippy", "--manifest-path", CLIPPY_CARGO_TOML_PATH])
.args(RUSTC_COLOR_ARGS)
.args(["--", "-D", "warnings", "-D", "clippy::float_cmp"])
.output() .output()
} .context("Failed to run Cargo")
}
.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<ExerciseOutput, ExerciseOutput> { pub fn run(&self) -> Result<Output> {
let arg = match self.mode { match self.mode {
Mode::Test => "--show-output", Mode::Compile => self.cargo_cmd("run", &[]),
_ => "", Mode::Test => self.cargo_cmd("test", &["--", "--nocapture"]),
}; Mode::Clippy => self.cargo_cmd(
let cmd = Command::new(temp_file()) "clippy",
.arg(arg) &["--", "-D", "warnings", "-D", "clippy::float_cmp"],
.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(),
};
if cmd.status.success() {
Ok(output)
} else {
Err(output)
} }
} }
@ -335,51 +225,13 @@ path = "{}.rs""#,
impl Display for Exercise { impl Display for Exercise {
fn fmt(&self, f: &mut Formatter) -> fmt::Result { 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)] #[cfg(test)]
mod test { mod test {
use super::*; 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] #[test]
fn test_pending_state() { fn test_pending_state() {
@ -442,8 +294,8 @@ mod test {
mode: Mode::Test, mode: Mode::Test,
hint: String::new(), hint: String::new(),
}; };
let out = exercise.compile().unwrap().run().unwrap(); let out = exercise.run().unwrap();
assert!(out.stdout.contains("THIS TEST TOO SHALL PASS")); assert_eq!(out.stdout, b"THIS TEST TOO SHALL PASS");
} }
#[test] #[test]

View file

@ -4,20 +4,18 @@ use crate::verify::verify;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use console::Emoji; use console::Emoji;
use embedded::EMBEDDED_FILES;
use notify_debouncer_mini::notify::{self, RecursiveMode}; use notify_debouncer_mini::notify::{self, RecursiveMode};
use notify_debouncer_mini::{new_debouncer, DebouncedEventKind}; use notify_debouncer_mini::{new_debouncer, DebouncedEventKind};
use shlex::Shlex; use shlex::Shlex;
use std::ffi::OsStr; use std::ffi::OsStr;
use std::fs; use std::io::{BufRead, Write};
use std::io::{self, prelude::*};
use std::path::Path; use std::path::Path;
use std::process::{exit, Command}; use std::process::{exit, Command};
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{channel, RecvTimeoutError}; use std::sync::mpsc::{channel, RecvTimeoutError};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration; use std::time::Duration;
use std::{io, thread};
#[macro_use] #[macro_use]
mod ui; mod ui;
@ -94,21 +92,16 @@ fn main() -> Result<()> {
println!("\n{WELCOME}\n"); println!("\n{WELCOME}\n");
} }
if which::which("rustc").is_err() { if which::which("cargo").is_err() {
println!("We cannot find `rustc`."); println!(
println!("Try running `rustc --version` to diagnose your problem."); "Failed to find `cargo`.
println!("For instructions on how to install Rust, check the README."); Did you already install Rust?
Try running `cargo --version` to diagnose the problem."
);
std::process::exit(1); std::process::exit(1);
} }
// Read a local `info.toml` if it exists. Mainly to let the tests work for now. let exercises = ExerciseList::parse()?.exercises;
let exercises = if let Ok(file_content) = fs::read_to_string("info.toml") {
toml_edit::de::from_str::<ExerciseList>(&file_content)
} else {
toml_edit::de::from_str::<ExerciseList>(EMBEDDED_FILES.info_toml_content)
}
.context("Failed to parse `info.toml`")?
.exercises;
if matches!(args.command, Some(Subcommands::Init)) { if matches!(args.command, Some(Subcommands::Init)) {
init::init_rustlings(&exercises).context("Initialization failed")?; init::init_rustlings(&exercises).context("Initialization failed")?;

View file

@ -1,4 +1,5 @@
use std::io; use anyhow::{bail, Result};
use std::io::{self, stdout, Write};
use std::time::Duration; use std::time::Duration;
use crate::embedded::{WriteStrategy, EMBEDDED_FILES}; use crate::embedded::{WriteStrategy, EMBEDDED_FILES};
@ -10,13 +11,11 @@ use indicatif::ProgressBar;
// and run the ensuing binary. // and run the ensuing binary.
// The verbose argument helps determine whether or not to show // The verbose argument helps determine whether or not to show
// the output from the test harnesses (if the mode of the exercise is test) // the output from the test harnesses (if the mode of the exercise is test)
pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> { pub fn run(exercise: &Exercise, verbose: bool) -> Result<()> {
match exercise.mode { match exercise.mode {
Mode::Test => test(exercise, verbose)?, Mode::Test => test(exercise, verbose),
Mode::Compile => compile_and_run(exercise)?, Mode::Compile | Mode::Clippy => compile_and_run(exercise),
Mode::Clippy => compile_and_run(exercise)?,
} }
Ok(())
} }
// Resets the exercise by stashing the changes. // Resets the exercise by stashing the changes.
@ -27,41 +26,21 @@ pub fn reset(exercise: &Exercise) -> io::Result<()> {
// Invoke the rust compiler on the path of the given exercise // Invoke the rust compiler on the path of the given exercise
// and run the ensuing binary. // and run the ensuing binary.
// This is strictly for non-test binaries, so output is displayed // This is strictly for non-test binaries, so output is displayed
fn compile_and_run(exercise: &Exercise) -> Result<(), ()> { fn compile_and_run(exercise: &Exercise) -> Result<()> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Compiling {exercise}...")); progress_bar.set_message(format!("Running {exercise}..."));
progress_bar.enable_steady_tick(Duration::from_millis(100)); progress_bar.enable_steady_tick(Duration::from_millis(100));
let compilation_result = exercise.compile(); let output = exercise.run()?;
let compilation = match compilation_result {
Ok(compilation) => compilation,
Err(output) => {
progress_bar.finish_and_clear(); progress_bar.finish_and_clear();
warn!(
"Compilation of {} failed!, Compiler error message:\n", stdout().write_all(&output.stdout)?;
exercise if !output.status.success() {
); stdout().write_all(&output.stderr)?;
println!("{}", output.stderr); warn!("Ran {} with errors", exercise);
return Err(()); bail!("TODO");
} }
};
progress_bar.set_message(format!("Running {exercise}..."));
let result = compilation.run();
progress_bar.finish_and_clear();
match result {
Ok(output) => {
println!("{}", output.stdout);
success!("Successfully ran {}", exercise); success!("Successfully ran {}", exercise);
Ok(()) Ok(())
} }
Err(output) => {
println!("{}", output.stdout);
println!("{}", output.stderr);
warn!("Ran {} with errors", exercise);
Err(())
}
}
}

View file

@ -1,7 +1,14 @@
use crate::exercise::{CompiledExercise, Exercise, Mode, State}; use anyhow::{bail, Result};
use console::style; use console::style;
use indicatif::{ProgressBar, ProgressStyle}; use indicatif::{ProgressBar, ProgressStyle};
use std::{env, time::Duration}; use std::{
env,
io::{stdout, Write},
process::Output,
time::Duration,
};
use crate::exercise::{Exercise, Mode, State};
// Verify that the provided container of Exercise objects // Verify that the provided container of Exercise objects
// can be compiled and run without any failures. // can be compiled and run without any failures.
@ -58,50 +65,44 @@ enum RunMode {
} }
// Compile and run the resulting test harness of the given Exercise // Compile and run the resulting test harness of the given Exercise
pub fn test(exercise: &Exercise, verbose: bool) -> Result<(), ()> { pub fn test(exercise: &Exercise, verbose: bool) -> Result<()> {
compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?; compile_and_test(exercise, RunMode::NonInteractive, verbose, false)?;
Ok(()) Ok(())
} }
// Invoke the rust compiler without running the resulting binary // Invoke the rust compiler without running the resulting binary
fn compile_only(exercise: &Exercise, success_hints: bool) -> Result<bool, ()> { fn compile_only(exercise: &Exercise, success_hints: bool) -> Result<bool> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Compiling {exercise}...")); progress_bar.set_message(format!("Compiling {exercise}..."));
progress_bar.enable_steady_tick(Duration::from_millis(100)); progress_bar.enable_steady_tick(Duration::from_millis(100));
let _ = compile(exercise, &progress_bar)?; let _ = exercise.run()?;
progress_bar.finish_and_clear(); progress_bar.finish_and_clear();
Ok(prompt_for_completion(exercise, None, success_hints)) Ok(prompt_for_completion(exercise, None, success_hints))
} }
// Compile the given Exercise and run the resulting binary in an interactive mode // Compile the given Exercise and run the resulting binary in an interactive mode
fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result<bool, ()> { fn compile_and_run_interactively(exercise: &Exercise, success_hints: bool) -> Result<bool> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Compiling {exercise}...")); progress_bar.set_message(format!("Running {exercise}..."));
progress_bar.enable_steady_tick(Duration::from_millis(100)); progress_bar.enable_steady_tick(Duration::from_millis(100));
let compilation = compile(exercise, &progress_bar)?; let output = exercise.run()?;
progress_bar.set_message(format!("Running {exercise}..."));
let result = compilation.run();
progress_bar.finish_and_clear(); progress_bar.finish_and_clear();
let output = match result { if !output.status.success() {
Ok(output) => output,
Err(output) => {
warn!("Ran {} with errors", exercise); warn!("Ran {} with errors", exercise);
println!("{}", output.stdout); {
println!("{}", output.stderr); let mut stdout = stdout().lock();
return Err(()); stdout.write_all(&output.stdout)?;
stdout.write_all(&output.stderr)?;
stdout.flush()?;
}
bail!("TODO");
} }
};
Ok(prompt_for_completion( Ok(prompt_for_completion(exercise, Some(output), success_hints))
exercise,
Some(output.stdout),
success_hints,
))
} }
// Compile the given Exercise as a test harness and display // Compile the given Exercise as a test harness and display
@ -111,62 +112,42 @@ fn compile_and_test(
run_mode: RunMode, run_mode: RunMode,
verbose: bool, verbose: bool,
success_hints: bool, success_hints: bool,
) -> Result<bool, ()> { ) -> Result<bool> {
let progress_bar = ProgressBar::new_spinner(); let progress_bar = ProgressBar::new_spinner();
progress_bar.set_message(format!("Testing {exercise}...")); progress_bar.set_message(format!("Testing {exercise}..."));
progress_bar.enable_steady_tick(Duration::from_millis(100)); progress_bar.enable_steady_tick(Duration::from_millis(100));
let compilation = compile(exercise, &progress_bar)?; let output = exercise.run()?;
let result = compilation.run();
progress_bar.finish_and_clear(); progress_bar.finish_and_clear();
match result { if !output.status.success() {
Ok(output) => { warn!(
if verbose { "Testing of {} failed! Please try again. Here's the output:",
println!("{}", output.stdout); exercise
);
{
let mut stdout = stdout().lock();
stdout.write_all(&output.stdout)?;
stdout.write_all(&output.stderr)?;
stdout.flush()?;
} }
bail!("TODO");
}
if verbose {
stdout().write_all(&output.stdout)?;
}
if run_mode == RunMode::Interactive { if run_mode == RunMode::Interactive {
Ok(prompt_for_completion(exercise, None, success_hints)) Ok(prompt_for_completion(exercise, None, success_hints))
} else { } else {
Ok(true) Ok(true)
} }
} }
Err(output) => {
warn!(
"Testing of {} failed! Please try again. Here's the output:",
exercise
);
println!("{}", output.stdout);
Err(())
}
}
}
// Compile the given Exercise and return an object with information
// about the state of the compilation
fn compile<'a>(
exercise: &'a Exercise,
progress_bar: &ProgressBar,
) -> Result<CompiledExercise<'a>, ()> {
let compilation_result = exercise.compile();
match compilation_result {
Ok(compilation) => Ok(compilation),
Err(output) => {
progress_bar.finish_and_clear();
warn!(
"Compiling of {} failed! Please try again. Here's the output:",
exercise
);
println!("{}", output.stderr);
Err(())
}
}
}
fn prompt_for_completion( fn prompt_for_completion(
exercise: &Exercise, exercise: &Exercise,
prompt_output: Option<String>, prompt_output: Option<Output>,
success_hints: bool, success_hints: bool,
) -> bool { ) -> bool {
let context = match exercise.state() { let context = match exercise.state() {
@ -200,10 +181,10 @@ fn prompt_for_completion(
} }
if let Some(output) = prompt_output { if let Some(output) = prompt_output {
println!( let separator = separator();
"Output:\n{separator}\n{output}\n{separator}\n", println!("Output:\n{separator}");
separator = separator(), stdout().write_all(&output.stdout).unwrap();
); println!("\n{separator}\n");
} }
if success_hints { if success_hints {
println!( println!(