From c82c3673245ca11d455b067c97fadda4a8406cb9 Mon Sep 17 00:00:00 2001 From: mo8it Date: Sat, 27 Apr 2024 04:14:59 +0200 Subject: [PATCH] Respect the target-dir config and show tests' output --- Cargo.lock | 69 ++++----------------- Cargo.toml | 2 +- src/app_state.rs | 12 +++- src/cmd.rs | 70 ++++++++++++++++++++++ src/exercise.rs | 145 +++++++++++++++++---------------------------- src/main.rs | 29 +++++++-- src/run.rs | 2 +- src/watch/state.rs | 5 +- 8 files changed, 176 insertions(+), 158 deletions(-) create mode 100644 src/cmd.rs diff --git a/Cargo.lock b/Cargo.lock index 5767267e..f9b48bd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,16 +271,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "filetime" version = "0.2.23" @@ -333,15 +323,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "home" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" -dependencies = [ - "windows-sys 0.52.0", -] - [[package]] name = "indexmap" version = "2.2.6" @@ -419,12 +400,6 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" -[[package]] -name = "linux-raw-sys" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" - [[package]] name = "lock_api" version = "0.4.11" @@ -664,19 +639,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" -[[package]] -name = "rustix" -version = "0.38.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" -dependencies = [ - "bitflags 2.5.0", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.52.0", -] - [[package]] name = "rustlings" version = "6.0.0-beta.3" @@ -692,8 +654,8 @@ dependencies = [ "ratatui", "rustlings-macros", "serde", + "serde_json", "toml_edit", - "which", ] [[package]] @@ -752,6 +714,17 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +dependencies = [ + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_spanned" version = "0.6.5" @@ -935,18 +908,6 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" -[[package]] -name = "which" -version = "6.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" -dependencies = [ - "either", - "home", - "rustix", - "winsafe", -] - [[package]] name = "winapi" version = "0.3.9" @@ -1126,12 +1087,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winsafe" -version = "0.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" - [[package]] name = "zerocopy" version = "0.7.32" diff --git a/Cargo.toml b/Cargo.toml index 1bc26c90..a77c84f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,9 +56,9 @@ notify-debouncer-mini = "0.4.1" os_pipe = "1.1.5" ratatui = "0.26.2" rustlings-macros = { path = "rustlings-macros", version = "=6.0.0-beta.3" } +serde_json = "1.0.116" serde.workspace = true toml_edit.workspace = true -which = "6.0.1" [dev-dependencies] assert_cmd = "2.0.14" diff --git a/src/app_state.rs b/src/app_state.rs index 476b5a9c..b980bdba 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -7,7 +7,7 @@ use crossterm::{ use std::{ fs::{self, File}, io::{Read, StdoutLock, Write}, - path::Path, + path::{Path, PathBuf}, process::{Command, Stdio}, }; @@ -39,6 +39,7 @@ pub struct AppState { final_message: String, file_buf: Vec, official_exercises: bool, + target_dir: PathBuf, } impl AppState { @@ -90,6 +91,7 @@ impl AppState { pub fn new( exercise_infos: Vec, final_message: String, + target_dir: PathBuf, ) -> (Self, StateFileStatus) { let exercises = exercise_infos .into_iter() @@ -127,6 +129,7 @@ impl AppState { final_message, file_buf: Vec::with_capacity(2048), official_exercises: !Path::new("info.toml").exists(), + target_dir, }; let state_file_status = slf.update_from_file(); @@ -154,6 +157,11 @@ impl AppState { &self.exercises[self.current_exercise_ind] } + #[inline] + pub fn target_dir(&self) -> &Path { + &self.target_dir + } + pub fn set_current_exercise_ind(&mut self, ind: usize) -> Result<()> { if ind >= self.exercises.len() { bail!(BAD_INDEX_ERR); @@ -313,7 +321,7 @@ impl AppState { write!(writer, "Running {exercise} ... ")?; writer.flush()?; - let success = exercise.run(&mut output)?; + let success = exercise.run(&mut output, &self.target_dir)?; if !success { writeln!(writer, "{}\n", "FAILED".red())?; diff --git a/src/cmd.rs b/src/cmd.rs new file mode 100644 index 00000000..28f21c55 --- /dev/null +++ b/src/cmd.rs @@ -0,0 +1,70 @@ +use anyhow::{Context, Result}; +use std::{io::Read, path::Path, process::Command}; + +pub fn run_cmd(mut cmd: Command, description: &str, output: &mut Vec) -> Result { + let (mut reader, writer) = os_pipe::pipe() + .with_context(|| format!("Failed to create a pipe to run the command `{description}``"))?; + + let writer_clone = writer.try_clone().with_context(|| { + format!("Failed to clone the pipe writer for the command `{description}`") + })?; + + let mut handle = cmd + .stdout(writer_clone) + .stderr(writer) + .spawn() + .with_context(|| format!("Failed to run the command `{description}`"))?; + + // Prevent pipe deadlock. + drop(cmd); + + reader + .read_to_end(output) + .with_context(|| format!("Failed to read the output of the command `{description}`"))?; + + output.push(b'\n'); + + handle + .wait() + .with_context(|| format!("Failed to wait on the command `{description}` to exit")) + .map(|status| status.success()) +} + +pub struct CargoCmd<'a> { + pub subcommand: &'a str, + pub args: &'a [&'a str], + pub exercise_name: &'a str, + pub description: &'a str, + pub hide_warnings: bool, + pub target_dir: &'a Path, + pub output: &'a mut Vec, + pub dev: bool, +} + +impl<'a> CargoCmd<'a> { + pub fn run(&mut self) -> Result { + let mut cmd = Command::new("cargo"); + cmd.arg(self.subcommand); + + // A hack to make `cargo run` work when developing Rustlings. + if self.dev { + cmd.arg("--manifest-path") + .arg("dev/Cargo.toml") + .arg("--target-dir") + .arg(self.target_dir); + } + + cmd.arg("--color") + .arg("always") + .arg("-q") + .arg("--bin") + .arg(self.exercise_name) + .args(self.args); + + if self.hide_warnings { + cmd.env("RUSTFLAGS", "-A warnings"); + } + + run_cmd(cmd, self.description, self.output) + } +} diff --git a/src/exercise.rs b/src/exercise.rs index 50f360e9..23dae6f5 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,57 +1,21 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use crossterm::style::{style, StyledContent, Stylize}; use std::{ fmt::{self, Display, Formatter}, - io::{Read, Write}, - process::{Command, Stdio}, + io::Write, + path::{Path, PathBuf}, + process::Command, }; -use crate::{in_official_repo, terminal_link::TerminalFileLink, DEBUG_PROFILE}; +use crate::{ + cmd::{run_cmd, CargoCmd}, + in_official_repo, + terminal_link::TerminalFileLink, + DEBUG_PROFILE, +}; pub const OUTPUT_CAPACITY: usize = 1 << 14; -fn run_command( - mut cmd: Command, - cmd_description: &str, - output: &mut Vec, - stderr: bool, -) -> Result { - let (mut reader, writer) = os_pipe::pipe().with_context(|| { - format!("Failed to create a pipe to run the command `{cmd_description}``") - })?; - - let (stdout, stderr) = if stderr { - ( - Stdio::from(writer.try_clone().with_context(|| { - format!("Failed to clone the pipe writer for the command `{cmd_description}`") - })?), - Stdio::from(writer), - ) - } else { - (Stdio::from(writer), Stdio::null()) - }; - - let mut handle = cmd - .stdout(stdout) - .stderr(stderr) - .spawn() - .with_context(|| format!("Failed to run the command `{cmd_description}`"))?; - - // Prevent pipe deadlock. - drop(cmd); - - reader - .read_to_end(output) - .with_context(|| format!("Failed to read the output of the command `{cmd_description}`"))?; - - output.push(b'\n'); - - handle - .wait() - .with_context(|| format!("Failed to wait on the command `{cmd_description}` to exit")) - .map(|status| status.success()) -} - pub struct Exercise { pub dir: Option<&'static str>, // Exercise's unique name @@ -66,11 +30,16 @@ pub struct Exercise { } impl Exercise { - fn run_bin(&self, output: &mut Vec) -> Result { + fn run_bin(&self, output: &mut Vec, target_dir: &Path) -> Result { writeln!(output, "{}", "Output".underlined())?; - let bin_path = format!("target/debug/{}", self.name); - let success = run_command(Command::new(&bin_path), &bin_path, output, true)?; + let mut bin_path = + PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + self.name.len()); + bin_path.push(target_dir); + bin_path.push("debug"); + bin_path.push(self.name); + + let success = run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)?; if !success { writeln!( @@ -85,43 +54,23 @@ impl Exercise { Ok(success) } - fn cargo_cmd( - &self, - command: &str, - args: &[&str], - cmd_description: &str, - output: &mut Vec, - dev: bool, - stderr: bool, - ) -> Result { - let mut cmd = Command::new("cargo"); - cmd.arg(command); - - // A hack to make `cargo run` work when developing Rustlings. - if dev { - cmd.arg("--manifest-path") - .arg("dev/Cargo.toml") - .arg("--target-dir") - .arg("target"); - } - - cmd.arg("--color") - .arg("always") - .arg("-q") - .arg("--bin") - .arg(self.name) - .args(args); - - run_command(cmd, cmd_description, output, stderr) - } - - pub fn run(&self, output: &mut Vec) -> Result { + pub fn run(&self, output: &mut Vec, target_dir: &Path) -> Result { output.clear(); // Developing the official Rustlings. let dev = DEBUG_PROFILE && in_official_repo(); - let build_success = self.cargo_cmd("build", &[], "cargo build …", output, dev, true)?; + let build_success = CargoCmd { + subcommand: "build", + args: &[], + exercise_name: self.name, + description: "cargo build …", + hide_warnings: false, + target_dir, + output, + dev, + } + .run()?; if !build_success { return Ok(false); } @@ -134,19 +83,28 @@ impl Exercise { } else { &["--profile", "test"] }; - let clippy_success = - self.cargo_cmd("clippy", clippy_args, "cargo clippy …", output, dev, true)?; + let clippy_success = CargoCmd { + subcommand: "clippy", + args: clippy_args, + exercise_name: self.name, + description: "cargo clippy …", + hide_warnings: false, + target_dir, + output, + dev, + } + .run()?; if !clippy_success { return Ok(false); } if !self.test { - return self.run_bin(output); + return self.run_bin(output, target_dir); } - let test_success = self.cargo_cmd( - "test", - &[ + let test_success = CargoCmd { + subcommand: "test", + args: &[ "--", "--color", "always", @@ -154,14 +112,17 @@ impl Exercise { "--format", "pretty", ], - "cargo test …", + exercise_name: self.name, + description: "cargo test …", + // Hide warnings because they are shown by Clippy. + hide_warnings: true, + target_dir, output, dev, - // Hide warnings because they are shown by Clippy. - false, - )?; + } + .run()?; - let run_success = self.run_bin(output)?; + let run_success = self.run_bin(output, target_dir)?; Ok(test_success && run_success) } diff --git a/src/main.rs b/src/main.rs index 7a142fdb..b03aa520 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,16 +5,18 @@ use crossterm::{ terminal::{Clear, ClearType}, ExecutableCommand, }; +use serde::Deserialize; use std::{ io::{self, BufRead, Write}, - path::Path, - process::exit, + path::{Path, PathBuf}, + process::{exit, Command, Stdio}, }; use self::{app_state::AppState, dev::DevCommands, info_file::InfoFile, watch::WatchExit}; mod app_state; mod cargo_toml; +mod cmd; mod dev; mod embedded; mod exercise; @@ -75,6 +77,11 @@ enum Subcommands { Dev(DevCommands), } +#[derive(Deserialize)] +struct CargoMetadata { + target_directory: PathBuf, +} + fn in_official_repo() -> bool { Path::new("dev/rustlings-repo.txt").exists() } @@ -86,7 +93,20 @@ fn main() -> Result<()> { bail!("{OLD_METHOD_ERR}"); } - which::which("cargo").context(CARGO_NOT_FOUND_ERR)?; + let metadata_output = Command::new("cargo") + .arg("metadata") + .arg("-q") + .arg("--format-version") + .arg("1") + .arg("--no-deps") + .stdin(Stdio::null()) + .stderr(Stdio::inherit()) + .output() + .context(CARGO_METADATA_ERR)? + .stdout; + let target_dir = serde_json::de::from_slice::(&metadata_output) + .context("Failed to read the field `target_directory` from the `cargo metadata` output")? + .target_directory; match args.command { Some(Subcommands::Init) => { @@ -122,6 +142,7 @@ fn main() -> Result<()> { let (mut app_state, state_file_status) = AppState::new( info_file.exercises, info_file.final_message.unwrap_or_default(), + target_dir, ); if let Some(welcome_message) = info_file.welcome_message { @@ -198,7 +219,7 @@ The new method doesn't include cloning the Rustlings' repository. Please follow the instructions in the README: https://github.com/rust-lang/rustlings#getting-started"; -const CARGO_NOT_FOUND_ERR: &str = "Failed to find `cargo`. +const CARGO_METADATA_ERR: &str = "Failed to run the command `cargo metadata …` Did you already install Rust? Try running `cargo --version` to diagnose the problem."; diff --git a/src/run.rs b/src/run.rs index cbc9ad70..9b5ddd34 100644 --- a/src/run.rs +++ b/src/run.rs @@ -11,7 +11,7 @@ use crate::{ pub fn run(app_state: &mut AppState) -> Result<()> { let exercise = app_state.current_exercise(); let mut output = Vec::with_capacity(OUTPUT_CAPACITY); - let success = exercise.run(&mut output)?; + let success = exercise.run(&mut output, app_state.target_dir())?; let mut stdout = io::stdout().lock(); stdout.write_all(&output)?; diff --git a/src/watch/state.rs b/src/watch/state.rs index 40c01bfc..82b745a4 100644 --- a/src/watch/state.rs +++ b/src/watch/state.rs @@ -50,7 +50,10 @@ impl<'a> WatchState<'a> { pub fn run_current_exercise(&mut self) -> Result<()> { self.show_hint = false; - let success = self.app_state.current_exercise().run(&mut self.output)?; + let success = self + .app_state + .current_exercise() + .run(&mut self.output, self.app_state.target_dir())?; if success { self.done_status = if let Some(solution_path) = self.app_state.current_solution_path()? {