mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
* perf(ci): overlap web-shell and standalone tests with the CLI suite The Linux Test job ran every workspace suite, the standalone runtime regression and scripts/tests one after another. The CLI suite keeps its workers busy for 13-20 minutes, while web-shell's suite spends most of its run on App.test.tsx alone and the standalone regression starts one runtime process at a time, so those two added their mostly idle minutes straight to the job. Run them in a side lane beside the other workspaces instead. .github/scripts/ci/run-unit-test-lanes.sh starts web-shell and the standalone regression in the background, runs every other workspace's test:ci (listed with npm pkg get, in npm's workspace order) and then scripts/tests, and replays the side lane's log after the main lane so the two never interleave. The step runs the script under the same setpriv wrapper, so every test command still runs without the DAC override capabilities. test:scripts now reaches its Vitest run through a new test:scripts:unit script with a trailing --, so the main lane can run it without the standalone regression while test:ci, test:ci:workspaces and test:scripts run what they ran before. * fix(ci): name the cause when the lane script cannot start a lane - Fail with an ::error:: line when mktemp cannot create the side lane's log file, instead of running the main lane alone and dying in tail. - Check the exit status of `npm pkg get` and of the parser. A failing `npm pkg get --json` prints an error object, which used to parse as an empty listing and report "Found no workspaces". - Note in the header that the replay puts side-lane failures after the main lane's in main-failure-signature.mjs's title and search markers. - Lane test: clear the caller's NODE_OPTIONS, assert the main lane's output is present before checking its order, and cover the failed listing and the missing temp dir. - Cross-check npm's workspace resolution against the selector in scripts/workspaces.js. * fix(ci): halve web-shell's worker cap beside the main lane Each Vitest run applies VITEST_MAX_FORKS / VITEST_MAX_THREADS on its own, so running two lanes at once doubled the step's worker cap, and on the hosted runners, where no cap is set, put two all-CPUs pools on 4 vCPU. The lane script now runs web-shell at half the cap, at least 1, and at 2 where the cap is unset; the main lane keeps the full cap. - Refuse a workspace name with whitespace in it: the main lane's arguments are read one per line, so a line break would split a name into other arguments, flags among them. - Lane test: web-shell and the main lane's suites now wait for each other, so a side lane serialized before or after the main lane fails; record each call's worker caps; run the script against npm's real listing and compare its main lane with the selector the release pins use; cover a name with a line break in it. - ci.yml: the standalone runtime cache comment names the lane script's side lane, not test:scripts, as what runs the regression in this job. - ci-variables.md: say the cap applies per Vitest run and what the two-lane step runs at its peak. - release-workflow.test.js: follow test:scripts through test:scripts:unit to vitest; #15102 pinned the shape before the split.
166 lines
5.0 KiB
JavaScript
166 lines
5.0 KiB
JavaScript
/**
|
|
* @license
|
|
* Copyright 2026 Qwen Team
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import {
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import {
|
|
getTestCiWorkspacePackageJsonPaths,
|
|
getWorkspacePackageJsonPaths,
|
|
} from '../workspaces.js';
|
|
|
|
describe('workspace helpers', () => {
|
|
const tempDirs = [];
|
|
|
|
afterEach(() => {
|
|
for (const tempDir of tempDirs.splice(0)) {
|
|
rmSync(tempDir, { force: true, recursive: true });
|
|
}
|
|
});
|
|
|
|
it('honors negated workspace patterns', () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), 'qwen-workspaces-'));
|
|
tempDirs.push(root);
|
|
|
|
for (const packagePath of [
|
|
'packages/cli/package.json',
|
|
'packages/core/package.json',
|
|
'packages/desktop/package.json',
|
|
'packages/channels/base/package.json',
|
|
]) {
|
|
writeFile(root, packagePath, '{}\n');
|
|
}
|
|
|
|
expect(
|
|
getWorkspacePackageJsonPaths(root, [
|
|
'packages/*',
|
|
'packages/channels/base',
|
|
'!packages/desktop',
|
|
]),
|
|
).toEqual([
|
|
'packages/channels/base/package.json',
|
|
'packages/cli/package.json',
|
|
'packages/core/package.json',
|
|
]);
|
|
});
|
|
|
|
it('normalizes Windows-style workspace patterns', () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), 'qwen-workspaces-'));
|
|
tempDirs.push(root);
|
|
|
|
for (const packagePath of [
|
|
'packages/cli/package.json',
|
|
'packages/core/package.json',
|
|
'packages/desktop/package.json',
|
|
'packages/channels/base/package.json',
|
|
]) {
|
|
writeFile(root, packagePath, '{}\n');
|
|
}
|
|
|
|
expect(
|
|
getWorkspacePackageJsonPaths(root, [
|
|
'packages\\*',
|
|
'packages\\channels\\base',
|
|
'!packages\\desktop',
|
|
]),
|
|
).toEqual([
|
|
'packages/channels/base/package.json',
|
|
'packages/cli/package.json',
|
|
'packages/core/package.json',
|
|
]);
|
|
});
|
|
|
|
it('returns exactly the package.json paths of workspaces with a test:ci script', () => {
|
|
const root = mkdtempSync(path.join(tmpdir(), 'qwen-workspaces-'));
|
|
tempDirs.push(root);
|
|
|
|
writeFile(
|
|
root,
|
|
'package.json',
|
|
'{"workspaces": ["integrations/*", "packages/*", ' +
|
|
'"packages/channels/nested-ci", "packages/channels/nested-no-ci"]}\n',
|
|
);
|
|
// Both repo layouts are witness-bearing: the flat integrations/* layout
|
|
// and the nested packages/channels/* layout. The nested members need
|
|
// explicit root-list entries because packages/* does not match nested
|
|
// directories — exactly why a filter mutation that drops the subtree
|
|
// (say `!path.startsWith('packages/channels/')`) would stay invisible
|
|
// to every consumer that derives its set from the selector. This
|
|
// exact-set pin is what catches it.
|
|
writeFile(
|
|
root,
|
|
'integrations/with-ci/package.json',
|
|
'{"scripts": {"test:ci": "vitest run"}}\n',
|
|
);
|
|
writeFile(
|
|
root,
|
|
'packages/without-ci/package.json',
|
|
'{"scripts": {"test": "vitest run"}}\n',
|
|
);
|
|
writeFile(
|
|
root,
|
|
'packages/channels/nested-ci/package.json',
|
|
'{"scripts": {"test:ci": "vitest run"}}\n',
|
|
);
|
|
writeFile(
|
|
root,
|
|
'packages/channels/nested-no-ci/package.json',
|
|
'{"scripts": {"test": "vitest run"}}\n',
|
|
);
|
|
|
|
expect(getTestCiWorkspacePackageJsonPaths(root)).toEqual([
|
|
'integrations/with-ci/package.json',
|
|
'packages/channels/nested-ci/package.json',
|
|
]);
|
|
});
|
|
|
|
it('selects the workspaces npm runs test:ci in', () => {
|
|
// npm resolves the workspaces itself for `npm run test:ci:workspaces`
|
|
// and for the Linux test job's lane script
|
|
// (.github/scripts/ci/run-unit-test-lanes.sh), which lists them with
|
|
// `npm pkg get`. The pins that gate on this selector's set must name the
|
|
// suites those runs start.
|
|
const root = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'../..',
|
|
);
|
|
const result = spawnSync(
|
|
'npm',
|
|
['pkg', 'get', 'scripts.test:ci', '--workspaces', '--json'],
|
|
{ cwd: root, encoding: 'utf8', shell: process.platform === 'win32' },
|
|
);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
const npmNames = Object.entries(JSON.parse(result.stdout))
|
|
.filter(([, script]) => typeof script === 'string')
|
|
.map(([name]) => name)
|
|
.sort();
|
|
const selectorNames = getTestCiWorkspacePackageJsonPaths(root)
|
|
.map(
|
|
(packageJsonPath) =>
|
|
JSON.parse(readFileSync(path.join(root, packageJsonPath), 'utf8'))
|
|
.name,
|
|
)
|
|
.sort();
|
|
expect(npmNames).toEqual(selectorNames);
|
|
expect(npmNames).toContain('@qwen-code/web-shell');
|
|
});
|
|
|
|
function writeFile(root, relativePath, content) {
|
|
const filePath = path.join(root, relativePath);
|
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
writeFileSync(filePath, content);
|
|
}
|
|
});
|