@@ -164,6 +164,8 @@ autofix=false
164164failfast=false
165165pattern=" "
166166papermill_opt=" "
167+ jobs=1
168+ data_dir=" "
167169
168170kernelspec=" python3"
169171
@@ -173,7 +175,7 @@ NB_OUTPUT_LINE_CAP=100
173175
174176function print_usage {
175177 echo " runner.sh [--no-run] [--no-checks] [--autofix] [-f/--failfast] [-p/--pattern <find pattern>] [-h/--help]"
176- echo " [-v/--version] [--verbose]"
178+ echo " [-v/--version] [--verbose] [-j/--jobs <N>] [--data-dir <path>] "
177179 echo " "
178180 echo " MONAI tutorials testing utilities. When running the notebooks, we first search for variables, such as"
179181 echo " \" max_epochs\" and set them to 1 to reduce testing time."
@@ -184,12 +186,21 @@ function print_usage {
184186 echo " --autofix : autofix where possible"
185187 echo " --cell-standard : check guidelines standards such as ## setup environment cell blocks"
186188 echo " --copyright : check whether every source code and notebook has a copyright header"
187- echo " -f, --failfast : stop on first error"
189+ echo " -f, --failfast : stop on first error (ignored when --jobs > 1) "
188190 echo " -p, --pattern : pattern of files to be run (added to \` find . -type f -name *.ipynb -and ! -wholename *.ipynb_checkpoints*\` )"
189191 echo " -h, --help : show this help message and exit"
190192 echo " -t, --test : shortcut to run a single notebook using pattern \` -and -wholename\` "
191193 echo " -v, --version : show MONAI and system version information and exit"
192- echo " --verbose : show papermill logs when testing the noteboobks"
194+ echo " --verbose : show papermill logs when testing the notebooks"
195+ echo " -j, --jobs N : run N notebooks in parallel (default: 1). Each notebook logs independently;"
196+ echo " logs are printed in original order after all jobs finish."
197+ echo " Note: parallel jobs share the same Python environment; use --jobs 1 when"
198+ echo " notebooks do conflicting pip installs."
199+ echo " --data-dir PATH : set MONAI_DATA_DIRECTORY to PATH before running notebooks. Notebooks that"
200+ echo " respect this env var will persist downloads there instead of a temp dir."
201+ echo " Tip: mount a host directory at this path in Docker to cache across runs:"
202+ echo " docker run ... -v /host/data:/data -e MONAI_DATA_DIRECTORY=/data ..."
203+ echo " or pass --data-dir /data to this script after the Docker bind-mount."
193204 echo " "
194205 echo " Examples:"
195206 echo " ./runner.sh # run full tests (${green} recommended before making pull requests${noColor} )."
@@ -201,6 +212,8 @@ function print_usage {
201212 echo " # check filenames containing \" read\" or \" load\" , but not if the"
202213 echo " whole path contains \" deepgrow\" ."
203214 echo " ./runner.sh --kernelspec \" kernel\" # Set the kernelspec value used to run notebooks, default is \" python3\" ."
215+ echo " ./runner.sh -j 4 --data-dir /data/monai_cache"
216+ echo " # run 4 notebooks in parallel; reuse cached downloads."
204217 echo " ./runner.sh --no-checks --no-run --copyright
205218 echo " # test if all notebooks and scripts have the copyright header"
206219 echo " ./runner.sh --no-checks --no-run --cell-standard
251264 echo $pattern
252265 shift
253266 ;;
267+ -j|--jobs)
268+ jobs=" $2 "
269+ shift
270+ ;;
271+ --data-dir)
272+ data_dir=" $2 "
273+ shift
274+ ;;
254275 -k|--kernelspec)
255276 kernelspec=" $2 "
256277 shift
433454base_path=" $( cd " $( dirname " $0 " ) " > /dev/null 2>&1 ; pwd -P ) "
434455cd " ${base_path} "
435456
457+ # Export MONAI_DATA_DIRECTORY so notebooks persist downloads across runs.
458+ # 109/117 runnable notebooks honour this env var (pattern:
459+ # directory = os.environ.get("MONAI_DATA_DIRECTORY")
460+ # root_dir = tempfile.mkdtemp() if directory is None else directory
461+ # Without it each notebook re-downloads on every container run.
462+ if [ -n " $data_dir " ]; then
463+ mkdir -p " $data_dir "
464+ export MONAI_DATA_DIRECTORY=" $data_dir "
465+ echo " Data cache: $data_dir (MONAI_DATA_DIRECTORY)"
466+ elif [ -n " ${MONAI_DATA_DIRECTORY:- } " ]; then
467+ echo " Data cache: $MONAI_DATA_DIRECTORY (MONAI_DATA_DIRECTORY from environment)"
468+ fi
469+
436470function replace_text {
437471 oldString=" ${s} \s*=\s*[0-9]\+"
438472 newString=" ${s} = 1"
485519
486520# #######################################################################
487521# #
488- # loop over files #
522+ # per-notebook logic (used by both sequential and parallel paths) #
489523# #
490524# #######################################################################
491- for file in " ${files[@]} " ; do
492- current_test_successful=0
525+ # _run_notebook FILE RESULT_FILE
526+ # Runs PEP8 checks and/or papermill for FILE.
527+ # Writes 0 (pass) or 1 (fail) to RESULT_FILE.
528+ # Must be called in a subshell so cwd changes are isolated.
529+ function _run_notebook {
530+ local file=" $1 "
531+ local result_file=" $2 "
532+ local current_test_successful=0
493533
494534 echo " ${separator}${blue} Running $file ${noColor} "
495535
496- # Get to file's folder and get file contents
536+ local path filename
497537 path=" $( dirname " ${file} " ) "
498538 filename=" $( basename " ${file} " ) "
499- cd ${base_path} /${path}
539+ cd " ${base_path} /${path} "
500540
501- # #######################################################################
502- # #
503- # code checks #
504- # #
505- # #######################################################################
541+ # ###################################################################
542+ # code checks #
543+ # ###################################################################
506544 if [ $doChecks = true ]; then
507545
508546 if [ $autofix = true ]; then
@@ -513,33 +551,26 @@ for file in "${files[@]}"; do
513551 --pipe " sed 's/ = list()/ = []/'"
514552 fi
515553
516- # to check flake8, convert to python script, don't check
517- # magic cells, and don't check line length for comment
518- # lines (as this includes markdown), and then run flake8
519554 echo Checking PEP8 compliance...
520555 jupytext " $filename " --opt custom_cell_magics=" writefile" -w --to script -o - | \
521556 sed ' s/\(^\s*\)%/\1pass # %/' | \
522557 sed ' s/\(^#.*\)$/\1 # noqa: E501/' | \
523558 flake8 - --show-source --extend-ignore=E203,N812,W503 --max-line-length 120
524- success=$?
525- if [ ${success} -ne 0 ]
526- then
559+ local success=$?
560+ if [ ${success} -ne 0 ]; then
527561 print_error_msg " Try running with autofixes: ${green} --autofix${noColor} "
528- test_fail ${success}
562+ current_test_successful=1
529563 fi
530564 fi
531565
532- # #######################################################################
533- # #
534- # run notebooks with papermill #
535- # #
536- # #######################################################################
537- if [ $doRun = true ]; then
538-
539- skipRun=false
566+ # ###################################################################
567+ # run notebook with papermill #
568+ # ###################################################################
569+ if [ $doRun = true ] && [ $current_test_successful -eq 0 ]; then
540570
571+ local skipRun=false
541572 for skip_pattern in " ${skip_run_papermill[@]} " ; do
542- if [[ $file =~ $skip_pattern ]]; then
573+ if [[ $file =~ $skip_pattern ]]; then
543574 echo " Skip Pattern Match"
544575 skipRun=true
545576 break
@@ -548,45 +579,112 @@ for file in "${files[@]}"; do
548579
549580 if [ $skipRun = true ]; then
550581 echo " Skipping"
551- continue
582+ echo " $current_test_successful " > " $result_file "
583+ return
552584 fi
553585
554586 echo Running notebook...
587+ local notebook
555588 notebook=$( cat " $filename " )
556589
557- # if compulsory keyword, max_epochs, missing...
558590 if [[ ! " $notebook " =~ " max_epochs" ]]; then
559- # and notebook isn't in list of those expected to not have that keyword...
560- should_contain_max_epochs=true
591+ local should_contain_max_epochs=true
561592 for e in " ${doesnt_contain_max_epochs[@]} " ; do
562593 [[ " $e " == " $filename " ]] && should_contain_max_epochs=false && break
563594 done
564- # then error
565595 if [[ $should_contain_max_epochs == true ]]; then
566596 print_error_msg " Couldn't find the keyword \" max_epochs\" , and the notebook wasn't on the list of expected exemptions (\" doesnt_contain_max_epochs\" )."
567- test_fail 1
597+ current_test_successful=1
598+ echo " $current_test_successful " > " $result_file "
599+ return
568600 fi
569601 fi
570602
571- # Set some variables to 1 to speed up proceedings
572- strings_to_replace=(max_epochs val_interval disc_train_interval disc_train_steps num_batches_for_histogram)
603+ local strings_to_replace=(max_epochs val_interval disc_train_interval disc_train_steps num_batches_for_histogram)
573604 for s in " ${strings_to_replace[@]} " ; do
574605 replace_text
575606 done
576607
577608 python -c ' import monai; monai.config.print_config()'
578609
610+ local cmd
579611 cmd=$( echo " papermill ${papermill_opt} --progress-bar --log-output -k ${kernelspec} " )
580612 echo " $cmd "
613+ local out
581614 time out=$( echo " $notebook " | eval " $cmd " )
582- success=$?
615+ local success=$?
583616 if [[ ${success} -ne 0 || " $out " =~ " \" status\" : \" failed\" " ]]; then
584- test_fail ${success}
617+ current_test_successful=1
585618 fi
586619 fi
587620
588- num_tested=$(( num_tested + 1 ))
589- if [[ ${current_test_successful} -eq 0 ]]; then
590- num_successful_tests=$(( num_successful_tests + 1 ))
591- fi
592- done
621+ echo " $current_test_successful " > " $result_file "
622+ }
623+
624+ # #######################################################################
625+ # #
626+ # loop over files — sequential (jobs=1) or parallel (jobs>1) #
627+ # #
628+ # #######################################################################
629+ if [ " $jobs " -le 1 ]; then
630+ # ---------------------------------------------------------------- #
631+ # Sequential path — original behaviour, unchanged #
632+ # ---------------------------------------------------------------- #
633+ for file in " ${files[@]} " ; do
634+ current_test_successful=0
635+ _result_file=$( mktemp)
636+
637+ ( trap - EXIT; _run_notebook " $file " " $_result_file " )
638+
639+ current_test_successful=$( cat " $_result_file " 2> /dev/null || echo 1)
640+ rm -f " $_result_file "
641+
642+ num_tested=$(( num_tested + 1 ))
643+ if [[ ${current_test_successful} -eq 0 ]]; then
644+ num_successful_tests=$(( num_successful_tests + 1 ))
645+ elif [ $failfast = true ]; then
646+ finish
647+ fi
648+ done
649+
650+ else
651+ # ---------------------------------------------------------------- #
652+ # Parallel path — N notebooks run concurrently #
653+ # ---------------------------------------------------------------- #
654+ echo " Running ${# files[@]} notebooks with --jobs $jobs "
655+ _work_dir=$( mktemp -d)
656+
657+ for file in " ${files[@]} " ; do
658+ # Throttle: wait until a slot is free
659+ while [ " $( jobs -rp | wc -l) " -ge " $jobs " ]; do
660+ wait -n 2> /dev/null || sleep 0.2
661+ done
662+
663+ _slug=$( printf ' %s' " $file " | tr ' /.' ' --' )
664+ _log=" ${_work_dir} /${_slug} .log"
665+ _result=" ${_work_dir} /${_slug} .result"
666+
667+ (
668+ trap - EXIT
669+ set +e
670+ _run_notebook " $file " " $_result "
671+ ) > " $_log " 2>&1 &
672+ done
673+
674+ wait # wait for all remaining background jobs
675+
676+ # Print logs in original notebook order; collect pass/fail counts
677+ for file in " ${files[@]} " ; do
678+ _slug=$( printf ' %s' " $file " | tr ' /.' ' --' )
679+ _log=" ${_work_dir} /${_slug} .log"
680+ _result=" ${_work_dir} /${_slug} .result"
681+
682+ cat " $_log "
683+ num_tested=$(( num_tested + 1 ))
684+ if [ " $( cat " $_result " 2> /dev/null) " = " 0" ]; then
685+ num_successful_tests=$(( num_successful_tests + 1 ))
686+ fi
687+ done
688+
689+ rm -rf " $_work_dir "
690+ fi
0 commit comments