Use os_pipe

This commit is contained in:
mo8it 2024-04-25 01:56:01 +02:00
parent d8c2ab8349
commit 67fa017742
7 changed files with 135 additions and 43 deletions

11
Cargo.lock generated
View file

@ -519,6 +519,16 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "os_pipe"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
version = "0.12.1" version = "0.12.1"
@ -677,6 +687,7 @@ dependencies = [
"crossterm", "crossterm",
"hashbrown", "hashbrown",
"notify-debouncer-mini", "notify-debouncer-mini",
"os_pipe",
"predicates", "predicates",
"ratatui", "ratatui",
"rustlings-macros", "rustlings-macros",

View file

@ -50,6 +50,7 @@ clap = { version = "4.5.4", features = ["derive"] }
crossterm = "0.27.0" crossterm = "0.27.0"
hashbrown = "0.14.3" hashbrown = "0.14.3"
notify-debouncer-mini = "0.4.1" notify-debouncer-mini = "0.4.1"
os_pipe = "1.1.5"
ratatui = "0.26.2" ratatui = "0.26.2"
rustlings-macros = { path = "rustlings-macros", version = "6.0.0-beta.0" } rustlings-macros = { path = "rustlings-macros", version = "6.0.0-beta.0" }
serde.workspace = true serde.workspace = true

View file

@ -11,7 +11,12 @@ use std::{
process::{Command, Stdio}, process::{Command, Stdio},
}; };
use crate::{embedded::EMBEDDED_FILES, exercise::Exercise, info_file::ExerciseInfo, DEBUG_PROFILE}; use crate::{
embedded::EMBEDDED_FILES,
exercise::{Exercise, OUTPUT_CAPACITY},
info_file::ExerciseInfo,
DEBUG_PROFILE,
};
const STATE_FILE_NAME: &str = ".rustlings-state.txt"; const STATE_FILE_NAME: &str = ".rustlings-state.txt";
const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises"; const BAD_INDEX_ERR: &str = "The current exercise index is higher than the number of exercises";
@ -302,11 +307,13 @@ impl AppState {
let Some(ind) = self.next_pending_exercise_ind() else { let Some(ind) = self.next_pending_exercise_ind() else {
writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?; writer.write_all(RERUNNING_ALL_EXERCISES_MSG)?;
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
for (exercise_ind, exercise) in self.exercises().iter().enumerate() { for (exercise_ind, exercise) in self.exercises().iter().enumerate() {
writer.write_fmt(format_args!("Running {exercise} ... "))?; writer.write_fmt(format_args!("Running {exercise} ... "))?;
writer.flush()?; writer.flush()?;
if !exercise.run()?.status.success() { let success = exercise.run(&mut output)?;
if !success {
writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?; writer.write_fmt(format_args!("{}\n\n", "FAILED".red()))?;
self.current_exercise_ind = exercise_ind; self.current_exercise_ind = exercise_ind;
@ -322,6 +329,8 @@ impl AppState {
} }
writer.write_fmt(format_args!("{}\n", "ok".green()))?; writer.write_fmt(format_args!("{}\n", "ok".green()))?;
output.clear();
} }
writer.execute(Clear(ClearType::All))?; writer.execute(Clear(ClearType::All))?;

View file

@ -2,11 +2,42 @@ use anyhow::{Context, Result};
use crossterm::style::{style, StyledContent, Stylize}; use crossterm::style::{style, StyledContent, Stylize};
use std::{ use std::{
fmt::{self, Display, Formatter}, fmt::{self, Display, Formatter},
path::Path, io::{Read, Write},
process::{Command, Output}, process::Command,
}; };
use crate::{info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE}; use crate::{in_official_repo, info_file::Mode, terminal_link::TerminalFileLink, DEBUG_PROFILE};
// TODO
pub const OUTPUT_CAPACITY: usize = 1 << 12;
fn run_command(mut cmd: Command, cmd_description: &str, output: &mut Vec<u8>) -> Result<bool> {
let (mut reader, writer) = os_pipe::pipe().with_context(|| {
format!("Failed to create a pipe to run the command `{cmd_description}``")
})?;
let mut handle = cmd
.stdout(writer.try_clone().with_context(|| {
format!("Failed to clone the pipe writer for the command `{cmd_description}`")
})?)
.stderr(writer)
.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 struct Exercise {
pub dir: Option<&'static str>, pub dir: Option<&'static str>,
@ -22,13 +53,30 @@ pub struct Exercise {
} }
impl Exercise { impl Exercise {
fn cargo_cmd(&self, command: &str, args: &[&str]) -> Result<Output> { fn run_bin(&self, output: &mut Vec<u8>) -> Result<bool> {
writeln!(output, "{}", "Output".bold().magenta().underlined())?;
let bin_path = format!("target/debug/{}", self.name);
run_command(Command::new(&bin_path), &bin_path, output)
}
fn cargo_cmd(
&self,
command: &str,
args: &[&str],
cmd_description: &str,
output: &mut Vec<u8>,
dev: bool,
) -> Result<bool> {
let mut cmd = Command::new("cargo"); let mut cmd = Command::new("cargo");
cmd.arg(command); cmd.arg(command);
// A hack to make `cargo run` work when developing Rustlings. // A hack to make `cargo run` work when developing Rustlings.
if DEBUG_PROFILE && Path::new("tests").exists() { if dev {
cmd.arg("--manifest-path").arg("dev/Cargo.toml"); cmd.arg("--manifest-path")
.arg("dev/Cargo.toml")
.arg("--target-dir")
.arg("target");
} }
cmd.arg("--color") cmd.arg("--color")
@ -36,15 +84,43 @@ impl Exercise {
.arg("-q") .arg("-q")
.arg("--bin") .arg("--bin")
.arg(self.name) .arg(self.name)
.args(args) .args(args);
.output()
.context("Failed to run Cargo") run_command(cmd, cmd_description, output)
}
fn cargo_cmd_with_bin_output(
&self,
command: &str,
args: &[&str],
cmd_description: &str,
output: &mut Vec<u8>,
dev: bool,
) -> Result<bool> {
// Discard the output of `cargo build` because it will be shown again by the Cargo command.
output.clear();
let cargo_cmd_success = self.cargo_cmd(command, args, cmd_description, output, dev)?;
let run_success = self.run_bin(output)?;
Ok(cargo_cmd_success && run_success)
}
pub fn run(&self, output: &mut Vec<u8>) -> Result<bool> {
output.clear();
// Developing the official Rustlings.
let dev = DEBUG_PROFILE && in_official_repo();
let build_success = self.cargo_cmd("build", &[], "cargo build …", output, dev)?;
if !build_success {
return Ok(false);
} }
pub fn run(&self) -> Result<Output> {
match self.mode { match self.mode {
Mode::Run => self.cargo_cmd("run", &[]), Mode::Run => self.run_bin(output),
Mode::Test => self.cargo_cmd( Mode::Test => self.cargo_cmd_with_bin_output(
"test", "test",
&[ &[
"--", "--",
@ -54,10 +130,16 @@ impl Exercise {
"--format", "--format",
"pretty", "pretty",
], ],
"cargo test …",
output,
dev,
), ),
Mode::Clippy => self.cargo_cmd( Mode::Clippy => self.cargo_cmd_with_bin_output(
"clippy", "clippy",
&["--", "-D", "warnings", "-D", "clippy::float_cmp"], &["--", "-D", "warnings"],
"cargo clippy …",
output,
dev,
), ),
} }
} }

View file

@ -75,10 +75,14 @@ enum Subcommands {
Dev(DevCommands), Dev(DevCommands),
} }
fn in_official_repo() -> bool {
Path::new("dev/rustlings-repo.txt").exists()
}
fn main() -> Result<()> { fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
if !DEBUG_PROFILE && Path::new("dev/rustlings-repo.txt").exists() { if !DEBUG_PROFILE && in_official_repo() {
bail!("{OLD_METHOD_ERR}"); bail!("{OLD_METHOD_ERR}");
} }

View file

@ -4,20 +4,19 @@ use std::io::{self, Write};
use crate::{ use crate::{
app_state::{AppState, ExercisesProgress}, app_state::{AppState, ExercisesProgress},
exercise::OUTPUT_CAPACITY,
terminal_link::TerminalFileLink, terminal_link::TerminalFileLink,
}; };
pub fn run(app_state: &mut AppState) -> Result<()> { pub fn run(app_state: &mut AppState) -> Result<()> {
let exercise = app_state.current_exercise(); let exercise = app_state.current_exercise();
let output = exercise.run()?; let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
let success = exercise.run(&mut output)?;
let mut stdout = io::stdout().lock(); let mut stdout = io::stdout().lock();
stdout.write_all(&output.stdout)?; stdout.write_all(&output)?;
stdout.write_all(b"\n")?;
stdout.write_all(&output.stderr)?;
stdout.flush()?;
if !output.status.success() { if !success {
app_state.set_pending(app_state.current_exercise_ind())?; app_state.set_pending(app_state.current_exercise_ind())?;
bail!( bail!(

View file

@ -8,6 +8,7 @@ use std::io::{self, StdoutLock, Write};
use crate::{ use crate::{
app_state::{AppState, ExercisesProgress}, app_state::{AppState, ExercisesProgress},
exercise::OUTPUT_CAPACITY,
progress_bar::progress_bar, progress_bar::progress_bar,
terminal_link::TerminalFileLink, terminal_link::TerminalFileLink,
}; };
@ -21,8 +22,7 @@ enum DoneStatus {
pub struct WatchState<'a> { pub struct WatchState<'a> {
writer: StdoutLock<'a>, writer: StdoutLock<'a>,
app_state: &'a mut AppState, app_state: &'a mut AppState,
stdout: Option<Vec<u8>>, output: Vec<u8>,
stderr: Option<Vec<u8>>,
show_hint: bool, show_hint: bool,
done_status: DoneStatus, done_status: DoneStatus,
manual_run: bool, manual_run: bool,
@ -35,8 +35,7 @@ impl<'a> WatchState<'a> {
Self { Self {
writer, writer,
app_state, app_state,
stdout: None, output: Vec::with_capacity(OUTPUT_CAPACITY),
stderr: None,
show_hint: false, show_hint: false,
done_status: DoneStatus::Pending, done_status: DoneStatus::Pending,
manual_run, manual_run,
@ -51,11 +50,8 @@ impl<'a> WatchState<'a> {
pub fn run_current_exercise(&mut self) -> Result<()> { pub fn run_current_exercise(&mut self) -> Result<()> {
self.show_hint = false; self.show_hint = false;
let output = self.app_state.current_exercise().run()?; let success = self.app_state.current_exercise().run(&mut self.output)?;
self.stdout = Some(output.stdout); if success {
if output.status.success() {
self.stderr = None;
self.done_status = self.done_status =
if let Some(solution_path) = self.app_state.current_solution_path()? { if let Some(solution_path) = self.app_state.current_solution_path()? {
DoneStatus::DoneWithSolution(solution_path) DoneStatus::DoneWithSolution(solution_path)
@ -66,7 +62,6 @@ impl<'a> WatchState<'a> {
self.app_state self.app_state
.set_pending(self.app_state.current_exercise_ind())?; .set_pending(self.app_state.current_exercise_ind())?;
self.stderr = Some(output.stderr);
self.done_status = DoneStatus::Pending; self.done_status = DoneStatus::Pending;
} }
@ -116,16 +111,7 @@ impl<'a> WatchState<'a> {
self.writer.execute(Clear(ClearType::All))?; self.writer.execute(Clear(ClearType::All))?;
if let Some(stdout) = &self.stdout { self.writer.write_all(&self.output)?;
self.writer.write_all(stdout)?;
self.writer.write_all(b"\n")?;
}
if let Some(stderr) = &self.stderr {
self.writer.write_all(stderr)?;
self.writer.write_all(b"\n")?;
}
self.writer.write_all(b"\n")?; self.writer.write_all(b"\n")?;
if self.show_hint { if self.show_hint {