diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index abc9f4b528..b6c4860fb8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -188,8 +188,10 @@ jobs: if: matrix.test_commit_records run: yarn test-upsert-comment + # The format check gives the same result everywhere; it runs on Linux + # only, where it is fastest. - name: Run tests - run: node scripts/test.js -all + run: node scripts/test.js ${{ runner.os == 'Linux' && '-all' || '-ounit -mocha -build -docstrings' }} - name: Check for diffs in tests folder run: git diff --ignore-cr-at-eol --exit-code tests @@ -393,38 +395,52 @@ jobs: # Checks that the website builds with the generated API docs. These # steps stay in this job rather than in one that needs build-compiler, - # which would wait for the slowest platform before starting. - - name: Checkout rescript-lang.org + # which would wait for the slowest platform before starting. The build + # depends only on the docs and the website commit, so an empty cache + # entry keyed on both records each pair that built, and a pull request + # whose docs match an earlier build skips it. + - name: Website build key + id: website-build-key if: ${{ matrix.generate_api_docs }} + env: + WEBSITE_BRANCH: ${{ steps.api-docs-plan.outputs.website-branch }} + run: | + sha=$(git ls-remote https://github.com/rescript-lang/rescript-lang.org.git "refs/heads/$WEBSITE_BRANCH" | cut -f1) + if [ -z "$sha" ]; then + echo "::error::The website has no branch $WEBSITE_BRANCH" + exit 1 + fi + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "value=website-build-v1-$sha-${{ hashFiles('scripts/res/apiDocs/**') }}" >> "$GITHUB_OUTPUT" + shell: bash + + - name: Look up website build + id: website-build + if: ${{ matrix.generate_api_docs }} + uses: actions/cache/restore@v6 + with: + path: tmp/website-build + key: ${{ steps.website-build-key.outputs.value }} + lookup-only: true + + - name: Checkout rescript-lang.org + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} uses: actions/checkout@v7 with: repository: rescript-lang/rescript-lang.org - ref: ${{ steps.api-docs-plan.outputs.website-branch }} + ref: ${{ steps.website-build-key.outputs.sha }} path: rescript-lang.org - - name: Check for API docs changes - id: api-docs-diff - if: ${{ matrix.generate_api_docs }} - run: | - cp -R scripts/res/apiDocs/. rescript-lang.org/data/api/ - git -C rescript-lang.org status - if [[ -z "$(git -C rescript-lang.org status --porcelain)" ]]; then - echo "changed=false" >> "$GITHUB_OUTPUT" - else - echo "changed=true" >> "$GITHUB_OUTPUT" - fi - shell: bash - # The website is checked out inside this repository, so its Yarn also # reads this repository's .yarnrc.yml and rejects the settings it does # not know yet. The steps that need this config are done. - name: Hide the compiler's Yarn config from the website - if: ${{ matrix.generate_api_docs && steps.api-docs-diff.outputs.changed == 'true' }} + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} run: mv .yarnrc.yml "$RUNNER_TEMP/rescript.yarnrc.yml" shell: bash - name: Use Node.js for website - if: ${{ matrix.generate_api_docs && steps.api-docs-diff.outputs.changed == 'true' }} + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} uses: actions/setup-node@v7 with: cache: yarn @@ -432,11 +448,27 @@ jobs: node-version-file: rescript-lang.org/.node-version - name: Build website with the API docs - if: ${{ matrix.generate_api_docs && steps.api-docs-diff.outputs.changed == 'true' }} + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} run: | + cp -R ../scripts/res/apiDocs/. data/api/ yarn yarn build working-directory: rescript-lang.org + shell: bash + + - name: Record website build + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} + run: mkdir -p tmp/website-build && echo "$KEY" > tmp/website-build/key + env: + KEY: ${{ steps.website-build-key.outputs.value }} + shell: bash + + - name: Save website build record + if: ${{ matrix.generate_api_docs && steps.website-build.outputs.cache-hit != 'true' }} + uses: actions/cache/save@v6 + with: + path: tmp/website-build + key: ${{ steps.website-build-key.outputs.value }} # Compares the words the compiler allocates on fixed inputs, and the # instructions it executes on some of them, with the base branch diff --git a/rewatch/tests/utils.sh b/rewatch/tests/utils.sh index 62a32ca6c0..9dec90d43c 100644 --- a/rewatch/tests/utils.sh +++ b/rewatch/tests/utils.sh @@ -60,14 +60,21 @@ replace() { fi } -wait_for_pid_gone() { - local pid="$1"; local timeout - timeout=${2:-20} - while process_is_running "$pid" && [ "$timeout" -gt 0 ]; do - sleep 1 - timeout=$((timeout - 1)) +# Runs a command every 0.1 seconds until it succeeds or the timeout in +# seconds (the first argument) has passed. A one-second interval made every +# wait in the suite take up to a second longer than needed. +poll() { + local deadline=$((SECONDS + $1)); shift + until "$@"; do + [ "$SECONDS" -lt "$deadline" ] || return 1 + sleep 0.1 done - ! process_is_running "$pid" +} + +process_is_gone() { ! process_is_running "$1"; } + +wait_for_pid_gone() { + poll "${2:-20}" process_is_gone "$1" } exit_watcher() { @@ -116,62 +123,25 @@ clear_locks() { rm -f lib/rescript/watch.lock lib/rescript/build.lock } -wait_for_file() { - local file="$1"; local timeout - timeout=${2:-60} - while [ "$timeout" -gt 0 ]; do - [ -f "$file" ] && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 -} +file_exists() { [ -f "$1" ]; } +file_is_gone() { [ ! -f "$1" ]; } +file_has_pattern() { grep -q "$2" "$1" 2>/dev/null; } -wait_for_pattern() { - local file="$1"; local pattern="$2"; local timeout - timeout=${3:-60} - while [ "$timeout" -gt 0 ]; do - grep -q "$pattern" "$file" 2>/dev/null && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 +file_lacks_pattern() { + [ -f "$1" ] && ! grep -q "$2" "$1" 2>/dev/null } -wait_for_pattern_count() { - local file="$1"; local pattern="$2"; local expected="$3"; local timeout - timeout=${4:-60} - while [ "$timeout" -gt 0 ]; do - local current_count - current_count=$(grep -c "$pattern" "$file" 2>/dev/null || true) - current_count=${current_count:-0} - [ "$current_count" -ge "$expected" ] && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 +file_has_pattern_count() { + local current_count + current_count=$(grep -c "$2" "$1" 2>/dev/null || true) + [ "${current_count:-0}" -ge "$3" ] } -wait_for_pattern_gone() { - local file="$1"; local pattern="$2"; local timeout - timeout=${3:-60} - while [ "$timeout" -gt 0 ]; do - if [ -f "$file" ] && ! grep -q "$pattern" "$file" 2>/dev/null; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 -} +wait_for_file() { poll "${2:-60}" file_exists "$1"; } +wait_for_file_gone() { poll "${2:-60}" file_is_gone "$1"; } +wait_for_pattern() { poll "${3:-60}" file_has_pattern "$1" "$2"; } +wait_for_pattern_gone() { poll "${3:-60}" file_lacks_pattern "$1" "$2"; } -wait_for_file_gone() { - local file="$1"; local timeout - timeout=${2:-60} - while [ "$timeout" -gt 0 ]; do - [ ! -f "$file" ] && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 +wait_for_pattern_count() { + poll "${4:-60}" file_has_pattern_count "$1" "$2" "$3" } diff --git a/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh b/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh index a41f7b10b8..489a2e6ca2 100755 --- a/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh +++ b/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh @@ -15,17 +15,12 @@ else exit 1 fi +completed_log_changed() { + [ -f "$1" ] && ! cmp -s "$1" "$2" && grep -q "#Done(" "$1" 2>/dev/null +} + wait_for_changed_completed_log() { - local file="$1"; local baseline="$2"; local timeout - timeout=${3:-60} - while [ "$timeout" -gt 0 ]; do - if [ -f "$file" ] && ! cmp -s "$file" "$baseline" && grep -q "#Done(" "$file" 2>/dev/null; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 + poll "${3:-60}" completed_log_changed "$1" "$2" } COMPILER_LOG="./packages/watch-warnings/lib/rescript/.compiler.log" diff --git a/rewatch/tests/watch/04-watch-config-change.sh b/rewatch/tests/watch/04-watch-config-change.sh index 15e79e6a8f..209e7c456a 100755 --- a/rewatch/tests/watch/04-watch-config-change.sh +++ b/rewatch/tests/watch/04-watch-config-change.sh @@ -42,17 +42,12 @@ wait_for_next_build() { "$completed_builds" 30 } +tracked_outputs_exist() { + [ -z "$(git ls-files --deleted -- '*.mjs')" ] +} + wait_for_tracked_outputs() { - local timeout - timeout=60 - while [ "$timeout" -gt 0 ]; do - if [ -z "$(git ls-files --deleted -- '*.mjs')" ]; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 + poll 60 tracked_outputs_exist } # Change the suffix in rescript.json (same approach as suffix test) @@ -118,12 +113,10 @@ else fi # The rebuild after the restore also removes the outputs with the new suffix. -timeout=30 -while [ "$timeout" -gt 0 ] && [ -n "$(find . -name '*.res.mjs' -print -quit)" ]; do - sleep 1 - timeout=$((timeout - 1)) -done -if [ -n "$(find . -name '*.res.mjs' -print -quit)" ]; then +suffixed_outputs_gone() { + [ -z "$(find . -name '*.res.mjs' -print -quit)" ] +} +if ! poll 30 suffixed_outputs_gone; then error "Outputs with the restored configuration's previous suffix were not removed" find . -name '*.res.mjs' git restore --worktree -- ./src/Test.res diff --git a/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh b/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh index 7de72226c2..90f8efa548 100755 --- a/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh +++ b/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh @@ -44,18 +44,13 @@ REWATCH_COMPILER_DOMAINS=1 "$REWATCH_EXECUTABLE" watch > rewatch.log 2>&1 & watch_pid=$! compiler_log=lib/rescript/.compiler.log +compiler_log_changed() { + [ -f "$compiler_log" ] && ! cmp -s "$compiler_log" "$1" && + grep -q '#Done(' "$compiler_log" +} + wait_for_changed_compiler_log() { - local baseline="$1"; local timeout - timeout=60 - while [ "$timeout" -gt 0 ]; do - if [ -f "$compiler_log" ] && ! cmp -s "$compiler_log" "$baseline" && - grep -q '#Done(' "$compiler_log"; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 + poll 60 compiler_log_changed "$1" } if ! wait_for_pattern_count rewatch.log "Finished .*compilation" 1 60; then diff --git a/rewatch/tests/watch/08-watch-overlapping-source-removed.sh b/rewatch/tests/watch/08-watch-overlapping-source-removed.sh index e8c8066b57..0b6b594708 100755 --- a/rewatch/tests/watch/08-watch-overlapping-source-removed.sh +++ b/rewatch/tests/watch/08-watch-overlapping-source-removed.sh @@ -42,18 +42,13 @@ retained_output() { rewatch_bg watch > rewatch.log 2>&1 & success "Watcher Started" +retained_output_has() { + output=$(retained_output) + [ -n "$output" ] && grep -q "$1" "$output" +} + wait_for_retained_value() { - local value=$1 - local timeout=30 - while [ "$timeout" -gt 0 ]; do - output=$(retained_output) - if [ -n "$output" ] && grep -q "$value" "$output"; then - return 0 - fi - sleep 1 - timeout=$((timeout - 1)) - done - return 1 + poll 30 retained_output_has "$1" } if ! wait_for_retained_value "= 1"; then @@ -70,12 +65,10 @@ success "Initial build completed" # watched on its own mkdir "$RETAINED/nested" echo 'let value = 1' > "$RETAINED/nested/Nested.res" -timeout=30 -until [ -n "$(find "$RETAINED/nested" -name 'Nested.*js')" ] || [ "$timeout" -le 0 ]; do - sleep 1 - timeout=$((timeout - 1)) -done -if [ -n "$(find "$RETAINED/nested" -name 'Nested.*js')" ]; then +nested_output_exists() { + [ -n "$(find "$RETAINED/nested" -name 'Nested.*js')" ] +} +if poll 30 nested_output_exists; then success "A folder created inside the retained folder is watched" else error "A folder created inside the retained folder is not watched"