2024-08-01 09:53:32 -04:00
use anyhow ::{ anyhow , bail , Context , Error , Result } ;
2024-04-17 12:19:08 -04:00
use std ::{
cmp ::Ordering ,
2024-04-17 16:46:21 -04:00
fs ::{ self , read_dir , OpenOptions } ,
2024-06-01 15:48:15 -04:00
io ::{ self , Read , Write } ,
2024-04-17 16:46:21 -04:00
path ::{ Path , PathBuf } ,
2024-08-01 13:14:09 -04:00
process ::{ Command , Stdio } ,
2024-06-10 11:42:11 -04:00
thread ,
2024-04-17 12:19:08 -04:00
} ;
2024-04-15 17:54:57 -04:00
2024-04-17 09:55:50 -04:00
use crate ::{
2024-06-01 09:01:18 -04:00
cargo_toml ::{ append_bins , bins_start_end_ind , BINS_BUFFER_CAPACITY } ,
2024-08-01 09:23:54 -04:00
cmd ::CmdRunner ,
2024-08-08 17:46:21 -04:00
collections ::{ hash_set_with_capacity , HashSet } ,
2024-06-01 15:48:15 -04:00
exercise ::{ RunnableExercise , OUTPUT_CAPACITY } ,
2024-04-17 09:55:50 -04:00
info_file ::{ ExerciseInfo , InfoFile } ,
2024-08-27 18:56:22 -04:00
CURRENT_FORMAT_VERSION ,
2024-04-17 09:55:50 -04:00
} ;
2024-04-15 21:30:28 -04:00
2024-08-27 18:56:22 -04:00
const MAX_EXERCISE_NAME_LEN : usize = 32 ;
2024-05-01 13:47:35 -04:00
// Find a char that isn't allowed in the exercise's `name` or `dir`.
2024-04-17 12:19:08 -04:00
fn forbidden_char ( input : & str ) -> Option < char > {
2024-06-01 09:10:43 -04:00
input . chars ( ) . find ( | c | ! c . is_alphanumeric ( ) & & * c ! = '_' )
2024-04-17 12:19:08 -04:00
}
2024-08-02 09:54:14 -04:00
// Check that the `Cargo.toml` file is up-to-date.
2024-06-01 15:48:15 -04:00
fn check_cargo_toml (
exercise_infos : & [ ExerciseInfo ] ,
2024-08-01 13:17:40 -04:00
cargo_toml_path : & str ,
2024-06-01 15:48:15 -04:00
exercise_path_prefix : & [ u8 ] ,
) -> Result < ( ) > {
2024-08-01 13:17:40 -04:00
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 ( & current_cargo_toml ) ? ;
2024-06-01 15:48:15 -04:00
let old_bins = & current_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 {
2024-08-01 09:23:54 -04:00
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 " ) ;
2024-06-01 15:48:15 -04:00
}
2024-07-31 19:07:56 -04:00
bail! ( " The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it. Then run `rustlings dev check` again " ) ;
2024-06-01 15:48:15 -04:00
}
Ok ( ( ) )
}
2024-05-01 13:47:35 -04:00
// Check the info of all exercises and return their paths in a set.
2024-08-07 19:23:58 -04:00
fn check_info_file_exercises ( info_file : & InfoFile ) -> Result < HashSet < PathBuf > > {
2024-08-08 17:46:21 -04:00
let mut names = hash_set_with_capacity ( info_file . exercises . len ( ) ) ;
let mut paths = hash_set_with_capacity ( info_file . exercises . len ( ) ) ;
2024-04-17 12:59:40 -04:00
2024-04-17 16:46:21 -04:00
let mut file_buf = String ::with_capacity ( 1 < < 14 ) ;
2024-04-17 12:19:08 -04:00
for exercise_info in & info_file . exercises {
2024-05-01 13:16:59 -04:00
let name = exercise_info . name . as_str ( ) ;
if name . is_empty ( ) {
2024-04-17 13:12:10 -04:00
bail! ( " Found an empty exercise name in `info.toml` " ) ;
}
2024-08-27 18:34:24 -04:00
if name . len ( ) > MAX_EXERCISE_NAME_LEN {
bail! ( " The length of the exercise name `{name}` is bigger than the maximum {MAX_EXERCISE_NAME_LEN} " ) ;
}
2024-05-01 13:16:59 -04:00
if let Some ( c ) = forbidden_char ( name ) {
bail! ( " Char `{c}` in the exercise name `{name}` is not allowed " ) ;
2024-04-17 12:19:08 -04:00
}
if let Some ( dir ) = & exercise_info . dir {
2024-04-17 13:12:10 -04:00
if dir . is_empty ( ) {
2024-05-01 13:16:59 -04:00
bail! ( " The exercise `{name}` has an empty dir name in `info.toml` " ) ;
2024-04-17 13:12:10 -04:00
}
2024-04-17 12:19:08 -04:00
if let Some ( c ) = forbidden_char ( dir ) {
bail! ( " Char `{c}` in the exercise dir `{dir}` is not allowed " ) ;
}
}
2024-08-02 10:28:05 -04:00
if exercise_info . hint . trim_ascii ( ) . is_empty ( ) {
2024-05-01 13:16:59 -04:00
bail! ( " The exercise `{name}` has an empty hint. Please provide a hint or at least tell the user why a hint isn't needed for this exercise " ) ;
2024-04-17 13:12:10 -04:00
}
2024-05-01 13:16:59 -04:00
if ! names . insert ( name ) {
bail! ( " The exercise name `{name}` is duplicated. Exercise names must all be unique " ) ;
2024-04-17 12:19:08 -04:00
}
2024-04-17 16:46:21 -04:00
let path = exercise_info . path ( ) ;
OpenOptions ::new ( )
. read ( true )
. open ( & path )
. with_context ( | | format! ( " Failed to open the file {path} " ) ) ?
. read_to_string ( & mut file_buf )
. with_context ( | | format! ( " Failed to read the file {path} " ) ) ? ;
if ! file_buf . contains ( " fn main() " ) {
bail! ( " The `main` function is missing in the file `{path}`. \n Create at least an empty `main` function to avoid language server errors " ) ;
}
2024-07-04 14:28:46 -04:00
if ! file_buf . contains ( " // TODO " ) {
bail! ( " Didn't find any `// TODO` comment in the file `{path}`. \n You need to have at least one such comment to guide the user. " ) ;
}
2024-08-20 07:49:48 -04:00
let contains_tests = file_buf . contains ( " #[test] \n " ) ;
if exercise_info . test {
if ! contains_tests {
bail! ( " The file `{path}` doesn't contain any tests. If you don't want to add tests to this exercise, set `test = false` for this exercise in the `info.toml` file " ) ;
}
} else if contains_tests {
2024-05-01 13:16:59 -04:00
bail! ( " The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file " ) ;
}
2024-04-17 16:46:21 -04:00
file_buf . clear ( ) ;
paths . insert ( PathBuf ::from ( path ) ) ;
2024-04-17 12:19:08 -04:00
}
Ok ( paths )
}
2024-06-01 18:03:48 -04:00
// Check `dir` for unexpected files.
// Only Rust files in `allowed_rust_files` and `README.md` files are allowed.
// Only one level of directory nesting is allowed.
2024-08-07 19:23:58 -04:00
fn check_unexpected_files ( dir : & str , allowed_rust_files : & HashSet < PathBuf > ) -> Result < ( ) > {
2024-06-01 18:03:48 -04:00
let unexpected_file = | path : & Path | {
anyhow! ( " Found the file `{}`. Only `README.md` and Rust files related to an exercise in `info.toml` are allowed in the `{dir}` directory " , path . display ( ) )
} ;
2024-04-17 12:59:40 -04:00
2024-06-01 18:03:48 -04:00
for entry in read_dir ( dir ) . with_context ( | | format! ( " Failed to open the ` {dir} ` directory " ) ) ? {
let entry = entry . with_context ( | | format! ( " Failed to read the ` {dir} ` directory " ) ) ? ;
2024-04-17 12:19:08 -04:00
if entry . file_type ( ) . unwrap ( ) . is_file ( ) {
let path = entry . path ( ) ;
let file_name = path . file_name ( ) . unwrap ( ) ;
if file_name = = " README.md " {
continue ;
}
2024-06-01 18:03:48 -04:00
if ! allowed_rust_files . contains ( & path ) {
2024-04-17 16:46:21 -04:00
return Err ( unexpected_file ( & path ) ) ;
2024-04-17 12:19:08 -04:00
}
continue ;
}
let dir_path = entry . path ( ) ;
for entry in read_dir ( & dir_path )
. with_context ( | | format! ( " Failed to open the directory {} " , dir_path . display ( ) ) ) ?
{
let entry = entry
. with_context ( | | format! ( " Failed to read the directory {} " , dir_path . display ( ) ) ) ? ;
let path = entry . path ( ) ;
if ! entry . file_type ( ) . unwrap ( ) . is_file ( ) {
2024-04-17 16:46:21 -04:00
bail! ( " Found `{}` but expected only files. Only one level of exercise nesting is allowed " , path . display ( ) ) ;
2024-04-17 12:19:08 -04:00
}
let file_name = path . file_name ( ) . unwrap ( ) ;
if file_name = = " README.md " {
continue ;
}
2024-06-01 18:03:48 -04:00
if ! allowed_rust_files . contains ( & path ) {
2024-04-17 16:46:21 -04:00
return Err ( unexpected_file ( & path ) ) ;
2024-04-17 12:19:08 -04:00
}
}
}
2024-04-17 16:46:21 -04:00
Ok ( ( ) )
2024-04-17 12:19:08 -04:00
}
2024-08-20 08:47:08 -04:00
fn check_exercises_unsolved (
info_file : & 'static InfoFile ,
cmd_runner : & 'static CmdRunner ,
) -> Result < ( ) > {
2024-08-19 17:52:22 -04:00
let mut stdout = io ::stdout ( ) . lock ( ) ;
stdout . write_all ( b " Running all exercises to check that they aren't already solved... \n " ) ? ;
2024-08-20 08:47:08 -04:00
let handles = info_file
. exercises
. iter ( )
. filter_map ( | exercise_info | {
if exercise_info . skip_check_unsolved {
return None ;
2024-08-02 09:54:14 -04:00
}
2024-08-19 17:52:22 -04:00
2024-08-20 08:47:08 -04:00
Some ( (
exercise_info . name . as_str ( ) ,
thread ::spawn ( | | exercise_info . run_exercise ( None , cmd_runner ) ) ,
) )
} )
. collect ::< Vec < _ > > ( ) ;
let n_handles = handles . len ( ) ;
write! ( stdout , " Progress: 0/{n_handles} " ) ? ;
stdout . flush ( ) ? ;
let mut handle_num = 1 ;
for ( exercise_name , handle ) in handles {
let Ok ( result ) = handle . join ( ) else {
bail! ( " Panic while trying to run the exericse {exercise_name} " ) ;
} ;
match result {
Ok ( true ) = > {
bail! ( " The exercise {exercise_name} is already solved. \n {SKIP_CHECK_UNSOLVED_HINT} " , )
}
Ok ( false ) = > ( ) ,
Err ( e ) = > return Err ( e ) ,
2024-08-02 09:54:14 -04:00
}
2024-08-20 08:47:08 -04:00
write! ( stdout , " \r Progress: {handle_num}/{n_handles} " ) ? ;
stdout . flush ( ) ? ;
handle_num + = 1 ;
}
stdout . write_all ( b " \n " ) ? ;
Ok ( ( ) )
2024-07-04 15:12:57 -04:00
}
2024-08-20 08:47:08 -04:00
fn check_exercises ( info_file : & 'static InfoFile , cmd_runner : & 'static CmdRunner ) -> Result < ( ) > {
2024-04-17 12:19:08 -04:00
match info_file . format_version . cmp ( & CURRENT_FORMAT_VERSION ) {
Ordering ::Less = > bail! ( " `format_version` < {CURRENT_FORMAT_VERSION} (supported version) \n Please migrate to the latest format version " ) ,
Ordering ::Greater = > bail! ( " `format_version` > {CURRENT_FORMAT_VERSION} (supported version) \n Try updating the Rustlings program " ) ,
Ordering ::Equal = > ( ) ,
}
2024-08-20 08:47:08 -04:00
let handle = thread ::spawn ( move | | check_exercises_unsolved ( info_file , cmd_runner ) ) ;
2024-04-17 12:19:08 -04:00
let info_file_paths = check_info_file_exercises ( info_file ) ? ;
2024-08-20 08:47:08 -04:00
check_unexpected_files ( " exercises " , & info_file_paths ) ? ;
2024-04-17 12:19:08 -04:00
2024-08-02 09:54:14 -04:00
handle . join ( ) . unwrap ( )
2024-04-17 12:19:08 -04:00
}
2024-08-01 09:53:32 -04:00
enum SolutionCheck {
Success { sol_path : String } ,
MissingOptional ,
RunFailure { output : Vec < u8 > } ,
Err ( Error ) ,
}
2024-08-01 09:23:54 -04:00
fn check_solutions (
require_solutions : bool ,
2024-08-20 08:47:08 -04:00
info_file : & 'static InfoFile ,
cmd_runner : & 'static CmdRunner ,
2024-08-01 09:23:54 -04:00
) -> Result < ( ) > {
2024-08-19 17:52:22 -04:00
let mut stdout = io ::stdout ( ) . lock ( ) ;
stdout . write_all ( b " Running all solutions... \n " ) ? ;
2024-08-20 08:47:08 -04:00
let handles = info_file
. exercises
. iter ( )
. map ( | exercise_info | {
thread ::spawn ( move | | {
let sol_path = exercise_info . sol_path ( ) ;
if ! Path ::new ( & sol_path ) . exists ( ) {
if require_solutions {
return SolutionCheck ::Err ( anyhow! (
" The solution of the exercise {} is missing " ,
exercise_info . name ,
) ) ;
2024-08-01 09:53:32 -04:00
}
2024-06-10 11:42:11 -04:00
2024-08-20 08:47:08 -04:00
return SolutionCheck ::MissingOptional ;
}
2024-08-19 17:52:22 -04:00
2024-08-20 08:47:08 -04:00
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 ) ,
}
} )
} )
. collect ::< Vec < _ > > ( ) ;
let mut sol_paths = hash_set_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 ( ) ) ;
let n_handles = handles . len ( ) ;
write! ( stdout , " Progress: 0/{n_handles} " ) ? ;
stdout . flush ( ) ? ;
let mut handle_num = 1 ;
for ( exercise_info , handle ) in info_file . exercises . iter ( ) . zip ( handles ) {
let Ok ( check_result ) = handle . join ( ) else {
bail! (
" Panic while trying to run the solution of the exericse {} " ,
exercise_info . name ,
) ;
} ;
match check_result {
SolutionCheck ::Success { sol_path } = > {
fmt_cmd . arg ( & sol_path ) ;
sol_paths . insert ( PathBuf ::from ( sol_path ) ) ;
}
SolutionCheck ::MissingOptional = > ( ) ,
SolutionCheck ::RunFailure { output } = > {
stdout . write_all ( b " \n \n " ) ? ;
stdout . write_all ( & output ) ? ;
2024-08-02 09:54:14 -04:00
bail! (
2024-08-20 08:47:08 -04:00
" Running the solution of the exercise {} failed with the error above " ,
2024-08-02 09:54:14 -04:00
exercise_info . name ,
) ;
2024-08-01 09:53:32 -04:00
}
2024-08-20 08:47:08 -04:00
SolutionCheck ::Err ( e ) = > return Err ( e ) ,
2024-06-01 15:50:11 -04:00
}
2024-08-20 08:47:08 -04:00
write! ( stdout , " \r Progress: {handle_num}/{n_handles} " ) ? ;
stdout . flush ( ) ? ;
handle_num + = 1 ;
}
stdout . write_all ( b " \n " ) ? ;
2024-04-17 09:55:50 -04:00
2024-08-20 08:47:08 -04:00
let handle = thread ::spawn ( move | | check_unexpected_files ( " solutions " , & sol_paths ) ) ;
2024-06-01 18:03:48 -04:00
2024-08-20 08:47:08 -04:00
if ! fmt_cmd
. status ( )
. context ( " Failed to run `rustfmt` on all solution files " ) ?
. success ( )
{
bail! ( " Some solutions aren't formatted. Run `rustfmt` on them " ) ;
}
handle . join ( ) . unwrap ( )
2024-04-17 09:55:50 -04:00
}
2024-06-01 18:11:41 -04:00
pub fn check ( require_solutions : bool ) -> Result < ( ) > {
2024-04-17 09:55:50 -04:00
let info_file = InfoFile ::parse ( ) ? ;
2024-04-15 21:30:28 -04:00
2024-08-01 09:23:54 -04:00
if cfg! ( debug_assertions ) {
// A hack to make `cargo run -- dev check` work when developing Rustlings.
2024-08-01 13:17:40 -04:00
check_cargo_toml ( & info_file . exercises , " dev/Cargo.toml " , b " ../ " ) ? ;
2024-04-17 09:55:50 -04:00
} else {
2024-08-01 13:17:40 -04:00
check_cargo_toml ( & info_file . exercises , " Cargo.toml " , b " " ) ? ;
2024-04-17 09:55:50 -04:00
}
2024-04-15 21:30:28 -04:00
2024-08-20 08:47:08 -04:00
// Leaking is fine since they are used until the end of the program.
let cmd_runner = Box ::leak ( Box ::new ( CmdRunner ::build ( ) ? ) ) ;
let info_file = Box ::leak ( Box ::new ( info_file ) ) ;
check_exercises ( info_file , cmd_runner ) ? ;
check_solutions ( require_solutions , info_file , cmd_runner ) ? ;
2024-06-01 15:48:15 -04:00
2024-08-01 13:19:25 -04:00
println! ( " Everything looks fine! " ) ;
2024-04-15 21:30:28 -04:00
Ok ( ( ) )
2024-04-15 17:54:57 -04:00
}
2024-06-10 11:42:11 -04:00
2024-08-02 09:54:14 -04:00
const SKIP_CHECK_UNSOLVED_HINT : & str = " If this is an introduction exercise that is intended to be already solved, add `skip_check_unsolved = true` to the exercise's metadata in the `info.toml` file " ;