Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 5 additions & 34 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,11 @@ jobs:
shell: bash
working-directory: ${{ steps.tmp-dir.outputs.path }}

- name: Test packaged CLI subcommands and watch shutdown
run: node packaged-cli-smoke.mjs
shell: bash
working-directory: ${{ steps.tmp-dir.outputs.path }}

test-installation-pnpm:
needs:
- pkg-pr-new
Expand Down Expand Up @@ -592,37 +597,3 @@ jobs:
node src/BeltTest.mjs
shell: bash
working-directory: ${{ steps.tmp-dir.outputs.path }}

test-integration-rewatch:
needs:
- pkg-pr-new
strategy:
fail-fast: false
matrix:
include:
- os: macos-15-intel
- os: macos-15
- os: ubuntu-24.04
- os: ubuntu-24.04-arm
- os: windows-2025
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
uses: actions/checkout@v7

- name: Use Node.js
uses: actions/setup-node@v7
with:
# Run integration tests with the oldest supported node version.
node-version: 20

- name: Install ReScript package in rewatch/testrepo
run: |
COMMIT_SHA="${{ needs.pkg-pr-new.outputs.commit_sha }}"
yarn add "rescript@https://pkg.pr.new/rescript-lang/rescript@${COMMIT_SHA}"
shell: bash
working-directory: rewatch/testrepo

- name: Run rewatch integration tests
run: ./rewatch/tests/suite.sh rewatch/testrepo/node_modules/.bin/rescript
shell: bash
95 changes: 95 additions & 0 deletions tests/package_tests/installation_test/packaged-cli-smoke.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { rm } from "node:fs/promises";
import path from "node:path";

const launcher = path.resolve("node_modules/rescript/cli/rescript.js");
const output = path.resolve("src/Test.mjs");
const watchLock = path.resolve("lib/watch.lock");
const timeoutMs = process.platform === "win32" ? 120000 : 30000;

function run(args, input) {
const result = spawnSync(process.execPath, [launcher, ...args], {
input,
encoding: "utf8",
timeout: timeoutMs,
});
if (result.error) throw result.error;
assert.equal(result.status, 0, `${args.join(" ")} failed: ${result.stderr}`);
return result.stdout;
}

async function waitForExit(promise) {
let timer;
try {
return await Promise.race([
promise,
new Promise((_, reject) => {
timer = setTimeout(
() => reject(new Error("watch did not exit after shutdown request")),
timeoutMs,
);
}),
]);
} finally {
clearTimeout(timer);
}
}

async function waitUntil(predicate, message) {
for (let attempt = 0; attempt < timeoutMs / 250; attempt++) {
if (predicate()) return;
await new Promise(resolve => setTimeout(resolve, 250));
}
throw new Error(message);
}

const formatted = run(["format", "--stdin", ".res"], "let x=1\n");
assert.match(formatted, /let x = 1/);

assert.ok(existsSync(output), "installation test should have built Test.mjs");
run(["clean"]);
assert.ok(!existsSync(output), "clean should remove Test.mjs");

const watcher = spawn(process.execPath, [launcher, "watch"], {
stdio: ["ignore", "pipe", "pipe"],
});
let watchOutput = "";
watcher.stdout.on("data", chunk => {
watchOutput += chunk;
});
watcher.stderr.on("data", chunk => {
watchOutput += chunk;
});
const watcherExit = new Promise((resolve, reject) => {
watcher.once("error", reject);
watcher.once("exit", (code, signal) => resolve({ code, signal }));
});

try {
await waitUntil(
() => existsSync(watchLock) && existsSync(output),
`watch did not build the project: ${watchOutput}`,
);
if (process.platform === "win32") {
// Windows cannot deliver POSIX SIGINT to a child process through kill().
await rm(watchLock);
} else {
assert.ok(watcher.kill("SIGINT"), "could not signal packaged CLI");
}
Comment on lines +78 to +80

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove the lock before awaiting watcher shutdown

On every non-Windows runner, this path only sends SIGINT, but Rewatch's cleanup_before_watch_exit does not delete lib/watch.lock, and main.rs never calls drop_lock for LockKind::Watch. The watcher therefore exits while the lock still exists, causing the subsequent !existsSync(watchLock) poll to time out and fail this npm installation job on macOS and Linux. Remove the lock and await the process on all platforms, or stop asserting that signal-driven shutdown removes it.

AGENTS.md reference: rewatch/AGENTS.md:L220-L220

Useful? React with 👍 / 👎.

await waitForExit(watcherExit);
await waitUntil(
() => !existsSync(watchLock),
`watch lock remained after shutdown: ${watchOutput}`,
);
assert.match(
watchOutput,
/Exiting\.\.\./,
"watcher did not report a clean shutdown",
);
} finally {
await rm(watchLock, { force: true });
if (watcher.exitCode === null && watcher.signalCode === null)
watcher.kill("SIGTERM");
}
Loading