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
93 changes: 93 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,93 @@
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("close", (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 thread
cknitt marked this conversation as resolved.
const { code, signal } = await waitForExit(watcherExit);
assert.equal(signal, null, `watcher was killed: ${watchOutput}`);
assert.equal(code, 0, `watcher exited with code ${code}: ${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