diff --git a/src/app_state.rs b/src/app_state.rs index 541534a2..c29fe07f 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -81,9 +81,11 @@ impl AppState { })?; let dir_canonical_path = term::canonicalize("exercises"); + let official_exercises = !Path::new("info.toml").exists(); let mut exercises = exercise_infos .into_iter() - .map(|exercise_info| { + .enumerate() + .map(|(i, exercise_info)| { let canonical_path = dir_canonical_path.as_deref().map(|dir_canonical_path| { let mut canonical_path; if let Some(dir) = exercise_info.dir { @@ -105,10 +107,16 @@ impl AppState { canonical_path.push_str(".rs"); canonical_path }); + let embedded_input_files = if official_exercises { + EMBEDDED_FILES.exercise_files[i].input_files + } else { + &[] + }; Exercise { name: exercise_info.name, dir: exercise_info.dir, + embedded_input_files, // LEAKING: For `Editor::open`. The app state is used until the end of the program. path: exercise_info.path().leak(), canonical_path, @@ -173,7 +181,7 @@ impl AppState { final_message, state_file, file_buf, - official_exercises: !Path::new("info.toml").exists(), + official_exercises, cmd_runner, // VS Code has its own file link handling emit_file_links: !vs_code_term, @@ -597,6 +605,7 @@ mod tests { Exercise { name: "0", dir: None, + embedded_input_files: &[], path: "exercises/0.rs", canonical_path: None, test: false, diff --git a/src/embedded.rs b/src/embedded.rs index 36263a9c..9e2527d6 100644 --- a/src/embedded.rs +++ b/src/embedded.rs @@ -10,7 +10,7 @@ use crate::info_file::ExerciseInfo; pub static EMBEDDED_FILES: EmbeddedFiles = rustlings_macros::include_files!(); // Files related to one exercise. -struct ExerciseFiles { +pub struct ExerciseFiles { // The content of the exercise file. exercise: &'static [u8], // The content of the solution file. @@ -18,7 +18,7 @@ struct ExerciseFiles { // Index of the related `ExerciseDir` in `EmbeddedFiles::exercise_dirs`. dir_ind: usize, // Files that are read by the exercise. - input_files: &'static [InputFile], + pub input_files: &'static [InputFile], } // Input files that may be read by exercises. @@ -64,7 +64,7 @@ impl ExerciseDir { pub struct EmbeddedFiles { /// The content of the `info.toml` file. pub info_file: &'static str, - exercise_files: &'static [ExerciseFiles], + pub exercise_files: &'static [ExerciseFiles], pub exercise_dirs: &'static [ExerciseDir], } diff --git a/src/exercise.rs b/src/exercise.rs index e31b3174..180e3b12 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use crossterm::{ QueueableCommand, style::{Attribute, Color, ResetColor, SetAttribute, SetForegroundColor}, @@ -7,6 +7,7 @@ use std::io::{self, StdoutLock, Write}; use crate::{ cmd::CmdRunner, + embedded::InputFile, term::{self, CountedWrite, file_path, terminal_file_link, write_ansi}, }; @@ -69,6 +70,7 @@ fn run_bin( pub struct Exercise { pub name: &'static str, pub dir: Option<&'static str>, + pub embedded_input_files: &'static [InputFile], /// Path of the exercise file starting with the `exercises/` directory. pub path: &'static str, pub canonical_path: Option, @@ -99,6 +101,7 @@ pub trait RunnableExercise { fn dir(&self) -> Option<&str>; fn strict_clippy(&self) -> bool; fn test(&self) -> bool; + fn embedded_input_files(&self) -> &[InputFile]; // Compile, check and run the exercise or its solution (depending on `bin_name´). // The output is written to the `output` buffer after clearing it. @@ -112,6 +115,19 @@ pub trait RunnableExercise { output.clear(); } + // Input files are already written to disk during `rustlings init`. + // We repeat it here to ensure an exercise doesn't fail because the + // input files were deleted or modified in the meantime. Note that + // `self.embedded_input_files()` is empty for community exercises. + for input_file in self.embedded_input_files() { + let name = input_file.name; + let path = match self.dir() { + Some(dir) => format!("exercises/{dir}/{name}"), + None => format!("exercises/{name}"), + }; + std::fs::write(path, input_file.content).context("failed to write input file")?; + } + let build_success = cmd_runner .cargo("build", bin_name, output.as_deref_mut()) .run("cargo build …")?; @@ -224,4 +240,8 @@ impl RunnableExercise for Exercise { fn test(&self) -> bool { self.test } + + fn embedded_input_files(&self) -> &[InputFile] { + self.embedded_input_files + } } diff --git a/src/info_file.rs b/src/info_file.rs index ece40afb..047e0c18 100644 --- a/src/info_file.rs +++ b/src/info_file.rs @@ -2,7 +2,10 @@ use anyhow::{Context, Error, Result, bail}; use serde::Deserialize; use std::{fs, io::ErrorKind}; -use crate::{embedded::EMBEDDED_FILES, exercise::RunnableExercise}; +use crate::{ + embedded::{EMBEDDED_FILES, InputFile}, + exercise::RunnableExercise, +}; /// Deserialized from the `info.toml` file. #[derive(Deserialize)] @@ -72,6 +75,11 @@ impl RunnableExercise for ExerciseInfo<'_> { fn test(&self) -> bool { self.test } + + fn embedded_input_files(&self) -> &[InputFile] { + // We don't have the input files embedded for communtiy exercises. + &[] + } } /// The deserialized `info.toml` file.