From 1b11690aa1e88f069ffa1d06fbfb04420a7ea1fc Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 22 Sep 2026 15:51:25 +0200 Subject: [PATCH 1/5] Fix deterministic rewatch scheduling Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 + rewatch/src/build/compile.rs | 38 ++++---- ...chedule-independent-modules-after-error.sh | 92 +++++++++++++++++++ rewatch/tests/suite.sh | 1 + 4 files changed, 117 insertions(+), 16 deletions(-) create mode 100755 rewatch/tests/compile/20-schedule-independent-modules-after-error.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index d444142eb85..e1aa7b066cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ #### :bug: Bug fix +- Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation. https://github.com/rescript-lang/rescript/pull/8667 + #### :memo: Documentation #### :nail_care: Polish diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 6a0868dd507..9f726e724b1 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -440,7 +440,11 @@ pub fn compile( rayon::in_place_scope(|scope| { let mut in_flight: usize = 0; loop { - while in_flight < capacity && !has_errors { + // Keep dispatching work that was already independent of any + // failed module. Otherwise diagnostics vary with worker count: + // a warning-producing module may or may not have started before + // an unrelated error finishes. + while in_flight < capacity { let Some(work) = ready_heap.pop() else { break }; let module_name = work.module_name.clone(); let is_dirty = dirty_set.contains(&module_name); @@ -462,10 +466,6 @@ pub fn compile( } if in_flight == 0 { - if !ready_heap.is_empty() { - // Errors suppressed new spawns; nothing left to drain. - break; - } if completed.len() < compile_universe_count && !has_errors { stalled = true; } @@ -475,7 +475,8 @@ pub fn compile( let Ok(msg) = rx.recv() else { break }; in_flight -= 1; - if msg.result.is_err() || msg.interface_result.as_ref().is_some_and(|r| r.is_err()) { + let failed = msg.result.is_err() || msg.interface_result.as_ref().is_some_and(|r| r.is_err()); + if failed { has_errors = true; } @@ -493,16 +494,21 @@ pub fn compile( if !compile_universe.contains(dep) { continue; } + // A successful interface can replace the CMI even when its + // implementation fails. Keep dependents dirty for recovery, + // but do not release them until the module succeeds. if !is_clean { dirty_set.insert(dep.clone()); } - let count = pending_deps.get_mut(dep).unwrap(); - *count -= 1; - if *count == 0 && !completed.contains(dep) { - ready_heap.push(WorkUnit { - priority: priorities[dep], - module_name: dep.clone(), - }); + if !failed { + let count = pending_deps.get_mut(dep).unwrap(); + *count -= 1; + if *count == 0 && !completed.contains(dep) { + ready_heap.push(WorkUnit { + priority: priorities[dep], + module_name: dep.clone(), + }); + } } } } @@ -523,9 +529,9 @@ pub fn compile( let mut num_compiled_modules = 0; // Persist propagated dirtiness back onto build_state. Modules that were - // marked dirty (because a predecessor's cmi changed) but never scheduled - // — e.g. the first compile error aborted further dispatch — must keep - // compile_dirty = true so the next incremental build recompiles them. + // marked dirty (because a predecessor's cmi changed) but blocked by a + // failed prerequisite must keep compile_dirty = true so the next + // incremental build recompiles them. // Successful recompiles in the result loop below override this back to // false for the modules that actually ran. for name in &dirty_set { diff --git a/rewatch/tests/compile/20-schedule-independent-modules-after-error.sh b/rewatch/tests/compile/20-schedule-independent-modules-after-error.sh new file mode 100755 index 00000000000..170b0f764a9 --- /dev/null +++ b/rewatch/tests/compile/20-schedule-independent-modules-after-error.sh @@ -0,0 +1,92 @@ +#!/bin/bash + +cd $(dirname $0) +source "../utils.sh" + +bold "Test: Independent modules compile after an unrelated error" + +fixture=$(mktemp -d 2>/dev/null || mktemp -d -t rewatch-schedule-after-error) +trap "rm -rf '$fixture'" EXIT + +mkdir -p "$fixture/src" + +cat > "$fixture/rescript.json" <<'EOF' +{ + "name": "rewatch-schedule-after-error", + "sources": { "dir": "src" }, + "warnings": { "number": "+26+27+32", "error": false }, + "package-specs": { "module": "esmodule", "in-source": true }, + "suffix": ".mjs" +} +EOF + +cat > "$fixture/src/Failing.res" <<'EOF' +let value = 1 +EOF + +cat > "$fixture/src/Blocked.res" <<'EOF' +let mirrored = Failing.value +EOF + +cat > "$fixture/src/IndependentWarning.res" <<'EOF' +let unusedValue = 42 + +let hello = () => Console.log("hello") +EOF + +cd "$fixture" +if ! RAYON_NUM_THREADS=1 RUST_BACKTRACE=1 "$REWATCH_EXECUTABLE" build > /dev/null 2>&1; then + error "Initial fixture build failed" + exit 1 +fi + +# Make the dependent's generated output an observable signal: the failing build +# must leave it absent, and the recovery build must recreate it. +rm -f src/Blocked.mjs + +cat > src/Failing.res <<'EOF' +let value: int = "not an int" +EOF + +cat > src/IndependentWarning.res <<'EOF' +let unusedValue = 42 + +let hello = () => { + let secondUnusedValue = 43 + Console.log("hello") +} +EOF + +compiler_output=$(RAYON_NUM_THREADS=1 RUST_BACKTRACE=1 "$REWATCH_EXECUTABLE" build 2>&1) +build_status=$? + +if [ $build_status -eq 0 ]; then + error "Build unexpectedly succeeded despite the type error" + printf "%s\n" "$compiler_output" >&2 + exit 1 +fi + +if ! printf '%s\n' "$compiler_output" | grep -q "unused variable secondUnusedValue"; then + error "Independent warning was not emitted after the unrelated failure" + printf "%s\n" "$compiler_output" >&2 + exit 1 +fi + +if [ -f src/Blocked.mjs ]; then + error "Blocked dependent was compiled despite its failed dependency" + exit 1 +fi + +printf 'let value = "recovered"\n' > src/Failing.res +if ! compiler_output=$(RAYON_NUM_THREADS=1 RUST_BACKTRACE=1 "$REWATCH_EXECUTABLE" build 2>&1); then + error "Build did not recover after fixing the failed dependency" + printf "%s\n" "$compiler_output" >&2 + exit 1 +fi + +if [ ! -f src/Blocked.mjs ]; then + error "Previously blocked dependent was not compiled after recovery" + exit 1 +fi + +success "Independent diagnostics and blocked dependents are scheduled correctly" diff --git a/rewatch/tests/suite.sh b/rewatch/tests/suite.sh index 22cc77935d7..69d888af06a 100755 --- a/rewatch/tests/suite.sh +++ b/rewatch/tests/suite.sh @@ -132,6 +132,7 @@ fi ./compile/17-prod-flag.sh && ./compile/18-external-dep-uncurried-dot.sh && ./compile/19-utf8-warning.sh && +./compile/20-schedule-independent-modules-after-error.sh && ./compile/14-no-testrepo-changes.sh && ./compile/15-no-new-files.sh && ./compile/16-snapshots-unchanged.sh && From 3a85c133d50d58e0d80551d235d7d40c687ec590 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Tue, 22 Sep 2026 15:51:36 +0200 Subject: [PATCH 2/5] Stabilize rewatch integration tests Signed-off-by: Christoph Knittel --- rewatch/AGENTS.md | 2 +- .../tests/clean/01-clean-single-project.sh | 2 +- .../tests/clean/02-clean-dev-dependencies.sh | 2 +- rewatch/tests/clean/03-clean-node-modules.sh | 2 +- .../04-clean-rebuild-no-compiler-update.sh | 15 +-- rewatch/tests/compile/08-remove-file.sh | 2 +- rewatch/tests/compile/09-dependency-cycle.sh | 2 +- .../11-dev-dependency-non-dev-source.sh | 2 +- .../compile/13-no-infinite-loop-with-cycle.sh | 2 +- rewatch/tests/compile/17-prod-flag.sh | 6 +- rewatch/tests/format/01-format-all-files.sh | 2 +- rewatch/tests/format/02-format-single-file.sh | 2 +- .../tests/format/04-format-current-project.sh | 2 +- rewatch/tests/suffix/01-custom-suffix.sh | 1 - rewatch/tests/suite.sh | 4 +- rewatch/tests/utils.sh | 70 ++++++++++++- rewatch/tests/watch/01-watch-recompile.sh | 24 ++++- .../02-watch-warnings-persist-atomic-save.sh | 21 ++-- .../tests/watch/02-watch-warnings-persist.sh | 46 ++++----- rewatch/tests/watch/03-watch-new-file.sh | 21 +++- rewatch/tests/watch/04-watch-config-change.sh | 71 +++++++++++--- .../watch/05-watch-ignores-non-source.sh | 1 - .../watch/06-watch-missing-source-folder.sh | 7 +- ...d-interface-after-failed-implementation.sh | 97 +++++++++++++++++++ 24 files changed, 310 insertions(+), 96 deletions(-) create mode 100755 rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh diff --git a/rewatch/AGENTS.md b/rewatch/AGENTS.md index e0e6846cd19..496d108d5f1 100644 --- a/rewatch/AGENTS.md +++ b/rewatch/AGENTS.md @@ -217,5 +217,5 @@ When clippy suggests refactoring that could impact performance, consider the tra ## CI Gotchas - **`sleep` is fragile** — Prefer polling (e.g., `wait_for_file`) over fixed sleeps. CI runners are slower than local machines. -- **`exit_watcher` is async** — It only signals the watcher to stop (removes the lock file), it doesn't wait for the process to exit. Avoid triggering config-change events before exiting, as the watcher may start a concurrent rebuild. +- **Wait for watcher shutdown with `exit_watcher`** — It removes the lock file and waits for the recorded watcher process to exit. Check its return status before continuing when later mutations could race with the watcher. - **`sed -i` differs across platforms** — macOS requires `sed -i '' ...`, Linux does not. Use the `replace` / `normalize_paths` helpers from `rewatch/tests/utils.sh` instead of raw `sed`. diff --git a/rewatch/tests/clean/01-clean-single-project.sh b/rewatch/tests/clean/01-clean-single-project.sh index 7d5aba613da..19971273fc2 100755 --- a/rewatch/tests/clean/01-clean-single-project.sh +++ b/rewatch/tests/clean/01-clean-single-project.sh @@ -42,7 +42,7 @@ other_project_compiled_files=$(find packages/new-namespace -type f -name '*.mjs' if [ "$other_project_compiled_files" -gt 0 ]; then success "Didn't clean other project files" - git restore . + git restore --worktree -- . else error "Expected files from new-namespace not to be cleaned" exit 1 diff --git a/rewatch/tests/clean/02-clean-dev-dependencies.sh b/rewatch/tests/clean/02-clean-dev-dependencies.sh index 2d039a9243d..a2f13f7528c 100755 --- a/rewatch/tests/clean/02-clean-dev-dependencies.sh +++ b/rewatch/tests/clean/02-clean-dev-dependencies.sh @@ -31,7 +31,7 @@ project_compiled_files=$(find packages/pure-dev -type f -name '*.mjs' | wc -l | if [ "$project_compiled_files" -eq 0 ]; then success "pure-dev cleaned" - git restore . + git restore --worktree -- . else error "Expected 0 .mjs files in pure-dev after clean, got $project_compiled_files" printf "%s\n" "$error_output" diff --git a/rewatch/tests/clean/03-clean-node-modules.sh b/rewatch/tests/clean/03-clean-node-modules.sh index 03630c4d55a..7fe0810062e 100755 --- a/rewatch/tests/clean/03-clean-node-modules.sh +++ b/rewatch/tests/clean/03-clean-node-modules.sh @@ -31,7 +31,7 @@ compiler_assets=$(find node_modules/rescript-nodejs/lib/ocaml -type f -name '*.* if [ $compiler_assets -eq 0 ]; then success "compiler assets from node_modules cleaned" - git restore . + git restore --worktree -- . else error "Expected 0 files in node_modules/rescript-nodejs/lib/ocaml after clean, got $compiler_assets" printf "%s\n" "$error_output" diff --git a/rewatch/tests/clean/04-clean-rebuild-no-compiler-update.sh b/rewatch/tests/clean/04-clean-rebuild-no-compiler-update.sh index e2b5108221c..d108707fdf3 100755 --- a/rewatch/tests/clean/04-clean-rebuild-no-compiler-update.sh +++ b/rewatch/tests/clean/04-clean-rebuild-no-compiler-update.sh @@ -18,24 +18,25 @@ else exit 1 fi -# Rebuild with snapshot output -snapshot_file=../tests/snapshots/clean-rebuild.txt -rewatch build &> $snapshot_file +# Rebuild with captured output. This test checks one lifecycle message; writing +# into a tracked snapshot made the fixture dirty whenever work counts changed. +output_file=$(mktemp "${TMPDIR:-/tmp}/rewatch-clean-rebuild.XXXXXX") +trap 'rm -f "$output_file"' EXIT +rewatch build &> "$output_file" build_status=$? -normalize_paths $snapshot_file if [ $build_status -eq 0 ]; then success "Repo Built" else error "Error Building Repo" - cat $snapshot_file >&2 + cat "$output_file" >&2 exit 1 fi # Verify the undesired message is NOT present -if grep -q "Cleaned previous build due to compiler update" $snapshot_file; then +if grep -q "Cleaned previous build due to compiler update" "$output_file"; then error "Unexpected compiler-update clean message present in rebuild logs" - cat $snapshot_file >&2 + cat "$output_file" >&2 exit 1 else success "No compiler-update clean message present after explicit clean" diff --git a/rewatch/tests/compile/08-remove-file.sh b/rewatch/tests/compile/08-remove-file.sh index b38d6a8fba7..19610b91ab3 100755 --- a/rewatch/tests/compile/08-remove-file.sh +++ b/rewatch/tests/compile/08-remove-file.sh @@ -11,7 +11,7 @@ rewatch build &> /dev/null rm packages/dep02/src/Dep02.res rewatch build &> ../tests/snapshots/remove-file.txt normalize_paths ../tests/snapshots/remove-file.txt -git checkout -- packages/dep02/src/Dep02.res +git restore --worktree -- packages/dep02/src/Dep02.res rewatch build &> /dev/null diff --git a/rewatch/tests/compile/09-dependency-cycle.sh b/rewatch/tests/compile/09-dependency-cycle.sh index 7c287b81d69..41ae9f8ef25 100755 --- a/rewatch/tests/compile/09-dependency-cycle.sh +++ b/rewatch/tests/compile/09-dependency-cycle.sh @@ -11,7 +11,7 @@ rewatch build &> /dev/null echo 'Dep01.log()' >> packages/new-namespace/src/NS_alias.res rewatch build &> ../tests/snapshots/dependency-cycle.txt normalize_paths ../tests/snapshots/dependency-cycle.txt -git checkout -- packages/new-namespace/src/NS_alias.res +git restore --worktree -- packages/new-namespace/src/NS_alias.res rewatch build &> /dev/null diff --git a/rewatch/tests/compile/11-dev-dependency-non-dev-source.sh b/rewatch/tests/compile/11-dev-dependency-non-dev-source.sh index 5fe5d7b7b8d..3352c08e8c7 100755 --- a/rewatch/tests/compile/11-dev-dependency-non-dev-source.sh +++ b/rewatch/tests/compile/11-dev-dependency-non-dev-source.sh @@ -13,7 +13,7 @@ rewatch build &> /dev/null echo 'open WebAPI' >> packages/with-dev-deps/src/FileToTest.res rewatch build &> ../tests/snapshots/dev-dependency-used-by-non-dev-source.txt normalize_paths ../tests/snapshots/dev-dependency-used-by-non-dev-source.txt -git checkout -- packages/with-dev-deps/src/FileToTest.res +git restore --worktree -- packages/with-dev-deps/src/FileToTest.res rewatch build &> /dev/null diff --git a/rewatch/tests/compile/13-no-infinite-loop-with-cycle.sh b/rewatch/tests/compile/13-no-infinite-loop-with-cycle.sh index cf369ae749c..e4e27a0a23d 100755 --- a/rewatch/tests/compile/13-no-infinite-loop-with-cycle.sh +++ b/rewatch/tests/compile/13-no-infinite-loop-with-cycle.sh @@ -7,7 +7,7 @@ bold "Test: It should not loop when clean building with a cycle" rewatch clean &> /dev/null echo 'Dep01.log()' >> packages/new-namespace/src/NS_alias.res -git checkout -- packages/new-namespace/src/NS_alias.res +git restore --worktree -- packages/new-namespace/src/NS_alias.res rewatch build &> /dev/null success "No infinite loop detected" diff --git a/rewatch/tests/compile/17-prod-flag.sh b/rewatch/tests/compile/17-prod-flag.sh index c3b5572deaa..faecce057fc 100755 --- a/rewatch/tests/compile/17-prod-flag.sh +++ b/rewatch/tests/compile/17-prod-flag.sh @@ -17,7 +17,7 @@ then success "Build without --prod correctly failed with nonexistent dev-dependency" else error "Build without --prod should have failed with nonexistent dev-dependency" - git checkout -- rescript.json + git restore --worktree -- rescript.json exit 1 fi @@ -27,12 +27,12 @@ then success "Build with --prod succeeded despite nonexistent dev-dependency" else error "Build with --prod should have succeeded by skipping dev-dependencies" - git checkout -- rescript.json + git restore --worktree -- rescript.json exit 1 fi rewatch clean &> /dev/null -git checkout -- rescript.json +git restore --worktree -- rescript.json # Test 2: --prod should skip dev source files rewatch clean &> /dev/null diff --git a/rewatch/tests/format/01-format-all-files.sh b/rewatch/tests/format/01-format-all-files.sh index 1b1ece46b6b..7631f9f788a 100755 --- a/rewatch/tests/format/01-format-all-files.sh +++ b/rewatch/tests/format/01-format-all-files.sh @@ -11,7 +11,7 @@ git_diff_file_count=$(git diff --name-only ./ | wc -l | xargs) if [ $? -eq 0 ] && [ $git_diff_file_count -eq 8 ]; then success "Test package formatted. Got $git_diff_file_count changed files." - git restore . + git restore --worktree -- . else error "Error formatting test package" echo "Expected 8 files to be changed, got $git_diff_file_count" diff --git a/rewatch/tests/format/02-format-single-file.sh b/rewatch/tests/format/02-format-single-file.sh index e5594956f2d..040d7b82b79 100755 --- a/rewatch/tests/format/02-format-single-file.sh +++ b/rewatch/tests/format/02-format-single-file.sh @@ -10,7 +10,7 @@ git_diff_file_count=$(git diff --name-only ./ | wc -l | xargs) if [ $? -eq 0 ] && [ $git_diff_file_count -eq 1 ]; then success "Single file formatted successfully" - git restore . + git restore --worktree -- . else error "Error formatting single file" echo $error_output diff --git a/rewatch/tests/format/04-format-current-project.sh b/rewatch/tests/format/04-format-current-project.sh index 80fc751553c..c8a2b30ff11 100755 --- a/rewatch/tests/format/04-format-current-project.sh +++ b/rewatch/tests/format/04-format-current-project.sh @@ -10,7 +10,7 @@ git_diff_file_count=$(git diff --name-only ./ | wc -l | xargs) if [ $? -eq 0 ] && [ $git_diff_file_count -eq 2 ]; then success "file-casing formatted" - git restore . + git restore --worktree -- . else error "Error formatting current project file-casing" echo "Expected 2 files to be changed, got $git_diff_file_count" diff --git a/rewatch/tests/suffix/01-custom-suffix.sh b/rewatch/tests/suffix/01-custom-suffix.sh index 1dd4669aefb..0485d49c2ab 100755 --- a/rewatch/tests/suffix/01-custom-suffix.sh +++ b/rewatch/tests/suffix/01-custom-suffix.sh @@ -6,7 +6,6 @@ cd ../../testrepo bold "Test: It should support custom suffixes" # Clean Repo -sleep 1 error_output=$(rewatch clean 2>&1) if [ $? -eq 0 ]; then diff --git a/rewatch/tests/suite.sh b/rewatch/tests/suite.sh index 69d888af06a..3209eae41d8 100755 --- a/rewatch/tests/suite.sh +++ b/rewatch/tests/suite.sh @@ -49,8 +49,7 @@ bold "Rescript version" # we need to reset the yarn.lock and package.json to the original state # so there is not diff in git. The CI will install new ReScript package bold "Reset package.json and yarn.lock" -git checkout ../testrepo/yarn.lock &> /dev/null -git checkout ../testrepo/package.json &> /dev/null +git restore --worktree -- ../testrepo/yarn.lock ../testrepo/package.json &> /dev/null success "Reset package.json and yarn.lock" bold "Make sure the testrepo is clean" @@ -145,6 +144,7 @@ fi ./watch/04-watch-config-change.sh && ./watch/05-watch-ignores-non-source.sh && ./watch/06-watch-missing-source-folder.sh && +./watch/07-changed-interface-after-failed-implementation.sh && # Lock tests ./lock/01-lock-when-watching.sh && diff --git a/rewatch/tests/utils.sh b/rewatch/tests/utils.sh index eee37596b55..72d9d482d5e 100644 --- a/rewatch/tests/utils.sh +++ b/rewatch/tests/utils.sh @@ -11,6 +11,25 @@ is_windows() { [[ $OSTYPE == 'msys'* || $OSTYPE == 'cygwin'* || $OSTYPE == 'win'* ]]; } +platform_timeout() { + local timeout="$1" + if is_windows; then + echo $((timeout * 4)) + else + echo "$timeout" + fi +} + +process_is_running() { + local pid="$1" + if is_windows; then + WATCH_PID="$pid" powershell.exe -NoProfile -NonInteractive -Command \ + 'if (Get-Process -Id ([int]$env:WATCH_PID) -ErrorAction SilentlyContinue) { exit 0 } else { exit 1 }' + else + kill -0 "$pid" 2> /dev/null + fi +} + # get pwd with forward slashes pwd_prefix() { if is_windows; then @@ -52,12 +71,13 @@ replace() { } wait_for_pid_gone() { - local pid="$1"; local timeout="${2:-10}" - while kill -0 "$pid" 2> /dev/null && [ "$timeout" -gt 0 ]; do + local pid="$1"; local timeout + timeout=$(platform_timeout "${2:-10}") + while process_is_running "$pid" && [ "$timeout" -gt 0 ]; do sleep 1 timeout=$((timeout - 1)) done - ! kill -0 "$pid" 2> /dev/null + ! process_is_running "$pid" } exit_watcher() { @@ -81,7 +101,8 @@ clear_locks() { } wait_for_file() { - local file="$1"; local timeout="${2:-30}" + local file="$1"; local timeout + timeout=$(platform_timeout "${2:-30}") while [ "$timeout" -gt 0 ]; do [ -f "$file" ] && return 0 sleep 1 @@ -90,8 +111,47 @@ wait_for_file() { return 1 } +wait_for_pattern() { + local file="$1"; local pattern="$2"; local timeout + timeout=$(platform_timeout "${3:-30}") + while [ "$timeout" -gt 0 ]; do + grep -q "$pattern" "$file" 2>/dev/null && return 0 + sleep 1 + timeout=$((timeout - 1)) + done + return 1 +} + +wait_for_pattern_count() { + local file="$1"; local pattern="$2"; local expected="$3"; local timeout + timeout=$(platform_timeout "${4:-30}") + 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 +} + +wait_for_pattern_gone() { + local file="$1"; local pattern="$2"; local timeout + timeout=$(platform_timeout "${3:-30}") + 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_gone() { - local file="$1"; local timeout="${2:-30}" + local file="$1"; local timeout + timeout=$(platform_timeout "${2:-30}") while [ "$timeout" -gt 0 ]; do [ ! -f "$file" ] && return 0 sleep 1 diff --git a/rewatch/tests/watch/01-watch-recompile.sh b/rewatch/tests/watch/01-watch-recompile.sh index 626df386161..2356561df70 100755 --- a/rewatch/tests/watch/01-watch-recompile.sh +++ b/rewatch/tests/watch/01-watch-recompile.sh @@ -32,6 +32,16 @@ if ! wait_for_file "$target" 20; then exit 1 fi +# The root output must not become observable before an output it imports. +# Otherwise consumers can load a superficially completed but unusable build. +dependency_target=./packages/dep01/src/Dep01.mjs +if [ ! -f "$dependency_target" ]; then + error "Dependency output was not published with the root output: $dependency_target" + tail -n 200 rewatch.log || true + exit_watcher + exit 1 +fi + if node ./packages/main/src/Main.mjs | grep 'added-by-test' &> /dev/null; then success "Output is correct" @@ -41,11 +51,21 @@ else exit 1 fi -sleep 1 +if ! wait_for_pattern_count rewatch.log "Finished .*compilation" 1 30; then + error "Initial build did not settle" + tail -n 200 rewatch.log || true + exit_watcher + exit 1 +fi replace '/Console.log("added-by-test")/d' ./packages/main/src/Main.res; -sleep 5 +if ! wait_for_pattern_gone "$target" "added-by-test" 20; then + error "Watcher did not rebuild after restoring the source" + tail -n 200 rewatch.log || true + exit_watcher + exit 1 +fi if git diff --exit-code ./ then 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 63adb0fd2bf..0841dc0a9ca 100755 --- a/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh +++ b/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh @@ -15,18 +15,9 @@ else exit 1 fi -wait_for_pattern() { - local file="$1"; local pattern="$2"; local timeout="${3:-30}" - while [ "$timeout" -gt 0 ]; do - grep -q "$pattern" "$file" 2>/dev/null && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 -} - wait_for_changed_completed_log() { - local file="$1"; local baseline="$2"; local timeout="${3:-30}" + local file="$1"; local baseline="$2"; local timeout + timeout=$(platform_timeout "${3:-30}") while [ "$timeout" -gt 0 ]; do if [ -f "$file" ] && ! cmp -s "$file" "$baseline" && grep -q "#Done(" "$file" 2>/dev/null; then return 0 @@ -53,7 +44,6 @@ cleanup() { cp "$BACKUP_B" "$B_FILE" fi exit_watcher - sleep 1 rm -rf "$TMP_DIR" rm -f "$WATCH_STDOUT" "$WATCH_STDERR" } @@ -113,10 +103,13 @@ else exit 1 fi +cp "$COMPILER_LOG" "$TMP_DIR/before-restore.compiler.log" cp "$BACKUP_B" "$B_FILE" -sleep 1 +if ! wait_for_changed_completed_log "$COMPILER_LOG" "$TMP_DIR/before-restore.compiler.log" 30; then + error "Compiler log did not complete a new cycle after restoring B.res" + exit 1 +fi exit_watcher -sleep 2 rm -f "$WATCH_STDOUT" "$WATCH_STDERR" if git diff --exit-code ./packages/watch-warnings > /dev/null 2>&1; diff --git a/rewatch/tests/watch/02-watch-warnings-persist.sh b/rewatch/tests/watch/02-watch-warnings-persist.sh index b67ad0e0aec..214a9bc9268 100755 --- a/rewatch/tests/watch/02-watch-warnings-persist.sh +++ b/rewatch/tests/watch/02-watch-warnings-persist.sh @@ -15,29 +15,6 @@ else exit 1 fi -# Wait until a pattern appears in a file (with timeout in seconds, default 30) -wait_for_pattern() { - local file="$1"; local pattern="$2"; local timeout="${3:-30}" - while [ "$timeout" -gt 0 ]; do - grep -q "$pattern" "$file" 2>/dev/null && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 -} - -# Wait until a pattern appears N times in a file (with timeout in seconds, default 30) -wait_for_pattern_count() { - local file="$1"; local pattern="$2"; local count="$3"; local timeout="${4:-30}" - while [ "$timeout" -gt 0 ]; do - local current_count=$(grep -c "$pattern" "$file" 2>/dev/null || echo "0") - [ "$current_count" -ge "$count" ] && return 0 - sleep 1 - timeout=$((timeout - 1)) - done - return 1 -} - # Start watcher and capture stderr (where warnings are printed) rewatch_bg watch > /dev/null 2> rewatch-stderr.log & @@ -67,13 +44,32 @@ warning_count=$(grep -c "unused value unusedValue" rewatch-stderr.log) success "Warning from ModuleA.res persists after recompiling B.res (count: $warning_count)" # Restore B.res +warning_count_before_restore=$(grep -c "unused value unusedValue" rewatch-stderr.log 2>/dev/null || true) +warning_count_before_restore=${warning_count_before_restore:-0} printf 'let world = () => Console.log("world")\n' > ./packages/watch-warnings/src/B.res -sleep 1 +if ! wait_for_pattern_count rewatch-stderr.log "unused value unusedValue" \ + "$((warning_count_before_restore + 1))" 20; then + error "Watcher did not finish rebuilding after restoring B.res" + cat rewatch-stderr.log + exit_watcher + exit 1 +fi exit_watcher -sleep 2 +# A new invocation has no retained in-memory warning state, so the watcher must +# leave a freshness marker that causes the warning-producing module to run. +next_output=$(rewatch build 2>&1) +next_status=$? +if [ "$next_status" -eq 0 ] \ + && printf '%s\n' "$next_output" | grep -q "unused value unusedValue"; then + success "Warning persists after watcher shutdown" +else + error "Warning was lost after watcher shutdown" + printf '%s\n' "$next_output" + exit 1 +fi # Clean up log file rm -f rewatch-stderr.log diff --git a/rewatch/tests/watch/03-watch-new-file.sh b/rewatch/tests/watch/03-watch-new-file.sh index 1d4cfa19ea5..a087ddf4274 100755 --- a/rewatch/tests/watch/03-watch-new-file.sh +++ b/rewatch/tests/watch/03-watch-new-file.sh @@ -28,7 +28,12 @@ if ! wait_for_file "./src/Test.mjs" 20; then fi success "Initial build completed" -sleep 1 +if ! wait_for_pattern_count rewatch.log "Finished .*compilation" 1 30; then + error "Initial build did not settle" + cat rewatch.log + exit_watcher + exit 1 +fi # Create a new file in the source directory cat > ./src/NewWatchTestFile.res << 'EOF' @@ -55,16 +60,22 @@ else exit 1 fi -# Clean up the new file +# Clean up the new file. Wait for the resulting rebuild so it cannot race with +# later tests; stale-output deletion is not part of this new-file watcher test. +completed_builds=$(grep -c "Finished .*compilation" rewatch.log || true) rm -f ./src/NewWatchTestFile.res -# Wait for the compiled output to be removed (full rebuild detects removal) -sleep 5 +if ! wait_for_pattern_count rewatch.log "Finished .*compilation" "$((completed_builds + 1))" 30; then + error "Watcher did not finish rebuilding after deleting the source" + cat rewatch.log + exit_watcher + exit 1 +fi + rm -f ./src/NewWatchTestFile.mjs exit_watcher -sleep 2 rm -f rewatch.log if git diff --exit-code . > /dev/null 2>&1 && [ -z "$(git ls-files --others --exclude-standard .)" ]; diff --git a/rewatch/tests/watch/04-watch-config-change.sh b/rewatch/tests/watch/04-watch-config-change.sh index 519f77f5e82..ce7d00c6268 100755 --- a/rewatch/tests/watch/04-watch-config-change.sh +++ b/rewatch/tests/watch/04-watch-config-change.sh @@ -28,25 +28,56 @@ if ! wait_for_file "./src/Test.mjs" 20; then fi success "Initial build completed" -sleep 2 +if ! wait_for_pattern_count rewatch.log "Finished .*compilation" 1 30; then + error "Initial build did not settle" + cat rewatch.log + exit_watcher + exit 1 +fi +completed_builds=$(grep -c "Finished .*compilation" rewatch.log 2>/dev/null || true) +completed_builds=${completed_builds:-0} +wait_for_next_build() { + completed_builds=$((completed_builds + 1)) + wait_for_pattern_count rewatch.log "Finished .*compilation" \ + "$completed_builds" 30 +} + +wait_for_tracked_outputs() { + local timeout + timeout=$(platform_timeout 30) + while [ "$timeout" -gt 0 ]; do + if [ -z "$(git ls-files --deleted -- '*.mjs')" ]; then + return 0 + fi + sleep 1 + timeout=$((timeout - 1)) + done + return 1 +} # Change the suffix in rescript.json (same approach as suffix test) replace "s/.mjs/.res.mjs/g" rescript.json +if ! wait_for_next_build; then + error "Configuration rebuild did not settle" + cat rewatch.log + replace "s/.res.mjs/.mjs/g" rescript.json + exit_watcher + exit 1 +fi # After a config change, the watcher does a full rebuild. However, a suffix # change alone may not recompile files (sources haven't changed). Trigger a # source change so the watcher compiles with the new suffix. -sleep 3 echo '// config-change-test' >> ./src/Test.res -# Wait for the file with the new suffix to appear -if wait_for_file "./src/Test.res.mjs" 20; then +# Wait for the file with the new suffix and the complete source rebuild. +if wait_for_file "./src/Test.res.mjs" 20 && wait_for_next_build; then success "Full rebuild triggered by rescript.json change (new suffix applied)" else error "No rebuild detected after rescript.json change" cat rewatch.log replace "s/.res.mjs/.mjs/g" rescript.json - git checkout -- ./src/Test.res + git restore --worktree -- ./src/Test.res exit_watcher exit 1 fi @@ -58,25 +89,33 @@ else error "Watcher crashed after config change" cat rewatch.log replace "s/.res.mjs/.mjs/g" rescript.json - git checkout -- ./src/Test.res + git restore --worktree -- ./src/Test.res exit 1 fi -# Restore rescript.json and source file +# Restore only the configuration while the watcher is running. Mixing the +# unrelated source cleanup into the same native event batch can make the batch +# look like an incremental source edit and obscure the configuration transition +# this test is intended to verify. replace "s/.res.mjs/.mjs/g" rescript.json -git checkout -- ./src/Test.res - -# Wait for rebuild with restored suffix (old .res.mjs should go away) -if wait_for_file_gone "./src/Test.res.mjs" 20; then - success "Rebuild after restore removed old suffix files" +if wait_for_next_build && wait_for_tracked_outputs; then + success "Rebuild after configuration restore completed" else - # Clean up manually if the watcher didn't remove them - find . -name "*.res.mjs" -delete 2>/dev/null + error "Configuration restore did not settle or restore tracked outputs" + git restore --worktree -- ./src/Test.res + exit_watcher + exit 1 fi -exit_watcher +# This test covers watcher reconfiguration, not migration of outputs generated +# with the previous suffix. +find . -name "*.res.mjs" -delete 2>/dev/null + +if ! exit_watcher; then + exit 1 +fi -sleep 2 +git restore --worktree -- ./src/Test.res rm -f rewatch.log if git diff --exit-code . > /dev/null 2>&1 && [ -z "$(git ls-files --others --exclude-standard .)" ]; diff --git a/rewatch/tests/watch/05-watch-ignores-non-source.sh b/rewatch/tests/watch/05-watch-ignores-non-source.sh index 0bd8f856559..01abb65562b 100755 --- a/rewatch/tests/watch/05-watch-ignores-non-source.sh +++ b/rewatch/tests/watch/05-watch-ignores-non-source.sh @@ -77,7 +77,6 @@ rm -rf ./random-dir ./another-dir exit_watcher -sleep 2 rm -f rewatch.log if git diff --exit-code . > /dev/null 2>&1 && [ -z "$(git ls-files --others --exclude-standard .)" ]; diff --git a/rewatch/tests/watch/06-watch-missing-source-folder.sh b/rewatch/tests/watch/06-watch-missing-source-folder.sh index 08436c4fe19..19d9d948003 100755 --- a/rewatch/tests/watch/06-watch-missing-source-folder.sh +++ b/rewatch/tests/watch/06-watch-missing-source-folder.sh @@ -33,7 +33,7 @@ success "Watcher Started" if ! wait_for_file "./src/Test.mjs" 20; then error "Initial build did not complete" cat rewatch.log - git checkout "$DEP01_CONFIG" + git restore --worktree -- "$DEP01_CONFIG" exit_watcher exit 1 fi @@ -45,7 +45,7 @@ if grep -q 'Could not read folder.*nonexistent-folder' rewatch.log; then else error "Missing source folder error was NOT reported" cat rewatch.log - git checkout "$DEP01_CONFIG" + git restore --worktree -- "$DEP01_CONFIG" exit_watcher exit 1 fi @@ -54,10 +54,9 @@ fi # where the config change triggers a full rebuild that runs concurrently # with the subsequent `rewatch build`. exit_watcher -sleep 1 # Restore dep01's rescript.json -git checkout "$DEP01_CONFIG" +git restore --worktree -- "$DEP01_CONFIG" # Rebuild to regenerate any artifacts that were removed by `rewatch clean` # but not rebuilt due to the modified config (e.g. Dep01.mjs). diff --git a/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh b/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh new file mode 100755 index 00000000000..346a6163139 --- /dev/null +++ b/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh @@ -0,0 +1,97 @@ +#!/bin/bash + +cd "$(dirname "$0")" +source "../utils.sh" + +bold "Test: A changed interface dirties dependents after its implementation fails" + +fixture=$(mktemp -d 2>/dev/null || mktemp -d -t rewatch-failed-implementation) +cleanup() { + cd "$fixture" || return + exit_watcher + if [ -n "${watch_pid:-}" ]; then + if process_is_running "$watch_pid"; then + kill "$watch_pid" 2>/dev/null || true + fi + wait "$watch_pid" 2>/dev/null || true + fi + rm -rf "$fixture" +} +trap cleanup EXIT + +mkdir -p "$fixture/src" +cat > "$fixture/rescript.json" <<'EOF' +{ + "name": "rewatch-failed-implementation", + "sources": { "dir": "src" }, + "package-specs": { "module": "esmodule", "in-source": true }, + "suffix": ".mjs" +} +EOF + +cat > "$fixture/src/Provider.resi" <<'EOF' +let value: int +EOF +cat > "$fixture/src/Provider.res" <<'EOF' +let value = 1 +EOF +cat > "$fixture/src/Consumer.res" <<'EOF' +let number: int = Provider.value +EOF + +cd "$fixture" || exit 1 +RAYON_NUM_THREADS=1 "$REWATCH_EXECUTABLE" watch > rewatch.log 2>&1 & +watch_pid=$! +compiler_log=lib/bs/.compiler.log + +wait_for_changed_compiler_log() { + local baseline="$1"; local timeout + timeout=$(platform_timeout 30) + 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 +} + +if ! wait_for_pattern_count rewatch.log "Finished .*compilation" 1 30; then + error "Initial watch build did not finish" + cat rewatch.log >&2 + exit 1 +fi +cp "$compiler_log" initial.compiler.log + +# The new interface writes a different CMI, but the old implementation cannot +# satisfy it. The dependent must remain blocked and dirty across this build. +printf 'let value: string\n' > src/Provider.resi +if ! wait_for_changed_compiler_log initial.compiler.log; then + error "Watch did not finish the failing interface-change build" + cat rewatch.log >&2 + exit 1 +fi +if ! grep -q "Provider.res" "$compiler_log"; then + error "Changed interface did not expose the implementation error" + cat rewatch.log >&2 + exit 1 +fi +cp "$compiler_log" failed.compiler.log + +# Once the implementation satisfies the interface, Consumer must compile +# against the new string type and report its now-invalid int annotation. +printf 'let value = "recovered"\n' > src/Provider.res +if ! wait_for_changed_compiler_log failed.compiler.log; then + error "Watch did not finish the recovery build" + cat rewatch.log >&2 + exit 1 +fi +if ! grep -q "Consumer.res" "$compiler_log"; then + error "Consumer was not recompiled against the changed interface" + cat rewatch.log >&2 + exit 1 +fi + +success "Changed CMI reaches blocked dependents after recovery" From b79362b905e84da600d6a4d12f3f03513bbb8369 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 23 Sep 2026 12:20:47 +0000 Subject: [PATCH 3/5] Preserve blocked dependents across full rewatch rebuilds Signed-off-by: Christoph Knittel --- CHANGELOG.md | 2 +- rewatch/src/watcher.rs | 39 +++++++++++++++---- ...d-interface-after-failed-implementation.sh | 7 ++-- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1aa7b066cc..311d5eee7ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ #### :bug: Bug fix -- Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation. https://github.com/rescript-lang/rescript/pull/8667 +- Make rewatch compile independent modules after an unrelated failure and recompile blocked dependents when a changed interface survives a failed implementation, including across full watcher rebuilds. https://github.com/rescript-lang/rescript/pull/8667 #### :memo: Documentation diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index e435c757857..afcaaacc7e1 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -172,7 +172,7 @@ fn unregister_watches(watcher: &mut RecommendedWatcher, watch_paths: &[(PathBuf, } } -fn carry_forward_compile_warnings(previous: &BuildCommandState, next: &mut BuildCommandState) { +fn carry_forward_compile_state(previous: &BuildCommandState, next: &mut BuildCommandState) { for (module_name, next_module) in next.build_state.modules.iter_mut() { let Some(previous_module) = previous.build_state.modules.get(module_name) else { continue; @@ -184,6 +184,12 @@ fn carry_forward_compile_warnings(previous: &BuildCommandState, next: &mut Build match (&previous_module.source_type, &mut next_module.source_type) { (SourceType::SourceFile(previous_source), SourceType::SourceFile(next_source)) => { if previous_source.implementation.path == next_source.implementation.path { + // A changed CMI can leave a dependent blocked behind a failed + // implementation. Asset timestamps alone cannot recover this + // dirtiness when a full watcher rebuild recreates the state. + if previous_module.compile_dirty { + next_module.compile_dirty = true; + } next_source.implementation.compile_warnings = previous_source.implementation.compile_warnings.clone(); @@ -501,9 +507,8 @@ async fn async_watch( .expect("Could not initialize build"); // Full rebuilds can be triggered by editor atomic saves that surface as rename events. - // Preserve warning state for unchanged modules so their warnings are re-emitted after the - // fresh build state replaces the previous one. - carry_forward_compile_warnings(&build_state, &mut next_build_state); + // Preserve warnings and blocked dirty modules when fresh state replaces the previous one. + carry_forward_compile_state(&build_state, &mut next_build_state); build_state = next_build_state; // Re-register watches based on the new build state @@ -804,7 +809,7 @@ mod tests { ); let mut next = test_build_state("ModuleA", test_module("src/ModuleA.res", None, None, None)); - carry_forward_compile_warnings(&previous, &mut next); + carry_forward_compile_state(&previous, &mut next); let module = next.get_module("ModuleA").expect("module should exist"); let SourceType::SourceFile(source_file) = &module.source_type else { @@ -826,7 +831,7 @@ mod tests { ); let mut next = test_build_state("ModuleA", test_module("src/ModuleARenamed.res", None, None, None)); - carry_forward_compile_warnings(&previous, &mut next); + carry_forward_compile_state(&previous, &mut next); let module = next.get_module("ModuleA").expect("module should exist"); let SourceType::SourceFile(source_file) = &module.source_type else { @@ -853,7 +858,7 @@ mod tests { test_module("src/ModuleA.res", None, Some("src/ModuleA.resi"), None), ); - carry_forward_compile_warnings(&previous, &mut next); + carry_forward_compile_state(&previous, &mut next); let module = next.get_module("ModuleA").expect("module should exist"); let SourceType::SourceFile(source_file) = &module.source_type else { @@ -864,4 +869,24 @@ mod tests { assert_eq!(interface.compile_warnings.as_deref(), Some("warning: interface")); assert_eq!(interface.compile_state, CompileState::Warning); } + + #[test] + fn carries_forward_blocked_dirtiness_only_for_matching_sources() { + let mut previous = test_build_state("ModuleA", test_module("src/ModuleA.res", None, None, None)); + previous + .build_state + .modules + .get_mut("ModuleA") + .unwrap() + .compile_dirty = true; + + let mut same_source = test_build_state("ModuleA", test_module("src/ModuleA.res", None, None, None)); + carry_forward_compile_state(&previous, &mut same_source); + assert!(same_source.get_module("ModuleA").unwrap().compile_dirty); + + let mut different_source = + test_build_state("ModuleA", test_module("src/Other.res", None, None, None)); + carry_forward_compile_state(&previous, &mut different_source); + assert!(!different_source.get_module("ModuleA").unwrap().compile_dirty); + } } 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 346a6163139..40e35e30935 100755 --- a/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh +++ b/rewatch/tests/watch/07-changed-interface-after-failed-implementation.sh @@ -80,9 +80,10 @@ if ! grep -q "Provider.res" "$compiler_log"; then fi cp "$compiler_log" failed.compiler.log -# Once the implementation satisfies the interface, Consumer must compile -# against the new string type and report its now-invalid int annotation. -printf 'let value = "recovered"\n' > src/Provider.res +# An atomic replacement makes the watcher reinitialize its build state. The +# blocked dependent must remain dirty through that full rebuild as well. +printf 'let value = "recovered"\n' > src/Provider.next +mv src/Provider.next src/Provider.res if ! wait_for_changed_compiler_log failed.compiler.log; then error "Watch did not finish the recovery build" cat rewatch.log >&2 From f4655439e59cc5014547e2b4ed4b4ef90cfbf47f Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 23 Sep 2026 14:16:42 +0000 Subject: [PATCH 4/5] Abort warning persistence test if watcher stays running Signed-off-by: Christoph Knittel --- rewatch/tests/watch/02-watch-warnings-persist.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rewatch/tests/watch/02-watch-warnings-persist.sh b/rewatch/tests/watch/02-watch-warnings-persist.sh index 214a9bc9268..45d3d9f4397 100755 --- a/rewatch/tests/watch/02-watch-warnings-persist.sh +++ b/rewatch/tests/watch/02-watch-warnings-persist.sh @@ -56,7 +56,9 @@ if ! wait_for_pattern_count rewatch-stderr.log "unused value unusedValue" \ exit 1 fi -exit_watcher +if ! exit_watcher; then + exit 1 +fi # A new invocation has no retained in-memory warning state, so the watcher must # leave a freshness marker that causes the warning-producing module to run. From 9153fbd7d982c228efc80a15692ae00cbc2153ec Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 23 Sep 2026 15:54:35 +0000 Subject: [PATCH 5/5] Abort watcher tests when shutdown times out Signed-off-by: Christoph Knittel --- rewatch/tests/lock/01-lock-when-watching.sh | 8 ++++++-- rewatch/tests/watch/01-watch-recompile.sh | 4 +++- .../watch/02-watch-warnings-persist-atomic-save.sh | 4 +++- rewatch/tests/watch/03-watch-new-file.sh | 4 +++- rewatch/tests/watch/05-watch-ignores-non-source.sh | 4 +++- .../tests/watch/06-watch-missing-source-folder.sh | 12 +++++++++--- 6 files changed, 27 insertions(+), 9 deletions(-) diff --git a/rewatch/tests/lock/01-lock-when-watching.sh b/rewatch/tests/lock/01-lock-when-watching.sh index a25f4395104..de76c3ae4be 100755 --- a/rewatch/tests/lock/01-lock-when-watching.sh +++ b/rewatch/tests/lock/01-lock-when-watching.sh @@ -76,7 +76,9 @@ else exit 1 fi -exit_watcher +if ! exit_watcher; then + exit 1 +fi if ! wait_for_file_gone "lib/watch.lock" 10; then error "watch.lock was not removed" @@ -93,4 +95,6 @@ else exit 1 fi -exit_watcher +if ! exit_watcher; then + exit 1 +fi diff --git a/rewatch/tests/watch/01-watch-recompile.sh b/rewatch/tests/watch/01-watch-recompile.sh index 2356561df70..bfd13eef04a 100755 --- a/rewatch/tests/watch/01-watch-recompile.sh +++ b/rewatch/tests/watch/01-watch-recompile.sh @@ -76,4 +76,6 @@ else exit 1 fi -exit_watcher +if ! exit_watcher; then + exit 1 +fi 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 0841dc0a9ca..4548fa11385 100755 --- a/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh +++ b/rewatch/tests/watch/02-watch-warnings-persist-atomic-save.sh @@ -109,7 +109,9 @@ if ! wait_for_changed_completed_log "$COMPILER_LOG" "$TMP_DIR/before-restore.com error "Compiler log did not complete a new cycle after restoring B.res" exit 1 fi -exit_watcher +if ! exit_watcher; then + exit 1 +fi rm -f "$WATCH_STDOUT" "$WATCH_STDERR" if git diff --exit-code ./packages/watch-warnings > /dev/null 2>&1; diff --git a/rewatch/tests/watch/03-watch-new-file.sh b/rewatch/tests/watch/03-watch-new-file.sh index a087ddf4274..380e54d0f29 100755 --- a/rewatch/tests/watch/03-watch-new-file.sh +++ b/rewatch/tests/watch/03-watch-new-file.sh @@ -74,7 +74,9 @@ fi rm -f ./src/NewWatchTestFile.mjs -exit_watcher +if ! exit_watcher; then + exit 1 +fi rm -f rewatch.log diff --git a/rewatch/tests/watch/05-watch-ignores-non-source.sh b/rewatch/tests/watch/05-watch-ignores-non-source.sh index 01abb65562b..2c1b978e3e3 100755 --- a/rewatch/tests/watch/05-watch-ignores-non-source.sh +++ b/rewatch/tests/watch/05-watch-ignores-non-source.sh @@ -75,7 +75,9 @@ success "File in nested non-source subdir was correctly ignored" rm -f ./src/WatchProbe.res ./src/WatchProbe.mjs rm -rf ./random-dir ./another-dir -exit_watcher +if ! exit_watcher; then + exit 1 +fi rm -f rewatch.log diff --git a/rewatch/tests/watch/06-watch-missing-source-folder.sh b/rewatch/tests/watch/06-watch-missing-source-folder.sh index 19d9d948003..3f478b32543 100755 --- a/rewatch/tests/watch/06-watch-missing-source-folder.sh +++ b/rewatch/tests/watch/06-watch-missing-source-folder.sh @@ -33,8 +33,10 @@ success "Watcher Started" if ! wait_for_file "./src/Test.mjs" 20; then error "Initial build did not complete" cat rewatch.log + if ! exit_watcher; then + exit 1 + fi git restore --worktree -- "$DEP01_CONFIG" - exit_watcher exit 1 fi success "Initial build completed" @@ -45,15 +47,19 @@ if grep -q 'Could not read folder.*nonexistent-folder' rewatch.log; then else error "Missing source folder error was NOT reported" cat rewatch.log + if ! exit_watcher; then + exit 1 + fi git restore --worktree -- "$DEP01_CONFIG" - exit_watcher exit 1 fi # Exit the watcher before restoring the config to avoid a race condition # where the config change triggers a full rebuild that runs concurrently # with the subsequent `rewatch build`. -exit_watcher +if ! exit_watcher; then + exit 1 +fi # Restore dep01's rescript.json git restore --worktree -- "$DEP01_CONFIG"