130130end_ns=$(date +%s%N)
131131duration_ms=$(( (end_ns - start_ns)/1000000 ))
132132printf 'curl %s duration_ms=%s exit_code=%s%s\n' "$*" "$duration_ms" "$exit_code" "$curl_extra" >>"$log_dir/curl.log"
133+ # Also emit structured JSON line for robust parsing
134+ json_extra=""
135+ if [[ -n "$curl_extra" ]]; then
136+ # extract numeric time_total if present
137+ tt=$(grep -oE 'time_total=[0-9.]+' <<<"$curl_extra" | cut -d= -f2 || true)
138+ if [[ -n "$tt" ]]; then
139+ json_extra=",\"time_total_s\":$tt"
140+ fi
141+ fi
142+ printf '{"url":"%s","duration_ms":%s,"exit_code":%s%s}\n' "${url:-UNKNOWN}" "$duration_ms" "$exit_code" "$json_extra" >>"$log_dir/curl.jsonl"
133143exit "$exit_code"
134144EOF
135145 chmod +x " $fixture /stubs/curl"
@@ -374,6 +384,50 @@ write_results_json() {
374384 fi
375385 done < " $p /.state/curl.log"
376386 fi
387+ # Parse structured JSON lines if present
388+ if [[ -f " $p /.state/curl.jsonl" ]]; then
389+ while IFS= read -r jline; do
390+ # Validate minimal JSON (contains url & duration_ms)
391+ if [[ " $jline " == ' {' * ' url' * ]]; then
392+ api_entries+=(" $jline " )
393+ fi
394+ done < " $p /.state/curl.jsonl"
395+ fi
396+ done
397+
398+ # Build aggregated stats per URL
399+ declare -A URL_DURS URL_FAILS URL_COUNT
400+ for entry in " ${api_entries[@]} " ; do
401+ url=$( grep -oE ' "url":"[^"]+"' <<< " $entry" | cut -d' "' -f4)
402+ dur=$( grep -oE ' "duration_ms":[0-9]+' <<< " $entry" | cut -d: -f2)
403+ ec=$( grep -oE ' "exit_code":[0-9]+' <<< " $entry" | cut -d: -f2)
404+ [[ -z " $url " ]] && continue
405+ URL_DURS[$url ]=" ${URL_DURS[$url]} $dur "
406+ URL_COUNT[$url ]=$(( ${URL_COUNT[$url]:- 0} + 1 ))
407+ if (( ec != 0 )) ; then
408+ URL_FAILS[$url ]=$(( ${URL_FAILS[$url]:- 0} + 1 ))
409+ fi
410+ done
411+
412+ api_stats_json=()
413+ for u in " ${! URL_COUNT[@]} " ; do
414+ # Compute avg, max, p95
415+ dlist=( ${URL_DURS[$u]} )
416+ sum=0; max=0
417+ for d in " ${dlist[@]} " ; do
418+ (( d > max )) && max=$d
419+ sum=$(( sum + d))
420+ done
421+ count=${URL_COUNT[$u]}
422+ avg=$(( sum / count ))
423+ # p95 (simple: sort and pick index ceil(0.95*n)-1)
424+ sorted=($( printf ' %s\n' " ${dlist[@]} " | sort -n) )
425+ idx=$(( (95 * count + 99 ) / 100 - 1 ))
426+ (( idx < 0 )) && idx=0
427+ (( idx >= count )) && idx=$(( count- 1 ))
428+ p95=${sorted[$idx]}
429+ fails=${URL_FAILS[$u]:- 0}
430+ api_stats_json+=(" {\" url\" :\" $u \" ,\" count\" :$count ,\" failures\" :$fails ,\" avg_ms\" :$avg ,\" max_ms\" :$max ,\" p95_ms\" :$p95 }" )
377431 done
378432
379433 {
@@ -388,10 +442,26 @@ write_results_json() {
388442 echo ' "tests": ['
389443 local i last=$(( ${# TEST_NAMES[@]} - 1 ))
390444 for i in " ${! TEST_NAMES[@]} " ; do
391- printf ' {"name": %q, "status": %q, "duration_ms": %s}' " ${TEST_NAMES[$i]} " " ${TEST_STATUSES[$i]} " " ${TEST_DURATIONS_MS[$i]} "
445+ # Use printf %q then convert bash escaping to JSON-friendly (remove leading/trailing quotes if present)
446+ tn=$( printf %q " ${TEST_NAMES[$i]} " )
447+ ts=$( printf %q " ${TEST_STATUSES[$i]} " )
448+ # Replace escaped spaces (\ ) with space, and escaped parentheses
449+ # Clean bash-style escapes (\ ) (\() (\)) left by %q for readability
450+ tn=${tn// \\ / }
451+ tn=${tn// \\ (/ (}
452+ tn=${tn// \\ )/ )}
453+ ts=${ts// \\ / }
454+ printf ' {"name": "%s", "status": "%s", "duration_ms": %s}' " $tn " " $ts " " ${TEST_DURATIONS_MS[$i]} "
392455 if (( i < last )) ; then echo ' ,' ; else echo ; fi
393456 done
394457 echo ' ],'
458+ echo ' "api_stats": ['
459+ local k last3=$(( ${# api_stats_json[@]} - 1 ))
460+ for k in " ${! api_stats_json[@]} " ; do
461+ printf ' %s' " ${api_stats_json[$k]} "
462+ if (( k < last3 )) ; then echo ' ,' ; else echo ; fi
463+ done
464+ echo ' ],'
395465 echo ' "api_calls": ['
396466 local j last2=$(( ${# api_entries[@]} - 1 ))
397467 for j in " ${! api_entries[@]} " ; do
0 commit comments