From bc8c0dbc3abeb0d2216306b3cd94dc4e3bd26f3c Mon Sep 17 00:00:00 2001 From: Dirk Baeumer Date: Wed, 14 Oct 2026 18:06:52 +0200 Subject: [PATCH] Make repository more agent ready --- README.md | 2 + build/bin/all.js | 11 ++- build/test/all.test.js | 121 ++++++++++++++++++++++++ client-node-tests/src/converter.test.ts | 32 +++++-- package.json | 3 +- 5 files changed, 159 insertions(+), 10 deletions(-) create mode 100644 build/test/all.test.js diff --git a/README.md b/README.md index 2a69b0ba2..2727f5405 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ language servers for [VSCode](https://code.visualstudio.com/). After cloning the repository, run `npm install` to install dependencies and `npm run symlink` to point packages in this repository to each other. +Run `npm run test:build` to test the package command runner, or `npm test` to also run the package test suites. The package command runner continues through the selected packages and exits with the first nonzero status if any command fails. Spawn errors and signal termination are reported and cause a nonzero exit status. + ## History ## 3.18.4 Protocol, 9.0.3 JSON-RPC, 10.1.2 Client and 10.1.2 Server. diff --git a/build/bin/all.js b/build/bin/all.js index f4814d18a..66b30bc64 100644 --- a/build/bin/all.js +++ b/build/bin/all.js @@ -38,6 +38,15 @@ const script = args[0] === 'run' ? args[1] : args[0]; for (const elem of folders.map(item => { return { folder: item.folder, scripts: new Set(item.scripts) }; } )) { if (elem.scripts.has(script)) { console.log(path.join(root, elem.folder)); - child_process.spawnSync(`npm ${args.join(' ')}`, { cwd: path.join(root, elem.folder), shell: true, stdio: 'inherit' }); + const result = child_process.spawnSync(`npm ${args.join(' ')}`, { cwd: path.join(root, elem.folder), shell: true, stdio: 'inherit' }); + if (result.error !== undefined) { + console.error(result.error); + } + if (result.signal !== null) { + console.error(`Command terminated by signal ${result.signal}.`); + } + if (result.error !== undefined || result.signal !== null || result.status !== 0) { + process.exitCode = process.exitCode || result.status || 1; + } } } \ No newline at end of file diff --git a/build/test/all.test.js b/build/test/all.test.js new file mode 100644 index 000000000..a242ef9ce --- /dev/null +++ b/build/test/all.test.js @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +const assert = require('assert'); +const child_process = require('child_process'); +const fs = require('fs'); +const path = require('path'); +const vm = require('vm'); + +const root = path.resolve(__dirname, '..', '..'); +const filename = path.join(root, 'build', 'bin', 'all.js'); +const source = fs.readFileSync(filename, 'utf8').replace(/^#![^\r\n]*/, ''); + +function run(args, results = [], env = {}) { + const calls = []; + const errors = []; + const process = { argv: ['node', filename, ...args], env, exitCode: 0 }; + const execute = vm.compileFunction(source, ['require', 'process', 'console', '__dirname'], { filename }); + execute((id) => { + if (id === 'child_process') { + return { + spawnSync(command, options) { + calls.push({ command, ...options }); + return { status: 0, signal: null, ...results[calls.length - 1] }; + } + }; + } + assert.strictEqual(id, 'path'); + return path; + }, process, { log() {}, error(error) { errors.push(error); } }, path.dirname(filename)); + return { calls, errors, exitCode: process.exitCode }; +} + +suite('Package command runner', () => { + for (const status of [0, 2]) { + test(`exits the Node process with status ${status}`, () => { + const result = child_process.spawnSync(process.execPath, ['-e', ` + require('child_process').spawnSync = () => ({ status: ${status}, signal: null }); + process.argv = ['node', ${JSON.stringify(filename)}, 'run', 'test']; + require(${JSON.stringify(filename)}); + `], { encoding: 'utf8' }); + assert.ifError(result.error); + assert.strictEqual(result.signal, null); + assert.strictEqual(result.status, status, result.stderr); + }); + } + + test('runs tests in the existing package order', () => { + const result = run(['run', 'test']); + assert.strictEqual(result.exitCode, 0); + assert.deepStrictEqual(result.errors, []); + assert.deepStrictEqual(result.calls.map(call => path.basename(call.cwd)), [ + 'textDocument', 'types', 'jsonrpc', 'protocol', 'server', 'client-node-tests' + ]); + for (const call of result.calls) { + assert.strictEqual(call.command, 'npm run test'); + assert.strictEqual(call.shell, true); + assert.strictEqual(call.stdio, 'inherit'); + assert.strictEqual(path.dirname(call.cwd), root); + } + }); + + for (const args of [['run', 'test'], ['run', 'lint'], ['install'], ['run', 'clean'], ['audit']]) { + test(`propagates a failed ${args.join(' ')} command and continues running packages`, () => { + const success = run(args); + const result = run(args, [{ status: 2 }]); + assert.strictEqual(result.exitCode, 2); + assert.deepStrictEqual(result.calls, success.calls); + }); + } + + test('preserves the first failure when later commands fail or succeed', () => { + const result = run(['run', 'test'], [{ status: 2 }, { status: 3 }]); + assert.strictEqual(result.exitCode, 2); + assert.strictEqual(result.calls.length, 6); + }); + + test('propagates a failure in the last package', () => { + const result = run(['run', 'test'], [{}, {}, {}, {}, {}, { status: 7 }]); + assert.strictEqual(result.exitCode, 7); + }); + + test('reports spawn errors and exits unsuccessfully', () => { + const error = new Error('Unable to spawn npm'); + const result = run(['run', 'test'], [{ status: null, error }]); + assert.strictEqual(result.exitCode, 1); + assert.deepStrictEqual(result.errors, [error]); + assert.strictEqual(result.calls.length, 6); + }); + + test('reports signal termination and exits unsuccessfully', () => { + const result = run(['run', 'test'], [{ status: null, signal: 'SIGTERM' }]); + assert.strictEqual(result.exitCode, 1); + assert.deepStrictEqual(result.errors, ['Command terminated by signal SIGTERM.']); + assert.strictEqual(result.calls.length, 6); + }); + + test('does not treat a missing exit status as success', () => { + assert.strictEqual(run(['run', 'test'], [{ status: null }]).exitCode, 1); + }); + + test('preserves additional command arguments', () => { + const result = run(['audit', 'fix']); + assert.strictEqual(result.exitCode, 0); + assert.strictEqual(result.calls.length, 7); + assert.ok(result.calls.every(call => call.command === 'npm audit fix')); + }); + + test('skips package installation for root-only installs', () => { + const result = run(['install'], [], { npm_config_root_only: 'true' }); + assert.deepStrictEqual(result.calls, []); + assert.strictEqual(result.exitCode, 0); + }); + + test('root-only installation setting does not skip tests', () => { + assert.strictEqual(run(['run', 'test'], [], { npm_config_root_only: 'true' }).calls.length, 6); + }); +}); diff --git a/client-node-tests/src/converter.test.ts b/client-node-tests/src/converter.test.ts index 85bbbb8c0..6ef7d9809 100644 --- a/client-node-tests/src/converter.test.ts +++ b/client-node-tests/src/converter.test.ts @@ -15,6 +15,7 @@ declare module 'vscode' { } import { strictEqual, deepEqual, ok, deepStrictEqual } from 'assert'; +import * as sinon from 'sinon'; import * as proto from 'vscode-languageclient'; import * as codeConverter from 'vscode-languageclient/$test/common/codeConverter'; @@ -89,6 +90,8 @@ function assertDiagnosticCode(value: string | number | DiagnosticCode | undefine suite('Async Array', () => { + let clock: sinon.SinonFakeTimers; + suiteSetup(() => { async.setTestMode(); }); @@ -97,14 +100,25 @@ suite('Async Array', () => { async.clearTestMode(); }); + setup(() => { + clock = sinon.useFakeTimers({ toFake: ['Date'] }); + }); + + teardown(() => { + clock.restore(); + }); + test('map', async() => { - const ranges: proto.Range[] = new Array(20000); + const ranges: proto.Range[] = new Array(10); for (let i = 0; i < ranges.length; i++) { ranges[i] = proto.Range.create(i, i, i, i); } let yielded = 0; - const converted = await async.map(ranges, p2c.asRange, undefined, { yieldAfter: 2, yieldCallback: () => { yielded++; }}); - ok(yielded > 0); + const converted = await async.map(ranges, (item) => { + clock.tick(2); + return p2c.asRange(item); + }, undefined, { yieldAfter: 2, yieldCallback: () => { yielded++; }}); + strictEqual(yielded, ranges.length); strictEqual(converted.length, ranges.length); for (let i = 0; i < converted.length; i++) { ok(converted[i] instanceof vscode.Range); @@ -113,7 +127,7 @@ suite('Async Array', () => { }).timeout(5000); test('map async', async() => { - const ranges: proto.Range[] = new Array(5000); + const ranges: proto.Range[] = new Array(10); for (let i = 0; i < ranges.length; i++) { ranges[i] = proto.Range.create(i, i, i, i); } @@ -121,11 +135,12 @@ suite('Async Array', () => { const converted = await async.mapAsync(ranges, (item): Promise => { return new Promise((resolve) => { proto.RAL().timer.setImmediate(() => { + clock.tick(2); resolve(p2c.asRange(item)); }); }); }, undefined, { yieldAfter: 2, yieldCallback: () => { yielded++; }}); - ok(yielded > 0); + strictEqual(yielded, ranges.length); strictEqual(converted.length, ranges.length); for (let i = 0; i < converted.length; i++) { ok(converted[i] instanceof vscode.Range); @@ -134,18 +149,19 @@ suite('Async Array', () => { }).timeout(5000); test('forEach', async() => { - const ranges: proto.Range[] = new Array(30000); + const ranges: proto.Range[] = new Array(10); for (let i = 0; i < ranges.length; i++) { ranges[i] = proto.Range.create(i + 1, 0, i + 2, 1); } let sum: number = 0; let yielded = 0; await async.forEach(ranges, (item) => { + clock.tick(2); const codeRange = p2c.asRange(item); sum += codeRange.start.line; }, undefined, { yieldAfter: 2, yieldCallback: () => { yielded++; }}); - ok(yielded > 0); - strictEqual(sum, 450015000); + strictEqual(yielded, ranges.length); + strictEqual(sum, 55); }).timeout(5000); }); diff --git a/package.json b/package.json index 832836af7..25a5f264f 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,8 @@ }, "scripts": { "postinstall": "node ./build/bin/all.js install && cd testbed && npm install && cd .. && npm run symlink:testbed && npx playwright install", - "test": "node ./build/bin/all.js run test", + "test": "npm run test:build && node ./build/bin/all.js run test", + "test:build": "mocha --ui tdd \"build/test/**/*.test.js\"", "clean": "node ./build/bin/all.js run clean", "symlink": "node ./build/bin/symlink.js", "symlink:tests": "node ./build/bin/symlink-tests.js",