mirror of
https://github.com/rust-lang/rustlings.git
synced 2024-12-26 00:00:03 +03:00
Compare commits
9 commits
2440af66e3
...
2a14bfc9c3
Author | SHA1 | Date | |
---|---|---|---|
2a14bfc9c3 | |||
d1ff4b5cf0 | |||
700a065abd | |||
3fc462f90f | |||
65a8f6bb4b | |||
e0f0944bff | |||
c7590dd752 | |||
33a5680328 | |||
455d87cadd |
2
.github/workflows/rust.yml
vendored
2
.github/workflows/rust.yml
vendored
|
@ -24,8 +24,6 @@ jobs:
|
|||
globs: "exercises/**/*.md"
|
||||
- name: Run cargo fmt
|
||||
run: cargo fmt --all --check
|
||||
- name: Run rustfmt on solutions
|
||||
run: rustfmt --check --edition 2021 --color always solutions/**/*.rs
|
||||
test:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
- Run the final check of all exercises in parallel.
|
||||
- Small exercise improvements.
|
||||
- `dev check`: Check that all solutions are formatted with `rustfmt`.
|
||||
|
||||
<a name="6.1.0"></a>
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ authors = [
|
|||
]
|
||||
repository = "https://github.com/rust-lang/rustlings"
|
||||
license = "MIT"
|
||||
edition = "2021"
|
||||
edition = "2021" # On Update: Update the edition of the `rustfmt` command that checks the solutions.
|
||||
|
||||
[workspace.dependencies]
|
||||
serde = { version = "1.0.204", features = ["derive"] }
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
use anyhow::{bail, Context, Error, Result};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
io::{Read, StdoutLock, Write},
|
||||
path::{Path, PathBuf},
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
clear_terminal,
|
||||
cmd::CmdRunner,
|
||||
embedded::EMBEDDED_FILES,
|
||||
exercise::{Exercise, RunnableExercise},
|
||||
info_file::ExerciseInfo,
|
||||
DEBUG_PROFILE,
|
||||
};
|
||||
|
||||
const STATE_FILE_NAME: &str = ".rustlings-state.txt";
|
||||
|
@ -34,31 +33,6 @@ pub enum StateFileStatus {
|
|||
NotRead,
|
||||
}
|
||||
|
||||
// Parses parts of the output of `cargo metadata`.
|
||||
#[derive(Deserialize)]
|
||||
struct CargoMetadata {
|
||||
target_directory: PathBuf,
|
||||
}
|
||||
|
||||
pub fn parse_target_dir() -> Result<PathBuf> {
|
||||
// Get the target directory from Cargo.
|
||||
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;
|
||||
|
||||
serde_json::de::from_slice::<CargoMetadata>(&metadata_output)
|
||||
.context("Failed to read the field `target_directory` from the `cargo metadata` output")
|
||||
.map(|metadata| metadata.target_directory)
|
||||
}
|
||||
|
||||
pub struct AppState {
|
||||
current_exercise_ind: usize,
|
||||
exercises: Vec<Exercise>,
|
||||
|
@ -68,8 +42,7 @@ pub struct AppState {
|
|||
// Preallocated buffer for reading and writing the state file.
|
||||
file_buf: Vec<u8>,
|
||||
official_exercises: bool,
|
||||
// Cargo's target directory.
|
||||
target_dir: PathBuf,
|
||||
cmd_runner: CmdRunner,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
|
@ -123,7 +96,7 @@ impl AppState {
|
|||
exercise_infos: Vec<ExerciseInfo>,
|
||||
final_message: String,
|
||||
) -> Result<(Self, StateFileStatus)> {
|
||||
let target_dir = parse_target_dir()?;
|
||||
let cmd_runner = CmdRunner::build()?;
|
||||
|
||||
let exercises = exercise_infos
|
||||
.into_iter()
|
||||
|
@ -157,7 +130,7 @@ impl AppState {
|
|||
final_message,
|
||||
file_buf: Vec::with_capacity(2048),
|
||||
official_exercises: !Path::new("info.toml").exists(),
|
||||
target_dir,
|
||||
cmd_runner,
|
||||
};
|
||||
|
||||
let state_file_status = slf.update_from_file();
|
||||
|
@ -186,8 +159,8 @@ impl AppState {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn target_dir(&self) -> &Path {
|
||||
&self.target_dir
|
||||
pub fn cmd_runner(&self) -> &CmdRunner {
|
||||
&self.cmd_runner
|
||||
}
|
||||
|
||||
// Write the state file.
|
||||
|
@ -336,7 +309,7 @@ impl AppState {
|
|||
/// Official exercises: Dump the solution file form the binary and return its path.
|
||||
/// Third-party exercises: Check if a solution file exists and return its path in that case.
|
||||
pub fn current_solution_path(&self) -> Result<Option<String>> {
|
||||
if DEBUG_PROFILE {
|
||||
if cfg!(debug_assertions) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
|
@ -386,7 +359,7 @@ impl AppState {
|
|||
.iter_mut()
|
||||
.map(|exercise| {
|
||||
s.spawn(|| {
|
||||
let success = exercise.run_exercise(None, &self.target_dir)?;
|
||||
let success = exercise.run_exercise(None, &self.cmd_runner)?;
|
||||
exercise.done = success;
|
||||
Ok::<_, Error>(success)
|
||||
})
|
||||
|
@ -434,10 +407,6 @@ impl AppState {
|
|||
}
|
||||
}
|
||||
|
||||
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.";
|
||||
|
||||
const RERUNNING_ALL_EXERCISES_MSG: &[u8] = b"
|
||||
All exercises seem to be done.
|
||||
Recompiling and running all exercises to make sure that all of them are actually done.
|
||||
|
@ -490,7 +459,7 @@ mod tests {
|
|||
final_message: String::new(),
|
||||
file_buf: Vec::new(),
|
||||
official_exercises: true,
|
||||
target_dir: PathBuf::new(),
|
||||
cmd_runner: CmdRunner::build().unwrap(),
|
||||
};
|
||||
|
||||
let mut assert = |done: [bool; 3], expected: [Option<usize>; 3]| {
|
||||
|
|
126
src/cmd.rs
126
src/cmd.rs
|
@ -1,13 +1,14 @@
|
|||
use anyhow::{Context, Result};
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
io::Read,
|
||||
path::Path,
|
||||
path::PathBuf,
|
||||
process::{Command, Stdio},
|
||||
};
|
||||
|
||||
/// Run a command with a description for a possible error and append the merged stdout and stderr.
|
||||
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||
pub fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
|
||||
fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
|
||||
let spawn = |mut cmd: Command| {
|
||||
// NOTE: The closure drops `cmd` which prevents a pipe deadlock.
|
||||
cmd.stdin(Stdio::null())
|
||||
|
@ -45,50 +46,107 @@ pub fn run_cmd(mut cmd: Command, description: &str, output: Option<&mut Vec<u8>>
|
|||
.map(|status| status.success())
|
||||
}
|
||||
|
||||
pub struct CargoCmd<'a> {
|
||||
pub subcommand: &'a str,
|
||||
pub args: &'a [&'a str],
|
||||
pub bin_name: &'a str,
|
||||
pub description: &'a str,
|
||||
/// RUSTFLAGS="-A warnings"
|
||||
pub hide_warnings: bool,
|
||||
/// Added as `--target-dir` if `Self::dev` is true.
|
||||
pub target_dir: &'a Path,
|
||||
/// The output buffer to append the merged stdout and stderr.
|
||||
pub output: Option<&'a mut Vec<u8>>,
|
||||
/// true while developing Rustlings.
|
||||
pub dev: bool,
|
||||
// Parses parts of the output of `cargo metadata`.
|
||||
#[derive(Deserialize)]
|
||||
struct CargoMetadata {
|
||||
target_directory: PathBuf,
|
||||
}
|
||||
|
||||
impl<'a> CargoCmd<'a> {
|
||||
/// Run `cargo SUBCOMMAND --bin EXERCISE_NAME … ARGS`.
|
||||
pub fn run(self) -> Result<bool> {
|
||||
pub struct CmdRunner {
|
||||
target_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl CmdRunner {
|
||||
pub fn build() -> Result<Self> {
|
||||
// Get the target directory from Cargo.
|
||||
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::<CargoMetadata>(&metadata_output)
|
||||
.context("Failed to read the field `target_directory` from the `cargo metadata` output")
|
||||
.map(|metadata| metadata.target_directory)?;
|
||||
|
||||
Ok(Self { target_dir })
|
||||
}
|
||||
|
||||
pub fn cargo<'out>(
|
||||
&self,
|
||||
subcommand: &str,
|
||||
bin_name: &str,
|
||||
output: Option<&'out mut Vec<u8>>,
|
||||
) -> CargoSubcommand<'out> {
|
||||
let mut cmd = Command::new("cargo");
|
||||
cmd.arg(self.subcommand);
|
||||
cmd.arg(subcommand).arg("-q").arg("--bin").arg(bin_name);
|
||||
|
||||
// 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);
|
||||
#[cfg(debug_assertions)]
|
||||
cmd.arg("--manifest-path")
|
||||
.arg("dev/Cargo.toml")
|
||||
.arg("--target-dir")
|
||||
.arg(&self.target_dir);
|
||||
|
||||
if output.is_some() {
|
||||
cmd.arg("--color").arg("always");
|
||||
}
|
||||
|
||||
cmd.arg("--color")
|
||||
.arg("always")
|
||||
.arg("-q")
|
||||
.arg("--bin")
|
||||
.arg(self.bin_name)
|
||||
.args(self.args);
|
||||
CargoSubcommand { cmd, output }
|
||||
}
|
||||
|
||||
if self.hide_warnings {
|
||||
cmd.env("RUSTFLAGS", "-A warnings");
|
||||
}
|
||||
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||
pub fn run_debug_bin(&self, bin_name: &str, output: Option<&mut Vec<u8>>) -> Result<bool> {
|
||||
// 7 = "/debug/".len()
|
||||
let mut bin_path =
|
||||
PathBuf::with_capacity(self.target_dir.as_os_str().len() + 7 + bin_name.len());
|
||||
bin_path.push(&self.target_dir);
|
||||
bin_path.push("debug");
|
||||
bin_path.push(bin_name);
|
||||
|
||||
run_cmd(cmd, self.description, self.output)
|
||||
run_cmd(Command::new(&bin_path), &bin_path.to_string_lossy(), output)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CargoSubcommand<'out> {
|
||||
cmd: Command,
|
||||
output: Option<&'out mut Vec<u8>>,
|
||||
}
|
||||
|
||||
impl<'out> CargoSubcommand<'out> {
|
||||
#[inline]
|
||||
pub fn args<'arg, I>(&mut self, args: I) -> &mut Self
|
||||
where
|
||||
I: IntoIterator<Item = &'arg str>,
|
||||
{
|
||||
self.cmd.args(args);
|
||||
self
|
||||
}
|
||||
|
||||
/// RUSTFLAGS="-A warnings"
|
||||
#[inline]
|
||||
pub fn hide_warnings(&mut self) -> &mut Self {
|
||||
self.cmd.env("RUSTFLAGS", "-A warnings");
|
||||
self
|
||||
}
|
||||
|
||||
/// The boolean in the returned `Result` is true if the command's exit status is success.
|
||||
#[inline]
|
||||
pub fn run(self, description: &str) -> Result<bool> {
|
||||
run_cmd(self.cmd, description, self.output)
|
||||
}
|
||||
}
|
||||
|
||||
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.";
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
|
|
@ -2,8 +2,6 @@ use anyhow::{bail, Context, Result};
|
|||
use clap::Subcommand;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::DEBUG_PROFILE;
|
||||
|
||||
mod check;
|
||||
mod new;
|
||||
mod update;
|
||||
|
@ -32,7 +30,7 @@ impl DevCommands {
|
|||
pub fn run(self) -> Result<()> {
|
||||
match self {
|
||||
Self::New { path, no_git } => {
|
||||
if DEBUG_PROFILE {
|
||||
if cfg!(debug_assertions) {
|
||||
bail!("Disabled in the debug build");
|
||||
}
|
||||
|
||||
|
|
164
src/dev/check.rs
164
src/dev/check.rs
|
@ -1,22 +1,20 @@
|
|||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use anyhow::{anyhow, bail, Context, Error, Result};
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fs::{self, read_dir, OpenOptions},
|
||||
io::{self, Read, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{self, AtomicBool},
|
||||
Mutex,
|
||||
},
|
||||
process::{Command, Stdio},
|
||||
sync::atomic::{self, AtomicBool},
|
||||
thread,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app_state::parse_target_dir,
|
||||
cargo_toml::{append_bins, bins_start_end_ind, BINS_BUFFER_CAPACITY},
|
||||
cmd::CmdRunner,
|
||||
exercise::{RunnableExercise, OUTPUT_CAPACITY},
|
||||
info_file::{ExerciseInfo, InfoFile},
|
||||
CURRENT_FORMAT_VERSION, DEBUG_PROFILE,
|
||||
CURRENT_FORMAT_VERSION,
|
||||
};
|
||||
|
||||
// Find a char that isn't allowed in the exercise's `name` or `dir`.
|
||||
|
@ -27,18 +25,21 @@ fn forbidden_char(input: &str) -> Option<char> {
|
|||
// Check that the Cargo.toml file is up-to-date.
|
||||
fn check_cargo_toml(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
current_cargo_toml: &str,
|
||||
cargo_toml_path: &str,
|
||||
exercise_path_prefix: &[u8],
|
||||
) -> Result<()> {
|
||||
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(current_cargo_toml)?;
|
||||
let current_cargo_toml = fs::read_to_string(cargo_toml_path)
|
||||
.with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?;
|
||||
|
||||
let (bins_start_ind, bins_end_ind) = bins_start_end_ind(¤t_cargo_toml)?;
|
||||
|
||||
let old_bins = ¤t_cargo_toml.as_bytes()[bins_start_ind..bins_end_ind];
|
||||
let mut new_bins = Vec::with_capacity(BINS_BUFFER_CAPACITY);
|
||||
append_bins(&mut new_bins, exercise_infos, exercise_path_prefix);
|
||||
|
||||
if old_bins != new_bins {
|
||||
if DEBUG_PROFILE {
|
||||
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it");
|
||||
if cfg!(debug_assertions) {
|
||||
bail!("The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it. Then run `cargo run -- dev check` again");
|
||||
}
|
||||
|
||||
bail!("The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again");
|
||||
|
@ -162,7 +163,7 @@ fn check_unexpected_files(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
|
||||
fn check_exercises_unsolved(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
|
||||
let error_occurred = AtomicBool::new(false);
|
||||
|
||||
println!(
|
||||
|
@ -184,7 +185,7 @@ fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<(
|
|||
error_occurred.store(true, atomic::Ordering::Relaxed);
|
||||
};
|
||||
|
||||
match exercise_info.run_exercise(None, target_dir) {
|
||||
match exercise_info.run_exercise(None, cmd_runner) {
|
||||
Ok(true) => error(b"Already solved!"),
|
||||
Ok(false) => (),
|
||||
Err(e) => error(e.to_string().as_bytes()),
|
||||
|
@ -200,7 +201,7 @@ fn check_exercises_unsolved(info_file: &InfoFile, target_dir: &Path) -> Result<(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
|
||||
fn check_exercises(info_file: &InfoFile, cmd_runner: &CmdRunner) -> Result<()> {
|
||||
match info_file.format_version.cmp(&CURRENT_FORMAT_VERSION) {
|
||||
Ordering::Less => bail!("`format_version` < {CURRENT_FORMAT_VERSION} (supported version)\nPlease migrate to the latest format version"),
|
||||
Ordering::Greater => bail!("`format_version` > {CURRENT_FORMAT_VERSION} (supported version)\nTry updating the Rustlings program"),
|
||||
|
@ -210,80 +211,113 @@ fn check_exercises(info_file: &InfoFile, target_dir: &Path) -> Result<()> {
|
|||
let info_file_paths = check_info_file_exercises(info_file)?;
|
||||
check_unexpected_files("exercises", &info_file_paths)?;
|
||||
|
||||
check_exercises_unsolved(info_file, target_dir)
|
||||
check_exercises_unsolved(info_file, cmd_runner)
|
||||
}
|
||||
|
||||
fn check_solutions(require_solutions: bool, info_file: &InfoFile, target_dir: &Path) -> Result<()> {
|
||||
let paths = Mutex::new(hashbrown::HashSet::with_capacity(info_file.exercises.len()));
|
||||
let error_occurred = AtomicBool::new(false);
|
||||
enum SolutionCheck {
|
||||
Success { sol_path: String },
|
||||
MissingRequired,
|
||||
MissingOptional,
|
||||
RunFailure { output: Vec<u8> },
|
||||
Err(Error),
|
||||
}
|
||||
|
||||
fn check_solutions(
|
||||
require_solutions: bool,
|
||||
info_file: &InfoFile,
|
||||
cmd_runner: &CmdRunner,
|
||||
) -> Result<()> {
|
||||
println!("Running all solutions. This may take a while…\n");
|
||||
thread::scope(|s| {
|
||||
for exercise_info in &info_file.exercises {
|
||||
s.spawn(|| {
|
||||
let error = |e| {
|
||||
let mut stderr = io::stderr().lock();
|
||||
stderr.write_all(e).unwrap();
|
||||
stderr
|
||||
.write_all(b"\nFailed to run the solution of the exercise ")
|
||||
.unwrap();
|
||||
stderr.write_all(exercise_info.name.as_bytes()).unwrap();
|
||||
stderr.write_all(SEPARATOR).unwrap();
|
||||
error_occurred.store(true, atomic::Ordering::Relaxed);
|
||||
};
|
||||
let handles = info_file
|
||||
.exercises
|
||||
.iter()
|
||||
.map(|exercise_info| {
|
||||
s.spawn(|| {
|
||||
let sol_path = exercise_info.sol_path();
|
||||
if !Path::new(&sol_path).exists() {
|
||||
if require_solutions {
|
||||
return SolutionCheck::MissingRequired;
|
||||
}
|
||||
|
||||
let path = exercise_info.sol_path();
|
||||
if !Path::new(&path).exists() {
|
||||
if require_solutions {
|
||||
error(b"Solution missing");
|
||||
return SolutionCheck::MissingOptional;
|
||||
}
|
||||
|
||||
// No solution to check.
|
||||
return;
|
||||
}
|
||||
|
||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||
match exercise_info.run_solution(Some(&mut output), target_dir) {
|
||||
Ok(true) => {
|
||||
paths.lock().unwrap().insert(PathBuf::from(path));
|
||||
let mut output = Vec::with_capacity(OUTPUT_CAPACITY);
|
||||
match exercise_info.run_solution(Some(&mut output), cmd_runner) {
|
||||
Ok(true) => SolutionCheck::Success { sol_path },
|
||||
Ok(false) => SolutionCheck::RunFailure { output },
|
||||
Err(e) => SolutionCheck::Err(e),
|
||||
}
|
||||
Ok(false) => error(&output),
|
||||
Err(e) => error(e.to_string().as_bytes()),
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut sol_paths = hashbrown::HashSet::with_capacity(info_file.exercises.len());
|
||||
let mut fmt_cmd = Command::new("rustfmt");
|
||||
fmt_cmd
|
||||
.arg("--check")
|
||||
.arg("--edition")
|
||||
.arg("2021")
|
||||
.arg("--color")
|
||||
.arg("always")
|
||||
.stdin(Stdio::null());
|
||||
|
||||
for (exercise_name, handle) in info_file
|
||||
.exercises
|
||||
.iter()
|
||||
.map(|exercise_info| &exercise_info.name)
|
||||
.zip(handles)
|
||||
{
|
||||
match handle.join() {
|
||||
Ok(SolutionCheck::Success { sol_path }) => {
|
||||
fmt_cmd.arg(&sol_path);
|
||||
sol_paths.insert(PathBuf::from(sol_path));
|
||||
}
|
||||
});
|
||||
Ok(SolutionCheck::MissingRequired) => {
|
||||
bail!("The solution of the exercise {exercise_name} is missing");
|
||||
}
|
||||
Ok(SolutionCheck::MissingOptional) => (),
|
||||
Ok(SolutionCheck::RunFailure { output }) => {
|
||||
io::stderr().lock().write_all(&output)?;
|
||||
bail!("Running the solution of the exercise {exercise_name} failed with the error above");
|
||||
}
|
||||
Ok(SolutionCheck::Err(e)) => return Err(e),
|
||||
Err(_) => {
|
||||
bail!("Panic while trying to run the solution of the exericse {exercise_name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if error_occurred.load(atomic::Ordering::Relaxed) {
|
||||
bail!("At least one solution failed. See the output above.");
|
||||
}
|
||||
let handle = s.spawn(move || check_unexpected_files("solutions", &sol_paths));
|
||||
|
||||
check_unexpected_files("solutions", &paths.into_inner().unwrap())?;
|
||||
if !fmt_cmd
|
||||
.status()
|
||||
.context("Failed to run `rustfmt` on all solution files")?
|
||||
.success()
|
||||
{
|
||||
bail!("Some solutions aren't formatted. Run `rustfmt` on them");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
handle.join().unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn check(require_solutions: bool) -> Result<()> {
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
// A hack to make `cargo run -- dev check` work when developing Rustlings.
|
||||
if DEBUG_PROFILE {
|
||||
check_cargo_toml(
|
||||
&info_file.exercises,
|
||||
include_str!("../../dev-Cargo.toml"),
|
||||
b"../",
|
||||
)?;
|
||||
if cfg!(debug_assertions) {
|
||||
// A hack to make `cargo run -- dev check` work when developing Rustlings.
|
||||
check_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")?;
|
||||
} else {
|
||||
let current_cargo_toml =
|
||||
fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?;
|
||||
check_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"")?;
|
||||
check_cargo_toml(&info_file.exercises, "Cargo.toml", b"")?;
|
||||
}
|
||||
|
||||
let target_dir = parse_target_dir()?;
|
||||
check_exercises(&info_file, &target_dir)?;
|
||||
check_solutions(require_solutions, &info_file, &target_dir)?;
|
||||
let cmd_runner = CmdRunner::build()?;
|
||||
check_exercises(&info_file, &cmd_runner)?;
|
||||
check_solutions(require_solutions, &info_file, &cmd_runner)?;
|
||||
|
||||
println!("\nEverything looks fine!");
|
||||
println!("Everything looks fine!");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,18 +4,19 @@ use std::fs;
|
|||
use crate::{
|
||||
cargo_toml::updated_cargo_toml,
|
||||
info_file::{ExerciseInfo, InfoFile},
|
||||
DEBUG_PROFILE,
|
||||
};
|
||||
|
||||
// Update the `Cargo.toml` file.
|
||||
fn update_cargo_toml(
|
||||
exercise_infos: &[ExerciseInfo],
|
||||
current_cargo_toml: &str,
|
||||
exercise_path_prefix: &[u8],
|
||||
cargo_toml_path: &str,
|
||||
exercise_path_prefix: &[u8],
|
||||
) -> Result<()> {
|
||||
let current_cargo_toml = fs::read_to_string(cargo_toml_path)
|
||||
.with_context(|| format!("Failed to read the file `{cargo_toml_path}`"))?;
|
||||
|
||||
let updated_cargo_toml =
|
||||
updated_cargo_toml(exercise_infos, current_cargo_toml, exercise_path_prefix)?;
|
||||
updated_cargo_toml(exercise_infos, ¤t_cargo_toml, exercise_path_prefix)?;
|
||||
|
||||
fs::write(cargo_toml_path, updated_cargo_toml)
|
||||
.context("Failed to write the `Cargo.toml` file")?;
|
||||
|
@ -26,21 +27,14 @@ fn update_cargo_toml(
|
|||
pub fn update() -> Result<()> {
|
||||
let info_file = InfoFile::parse()?;
|
||||
|
||||
// A hack to make `cargo run -- dev update` work when developing Rustlings.
|
||||
if DEBUG_PROFILE {
|
||||
update_cargo_toml(
|
||||
&info_file.exercises,
|
||||
include_str!("../../dev-Cargo.toml"),
|
||||
b"../",
|
||||
"dev/Cargo.toml",
|
||||
)
|
||||
.context("Failed to update the file `dev/Cargo.toml`")?;
|
||||
if cfg!(debug_assertions) {
|
||||
// A hack to make `cargo run -- dev update` work when developing Rustlings.
|
||||
update_cargo_toml(&info_file.exercises, "dev/Cargo.toml", b"../")
|
||||
.context("Failed to update the file `dev/Cargo.toml`")?;
|
||||
|
||||
println!("Updated `dev/Cargo.toml`");
|
||||
} else {
|
||||
let current_cargo_toml =
|
||||
fs::read_to_string("Cargo.toml").context("Failed to read the file `Cargo.toml`")?;
|
||||
update_cargo_toml(&info_file.exercises, ¤t_cargo_toml, b"", "Cargo.toml")
|
||||
update_cargo_toml(&info_file.exercises, "Cargo.toml", &[])
|
||||
.context("Failed to update the file `Cargo.toml`")?;
|
||||
|
||||
println!("Updated `Cargo.toml`");
|
||||
|
|
105
src/exercise.rs
105
src/exercise.rs
|
@ -3,38 +3,25 @@ use ratatui::crossterm::style::{style, StyledContent, Stylize};
|
|||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cmd::{run_cmd, CargoCmd},
|
||||
in_official_repo,
|
||||
terminal_link::TerminalFileLink,
|
||||
DEBUG_PROFILE,
|
||||
};
|
||||
use crate::{cmd::CmdRunner, terminal_link::TerminalFileLink};
|
||||
|
||||
/// The initial capacity of the output buffer.
|
||||
pub const OUTPUT_CAPACITY: usize = 1 << 14;
|
||||
|
||||
// Run an exercise binary and append its output to the `output` buffer.
|
||||
// Compilation must be done before calling this method.
|
||||
fn run_bin(bin_name: &str, mut output: Option<&mut Vec<u8>>, target_dir: &Path) -> Result<bool> {
|
||||
fn run_bin(
|
||||
bin_name: &str,
|
||||
mut output: Option<&mut Vec<u8>>,
|
||||
cmd_runner: &CmdRunner,
|
||||
) -> Result<bool> {
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
writeln!(output, "{}", "Output".underlined())?;
|
||||
}
|
||||
|
||||
// 7 = "/debug/".len()
|
||||
let mut bin_path = PathBuf::with_capacity(target_dir.as_os_str().len() + 7 + bin_name.len());
|
||||
bin_path.push(target_dir);
|
||||
bin_path.push("debug");
|
||||
bin_path.push(bin_name);
|
||||
|
||||
let success = run_cmd(
|
||||
Command::new(&bin_path),
|
||||
&bin_path.to_string_lossy(),
|
||||
output.as_deref_mut(),
|
||||
)?;
|
||||
let success = cmd_runner.run_debug_bin(bin_name, output.as_deref_mut())?;
|
||||
|
||||
if let Some(output) = output {
|
||||
if !success {
|
||||
|
@ -89,26 +76,20 @@ pub trait RunnableExercise {
|
|||
&self,
|
||||
bin_name: &str,
|
||||
mut output: Option<&mut Vec<u8>>,
|
||||
target_dir: &Path,
|
||||
cmd_runner: &CmdRunner,
|
||||
) -> Result<bool> {
|
||||
if let Some(output) = output.as_deref_mut() {
|
||||
let output_is_none = if let Some(output) = output.as_deref_mut() {
|
||||
output.clear();
|
||||
}
|
||||
false
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
// Developing the official Rustlings.
|
||||
let dev = DEBUG_PROFILE && in_official_repo();
|
||||
|
||||
let build_success = CargoCmd {
|
||||
subcommand: "build",
|
||||
args: &[],
|
||||
bin_name,
|
||||
description: "cargo build …",
|
||||
hide_warnings: false,
|
||||
target_dir,
|
||||
output: output.as_deref_mut(),
|
||||
dev,
|
||||
let mut build_cmd = cmd_runner.cargo("build", bin_name, output.as_deref_mut());
|
||||
if output_is_none {
|
||||
build_cmd.hide_warnings();
|
||||
}
|
||||
.run()?;
|
||||
let build_success = build_cmd.run("cargo build …")?;
|
||||
if !build_success {
|
||||
return Ok(false);
|
||||
}
|
||||
|
@ -118,45 +99,33 @@ pub trait RunnableExercise {
|
|||
output.clear();
|
||||
}
|
||||
|
||||
let mut clippy_cmd = cmd_runner.cargo("clippy", bin_name, output.as_deref_mut());
|
||||
|
||||
// `--profile test` is required to also check code with `[cfg(test)]`.
|
||||
let clippy_args: &[&str] = if self.strict_clippy() {
|
||||
&["--profile", "test", "--", "-D", "warnings"]
|
||||
if self.strict_clippy() {
|
||||
clippy_cmd.args(["--profile", "test", "--", "-D", "warnings"]);
|
||||
} else {
|
||||
&["--profile", "test"]
|
||||
};
|
||||
let clippy_success = CargoCmd {
|
||||
subcommand: "clippy",
|
||||
args: clippy_args,
|
||||
bin_name,
|
||||
description: "cargo clippy …",
|
||||
hide_warnings: false,
|
||||
target_dir,
|
||||
output: output.as_deref_mut(),
|
||||
dev,
|
||||
clippy_cmd.args(["--profile", "test"]);
|
||||
}
|
||||
.run()?;
|
||||
|
||||
let clippy_success = clippy_cmd.run("cargo clippy …")?;
|
||||
if !clippy_success {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !self.test() {
|
||||
return run_bin(bin_name, output.as_deref_mut(), target_dir);
|
||||
return run_bin(bin_name, output.as_deref_mut(), cmd_runner);
|
||||
}
|
||||
|
||||
let test_success = CargoCmd {
|
||||
subcommand: "test",
|
||||
args: &["--", "--color", "always", "--show-output"],
|
||||
bin_name,
|
||||
description: "cargo test …",
|
||||
// Hide warnings because they are shown by Clippy.
|
||||
hide_warnings: true,
|
||||
target_dir,
|
||||
output: output.as_deref_mut(),
|
||||
dev,
|
||||
let mut test_cmd = cmd_runner.cargo("test", bin_name, output.as_deref_mut());
|
||||
if !output_is_none {
|
||||
test_cmd.args(["--", "--color", "always", "--show-output"]);
|
||||
}
|
||||
.run()?;
|
||||
// Hide warnings because they are shown by Clippy.
|
||||
test_cmd.hide_warnings();
|
||||
let test_success = test_cmd.run("cargo test …")?;
|
||||
|
||||
let run_success = run_bin(bin_name, output.as_deref_mut(), target_dir)?;
|
||||
let run_success = run_bin(bin_name, output, cmd_runner)?;
|
||||
|
||||
Ok(test_success && run_success)
|
||||
}
|
||||
|
@ -164,19 +133,19 @@ pub trait RunnableExercise {
|
|||
/// Compile, check and run the exercise.
|
||||
/// The output is written to the `output` buffer after clearing it.
|
||||
#[inline]
|
||||
fn run_exercise(&self, output: Option<&mut Vec<u8>>, target_dir: &Path) -> Result<bool> {
|
||||
self.run(self.name(), output, target_dir)
|
||||
fn run_exercise(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
|
||||
self.run(self.name(), output, cmd_runner)
|
||||
}
|
||||
|
||||
/// Compile, check and run the exercise's solution.
|
||||
/// The output is written to the `output` buffer after clearing it.
|
||||
fn run_solution(&self, output: Option<&mut Vec<u8>>, target_dir: &Path) -> Result<bool> {
|
||||
fn run_solution(&self, output: Option<&mut Vec<u8>>, cmd_runner: &CmdRunner) -> Result<bool> {
|
||||
let name = self.name();
|
||||
let mut bin_name = String::with_capacity(name.len());
|
||||
let mut bin_name = String::with_capacity(name.len() + 4);
|
||||
bin_name.push_str(name);
|
||||
bin_name.push_str("_sol");
|
||||
|
||||
self.run(&bin_name, output, target_dir)
|
||||
self.run(&bin_name, output, cmd_runner)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
18
src/main.rs
18
src/main.rs
|
@ -24,22 +24,6 @@ mod terminal_link;
|
|||
mod watch;
|
||||
|
||||
const CURRENT_FORMAT_VERSION: u8 = 1;
|
||||
const DEBUG_PROFILE: bool = {
|
||||
#[allow(unused_assignments, unused_mut)]
|
||||
let mut debug_profile = false;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
debug_profile = true;
|
||||
}
|
||||
|
||||
debug_profile
|
||||
};
|
||||
|
||||
// The current directory is the official Rustligns repository.
|
||||
fn in_official_repo() -> bool {
|
||||
Path::new("dev/rustlings-repo.txt").exists()
|
||||
}
|
||||
|
||||
fn clear_terminal(stdout: &mut StdoutLock) -> io::Result<()> {
|
||||
stdout.write_all(b"\x1b[H\x1b[2J\x1b[3J")
|
||||
|
@ -89,7 +73,7 @@ enum Subcommands {
|
|||
fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
if !DEBUG_PROFILE && in_official_repo() {
|
||||
if cfg!(not(debug_assertions)) && Path::new("dev/rustlings-repo.txt").exists() {
|
||||
bail!("{OLD_METHOD_ERR}");
|
||||
}
|
||||
|
||||
|
|
|
@ -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_exercise(Some(&mut output), app_state.target_dir())?;
|
||||
let success = exercise.run_exercise(Some(&mut output), app_state.cmd_runner())?;
|
||||
|
||||
let mut stdout = io::stdout().lock();
|
||||
stdout.write_all(&output)?;
|
||||
|
|
|
@ -54,7 +54,7 @@ impl<'a> WatchState<'a> {
|
|||
let success = self
|
||||
.app_state
|
||||
.current_exercise()
|
||||
.run_exercise(Some(&mut self.output), self.app_state.target_dir())?;
|
||||
.run_exercise(Some(&mut self.output), self.app_state.cmd_runner())?;
|
||||
if success {
|
||||
self.done_status =
|
||||
if let Some(solution_path) = self.app_state.current_solution_path()? {
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
bin = [
|
||||
{ name = "compilation_success", path = "exercises/compilation_success.rs" },
|
||||
{ name = "compilation_failure", path = "exercises/compilation_failure.rs" },
|
||||
{ name = "test_success", path = "exercises/test_success.rs" },
|
||||
{ name = "test_failure", path = "exercises/test_failure.rs" },
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "test_exercises"
|
||||
edition = "2021"
|
||||
publish = false
|
11
tests/test_exercises/dev/Cargo.toml
Normal file
11
tests/test_exercises/dev/Cargo.toml
Normal file
|
@ -0,0 +1,11 @@
|
|||
bin = [
|
||||
{ name = "compilation_success", path = "../exercises/compilation_success.rs" },
|
||||
{ name = "compilation_failure", path = "../exercises/compilation_failure.rs" },
|
||||
{ name = "test_success", path = "../exercises/test_success.rs" },
|
||||
{ name = "test_failure", path = "../exercises/test_failure.rs" },
|
||||
]
|
||||
|
||||
[package]
|
||||
name = "test_exercises"
|
||||
edition = "2021"
|
||||
publish = false
|
Loading…
Reference in a new issue