blob: 8af252ca1462fda423c51d90095126427008e207 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import assert from 'node:assert/strict';
import { existsSync, readFileSync } from 'node:fs';
import test from 'node:test';
import {
collectWorkspaceSourceClosure,
windowsPackageSourceEntrypoints,
windowsReleasePatternCoversSource,
} from './windows-package-source-closure.mjs';
import { readPullRequestPathFilter } from './workflow-pull-request-paths.mjs';
/**
* The `packages/` half of this lane's filter that the import closure does not
* account for. Each entry schedules a 25-minute Windows runner on its own, so
* it has to earn that here rather than sit indistinguishable among the twenty
* derived entries — which is the only way a rotted one is ever noticed.
*/
const UNDERIVED_PACKAGE_PATTERNS = new Set([
// Regenerated and diffed by `check:release`, and consumed by the packaging
// step rather than imported by the worker.
'packages/cli/RUNTIME_HOST_PEER_DEPENDENCIES.rust.tsv',
'packages/cli/RUNTIME_HOST_PEER_THIRD_PARTY_NOTICES.txt',
// Candidate election runs in the packaged app, not in the worker closure, and
// has broken this path before.
'packages/runtime-host/src/client/connect-or-spawn.ts',
'packages/runtime-host/src/client/launcher.ts',
// Builds the worker the closure starts from, so it precedes rather than joins
// it.
'packages/runtime/scripts/build-filesystem-worker.mjs',
]);
test('the Windows package trigger is exactly its closure plus declared exceptions', async () => {
const closure = await collectWorkspaceSourceClosure(windowsPackageSourceEntrypoints);
const patterns = readPullRequestPathFilter('release-windows-check.yml');
assert.ok(patterns.length > 0, 'release-windows-check.yml declares no pull_request.paths');
const missing = closure.filter(
(sourcePath) =>
!patterns.some((pattern) => windowsReleasePatternCoversSource(sourcePath, pattern)),
);
for (const expected of [
'packages/core/src/absolute-path.ts',
'packages/core/src/sandbox-boundary.ts',
'packages/core/src/serialized-byte-length.ts',
'packages/core/src/windows-path.ts',
'packages/runtime/src/child-fd-input.ts',
'packages/runtime/src/child-process-lifecycle.ts',
'packages/runtime/src/process-tree-terminator.ts',
]) {
assert.ok(closure.includes(expected), `closure omitted ${expected}`);
}
assert.deepEqual(missing, []);
// The other direction, which containment alone cannot see: a pattern backed
// by nothing is dead weight that still books a Windows runner every time it
// matches, and nothing reports it. Exceptions are declared above, so this
// fails both when the closure stops reaching an entry and when a stale
// exception outlives its reason.
const underived = patterns
.filter((pattern) => pattern.startsWith('packages/'))
.filter((pattern) => !closure.some((path) => windowsReleasePatternCoversSource(path, pattern)))
.sort();
assert.deepEqual(underived, [...UNDERIVED_PACKAGE_PATTERNS].sort());
});
test('the Windows package workflow path list has no duplicate entries', () => {
const patterns = readPullRequestPathFilter('release-windows-check.yml');
assert.equal(new Set(patterns).size, patterns.length);
});
/**
* This belongs beside the other closure contract rather than with the planner
* tests, because computing a closure needs esbuild and the planner tests run
* before `npm ci` installs it. `.github/workflows/windows-recovery.yml` is in
* `RELEASE_CONTRACT_FILES` so that editing the filter selects the gate that
* checks it.
*/
test('the recovery filter is exactly the Windows-branching closure of its tests', async () => {
const workflow = readFileSync(
new URL('../.github/workflows/windows-recovery.yml', import.meta.url),
'utf8',
);
const filtered = readPullRequestPathFilter('windows-recovery.yml')
.filter((path) => path.startsWith('packages/'))
.sort();
// Derived from the dist tests the steps actually execute, mapped back to
// source. A suite added to a step joins this set without anyone remembering
// to widen the filter.
// The separator class matches the backslash form too, because these steps run
// under pwsh where both are legal — a suite spelled with backslashes would
// otherwise be dropped from the set that decides the filter, which is the
// fail-open direction.
const entrypoints = [
...new Set(
[
...workflow.matchAll(
/packages[/\\]([\w-]+)[/\\]dist[/\\]__tests__[/\\]([\w.-]+)\.test\.js/gu,
),
].map(([, workspace, name]) => `packages/${workspace}/src/__tests__/${name}.test.ts`),
),
].sort();
assert.ok(entrypoints.length > 0, 'no executed suite was recognised');
for (const entrypoint of entrypoints) {
assert.ok(existsSync(new URL(`../${entrypoint}`, import.meta.url)), entrypoint);
}
// Set equality, not containment, and in both directions on purpose. A subset
// check cannot see an omission, which is how `stable-storage.ts` — reached
// through the two listed lock authorities — sat outside a filter that was
// supposed to cover exactly it. A superset check cannot see a file that
// stopped branching and now schedules a Windows runner for nothing.
//
// `win32` is the criterion because it is the one a machine can check, not
// because it names every Windows-only path: `git-worktree-child-executor.ts`
// carries a genuine Windows workaround with no such literal. A branch written
// without it stays outside this filter, and the lane's schedule is what
// observes it.
const closure = await collectWorkspaceSourceClosure(entrypoints);
const windowsBranching = closure
.filter((path) =>
readFileSync(new URL(`../${path}`, import.meta.url), 'utf8').includes('win32'),
)
.sort();
assert.deepEqual(filtered, windowsBranching);
});