2024-06-01 23:03:48 +01:00
use anyhow ::{ anyhow , bail , Context , Result } ;
2024-04-17 17:19:08 +01:00
use std ::{
cmp ::Ordering ,
2024-04-17 21:46:21 +01:00
fs ::{ self , read_dir , OpenOptions } ,
2024-06-01 20:48:15 +01:00
io ::{ self , Read , Write } ,
2024-04-17 21:46:21 +01:00
path ::{ Path , PathBuf } ,
2024-06-10 16:42:11 +01:00
sync ::{
atomic ::{ self , AtomicBool } ,
Mutex ,
} ,
thread ,
2024-04-17 17:19:08 +01:00
} ;
2024-04-15 22:54:57 +01:00
2024-04-17 14:55:50 +01:00
use crate ::{
2024-06-01 20:48:15 +01:00
app_state ::parse_target_dir ,
2024-06-01 14:01:18 +01:00
cargo_toml ::{ append_bins , bins_start_end_ind , BINS_BUFFER_CAPACITY } ,
2024-06-01 20:48:15 +01:00
exercise ::{ RunnableExercise , OUTPUT_CAPACITY } ,
2024-04-17 14:55:50 +01:00
info_file ::{ ExerciseInfo , InfoFile } ,
2024-04-21 18:26:19 +01:00
CURRENT_FORMAT_VERSION , DEBUG_PROFILE ,
2024-04-17 14:55:50 +01:00
} ;
2024-04-16 02:30:28 +01:00
2024-05-01 18:47:35 +01:00
// Find a char that isn't allowed in the exercise's `name` or `dir`.
2024-04-17 17:19:08 +01:00
fn forbidden_char ( input : & str ) -> Option < char > {
2024-06-01 14:10:43 +01:00
input . chars ( ) . find ( | c | ! c . is_alphanumeric ( ) & & * c ! = '_' )
2024-04-17 17:19:08 +01:00
}
2024-06-01 20:48:15 +01:00
// Check that the Cargo.toml file is up-to-date.
fn check_cargo_toml (
exercise_infos : & [ ExerciseInfo ] ,
current_cargo_toml : & str ,
exercise_path_prefix : & [ u8 ] ,
) -> Result < ( ) > {
let ( bins_start_ind , bins_end_ind ) = bins_start_end_ind ( current_cargo_toml ) ? ;
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 {
if DEBUG_PROFILE {
bail! ( " The file `dev/Cargo.toml` is outdated. Please run `cargo run -- dev update` to update it " ) ;
}
bail! ( " The file `Cargo.toml` is outdated. Please run `rustlings dev update` to update it " ) ;
}
Ok ( ( ) )
}
2024-05-01 18:47:35 +01:00
// Check the info of all exercises and return their paths in a set.
2024-04-17 17:19:08 +01:00
fn check_info_file_exercises ( info_file : & InfoFile ) -> Result < hashbrown ::HashSet < PathBuf > > {
let mut names = hashbrown ::HashSet ::with_capacity ( info_file . exercises . len ( ) ) ;
let mut paths = hashbrown ::HashSet ::with_capacity ( info_file . exercises . len ( ) ) ;
2024-04-17 17:59:40 +01:00
2024-04-17 21:46:21 +01:00
let mut file_buf = String ::with_capacity ( 1 < < 14 ) ;
2024-04-17 17:19:08 +01:00
for exercise_info in & info_file . exercises {
2024-05-01 18:16:59 +01:00
let name = exercise_info . name . as_str ( ) ;
if name . is_empty ( ) {
2024-04-17 18:12:10 +01:00
bail! ( " Found an empty exercise name in `info.toml` " ) ;
}
2024-05-01 18:16:59 +01:00
if let Some ( c ) = forbidden_char ( name ) {
bail! ( " Char `{c}` in the exercise name `{name}` is not allowed " ) ;
2024-04-17 17:19:08 +01:00
}
if let Some ( dir ) = & exercise_info . dir {
2024-04-17 18:12:10 +01:00
if dir . is_empty ( ) {
2024-05-01 18:16:59 +01:00
bail! ( " The exercise `{name}` has an empty dir name in `info.toml` " ) ;
2024-04-17 18:12:10 +01:00
}
2024-04-17 17:19:08 +01:00
if let Some ( c ) = forbidden_char ( dir ) {
bail! ( " Char `{c}` in the exercise dir `{dir}` is not allowed " ) ;
}
}
2024-04-17 18:16:48 +01:00
if exercise_info . hint . trim ( ) . is_empty ( ) {
2024-05-01 18:16:59 +01: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 18:12:10 +01:00
}
2024-05-01 18:16:59 +01:00
if ! names . insert ( name ) {
bail! ( " The exercise name `{name}` is duplicated. Exercise names must all be unique " ) ;
2024-04-17 17:19:08 +01:00
}
2024-04-17 21:46:21 +01: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 19:28:46 +01: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-05-01 18:16:59 +01:00
if ! exercise_info . test & & file_buf . contains ( " #[test] " ) {
bail! ( " The file `{path}` contains tests annotated with `#[test]` but the exercise `{name}` has `test = false` in the `info.toml` file " ) ;
}
2024-04-17 21:46:21 +01:00
file_buf . clear ( ) ;
paths . insert ( PathBuf ::from ( path ) ) ;
2024-04-17 17:19:08 +01:00
}
Ok ( paths )
}
2024-06-01 23:03:48 +01: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.
fn check_unexpected_files (
dir : & str ,
allowed_rust_files : & hashbrown ::HashSet < PathBuf > ,
) -> Result < ( ) > {
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 17:59:40 +01:00
2024-06-01 23:03:48 +01: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 17:19:08 +01: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 23:03:48 +01:00
if ! allowed_rust_files . contains ( & path ) {
2024-04-17 21:46:21 +01:00
return Err ( unexpected_file ( & path ) ) ;
2024-04-17 17:19:08 +01: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 21:46:21 +01:00
bail! ( " Found `{}` but expected only files. Only one level of exercise nesting is allowed " , path . display ( ) ) ;
2024-04-17 17:19:08 +01:00
}
let file_name = path . file_name ( ) . unwrap ( ) ;
if file_name = = " README.md " {
continue ;
}
2024-06-01 23:03:48 +01:00
if ! allowed_rust_files . contains ( & path ) {
2024-04-17 21:46:21 +01:00
return Err ( unexpected_file ( & path ) ) ;
2024-04-17 17:19:08 +01:00
}
}
}
2024-04-17 21:46:21 +01:00
Ok ( ( ) )
2024-04-17 17:19:08 +01:00
}
2024-07-04 20:12:57 +01:00
fn check_exercises_unsolved ( info_file : & InfoFile , target_dir : & Path ) -> Result < ( ) > {
let error_occurred = AtomicBool ::new ( false ) ;
println! (
" Running all exercises to check that they aren't already solved. This may take a while… \n " ,
) ;
thread ::scope ( | s | {
for exercise_info in & info_file . exercises {
if exercise_info . skip_check_unsolved {
continue ;
}
s . spawn ( | | {
let error = | e | {
let mut stderr = io ::stderr ( ) . lock ( ) ;
stderr . write_all ( e ) . unwrap ( ) ;
stderr . write_all ( b " \n Problem with 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 mut output = Vec ::with_capacity ( OUTPUT_CAPACITY ) ;
match exercise_info . run_exercise ( & mut output , target_dir ) {
Ok ( true ) = > error ( b " Already solved! " ) ,
Ok ( false ) = > ( ) ,
Err ( e ) = > error ( e . to_string ( ) . as_bytes ( ) ) ,
}
} ) ;
}
} ) ;
if error_occurred . load ( atomic ::Ordering ::Relaxed ) {
bail! ( CHECK_EXERCISES_UNSOLVED_ERR ) ;
}
Ok ( ( ) )
}
fn check_exercises ( info_file : & InfoFile , target_dir : & Path ) -> Result < ( ) > {
2024-04-17 17:19:08 +01: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 = > ( ) ,
}
let info_file_paths = check_info_file_exercises ( info_file ) ? ;
2024-06-01 23:03:48 +01:00
check_unexpected_files ( " exercises " , & info_file_paths ) ? ;
2024-04-17 17:19:08 +01:00
2024-07-04 20:12:57 +01:00
check_exercises_unsolved ( info_file , target_dir )
2024-04-17 17:19:08 +01:00
}
2024-07-04 20:12:57 +01:00
fn check_solutions ( require_solutions : bool , info_file : & InfoFile , target_dir : & Path ) -> Result < ( ) > {
2024-06-10 16:42:11 +01:00
let paths = Mutex ::new ( hashbrown ::HashSet ::with_capacity ( info_file . exercises . len ( ) ) ) ;
2024-07-02 13:28:08 +01:00
let error_occurred = AtomicBool ::new ( false ) ;
2024-06-10 16:42:11 +01:00
2024-07-04 20:12:57 +01:00
println! ( " Running all solutions. This may take a while… \n " ) ;
2024-06-10 16:42:11 +01:00
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 " \n Failed to run the solution of the exercise " )
. unwrap ( ) ;
stderr . write_all ( exercise_info . name . as_bytes ( ) ) . unwrap ( ) ;
stderr . write_all ( SEPARATOR ) . unwrap ( ) ;
2024-07-02 13:28:08 +01:00
error_occurred . store ( true , atomic ::Ordering ::Relaxed ) ;
2024-06-10 16:42:11 +01:00
} ;
let path = exercise_info . sol_path ( ) ;
if ! Path ::new ( & path ) . exists ( ) {
if require_solutions {
error ( b " Solution missing " ) ;
}
// No solution to check.
return ;
}
let mut output = Vec ::with_capacity ( OUTPUT_CAPACITY ) ;
2024-07-04 20:12:57 +01:00
match exercise_info . run_solution ( & mut output , target_dir ) {
2024-06-10 16:42:11 +01:00
Ok ( true ) = > {
paths . lock ( ) . unwrap ( ) . insert ( PathBuf ::from ( path ) ) ;
}
Ok ( false ) = > error ( & output ) ,
Err ( e ) = > error ( e . to_string ( ) . as_bytes ( ) ) ,
}
} ) ;
2024-06-01 20:50:11 +01:00
}
2024-06-10 16:42:11 +01:00
} ) ;
2024-06-01 20:50:11 +01:00
2024-07-02 13:28:08 +01:00
if error_occurred . load ( atomic ::Ordering ::Relaxed ) {
2024-06-10 16:42:11 +01:00
bail! ( " At least one solution failed. See the output above. " ) ;
2024-04-17 14:55:50 +01:00
}
2024-06-10 16:42:11 +01:00
check_unexpected_files ( " solutions " , & paths . into_inner ( ) . unwrap ( ) ) ? ;
2024-06-01 23:03:48 +01:00
2024-04-17 14:55:50 +01:00
Ok ( ( ) )
}
2024-06-01 23:11:41 +01:00
pub fn check ( require_solutions : bool ) -> Result < ( ) > {
2024-04-17 14:55:50 +01:00
let info_file = InfoFile ::parse ( ) ? ;
2024-04-16 02:30:28 +01:00
2024-05-01 18:47:35 +01:00
// A hack to make `cargo run -- dev check` work when developing Rustlings.
2024-04-21 18:26:19 +01:00
if DEBUG_PROFILE {
2024-04-17 14:55:50 +01:00
check_cargo_toml (
& info_file . exercises ,
2024-04-25 18:58:55 +01:00
include_str! ( " ../../dev-Cargo.toml " ) ,
2024-04-17 14:55:50 +01:00
b " ../ " ,
2024-04-21 19:22:01 +01:00
) ? ;
2024-04-17 14:55:50 +01:00
} else {
let current_cargo_toml =
fs ::read_to_string ( " Cargo.toml " ) . context ( " Failed to read the file `Cargo.toml` " ) ? ;
2024-04-21 19:22:01 +01:00
check_cargo_toml ( & info_file . exercises , & current_cargo_toml , b " " ) ? ;
2024-04-17 14:55:50 +01:00
}
2024-04-16 02:30:28 +01:00
2024-07-04 20:12:57 +01:00
let target_dir = parse_target_dir ( ) ? ;
check_exercises ( & info_file , & target_dir ) ? ;
check_solutions ( require_solutions , & info_file , & target_dir ) ? ;
2024-06-01 20:48:15 +01:00
2024-04-16 02:35:23 +01:00
println! ( " \n Everything looks fine! " ) ;
2024-04-16 02:30:28 +01:00
Ok ( ( ) )
2024-04-15 22:54:57 +01:00
}
2024-06-10 16:42:11 +01:00
const SEPARATOR : & [ u8 ] =
b " \n ======================================================================================== \n " ;
2024-07-04 20:12:57 +01:00
const CHECK_EXERCISES_UNSOLVED_ERR : & str = " At least one exercise is already solved or failed to run. See the output above.
If this is an intro exercise that is intended to be already solved , add ` skip_check_unsolved = true ` to the exercise ' s metadata in the ` info . toml ` file . " ;