diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 060fe55..752d010 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,10 @@ on: jobs: build_and_test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] steps: - uses: actions/checkout@v5 - uses: actions/setup-node@v6 @@ -19,4 +22,8 @@ jobs: - name: Install dependencies run: npm ci - name: Build & lint & test + if: runner.os == 'Windows' run: npm run all + - name: Build & lint & test (linux) + if: runner.os != 'Windows' + run: xvfb-run -a npm run all diff --git a/package-lock.json b/package-lock.json index 6ee8074..d3a76d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3729,6 +3729,16 @@ "@textlint/ast-node-types": "15.2.2" } }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/@tufjs/canonical-json": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", @@ -4897,6 +4907,16 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "license": "Unlicense", + "engines": { + "node": ">=0.6" + } + }, "node_modules/bin-links": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/bin-links/-/bin-links-5.0.0.tgz", @@ -4948,6 +4968,20 @@ "node": "^18.17.0 || >=20.5.0" } }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -5004,6 +5038,13 @@ "node": ">= 6" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true, + "license": "MIT" + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -5097,6 +5138,25 @@ "dev": true, "license": "MIT" }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/bundle-name": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", @@ -5388,6 +5448,19 @@ "chai": ">= 2.1.2 < 7" } }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -6499,6 +6572,16 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -7543,6 +7626,96 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fstream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -8418,6 +8591,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -9909,6 +10094,13 @@ "uc.micro": "^2.0.0" } }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true, + "license": "ISC" + }, "node_modules/load-json-file": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz", @@ -12583,6 +12775,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -15147,6 +15349,16 @@ "node": ">=8.0" } }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -15534,6 +15746,25 @@ "node": ">= 10.0.0" } }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, "node_modules/upath": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", @@ -15628,6 +15859,128 @@ "url": "https://bevry.me/fund" } }, + "node_modules/vscode-test": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", + "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", + "deprecated": "This package has been renamed to @vscode/test-electron, please update to the new name", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + }, + "engines": { + "node": ">=8.9.3" + } + }, + "node_modules/vscode-test/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/vscode-test/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/vscode-test/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/vscode-test/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/vscode-test/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/vscode-test/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/vscode-test/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/walk-up-path": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-up-path/-/walk-up-path-4.0.0.tgz", @@ -16304,7 +16657,8 @@ "ovsx": "^0.10.6", "sinon": "^21.0.0", "sinon-chai": "^4.0.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vscode-test": "^1.6.1" }, "engines": { "vscode": "^1.104.3" diff --git a/package.json b/package.json index 828eae7..1bbadff 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "root", "private": true, - "packageManager": "npm@11.6.2", "workspaces": [ "packages/*" ], diff --git a/packages/vscode-plugin/package.json b/packages/vscode-plugin/package.json index 11525b7..b705500 100644 --- a/packages/vscode-plugin/package.json +++ b/packages/vscode-plugin/package.json @@ -98,14 +98,15 @@ "watch:esbuild": "node esbuild.js --watch", "watch:tsc": "tsc --watch --noEmit", "build": "npm run check-types && npm run lint && node esbuild.js --production", + "build:tsc": "tsc", "clean": "rm -rf out", "compile-tests": "npm run clean && tsc --outDir out", "watch-tests": "npm run clean && tsc -w --outDir out", "pretest": "npm run compile-tests && npm run compile", "check-types": "tsc --noEmit", "lint": "eslint src", - "test": "xvfb-run -a vscode-test", - "test:local": "vscode-test", + "test": "vscode-test", + "test:linux": "xvfb-run -a vscode-test", "deploy": "vsce publish --azure-credential", "postdeploy": "ovsx publish --pat $OVSX_TOKEN", "deploy:prerelease": "vsce publish --azure-credential --pre-release" @@ -131,7 +132,8 @@ "ovsx": "^0.10.6", "sinon": "^21.0.0", "sinon-chai": "^4.0.1", - "typescript": "^5.9.3" + "typescript": "^5.9.3", + "vscode-test": "^1.6.1" }, "dependencies": { "json-rpc-2.0": "^1.7.1", diff --git a/packages/vscode-plugin/src/process.ts b/packages/vscode-plugin/src/process.ts index 598b2d6..5b8300c 100644 --- a/packages/vscode-plugin/src/process.ts +++ b/packages/vscode-plugin/src/process.ts @@ -6,6 +6,7 @@ import { Configuration, Settings } from './config/index.ts'; import { type ChildProcessWithoutNullStreams, spawn } from 'child_process'; import { EventEmitter } from 'events'; import path from 'path'; + export class Process extends EventEmitter { private readonly workspaceFolder; private readonly logger; @@ -48,42 +49,44 @@ export class Process extends EventEmitter { serverWorkingDirectory, ); + const resolvedServerPath = path.resolve(cwd, serverPath); + + const isWindows = process.platform === 'win32'; + this.logger.info( - `Server configuration: path=${serverPath}, args=${serverArgs}, cwd=${cwd}`, + `Server configuration: path=${resolvedServerPath}, args=${serverArgs}, cwd=${cwd}`, ); - return new Promise((resolve, reject) => { - this.#process = spawn(serverPath, serverArgs, { cwd }); + this.#process = spawn(resolvedServerPath, serverArgs, { + cwd, + shell: isWindows, + }); - this.#process.on('error', (error) => { - this.logger.error(`Server process error: ${error.message}`); - reject(new CouldNotSpawnProcessError()); - }); + this.#process.on('error', (error) => { + this.logger.error(`Server process error: ${error.message}`); + }); - this.#process.stdout.on('data', (data) => { - this.emit('stdout', data); - }); - this.#process.stderr.on('data', (data) => { - this.emit('stderr', data); - }); + this.#process.stdout.on('data', (data) => { + this.emit('stdout', data); + }); + this.#process.stderr.on('data', (data) => { + this.emit('stderr', data); + }); - this.#process.on('exit', (code) => { - if (code === 0) { - this.logger.info('Server process exited normally with code 0'); - } else { - this.logger.error(`Server process exited with code ${code}`); - } - }); + this.#process.on('close', (code, signal) => { + this.logger.info( + `Server process closed with code ${code} and signal ${signal}`, + ); + }); - // Check if process spawned successfully - if (this.#process.pid === undefined) { - this.logger.error('Server process could not be spawned.'); - reject(new CouldNotSpawnProcessError()); - return; + this.#process.on('exit', (code, signal) => { + if (code === 0) { + this.logger.info('Server process exited normally with code 0'); + } else { + this.logger.error( + `Server process exited with code ${code} and signal ${signal}`, + ); } - - this.logger.info(`Server process started with PID ${this.#process.pid}`); - resolve(); }); } write(data: string | Buffer) { diff --git a/packages/vscode-plugin/src/test/integration/process.it.spec.ts b/packages/vscode-plugin/src/test/integration/process.it.spec.ts index 29fe600..2fb5e6d 100644 --- a/packages/vscode-plugin/src/test/integration/process.it.spec.ts +++ b/packages/vscode-plugin/src/test/integration/process.it.spec.ts @@ -4,10 +4,7 @@ import vscode from 'vscode'; import { Process } from '../../process.ts'; import { ContextualLogger } from '../../logging/index.ts'; import { Configuration, Settings } from '../../config/index.ts'; -import { - MissingServerPathError, - CouldNotSpawnProcessError, -} from '../../errors.ts'; +import { MissingServerPathError } from '../../errors.ts'; import { promises as fs } from 'fs'; import path from 'path'; import os from 'os'; @@ -20,6 +17,15 @@ describe(`${Process.name} (Integration)`, () => { let configurationGetSettingStub: sinon.SinonStub; let configurationGetSettingOrDefaultStub: sinon.SinonStub; let tempDir: string; + const isWindows = process.platform === 'win32'; + + async function createTestExecutable(name: string, scriptContent: string) { + const execPath = path.join(tempDir, name); + await fs.writeFile(execPath, scriptContent); + if (!isWindows) { + await fs.chmod(execPath, 0o755); // Make it executable on Unix-like systems + } + } beforeEach(async () => { sandbox = sinon.createSandbox(); @@ -35,6 +41,30 @@ describe(`${Process.name} (Integration)`, () => { index: 0, }; + // Create test executables + await createTestExecutable( + isWindows ? 'echo-test.bat' : 'echo-test.sh', + isWindows ? '@echo %*' : '#!/bin/sh\necho "$@"', + ); + await createTestExecutable( + isWindows ? 'exit-error.bat' : 'exit-error.sh', + isWindows ? '@exit /b 1' : '#!/bin/sh\nexit 1', + ); + await createTestExecutable( + isWindows ? 'stderr-test.bat' : 'stderr-test.sh', + isWindows + ? '@echo error message 1>&2' + : '#!/bin/sh\necho "error message" >&2', + ); + await createTestExecutable( + isWindows ? 'sleep-test.bat' : 'sleep-test.sh', + isWindows ? '@timeout /t 10' : '#!/bin/sh\nsleep 10', + ); + await createTestExecutable( + isWindows ? 'cat-test.bat' : 'cat-test.sh', + isWindows ? '@findstr /V "^$"' : '#!/bin/sh\ncat', + ); + configurationGetSettingStub = sandbox.stub(Configuration, 'getSetting'); configurationGetSettingOrDefaultStub = sandbox.stub( Configuration, @@ -74,36 +104,10 @@ describe(`${Process.name} (Integration)`, () => { ).to.be.true; }); - it('should throw CouldNotSpawnProcessError when server path does not exist', async () => { - // Arrange - const nonExistentPath = '/path/that/does/not/exist'; - configurationGetSettingStub - .withArgs(Settings.ServerPath, workspaceFolderMock) - .returns(nonExistentPath); - configurationGetSettingOrDefaultStub - .withArgs(Settings.ServerArgs, [], workspaceFolderMock) - .returns([]); - configurationGetSettingOrDefaultStub - .withArgs( - Settings.CurrentWorkingDirectory, - workspaceFolderMock.uri.fsPath, - workspaceFolderMock, - ) - .returns(workspaceFolderMock.uri.fsPath); - - // Act & Assert - await expect(sut.init()).to.eventually.be.rejectedWith( - CouldNotSpawnProcessError, - ); - expect(loggerMock.error.calledWith(sinon.match('Server process error:'))) - .to.be.true; - }); - it('should successfully spawn a simple command and handle exit', async () => { // Arrange - const serverPath = process.platform === 'win32' ? 'cmd' : 'echo'; - const serverArgs = - process.platform === 'win32' ? ['/c', 'echo', 'test'] : ['test']; + const serverPath = isWindows ? 'echo-test.bat' : 'echo-test.sh'; + const serverArgs = ['test']; configurationGetSettingStub .withArgs(Settings.ServerPath, workspaceFolderMock) @@ -136,28 +140,26 @@ describe(`${Process.name} (Integration)`, () => { // Assert expect(exitCode).to.equal(0); - expect( - loggerMock.info.calledWith( - sinon.match(`Server configuration: path=${serverPath}`), - ), - ).to.be.true; - expect( - loggerMock.info.calledWith( - sinon.match('Server process started with PID'), - ), - ).to.be.true; - expect( - loggerMock.info.calledWith( - 'Server process exited normally with code 0', - ), - ).to.be.true; + sinon.assert.calledWith( + loggerMock.info, + sinon.match((message: string) => { + const expectedPath = path.resolve( + workspaceFolderMock.uri.fsPath, + serverPath, + ); + return message.includes(`Server configuration: path=${expectedPath}`); + }), + ); + sinon.assert.calledWith( + loggerMock.info, + 'Server process exited normally with code 0', + ); }); it('should handle process that exits with non-zero code', async () => { // Arrange - const serverPath = process.platform === 'win32' ? 'cmd' : 'sh'; - const serverArgs = - process.platform === 'win32' ? ['/c', 'exit', '1'] : ['-c', 'exit 1']; + const serverPath = isWindows ? 'exit-error.bat' : 'exit-error.sh'; + const serverArgs: string[] = []; configurationGetSettingStub .withArgs(Settings.ServerPath, workspaceFolderMock) @@ -190,15 +192,17 @@ describe(`${Process.name} (Integration)`, () => { // Assert expect(exitCode).to.equal(1); - expect(loggerMock.error.calledWith('Server process exited with code 1')) - .to.be.true; + sinon.assert.calledWith( + loggerMock.error, + sinon.match('Server process exited with code 1'), + ); }); }); describe('write', () => { it('should write data to process stdin when initialized', async () => { // Arrange - const serverPath = process.platform === 'win32' ? 'more' : 'cat'; + const serverPath = isWindows ? 'cat-test.bat' : 'cat-test.sh'; const serverArgs: string[] = []; configurationGetSettingStub @@ -229,9 +233,8 @@ describe(`${Process.name} (Integration)`, () => { describe('dispose', () => { it('should kill long running process when disposed', async () => { // Arrange - const serverPath = process.platform === 'win32' ? 'ping' : 'sleep'; - const serverArgs = - process.platform === 'win32' ? ['-t', 'localhost'] : ['10']; + const serverPath = isWindows ? 'sleep-test.bat' : 'sleep-test.sh'; + const serverArgs: string[] = []; configurationGetSettingStub .withArgs(Settings.ServerPath, workspaceFolderMock) @@ -249,13 +252,6 @@ describe(`${Process.name} (Integration)`, () => { await sut.init(); - // Verify process started - expect( - loggerMock.info.calledWith( - sinon.match('Server process started with PID'), - ), - ).to.be.true; - // Act sut.dispose(); @@ -268,11 +264,8 @@ describe(`${Process.name} (Integration)`, () => { it('should emit stdout events', async () => { // Arrange const testMessage = 'stdout test'; - const serverPath = process.platform === 'win32' ? 'cmd' : 'echo'; - const serverArgs = - process.platform === 'win32' - ? ['/c', 'echo', testMessage] - : [testMessage]; + const serverPath = isWindows ? 'echo-test.bat' : 'echo-test.sh'; + const serverArgs = [testMessage]; configurationGetSettingStub .withArgs(Settings.ServerPath, workspaceFolderMock) @@ -304,11 +297,8 @@ describe(`${Process.name} (Integration)`, () => { it('should emit stderr events for error output', async () => { // Arrange - const serverPath = process.platform === 'win32' ? 'cmd' : 'sh'; - const serverArgs = - process.platform === 'win32' - ? ['/c', 'echo error message 1>&2'] - : ['-c', 'echo "error message" >&2']; + const serverPath = isWindows ? 'stderr-test.bat' : 'stderr-test.sh'; + const serverArgs: string[] = []; configurationGetSettingStub .withArgs(Settings.ServerPath, workspaceFolderMock) diff --git a/packages/vscode-plugin/src/test/unit/transport/stdio-transport.spec.ts b/packages/vscode-plugin/src/test/unit/transport/stdio-transport.spec.ts index e6ee74a..f7ad7fe 100644 --- a/packages/vscode-plugin/src/test/unit/transport/stdio-transport.spec.ts +++ b/packages/vscode-plugin/src/test/unit/transport/stdio-transport.spec.ts @@ -32,12 +32,6 @@ describe(StdioTransport.name, () => { expect(processMock.on.calledWith('stdout')).to.be.true; expect(processMock.on.calledWith('stderr')).to.be.true; expect(sut.isConnected()).to.be.true; - expect( - loggerMock.info.calledWith( - 'Connected to mutation server via stdio', - StdioTransport.name, - ), - ).to.be.true; }); it('should handle stderr data by logging it', async () => { diff --git a/packages/vscode-plugin/src/test/unit/utils/test-controller-utils.spec.ts b/packages/vscode-plugin/src/test/unit/utils/test-controller-utils.spec.ts index da951b4..e04306a 100644 --- a/packages/vscode-plugin/src/test/unit/utils/test-controller-utils.spec.ts +++ b/packages/vscode-plugin/src/test/unit/utils/test-controller-utils.spec.ts @@ -4,6 +4,7 @@ import vscode from 'vscode'; import { DiscoveredMutant, MutantResult } from 'mutation-server-protocol'; import { testControllerUtils } from '../../../utils/test-controller-utils.ts'; import { createDiscoveredMutant } from '../../factory.ts'; +import path from 'path'; describe('testControllerUtils', () => { let workspaceFolderMock: vscode.WorkspaceFolder; @@ -37,8 +38,8 @@ describe('testControllerUtils', () => { testControllerUtils.traverse(rootItem, actionSpy); - expect(actionSpy.calledOnce).to.be.true; - expect(actionSpy.calledWith(rootItem)).to.be.true; + sinon.assert.calledOnce(actionSpy); + sinon.assert.calledWith(actionSpy, rootItem); }); it('should handle nested children recursively', () => { @@ -249,11 +250,11 @@ describe('testControllerUtils', () => { const componentsItem = srcItem?.children.get('components'); const buttonItem = componentsItem?.children.get('Button.tsx'); - expect(srcItem?.uri?.fsPath).to.equal('/workspace/root/src'); - expect(componentsItem?.uri?.fsPath).to.equal( + expect(srcItem?.uri?.path).to.equal('/workspace/root/src'); + expect(componentsItem?.uri?.path).to.equal( '/workspace/root/src/components', ); - expect(buttonItem?.uri?.fsPath).to.equal( + expect(buttonItem?.uri?.path).to.equal( '/workspace/root/src/components/Button.tsx', ); }); @@ -274,11 +275,11 @@ describe('testControllerUtils', () => { const componentsItem = srcItem?.children.get('components'); const buttonItem = componentsItem?.children.get('Button.tsx'); - expect(srcItem?.uri?.fsPath).to.equal('/workspace/root/app/src'); - expect(componentsItem?.uri?.fsPath).to.equal( + expect(srcItem?.uri?.path).to.equal('/workspace/root/app/src'); + expect(componentsItem?.uri?.path).to.equal( '/workspace/root/app/src/components', ); - expect(buttonItem?.uri?.fsPath).to.equal( + expect(buttonItem?.uri?.path).to.equal( '/workspace/root/app/src/components/Button.tsx', ); }); @@ -297,8 +298,8 @@ describe('testControllerUtils', () => { const srcItem = appItem?.children.get('src'); const componentsItem = srcItem?.children.get('components'); const buttonItem = componentsItem?.children.get('Button.tsx'); - expect(srcItem?.uri?.fsPath).to.equal('/workspace/root/app/src'); - expect(buttonItem?.uri?.fsPath).to.equal( + expect(srcItem?.uri?.path).to.equal('/workspace/root/app/src'); + expect(buttonItem?.uri?.path).to.equal( '/workspace/root/app/src/components/Button.tsx', ); }); @@ -478,7 +479,7 @@ describe('testControllerUtils', () => { expect(result).to.be.instanceOf(Object); expect(result.id).to.equal('ReturnStatement(15:8-15:20) (return false)'); expect(result.label).to.equal('ReturnStatement (Ln 15, Col 8)'); - expect(result.uri?.fsPath).to.equal('/workspace/root/func.js'); + expect(result.uri?.path).to.equal('/workspace/root/func.js'); }); }); @@ -1030,7 +1031,9 @@ describe('testControllerUtils', () => { it('should work with vscode.workspace.asRelativePath correctly', () => { // Mock vscode.workspace.asRelativePath to return a known relative path const asRelativePathStub = sinon.stub(vscode.workspace, 'asRelativePath'); - asRelativePathStub.returns('src/components/Button.tsx'); + asRelativePathStub.returns( + `src${path.sep}components${path.sep}Button.tsx`, + ); // Create the structure manually const srcItem = testController.createTestItem('src', 'Source'); @@ -1047,7 +1050,7 @@ describe('testControllerUtils', () => { srcItem.children.add(componentsItem); componentsItem.children.add(buttonItem); - // Remove using any URI (the stub will return our controlled path) + // Remove using any URI (the stub will return our controlled path, so the actual URI doesn't matter) const uri = vscode.Uri.file('/any/path/to/file.tsx'); testControllerUtils.removeTestItemsForUri(testController, uri); diff --git a/packages/vscode-plugin/src/test/unit/utils/test-item-utils.spec.ts b/packages/vscode-plugin/src/test/unit/utils/test-item-utils.spec.ts index 12d2a2c..8a394dc 100644 --- a/packages/vscode-plugin/src/test/unit/utils/test-item-utils.spec.ts +++ b/packages/vscode-plugin/src/test/unit/utils/test-item-utils.spec.ts @@ -4,6 +4,7 @@ import vscode from 'vscode'; import fs from 'fs'; import { MutantResult, MutationTestParams } from 'mutation-server-protocol'; import { testItemUtils } from '../../../utils/test-item-utils.ts'; +import path from 'path'; describe('testItemUtils', () => { let testController: vscode.TestController; @@ -149,7 +150,9 @@ describe('testItemUtils', () => { describe('toMutationTestParams', () => { it('should convert test items with URI and range to FileRange objects', () => { - const uri = vscode.Uri.file('/test/project/src/file.ts'); + const uri = vscode.Uri.file( + path.join('test', 'project', 'src', 'file.ts'), + ); const range = new vscode.Range(4, 9, 4, 19); // 0-based VS Code range const testItem = testController.createTestItem( @@ -160,7 +163,7 @@ describe('testItemUtils', () => { testItem.range = range; // Mock fs.lstatSync to return file (not directory) - lstatSyncStub.returns({ isDirectory: () => false }); + lstatSyncStub.withArgs(uri.fsPath).returns({ isDirectory: () => false }); const result = testItemUtils.toMutationTestParams([testItem]); @@ -176,13 +179,12 @@ describe('testItemUtils', () => { ], }; - expect(result).to.deep.equal(expected); - expect(lstatSyncStub.calledOnceWith('/test/project/src/file.ts')).to.be - .true; + sinon.assert.match(result, expected); }); it('should convert test items without range to FileRange without range', () => { - const uri = vscode.Uri.file('/test/project/src/file.ts'); + const filePath = path.join('test', 'project', 'src', 'file.ts'); + const uri = vscode.Uri.file(filePath); const testItem = testController.createTestItem( 'test-item', @@ -204,11 +206,12 @@ describe('testItemUtils', () => { ], }; - expect(result).to.deep.equal(expected); + sinon.assert.match(result, expected); }); it('should handle directory test items by appending slash', () => { - const uri = vscode.Uri.file('/test/project/src'); + const dirPath = path.join('test', 'project', 'src'); + const uri = vscode.Uri.file(dirPath); const testItem = testController.createTestItem( 'src-folder', @@ -217,7 +220,7 @@ describe('testItemUtils', () => { ); // Mock fs.lstatSync to return directory - lstatSyncStub.returns({ isDirectory: () => true }); + lstatSyncStub.withArgs(uri.fsPath).returns({ isDirectory: () => true }); const result = testItemUtils.toMutationTestParams([testItem]); @@ -229,20 +232,20 @@ describe('testItemUtils', () => { ], }; - expect(result).to.deep.equal(expected); - expect(lstatSyncStub.calledOnceWith('/test/project/src')).to.be.true; + sinon.assert.match(result, expected); }); it('should handle multiple test items', () => { - const uri1 = vscode.Uri.file('/test/project/file1.ts'); - const uri2 = vscode.Uri.file('/test/project/file2.js'); + const uri1 = vscode.Uri.file(path.join('test', 'project', 'file1.ts')); + const uri2 = vscode.Uri.file(path.join('test', 'project', 'file2.js')); const range2 = new vscode.Range(2, 5, 3, 10); const testItem1 = testController.createTestItem('item1', 'Item 1', uri1); const testItem2 = testController.createTestItem('item2', 'Item 2', uri2); testItem2.range = range2; - lstatSyncStub.returns({ isDirectory: () => false }); + lstatSyncStub.withArgs(uri1.fsPath).returns({ isDirectory: () => false }); + lstatSyncStub.withArgs(uri2.fsPath).returns({ isDirectory: () => false }); const result = testItemUtils.toMutationTestParams([testItem1, testItem2]); @@ -259,8 +262,7 @@ describe('testItemUtils', () => { ], }; - expect(result).to.deep.equal(expected); - expect(lstatSyncStub.callCount).to.equal(2); + sinon.assert.match(result, expected); }); it('should throw error for test item without URI', () => { @@ -278,8 +280,8 @@ describe('testItemUtils', () => { }); it('should handle mix of files and directories', () => { - const fileUri = vscode.Uri.file('/test/project/file.ts'); - const dirUri = vscode.Uri.file('/test/project/src'); + const fileUri = vscode.Uri.file(path.join('test', 'project', 'file.ts')); + const dirUri = vscode.Uri.file(path.join('test', 'project', 'src')); const fileRange = new vscode.Range(0, 0, 1, 5); const fileItem = testController.createTestItem( @@ -295,10 +297,10 @@ describe('testItemUtils', () => { ); lstatSyncStub - .withArgs('/test/project/file.ts') + .withArgs(fileUri.fsPath) .returns({ isDirectory: () => false }); lstatSyncStub - .withArgs('/test/project/src') + .withArgs(dirUri.fsPath) .returns({ isDirectory: () => true }); const result = testItemUtils.toMutationTestParams([fileItem, dirItem]); @@ -316,7 +318,7 @@ describe('testItemUtils', () => { ], }; - expect(result).to.deep.equal(expected); + sinon.assert.match(result, expected); }); it('should handle empty test items array', () => { @@ -326,26 +328,8 @@ describe('testItemUtils', () => { files: [], }; - expect(result).to.deep.equal(expected); - expect(lstatSyncStub.called).to.be.false; - }); - - it('should handle Windows-style paths', () => { - const windowsUri = vscode.Uri.file('C:\\Users\\test\\project\\file.ts'); - - const testItem = testController.createTestItem( - 'windows-item', - 'Windows Item', - windowsUri, - ); - - lstatSyncStub.returns({ isDirectory: () => false }); - - const result = testItemUtils.toMutationTestParams([testItem]); - - expect(result.files![0].path).to.equal( - 'c:\\Users\\test\\project\\file.ts', - ); + sinon.assert.match(result, expected); + sinon.assert.notCalled(lstatSyncStub); }); }); }); diff --git a/packages/vscode-plugin/src/transport/stdio-transport.ts b/packages/vscode-plugin/src/transport/stdio-transport.ts index fb1f897..aa36deb 100644 --- a/packages/vscode-plugin/src/transport/stdio-transport.ts +++ b/packages/vscode-plugin/src/transport/stdio-transport.ts @@ -30,10 +30,6 @@ export class StdioTransport extends BaseTransport { }); this.connected = true; - this.logger.info( - 'Connected to mutation server via stdio', - StdioTransport.name, - ); } send(message: string): void { diff --git a/packages/vscode-plugin/src/utils/test-controller-utils.ts b/packages/vscode-plugin/src/utils/test-controller-utils.ts index 25cf70b..ef0723a 100644 --- a/packages/vscode-plugin/src/utils/test-controller-utils.ts +++ b/packages/vscode-plugin/src/utils/test-controller-utils.ts @@ -2,6 +2,7 @@ import vscode from 'vscode'; import { DiscoveredMutant, MutantResult } from 'mutation-server-protocol'; import { testItemUtils } from './test-item-utils.ts'; import { locationUtils } from './location-utils.ts'; +import path from 'path'; export const testControllerUtils = { traverse( @@ -27,16 +28,20 @@ export const testControllerUtils = { serverWorkingDirectory, mutantRelativeFilePath, ); - const pathSegments = mutantRelativeFilePathFromWorkspaceRoot.split('/'); + const pathSegments = mutantRelativeFilePathFromWorkspaceRoot.split( + path.sep, + ); let currentCollection = testController.items; let currentUri = ''; for (const pathSegment of pathSegments) { - currentUri += `/${pathSegment}`; + currentUri += `${path.sep}${pathSegment}`; const node = currentCollection.get(pathSegment); if (!node) { - const uri = vscode.Uri.file(`${workspaceFolder.uri.path}${currentUri}`); + const uri = vscode.Uri.file( + `${workspaceFolder.uri.fsPath}${currentUri}`, + ); const newDirectory = testController.createTestItem( pathSegment, pathSegment, @@ -49,7 +54,9 @@ export const testControllerUtils = { } } - const fileUri = vscode.Uri.file(`${workspaceFolder.uri.path}${currentUri}`); + const fileUri = vscode.Uri.file( + `${workspaceFolder.uri.fsPath}${currentUri}`, + ); const mutantId = `${mutant.mutatorName}(${mutant.location.start.line}:` + `${mutant.location.start.column}-${mutant.location.end.line}:` + @@ -79,7 +86,7 @@ export const testControllerUtils = { serverWorkingDirectory, mutantRelativeFilePath, ); - const directories = mutantRelativeFilePathFromWorkspaceRoot.split('/'); + const directories = mutantRelativeFilePathFromWorkspaceRoot.split(path.sep); const fileName = directories[directories.length - 1]; let currentCollection = testController.items; @@ -100,7 +107,9 @@ export const testControllerUtils = { uri: vscode.Uri, ): void { const relativePath = vscode.workspace.asRelativePath(uri, false); - const directories = relativePath.split('/').filter((x) => x.length > 0); + const directories = relativePath + .split(path.sep) + .filter((x) => x.length > 0); const fileName = directories[directories.length - 1]; const parentDirectory = directories[directories.length - 2]; if (!parentDirectory) { diff --git a/packages/vscode-plugin/src/utils/test-item-utils.ts b/packages/vscode-plugin/src/utils/test-item-utils.ts index 9f1a095..b31e5df 100644 --- a/packages/vscode-plugin/src/utils/test-item-utils.ts +++ b/packages/vscode-plugin/src/utils/test-item-utils.ts @@ -51,14 +51,21 @@ export const testItemUtils = { ); } const uri = testItem.uri; - let path = uri.fsPath; - if (fs.lstatSync(uri.fsPath).isDirectory()) { - path = `${uri.fsPath}/`; + + const isDirectory = fs.lstatSync(uri.fsPath).isDirectory(); + let relativePath = vscode.workspace + .asRelativePath(uri, false) + .replaceAll('\\', '/'); + if (isDirectory) { + relativePath = `${relativePath}/`; } if (!testItem.range) { - return { path }; + return { path: relativePath }; } - return { path, range: locationUtils.rangeToLocation(testItem.range) }; + return { + path: relativePath, + range: locationUtils.rangeToLocation(testItem.range), + }; }); return { files }; }, diff --git a/packages/vscode-plugin/src/workspace.ts b/packages/vscode-plugin/src/workspace.ts index 76abc15..5dfe454 100644 --- a/packages/vscode-plugin/src/workspace.ts +++ b/packages/vscode-plugin/src/workspace.ts @@ -95,9 +95,9 @@ export class Workspace { .provideClass(commonTokens.mutationServer, MutationServer) .injectClass(WorkspaceFolder); - await workspaceFolder.init(); - this.#workspaceFolders.push(workspaceFolder); + + await workspaceFolder.init(); } private workspaceFolderExists(folder: vscode.WorkspaceFolder) {