@@ -99,6 +99,13 @@ function bashunit::coverage::init() {
9999 _BASHUNIT_COVERAGE_TRACK_CACHE=" "
100100 _BASHUNIT_COVERAGE_PATH_CACHE=" "
101101 _BASHUNIT_COVERAGE_IS_PARALLEL=" "
102+ _BASHUNIT_COVERAGE_STATS_FILES=()
103+ _BASHUNIT_COVERAGE_STATS_EXEC=()
104+ _BASHUNIT_COVERAGE_STATS_HIT=()
105+ _BASHUNIT_COVERAGE_STATS_PCT=()
106+ _BASHUNIT_COVERAGE_STATS_CLASS=()
107+ _BASHUNIT_COVERAGE_STATS_COUNT=0
108+ _BASHUNIT_COVERAGE_STATS_LOOKUP=" "
102109
103110 export _BASHUNIT_COVERAGE_DATA_FILE
104111 export _BASHUNIT_COVERAGE_TRACKED_FILES
@@ -190,6 +197,61 @@ function bashunit::coverage::get_file_stats() {
190197 echo " ${executable} :${hit} :${pct} :${class} "
191198}
192199
200+ # Pre-computed file stats cache (avoids redundant per-file reads across reports)
201+ _BASHUNIT_COVERAGE_STATS_FILES=()
202+ _BASHUNIT_COVERAGE_STATS_EXEC=()
203+ _BASHUNIT_COVERAGE_STATS_HIT=()
204+ _BASHUNIT_COVERAGE_STATS_PCT=()
205+ _BASHUNIT_COVERAGE_STATS_CLASS=()
206+ _BASHUNIT_COVERAGE_STATS_COUNT=0
207+ _BASHUNIT_COVERAGE_STATS_LOOKUP=" "
208+
209+ # Pre-compute stats for all tracked files (call once before reports)
210+ function bashunit::coverage::precompute_file_stats() {
211+ _BASHUNIT_COVERAGE_STATS_FILES=()
212+ _BASHUNIT_COVERAGE_STATS_EXEC=()
213+ _BASHUNIT_COVERAGE_STATS_HIT=()
214+ _BASHUNIT_COVERAGE_STATS_PCT=()
215+ _BASHUNIT_COVERAGE_STATS_CLASS=()
216+ _BASHUNIT_COVERAGE_STATS_COUNT=0
217+ _BASHUNIT_COVERAGE_STATS_LOOKUP=" "
218+
219+ local file
220+ while IFS= read -r file; do
221+ { [ -z " $file " ] || [ ! -f " $file " ]; } && continue
222+
223+ local stats executable hit pct class
224+ stats=$( bashunit::coverage::compute_file_coverage " $file " )
225+ executable=" ${stats%%:* } "
226+ hit=" ${stats##*: } "
227+ pct=$( bashunit::coverage::calculate_percentage " $hit " " $executable " )
228+ class=$( bashunit::coverage::get_coverage_class " $pct " )
229+
230+ local idx=" $_BASHUNIT_COVERAGE_STATS_COUNT "
231+ _BASHUNIT_COVERAGE_STATS_FILES[idx]=" $file "
232+ _BASHUNIT_COVERAGE_STATS_EXEC[idx]=" $executable "
233+ _BASHUNIT_COVERAGE_STATS_HIT[idx]=" $hit "
234+ _BASHUNIT_COVERAGE_STATS_PCT[idx]=" $pct "
235+ _BASHUNIT_COVERAGE_STATS_CLASS[idx]=" $class "
236+ _BASHUNIT_COVERAGE_STATS_COUNT=$(( idx + 1 ))
237+ _BASHUNIT_COVERAGE_STATS_LOOKUP=" ${_BASHUNIT_COVERAGE_STATS_LOOKUP} |${file} =${idx} |"
238+ done < <( bashunit::coverage::get_tracked_files)
239+ }
240+
241+ # Look up cached stats for a file, returns "executable:hit:pct:class"
242+ function bashunit::coverage::get_cached_stats() {
243+ local file=" $1 "
244+ case " $_BASHUNIT_COVERAGE_STATS_LOOKUP " in
245+ * " |${file} =" * )
246+ local idx=" ${_BASHUNIT_COVERAGE_STATS_LOOKUP#* " |${file} =" } "
247+ idx=" ${idx%% " |" * } "
248+ echo " ${_BASHUNIT_COVERAGE_STATS_EXEC[idx]} :${_BASHUNIT_COVERAGE_STATS_HIT[idx]} :${_BASHUNIT_COVERAGE_STATS_PCT[idx]} :${_BASHUNIT_COVERAGE_STATS_CLASS[idx]} "
249+ return 0
250+ ;;
251+ esac
252+ bashunit::coverage::get_file_stats " $file "
253+ }
254+
193255function bashunit::coverage::record_line() {
194256 local file=" $1 "
195257 local lineno=" $2 "
@@ -459,8 +521,28 @@ function bashunit::coverage::is_executable_line() {
459521 # Skip empty lines (line with only whitespace) — built-in, no subshell
460522 [ -z " ${line// / } " ] && return 1
461523
462- # Single combined grep covers every non-executable pattern
463- [ " $( echo " $line " | " $GREP " -cE " $_BASHUNIT_COVERAGE_NONEXEC_PATTERN " || true) " -gt 0 ] && return 1
524+ # Fast path: pure Bash checks for common non-executable patterns (no subshell)
525+ local stripped=" ${line# " ${line%% [![:space:]]* } " } "
526+ local _trail=" ${stripped##* [![:space:]]} "
527+ local trimmed=" ${stripped% " $_trail " } "
528+
529+ case " $trimmed " in
530+ ' #' * ) return 1 ;; # Comments (including shebang)
531+ ' {' | ' }' ) return 1 ;; # Braces only
532+ esac
533+
534+ local first=" ${trimmed%% [[:space:]]* } "
535+ case " $first " in
536+ ' then' | ' else' | ' fi' | ' do' | ' done' | ' esac' | ' in' | ' ;;' | ' ;;&' | ' ;&' | ' )' )
537+ local rest=" ${trimmed# " $first " } "
538+ local _rl=" ${rest%% [![:space:]]* } "
539+ rest=" ${rest# " $_rl " } "
540+ case " $rest " in ' ' | ' #' * ) return 1 ;; esac
541+ ;;
542+ esac
543+
544+ # Fallback: grep for complex patterns (function declarations, case patterns, done+redirection)
545+ [ " $( printf ' %s' " $line " | " $GREP " -cE " $_BASHUNIT_COVERAGE_NONEXEC_PATTERN " || true) " -gt 0 ] && return 1
464546
465547 return 0
466548}
@@ -499,11 +581,20 @@ function bashunit::coverage::get_hit_lines() {
499581
500582 # Only count hits that correspond to executable lines
501583 # This prevents >100% coverage when DEBUG trap fires on non-executable lines
584+
585+ # Pre-load file lines into indexed array (avoids sed per line)
586+ local -a file_lines=()
587+ local _idx=0 _fl
588+ while IFS= read -r _fl || [ -n " $_fl " ]; do
589+ file_lines[_idx]=" $_fl "
590+ (( ++ _idx))
591+ done < " $file "
592+
502593 local count=0
503594 local line_num
504595 for line_num in $hit_lines ; do
505- local line_content
506- line_content= $( sed -n " ${line_num} p " " $file " 2> /dev/null ) || continue
596+ local line_content= " ${file_lines[$((line_num - 1))] :- } "
597+ [ -z " $line_content " ] && continue
507598 if bashunit::coverage::is_executable_line " $line_content " " $line_num " ; then
508599 (( ++ count))
509600 fi
@@ -539,13 +630,20 @@ function bashunit::coverage::compute_file_coverage() {
539630 done < <( bashunit::coverage::get_all_line_hits " $file " )
540631
541632 local executable=0 hit=0 lineno=0 line line_hits
542- while IFS= read -r line || [ -n " $line " ]; do
543- lineno=$(( lineno + 1 ))
633+ local -a cv_lines=()
634+ local _cli=0 _cl
635+ while IFS= read -r _cl || [ -n " $_cl " ]; do
636+ cv_lines[_cli]=" $_cl "
637+ (( ++ _cli))
638+ done < " $file "
639+
640+ for line in " ${cv_lines[@]} " ; do
641+ (( ++ lineno))
544642 bashunit::coverage::is_executable_line " $line " " $lineno " || continue
545- executable= $(( executable + 1 ))
643+ (( ++ executable ))
546644 line_hits=${hits_by_line[lineno]:- 0}
547- [ " $line_hits " -gt 0 ] && hit= $(( hit + 1 ))
548- done < " $file "
645+ [ " $line_hits " -gt 0 ] && (( ++ hit ))
646+ done
549647
550648 echo " ${executable} :${hit} "
551649}
@@ -604,16 +702,33 @@ function bashunit::coverage::extract_functions() {
604702 if [ " $in_function " -eq 0 ]; then
605703 local fn_name=" "
606704
607- # Match: name() with optional `function` keyword (parens form)
608- local _re=' ^[[:space:]]*(function[[:space:]]+)?([a-zA-Z_][a-zA-Z0-9_:]*)[[:space:]]*\(\)[[:space:]]*\{?[[:space:]]*(#.*)?$'
609- if [[ " $line " =~ $_re ]]; then
610- fn_name=" ${BASH_REMATCH[2]} "
611- else
612- # Match: function name { (keyword form, no parens)
613- _re=' ^[[:space:]]*(function[[:space:]]+)([a-zA-Z_][a-zA-Z0-9_:]*)[[:space:]]*\{[[:space:]]*(#.*)?$'
614- if [[ " $line " =~ $_re ]]; then
615- fn_name=" ${BASH_REMATCH[2]} "
616- fi
705+ # Extract function name using pure Bash string operations (avoids sed subshell)
706+ local stripped=" ${line# " ${line%% [![:space:]]* } " } "
707+
708+ # Strip "function " prefix if present
709+ case " $stripped " in
710+ function[\ \ ]* )
711+ stripped=" ${stripped# function} "
712+ stripped=" ${stripped# " ${stripped%% [![:space:]]* } " } "
713+ ;;
714+ esac
715+
716+ # Extract first word as candidate function name
717+ fn_name=" ${stripped%% [[:space:]\(\{ ]* } "
718+
719+ # Validate: must start with valid identifier char, and rest must have () or {
720+ if [ -n " $fn_name " ]; then
721+ case " $fn_name " in
722+ [a-zA-Z_]* )
723+ local after_name=" ${stripped# " $fn_name " } "
724+ after_name=" ${after_name# " ${after_name%% [![:space:]]* } " } "
725+ case " $after_name " in
726+ ' ()' * | ' {' * ) ;;
727+ * ) fn_name=" " ;;
728+ esac
729+ ;;
730+ * ) fn_name=" " ;;
731+ esac
617732 fi
618733
619734 if [ -n " $fn_name " ]; then
@@ -631,7 +746,7 @@ function bashunit::coverage::extract_functions() {
631746
632747 # Single-line function: braces balance on same line and both present
633748 if [ " $brace_count " -eq 0 ] && [ " $open_count " -gt 0 ] && [ " $close_count " -gt 0 ]; then
634- echo " ${current_fn} : ${fn_start} : ${lineno} "
749+ echo " ${current_fn} | ${fn_start} | ${lineno} "
635750 in_function=0
636751 current_fn=" "
637752 fi
@@ -676,9 +791,16 @@ function bashunit::coverage::get_function_coverage() {
676791 local hit=0
677792 local lineno=0
678793
794+ # Pre-load file lines into indexed array (avoids sed per line)
795+ local -a fn_lines=()
796+ local _fli=0 _fl
797+ while IFS= read -r _fl || [ -n " $_fl " ]; do
798+ fn_lines[_fli]=" $_fl "
799+ (( ++ _fli))
800+ done < " $file "
801+
679802 for (( lineno = fn_start; lineno <= fn_end; lineno++ )) ; do
680- local line_content
681- line_content=$( sed -n " ${lineno} p" " $file " 2> /dev/null) || continue
803+ local line_content=" ${fn_lines[$((lineno - 1))]:- } "
682804
683805 if bashunit::coverage::is_executable_line " $line_content " " $lineno " ; then
684806 (( ++ executable))
@@ -701,16 +823,24 @@ function bashunit::coverage::get_percentage() {
701823 local total_executable=0
702824 local total_hit=0
703825
704- while IFS= read -r file; do
705- { [ -z " $file " ] || [ ! -f " $file " ]; } && continue
826+ if [ " $_BASHUNIT_COVERAGE_STATS_COUNT " -gt 0 ]; then
827+ local i
828+ for (( i = 0 ; i < _BASHUNIT_COVERAGE_STATS_COUNT; i++ )) ; do
829+ total_executable=$(( total_executable + _BASHUNIT_COVERAGE_STATS_EXEC[i]))
830+ total_hit=$(( total_hit + _BASHUNIT_COVERAGE_STATS_HIT[i]))
831+ done
832+ else
833+ while IFS= read -r file; do
834+ { [ -z " $file " ] || [ ! -f " $file " ]; } && continue
706835
707- local executable hit
708- executable=$( bashunit::coverage::get_executable_lines " $file " )
709- hit=$( bashunit::coverage::get_hit_lines " $file " )
836+ local executable hit
837+ executable=$( bashunit::coverage::get_executable_lines " $file " )
838+ hit=$( bashunit::coverage::get_hit_lines " $file " )
710839
711- total_executable=$(( total_executable + executable))
712- total_hit=$(( total_hit + hit))
713- done < <( bashunit::coverage::get_tracked_files)
840+ total_executable=$(( total_executable + executable))
841+ total_hit=$(( total_hit + hit))
842+ done < <( bashunit::coverage::get_tracked_files)
843+ fi
714844
715845 bashunit::coverage::calculate_percentage " $total_hit " " $total_executable "
716846}
@@ -733,14 +863,14 @@ function bashunit::coverage::report_text() {
733863 { [ -z " $file " ] || [ ! -f " $file " ]; } && continue
734864 has_files=true
735865
736- local stats executable hit pct class
737- stats=$( bashunit::coverage::get_file_stats " $file " )
866+ local executable hit pct class stats rest
867+ stats=$( bashunit::coverage::get_cached_stats " $file " )
738868 executable=" ${stats%%:* } "
739- stats =" ${stats#*: } "
740- hit=" ${stats %%:* } "
741- stats =" ${stats #*: } "
742- pct=" ${stats %%:* } "
743- class=" ${stats # #*: } "
869+ rest =" ${stats#*: } "
870+ hit=" ${rest %%:* } "
871+ rest =" ${rest #*: } "
872+ pct=" ${rest %%:* } "
873+ class=" ${rest #*: } "
744874
745875 total_executable=$(( total_executable + executable))
746876 total_hit=$(( total_hit + hit))
@@ -806,16 +936,22 @@ function bashunit::coverage::report_lcov() {
806936 done < <( bashunit::coverage::get_all_line_hits " $file " )
807937
808938 local lineno=0 executable=0 hit=0 line line_hits
809- # shellcheck disable=SC2094
810- while IFS= read -r line || [ -n " $line " ]; do
811- lineno=$(( lineno + 1 ))
812- bashunit::coverage::is_executable_line " $line " " $lineno " || continue
813- executable=$(( executable + 1 ))
814- line_hits=${hits_by_line[lineno]:- 0}
815- [ " $line_hits " -gt 0 ] && hit=$(( hit + 1 ))
816- echo " DA:${lineno} ,${line_hits} "
939+ local -a lcov_lines=()
940+ local _lli=0 _ll
941+ while IFS= read -r _ll || [ -n " $_ll " ]; do
942+ lcov_lines[_lli]=" $_ll "
943+ (( ++ _lli))
817944 done < " $file "
818945
946+ for line in " ${lcov_lines[@]} " ; do
947+ (( ++ lineno))
948+ bashunit::coverage::is_executable_line " $line " " $lineno " || continue
949+ (( ++ executable))
950+ local lh=" ${hits_by_line[$lineno]:- 0} "
951+ [ " $lh " -gt 0 ] && (( ++ hit))
952+ echo " DA:${lineno} ,${lh} "
953+ done
954+
819955 echo " LF:$executable "
820956 echo " LH:$hit "
821957 echo " end_of_record"
@@ -878,7 +1014,7 @@ function bashunit::coverage::report_html() {
8781014 { [ -z " $file " ] || [ ! -f " $file " ]; } && continue
8791015
8801016 local stats executable hit pct
881- stats=$( bashunit::coverage::get_file_stats " $file " )
1017+ stats=$( bashunit::coverage::get_cached_stats " $file " )
8821018 executable=" ${stats%%:* } "
8831019 stats=" ${stats#*: } "
8841020 hit=" ${stats%%:* } "
@@ -1285,11 +1421,14 @@ function bashunit::coverage::generate_file_html() {
12851421 local output_file=" $2 "
12861422
12871423 local display_file=" ${file# " $( pwd) " / } "
1288- local executable hit pct class
1289- executable=$( bashunit::coverage::get_executable_lines " $file " )
1290- hit=$( bashunit::coverage::get_hit_lines " $file " )
1291- pct=$( bashunit::coverage::calculate_percentage " $hit " " $executable " )
1292- class=$( bashunit::coverage::get_coverage_class " $pct " )
1424+ local executable hit pct class stats rest
1425+ stats=$( bashunit::coverage::get_cached_stats " $file " )
1426+ executable=" ${stats%%:* } "
1427+ rest=" ${stats#*: } "
1428+ hit=" ${rest%%:* } "
1429+ rest=" ${rest#*: } "
1430+ pct=" ${rest%%:* } "
1431+ class=" ${rest#*: } "
12931432 local uncovered=$(( executable - hit))
12941433
12951434 # Pre-load all line hits into indexed array (performance optimization)
@@ -1299,6 +1438,14 @@ function bashunit::coverage::generate_file_html() {
12991438 hits_by_line[_ln]=$_cnt
13001439 done < <( bashunit::coverage::get_all_line_hits " $file " )
13011440
1441+ # Pre-load all file lines into indexed array (avoids sed per line)
1442+ local -a file_lines=()
1443+ local _fli=0 _fl
1444+ while IFS= read -r _fl || [ -n " $_fl " ]; do
1445+ file_lines[_fli]=" $_fl "
1446+ (( ++ _fli))
1447+ done < " $file "
1448+
13021449 # Pre-load test hits data into indexed array (for tooltips)
13031450 # Index: line number, Value: newline-separated list of "test_file:test_function"
13041451 # Using indexed array for Bash 3.0 compatibility (no associative arrays)
@@ -1567,7 +1714,7 @@ EOF
15671714 local ln
15681715 for (( ln = fn_start; ln <= fn_end; ln++ )) ; do
15691716 local ln_content
1570- ln_content=$( sed -n " ${ln} p " " $file " 2> /dev/null ) || continue
1717+ ln_content=" ${file_lines[$((ln - 1))] :- } "
15711718 if bashunit::coverage::is_executable_line " $ln_content " " $ln " ; then
15721719 (( ++ fn_executable))
15731720 local ln_hits=${hits_by_line[$ln]:- 0}
@@ -1622,7 +1769,7 @@ EOF
16221769
16231770 local lineno=0
16241771 local line
1625- while IFS= read -r line || [ -n " $line " ] ; do
1772+ for line in " ${file_lines[@]} " ; do
16261773 (( ++ lineno))
16271774
16281775 local escaped_line
@@ -1666,7 +1813,7 @@ EOF
16661813 echo " <td class=\" hits\" >$hits_display </td>"
16671814 echo " <td class=\" code\" >$escaped_line </td>"
16681815 echo " </tr>"
1669- done < " $file "
1816+ done
16701817
16711818 cat << 'EOF '
16721819 </table>
0 commit comments