| 1 | #!/usr/bin/env bash
|
| 2 | #
|
| 3 | # Run tests against multiple shells with the sh_spec framework.
|
| 4 | #
|
| 5 | # Usage:
|
| 6 | # test/spec-runner.sh <function name>
|
| 7 |
|
| 8 | set -o nounset
|
| 9 | set -o pipefail
|
| 10 | set -o errexit
|
| 11 | shopt -s strict:all 2>/dev/null || true # dogfood for OSH
|
| 12 |
|
| 13 | REPO_ROOT=$(cd "$(dirname $0)/.."; pwd)
|
| 14 |
|
| 15 | source build/dev-shell.sh
|
| 16 | source test/common.sh
|
| 17 | source test/spec-common.sh
|
| 18 | source test/tsv-lib.sh # $TAB
|
| 19 |
|
| 20 | NUM_SPEC_TASKS=${NUM_SPEC_TASKS:-400}
|
| 21 |
|
| 22 | # Option to use our xargs implementation.
|
| 23 | #xargs() {
|
| 24 | # echo "Using ~/git/oilshell/xargs.py/xargs.py"
|
| 25 | # ~/git/oilshell/xargs.py/xargs.py "$@"
|
| 26 | #}
|
| 27 |
|
| 28 | #
|
| 29 | # Test Runner
|
| 30 | #
|
| 31 |
|
| 32 | write-suite-manifests() {
|
| 33 | #test/sh_spec.py --print-table spec/*.test.sh
|
| 34 | { test/sh_spec.py --print-table spec/*.test.sh | while read suite name; do
|
| 35 | case $suite in
|
| 36 | osh) echo $name >& $osh ;;
|
| 37 | ysh) echo $name >& $ysh ;;
|
| 38 | disabled) ;; # ignore
|
| 39 | *) die "Invalid suite $suite" ;;
|
| 40 | esac
|
| 41 | done
|
| 42 | } {osh}>_tmp/spec/SUITE-osh.txt \
|
| 43 | {ysh}>_tmp/spec/SUITE-ysh.txt \
|
| 44 | {needs_terminal}>_tmp/spec/SUITE-needs-terminal.txt
|
| 45 |
|
| 46 | # These are kind of pseudo-suites, not the main 3
|
| 47 | test/sh_spec.py --print-tagged interactive \
|
| 48 | spec/*.test.sh > _tmp/spec/SUITE-interactive.txt
|
| 49 |
|
| 50 | test/sh_spec.py --print-tagged dev-minimal \
|
| 51 | spec/*.test.sh > _tmp/spec/SUITE-osh-minimal.txt
|
| 52 | }
|
| 53 |
|
| 54 |
|
| 55 | diff-manifest() {
|
| 56 | ### temporary test
|
| 57 |
|
| 58 | write-suite-manifests
|
| 59 | #return
|
| 60 |
|
| 61 | # crazy sorting, affects glob
|
| 62 | # doesn't work
|
| 63 | #LANG=C
|
| 64 | #LC_COLLATE=C
|
| 65 | #LC_ALL=C
|
| 66 | #export LANG LC_COLLATE LC_ALL
|
| 67 |
|
| 68 | for suite in osh ysh interactive osh-minimal; do
|
| 69 | echo
|
| 70 | echo [$suite]
|
| 71 | echo
|
| 72 |
|
| 73 | diff -u -r <(sort spec2/SUITE-$suite.txt) <(sort _tmp/spec/SUITE-$suite.txt) #|| true
|
| 74 | done
|
| 75 | }
|
| 76 |
|
| 77 | dispatch-one() {
|
| 78 | # Determines what binaries to compare against: compare-py | compare-cpp | release-alpine
|
| 79 | local compare_mode=${1:-compare-py}
|
| 80 | # Which subdir of _tmp/spec: osh-py ysh-py osh-cpp ysh-cpp smoosh
|
| 81 | local spec_subdir=${2:-osh-py}
|
| 82 | local spec_name=$3
|
| 83 | shift 3 # rest are more flags
|
| 84 |
|
| 85 | log "__ $spec_name"
|
| 86 |
|
| 87 | local -a prefix
|
| 88 | case $compare_mode in
|
| 89 |
|
| 90 | compare-py) prefix=(test/spec.sh) ;;
|
| 91 |
|
| 92 | compare-cpp) prefix=(test/spec-cpp.sh run-file) ;;
|
| 93 |
|
| 94 | # For interactive comparison
|
| 95 | osh-only) prefix=(test/spec-util.sh run-file-with-osh) ;;
|
| 96 | bash-only) prefix=(test/spec-util.sh run-file-with-bash) ;;
|
| 97 |
|
| 98 | release-alpine) prefix=(test/spec-alpine.sh run-file) ;;
|
| 99 |
|
| 100 | *) die "Invalid compare mode $compare_mode" ;;
|
| 101 | esac
|
| 102 |
|
| 103 | local base_dir=_tmp/spec/$spec_subdir
|
| 104 |
|
| 105 | # TODO: Could --stats-{file,template} be a separate awk step on .tsv files?
|
| 106 | run-task-with-status \
|
| 107 | $base_dir/${spec_name}.task.txt \
|
| 108 | "${prefix[@]}" $spec_name \
|
| 109 | --format html \
|
| 110 | --stats-file $base_dir/${spec_name}.stats.txt \
|
| 111 | --stats-template \
|
| 112 | '%(num_cases)d %(oils_num_passed)d %(oils_num_failed)d %(oils_failures_allowed)d %(oils_ALT_delta)d' \
|
| 113 | "$@" \
|
| 114 | > $base_dir/${spec_name}.html
|
| 115 | }
|
| 116 |
|
| 117 |
|
| 118 | _html-summary() {
|
| 119 | ### Print an HTML summary to stdout and return whether all tests succeeded
|
| 120 |
|
| 121 | local sh_label=$1 # osh or ysh
|
| 122 | local base_dir=$2 # e.g. _tmp/spec/ysh-cpp
|
| 123 | local totals=$3 # path to print HTML to
|
| 124 | local manifest=$4
|
| 125 |
|
| 126 | html-head --title "Spec Test Summary" \
|
| 127 | ../../../web/base.css ../../../web/spec-tests.css
|
| 128 |
|
| 129 | cat <<EOF
|
| 130 | <body class="width50">
|
| 131 |
|
| 132 | <p id="home-link">
|
| 133 | <!-- The release index is two dirs up -->
|
| 134 | <a href="../..">Up</a> |
|
| 135 | <a href="/">oilshell.org</a>
|
| 136 | </p>
|
| 137 |
|
| 138 | <h1>Spec Test Results Summary</h1>
|
| 139 |
|
| 140 | <table>
|
| 141 | <thead>
|
| 142 | <tr>
|
| 143 | <td>name</td>
|
| 144 | <td># cases</td> <td>$sh_label # passed</td> <td>$sh_label # failed</td>
|
| 145 | <td>$sh_label failures allowed</td>
|
| 146 | <td>$sh_label ALT delta</td>
|
| 147 | <td>Elapsed Seconds</td>
|
| 148 | </tr>
|
| 149 | </thead>
|
| 150 | <!-- TOTALS -->
|
| 151 | EOF
|
| 152 |
|
| 153 | # Awk notes:
|
| 154 | # - "getline" is kind of like bash "read", but it doesn't allow you do
|
| 155 | # specify variable names. You have to destructure it yourself.
|
| 156 | # - Lack of string interpolation is very annoying
|
| 157 |
|
| 158 | head -n $NUM_SPEC_TASKS $manifest | sort | awk -v totals=$totals -v base_dir=$base_dir '
|
| 159 | # Awk problem: getline errors are ignored by default!
|
| 160 | function error(path) {
|
| 161 | print "Error reading line from file: " path > "/dev/stderr"
|
| 162 | exit(1)
|
| 163 | }
|
| 164 |
|
| 165 | {
|
| 166 | spec_name = $0
|
| 167 |
|
| 168 | # Read from the task files
|
| 169 | path = ( base_dir "/" spec_name ".task.txt" )
|
| 170 | n = getline < path
|
| 171 | if (n != 1) {
|
| 172 | error(path)
|
| 173 | }
|
| 174 | status = $1
|
| 175 | wall_secs = $2
|
| 176 |
|
| 177 | path = ( base_dir "/" spec_name ".stats.txt" )
|
| 178 | n = getline < path
|
| 179 | if (n != 1) {
|
| 180 | error(path)
|
| 181 | }
|
| 182 | num_cases = $1
|
| 183 | oils_num_passed = $2
|
| 184 | oils_num_failed = $3
|
| 185 | oils_failures_allowed = $4
|
| 186 | oils_ALT_delta = $5
|
| 187 |
|
| 188 | sum_status += status
|
| 189 | sum_wall_secs += wall_secs
|
| 190 | sum_num_cases += num_cases
|
| 191 | sum_oils_num_passed += oils_num_passed
|
| 192 | sum_oils_num_failed += oils_num_failed
|
| 193 | sum_oils_failures_allowed += oils_failures_allowed
|
| 194 | sum_oils_ALT_delta += oils_ALT_delta
|
| 195 | num_rows += 1
|
| 196 |
|
| 197 | # For the console
|
| 198 | if (status == 0) {
|
| 199 | num_passed += 1
|
| 200 | } else {
|
| 201 | num_failed += 1
|
| 202 | print spec_name " failed with status " status > "/dev/stderr"
|
| 203 | }
|
| 204 |
|
| 205 | if (status != 0) {
|
| 206 | css_class = "failed"
|
| 207 | } else if (oils_num_failed != 0) {
|
| 208 | css_class = "osh-allow-fail"
|
| 209 | } else if (oils_num_passed != 0) {
|
| 210 | css_class = "osh-pass"
|
| 211 | } else {
|
| 212 | css_class = ""
|
| 213 | }
|
| 214 | print "<tr class=" css_class ">"
|
| 215 | print "<td><a href=" spec_name ".html>" spec_name "</a></td>"
|
| 216 | print "<td>" num_cases "</td>"
|
| 217 | print "<td>" oils_num_passed "</td>"
|
| 218 | print "<td>" oils_num_failed "</td>"
|
| 219 | print "<td>" oils_failures_allowed "</td>"
|
| 220 | print "<td>" oils_ALT_delta "</td>"
|
| 221 | printf("<td>%.2f</td>\n", wall_secs);
|
| 222 | print "</tr>"
|
| 223 | }
|
| 224 |
|
| 225 | END {
|
| 226 | print "<tr class=totals>" >totals
|
| 227 | print "<td>TOTAL (" num_rows " rows) </td>" >totals
|
| 228 | print "<td>" sum_num_cases "</td>" >totals
|
| 229 | print "<td>" sum_oils_num_passed "</td>" >totals
|
| 230 | print "<td>" sum_oils_num_failed "</td>" >totals
|
| 231 | print "<td>" sum_oils_failures_allowed "</td>" >totals
|
| 232 | print "<td>" sum_oils_ALT_delta "</td>" >totals
|
| 233 | printf("<td>%.2f</td>\n", sum_wall_secs) > totals
|
| 234 | print "</tr>" >totals
|
| 235 |
|
| 236 | print "<tfoot>"
|
| 237 | print "<!-- TOTALS -->"
|
| 238 | print "</tfoot>"
|
| 239 |
|
| 240 | # For the console
|
| 241 | print "" > "/dev/stderr"
|
| 242 | if (num_failed == 0) {
|
| 243 | print "*** All " num_passed " tests PASSED" > "/dev/stderr"
|
| 244 | } else {
|
| 245 | print "*** " num_failed " tests FAILED" > "/dev/stderr"
|
| 246 | exit(1) # failure
|
| 247 | }
|
| 248 | }
|
| 249 | '
|
| 250 | all_passed=$?
|
| 251 |
|
| 252 | cat <<EOF
|
| 253 | </table>
|
| 254 |
|
| 255 | <h3>Version Information</h3>
|
| 256 | <pre>
|
| 257 | EOF
|
| 258 |
|
| 259 | # TODO: can pass shells here, e.g. for test/spec-cpp.sh
|
| 260 | test/spec-version.sh ${suite}-version-text
|
| 261 |
|
| 262 | cat <<EOF
|
| 263 | </pre>
|
| 264 | </body>
|
| 265 | </html>
|
| 266 | EOF
|
| 267 |
|
| 268 | return $all_passed
|
| 269 | }
|
| 270 |
|
| 271 | html-summary() {
|
| 272 | local suite=$1
|
| 273 | local base_dir=$2
|
| 274 |
|
| 275 | local manifest="_tmp/spec/SUITE-$suite.txt"
|
| 276 |
|
| 277 | local totals=$base_dir/totals-$suite.html
|
| 278 | local tmp=$base_dir/tmp-$suite.html
|
| 279 |
|
| 280 | local out=$base_dir/index.html
|
| 281 |
|
| 282 | # TODO: Do we also need $base_dir/{osh,oil}-details-for-toil.json
|
| 283 | # osh failures, and all failures
|
| 284 | # When deploying, if they exist, them copy them outside?
|
| 285 | # I guess toil_web.py can use the zipfile module?
|
| 286 | # To get _tmp/spec/...
|
| 287 | # it can read JSON like:
|
| 288 | # { "task_tsv": "_tmp/toil/INDEX.tsv",
|
| 289 | # "details_json": [ ... ],
|
| 290 | # }
|
| 291 |
|
| 292 | set +o errexit
|
| 293 | _html-summary $suite $base_dir $totals $manifest > $tmp
|
| 294 | all_passed=$?
|
| 295 | set -o errexit
|
| 296 |
|
| 297 | # Total rows are displayed at both the top and bottom.
|
| 298 | awk -v totals="$(cat $totals)" '
|
| 299 | /<!-- TOTALS -->/ {
|
| 300 | print totals
|
| 301 | next
|
| 302 | }
|
| 303 | { print }
|
| 304 | ' < $tmp > $out
|
| 305 |
|
| 306 | echo
|
| 307 | echo "Results: file://$PWD/$out"
|
| 308 |
|
| 309 | return $all_passed
|
| 310 | }
|
| 311 |
|
| 312 | _all-parallel() {
|
| 313 | local suite=${1:-osh}
|
| 314 | local compare_mode=${2:-compare-py}
|
| 315 | local spec_subdir=${3:-survey}
|
| 316 |
|
| 317 | # The rest are more flags
|
| 318 | shift 3
|
| 319 |
|
| 320 | local manifest="_tmp/spec/SUITE-$suite.txt"
|
| 321 | local output_base_dir="_tmp/spec/$spec_subdir"
|
| 322 | mkdir -p $output_base_dir
|
| 323 |
|
| 324 | write-suite-manifests
|
| 325 |
|
| 326 | # The exit codes are recorded in files for html-summary to aggregate.
|
| 327 | set +o errexit
|
| 328 | head -n $NUM_SPEC_TASKS $manifest \
|
| 329 | | xargs -I {} -P $MAX_PROCS -- \
|
| 330 | $0 dispatch-one $compare_mode $spec_subdir {} "$@"
|
| 331 | set -o errexit
|
| 332 |
|
| 333 | all-tests-to-html $manifest $output_base_dir
|
| 334 |
|
| 335 | # note: the HTML links to ../../web/, which is in the repo.
|
| 336 | html-summary $suite $output_base_dir # returns whether all passed
|
| 337 | }
|
| 338 |
|
| 339 | all-parallel() {
|
| 340 | ### Run spec tests in parallel.
|
| 341 |
|
| 342 | # Note that this function doesn't fail because 'run-file' saves the status
|
| 343 | # to a file.
|
| 344 |
|
| 345 | time $0 _all-parallel "$@"
|
| 346 | }
|
| 347 |
|
| 348 | all-tests-to-html() {
|
| 349 | local manifest=$1
|
| 350 | local output_base_dir=$2
|
| 351 | # ignore attrs output
|
| 352 | head -n $NUM_SPEC_TASKS $manifest \
|
| 353 | | xargs --verbose -- doctools/src_tree.py spec-files $output_base_dir >/dev/null
|
| 354 |
|
| 355 | #| xargs -n 1 -P $MAX_PROCS -- $0 test-to-html $output_base_dir
|
| 356 | log "done: all-tests-to-html"
|
| 357 | }
|
| 358 |
|
| 359 | shell-sanity-check() {
|
| 360 | echo "PWD = $PWD"
|
| 361 | echo "PATH = $PATH"
|
| 362 |
|
| 363 | for sh in "$@"; do
|
| 364 | # note: shells are in $PATH, but not $OSH_LIST
|
| 365 | if ! $sh -c 'echo -n "hello from $0: "; command -v $0 || true'; then
|
| 366 | echo "ERROR: $sh failed sanity check"
|
| 367 | return 1
|
| 368 | fi
|
| 369 | done
|
| 370 | }
|
| 371 |
|
| 372 | filename=$(basename $0)
|
| 373 | if test "$filename" = 'spec-runner.sh'; then
|
| 374 | "$@"
|
| 375 | fi
|