This commit is contained in:
Roberto Vidal 2020-11-01 03:29:04 -04:00 committed by GitHub
commit 25d9a4ec33
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 446 additions and 346 deletions

2
.gitignore vendored
View file

@ -7,3 +7,5 @@ exercises/clippy/Cargo.toml
exercises/clippy/Cargo.lock
.idea
.vscode
exercises/Cargo.lock
exercises/Cargo.toml

602
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ clap = "2.32.0"
indicatif = "0.10.3"
console = "0.7.7"
notify = "4.0.15"
toml = "0.4.10"
toml = "0.5.6"
regex = "1.1.6"
serde = {version = "1.0.10", features = ["derive"]}

View file

@ -103,6 +103,19 @@ exercise:
rustlings hint myExercise1
```
### Using an IDE
**TL,DR**: open the `exercises/` folder in your editor while running `rustlings watch`.
There are several editors and plugins that are able to analyze Rust code for a full blown IDE (or IDE-like) experience. The official language plugin of the Rust project is [rust-analyzer](rust-analyzer), but there are others, like [IntelliJ Rust](intellij) or [rls](rls).
These tools usually rely on the default structure of a Cargo project to perform their analyses. Conversely, Rustlings has a _atypical_ structure: it's a collection of isolated exercises. Your editor might not be able to properly analyze the code of the exercises.
However, if you run `rustlings watch`, we auto-generate a "fake" project manifest in `exercises/Cargo.toml` that points to the currently active exercise. If you open the `exercises/` folder with your Rust-enabled editor, you should be able to enjoy its capabilities. Note that we update the manifest so it always points to the current exercise, so ideally your editor should be able to refresh the project metadata automatically.
[rust-analyzer]: https://rust-analyzer.github.io/
[rls]: https://github.com/rust-lang/rls
[intellij]: https://intellij-rust.github.io/
## Testing yourself
After every couple of sections, there will be a quiz that'll test your knowledge on a bunch of sections at once. These quizzes are found in `exercises/quizN.rs`.

View file

@ -1,3 +1,5 @@
root = "exercises"
# VARIABLES
[[exercises]]

View file

@ -1,13 +1,13 @@
use regex::Regex;
use serde::Deserialize;
use std::fmt::{self, Display, Formatter};
use std::fs::{self, remove_file, File};
use std::io::Read;
use std::fs::{self, remove_file};
use std::path::PathBuf;
use std::process::{self, Command};
const RUSTC_COLOR_ARGS: &[&str] = &["--color", "always"];
const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE";
const MAIN_FN_REGEX: &str = r"(?m)^\s*fn\s+main\s*\(\s*\)";
const CONTEXT: usize = 2;
const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/clippy/Cargo.toml";
@ -35,7 +35,8 @@ pub enum Mode {
}
#[derive(Deserialize)]
pub struct ExerciseList {
pub struct ExerciseInfo {
pub root: Option<String>,
pub exercises: Vec<Exercise>,
}
@ -192,17 +193,7 @@ path = "{}.rs""#,
}
pub fn state(&self) -> State {
let mut source_file =
File::open(&self.path).expect("We were unable to open the exercise file!");
let source = {
let mut s = String::new();
source_file
.read_to_string(&mut s)
.expect("We were unable to read the exercise file!");
s
};
let source = self.source();
let re = Regex::new(I_AM_DONE_REGEX).unwrap();
if !re.is_match(&source) {
@ -232,6 +223,16 @@ path = "{}.rs""#,
State::Pending(context)
}
pub fn is_binary(&self) -> bool {
let source = self.source();
let re = Regex::new(MAIN_FN_REGEX).unwrap();
re.is_match(&source)
}
fn source(&self) -> String {
fs::read_to_string(&self.path).expect("Unable to read exercise source")
}
}
impl Display for Exercise {
@ -248,7 +249,7 @@ fn clean() {
#[cfg(test)]
mod test {
use super::*;
use std::path::Path;
use std::{fs::File, path::Path};
#[test]
fn test_clean() {

View file

@ -1,4 +1,4 @@
use crate::exercise::{Exercise, ExerciseList};
use crate::exercise::{Exercise, ExerciseInfo};
use crate::run::run;
use crate::verify::verify;
use clap::{crate_version, App, Arg, SubCommand};
@ -19,6 +19,7 @@ use std::time::Duration;
mod ui;
mod exercise;
mod manifest;
mod run;
mod verify;
@ -85,7 +86,7 @@ fn main() {
}
let toml_str = &fs::read_to_string("info.toml").unwrap();
let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises;
let ExerciseInfo { exercises, root } = toml::from_str::<ExerciseInfo>(toml_str).unwrap();
let verbose = matches.is_present("nocapture");
if let Some(ref matches) = matches.subcommand_matches("run") {
@ -116,10 +117,12 @@ fn main() {
}
if matches.subcommand_matches("verify").is_some() {
verify(&exercises, verbose).unwrap_or_else(|_| std::process::exit(1));
verify(&exercises, root.as_deref(), verbose).unwrap_or_else(|_| std::process::exit(1));
}
if matches.subcommand_matches("watch").is_some() && watch(&exercises, verbose).is_ok() {
if matches.subcommand_matches("watch").is_some()
&& watch(&exercises, root.as_deref(), verbose).is_ok()
{
println!(
"{emoji} All exercises completed! {emoji}",
emoji = Emoji("🎉", "")
@ -162,7 +165,7 @@ fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>) {
});
}
fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<()> {
fn watch(exercises: &[Exercise], root: Option<&str>, verbose: bool) -> notify::Result<()> {
/* Clears the terminal with an ANSI escape code.
Works in UNIX and newer Windows terminals. */
fn clear_screen() {
@ -177,7 +180,7 @@ fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<()> {
clear_screen();
let to_owned_hint = |t: &Exercise| t.hint.to_owned();
let failed_exercise_hint = match verify(exercises.iter(), verbose) {
let failed_exercise_hint = match verify(exercises.iter(), root, verbose) {
Ok(_) => return Ok(()),
Err(exercise) => Arc::new(Mutex::new(Some(to_owned_hint(exercise)))),
};
@ -192,7 +195,7 @@ fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<()> {
.iter()
.skip_while(|e| !filepath.ends_with(&e.path));
clear_screen();
match verify(pending_exercises, verbose) {
match verify(pending_exercises, root, verbose) {
Ok(_) => return Ok(()),
Err(exercise) => {
let mut failed_exercise_hint = failed_exercise_hint.lock().unwrap();

76
src/manifest.rs Normal file
View file

@ -0,0 +1,76 @@
use std::fs;
use serde::Serialize;
use toml;
use crate::exercise::Exercise;
pub fn update(exercise: &Exercise, root: Option<&str>) {
let root = if let Some(root) = root {
root
} else {
return;
};
let path = exercise
.path
.strip_prefix(root)
.expect("Invalid path outside of root")
.to_string_lossy()
.to_string();
let target = Target {
name: exercise.name.clone(),
path,
};
let (bins, lib) = if exercise.is_binary() {
(Some(vec![target]), None)
} else {
(None, Some(target))
};
let manifest = Manifest {
package: Package {
name: "exercises",
version: "0.1.0",
authors: &["The Rustlings Maintainers"],
edition: "2018",
publish: false,
},
bin: bins,
lib,
};
let source = format!(
"# This Cargo.toml is AUTOGENERATED, you shouldn't need to edit it.\n\
# The `rustling` commands do not rely on this manifest at all, its only purpose\n\
# is to help editors analyze your code.\n\
{}",
toml::ser::to_string_pretty(&manifest).expect("Invalid toml")
);
fs::write(format!("{}/Cargo.toml", root), source).expect("Unable to update manifest")
}
#[derive(Serialize)]
struct Manifest {
package: Package,
bin: Option<Vec<Target>>,
lib: Option<Target>,
}
#[derive(Serialize)]
struct Package {
name: &'static str,
version: &'static str,
edition: &'static str,
authors: &'static [&'static str],
publish: bool,
}
#[derive(Serialize)]
struct Target {
name: String,
path: String,
}

View file

@ -1,4 +1,7 @@
use crate::exercise::{CompiledExercise, Exercise, Mode, State};
use crate::{
exercise::{CompiledExercise, Exercise, Mode, State},
manifest,
};
use console::style;
use indicatif::ProgressBar;
@ -9,6 +12,7 @@ use indicatif::ProgressBar;
// determines whether or not the test harness outputs are displayed.
pub fn verify<'a>(
start_at: impl IntoIterator<Item = &'a Exercise>,
root: Option<&str>,
verbose: bool,
) -> Result<(), &'a Exercise> {
for exercise in start_at {
@ -18,6 +22,7 @@ pub fn verify<'a>(
Mode::Clippy => compile_only(&exercise),
};
if !compile_result.unwrap_or(false) {
manifest::update(exercise, root);
return Err(exercise);
}
}

View file

@ -1,8 +1,5 @@
use assert_cmd::prelude::*;
use glob::glob;
use predicates::boolean::PredicateBooleanExt;
use std::fs::File;
use std::io::Read;
use std::process::Command;
#[test]
@ -121,23 +118,6 @@ fn get_hint_for_single_test() {
.stdout("Hello!\n");
}
#[test]
fn all_exercises_require_confirmation() {
for exercise in glob("exercises/**/*.rs").unwrap() {
let path = exercise.unwrap();
let source = {
let mut file = File::open(&path).unwrap();
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
s
};
source.matches("// I AM NOT DONE").next().expect(&format!(
"There should be an `I AM NOT DONE` annotation in {:?}",
path
));
}
}
#[test]
fn run_compile_exercise_does_not_prompt() {
Command::cargo_bin("rustlings")

View file

@ -0,0 +1,20 @@
use glob::glob;
use std::fs;
use std::path::PathBuf;
#[test]
fn all_exercises_require_confirmation() {
for path in all_exercises() {
let source = fs::read_to_string(&path).unwrap();
source.matches("// I AM NOT DONE").next().expect(&format!(
"There should be an `I AM NOT DONE` annotation in {:?}",
path
));
}
}
fn all_exercises() -> impl Iterator<Item = PathBuf> {
glob("exercises/**/*.rs")
.unwrap()
.map(|result| result.expect("Unable to traverse exercises folder"))
}