Skip to content
Merged
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
22 changes: 19 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,28 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Install Node.js
- name: Install Node.js
if: runner.os != 'Linux'
uses: actions/setup-node@v4
with:
node-version: lts/*

- name: Install Node.js via NVM (Linux)
if: runner.os == 'Linux'
shell: bash
run: |
export NVM_DIR="$HOME/.nvm"
source "$NVM_DIR/nvm.sh"
echo $NVM_DIR >> $GITHUB_PATH
echo "NVM_DIR=$NVM_DIR" >> $GITHUB_ENV
nvm install $(cat test-workspaces/nvm/.nvmrc)
nvm install lts/*

- name: Install dependencies
run: npm install
run: |
node --version
npm --version
npm install

- name: Compile
run: npm run compile:test
Expand Down Expand Up @@ -70,11 +85,12 @@ jobs:
if: always()
run: npm run lint

- uses: dorny/test-reporter@v1
- uses: dorny/test-reporter@1a288b62f8b75c0f433cbfdbc2e4800fbae50bd7
if: ${{ (success() || failure()) && github.event.pull_request.head.repo.full_name == github.repository }}
with:
name: VS Code Test Results (${{matrix.os}}, ${{matrix.vscode-version}}, ${{matrix.vscode-platform}})
path: 'test-results/*.json'
use-actions-summary: 'true'
reporter: mocha-json

- uses: actions/upload-artifact@v4
Expand Down
5 changes: 2 additions & 3 deletions .vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';


const dirname = fileURLToPath(new URL('.', import.meta.url));
const integrationTestDir = path.join(dirname, 'out/test/integration');
const workspaceBaseDir = path.join(dirname, 'test-workspaces');
Expand All @@ -15,8 +14,8 @@ let extensionDevelopmentPath = '';

const testMode = process.env.TEST_MODE ?? 'normal';

const tempDir = process.env.TEST_TEMP ?? path.join(dirname, 'tmp');
if (testMode === 'vsix') {
const tempDir = process.env.TEST_TEMP ?? path.join(dirname, 'tmp')
extensionDevelopmentPath = path.resolve(path.join(tempDir, 'vsix', 'extension'));
}

Expand All @@ -27,6 +26,7 @@ function createCommonOptions(label) {
version: vsCodeVersion,
env: {
MOCHA_VSCODE_TEST: 'true',
TEST_TEMP: tempDir,
},
mocha: {
ui: 'bdd',
Expand All @@ -48,7 +48,6 @@ function createCommonOptions(label) {
options.extensionDevelopmentPath = extensionDevelopmentPath;
}


return options;
}

Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@
"esbuild": "^0.24.2"
},
"mocha-vscode": {
"version": "v1.2.3+d27e65f",
"date": "2024-10-20T15:38:24.261Z"
"version": "v1.2.4+9a52d28",
"date": "2025-02-02T11:56:15.601Z"
}
}
}
76 changes: 63 additions & 13 deletions src/configurationFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import * as path from 'path';
import * as vscode from 'vscode';
import { DisposableStore } from './disposable';
import { HumanError } from './errors';
import { getPathToNode } from './node';
import { getPathToNode, isNvmInstalled } from './node';

type OptionsModule = {
loadOptions(): IResolvedConfiguration;
Expand All @@ -41,6 +41,7 @@ export class ConfigurationFile implements vscode.Disposable {
private _optionsModule?: OptionsModule;
private _configModule?: ConfigModule;
private _pathToMocha?: string;
private _pathToNvmRc?: string;

/** Cached read promise, invalided on file change. */
private readPromise?: Promise<ConfigurationList>;
Expand Down Expand Up @@ -140,14 +141,23 @@ export class ConfigurationFile implements vscode.Disposable {

async getMochaSpawnArgs(customArgs: readonly string[]): Promise<string[]> {
this._pathToMocha ??= await this._resolveLocalMochaBinPath();
this._pathToNvmRc ??= await this._resolveNvmRc();

let nodeSpawnArgs: string[];
if (
this._pathToNvmRc &&
(await fs.promises
.access(this._pathToNvmRc)
.then(() => true)
.catch(() => false))
) {
nodeSpawnArgs = ['nvm', 'run'];
} else {
this._pathToNvmRc = undefined;
nodeSpawnArgs = [await getPathToNode(this.logChannel)];
}

return [
await getPathToNode(this.logChannel),
this._pathToMocha,
'--config',
this.uri.fsPath,
...customArgs,
];
return [...nodeSpawnArgs, this._pathToMocha, '--config', this.uri.fsPath, ...customArgs];
}

private getResolver() {
Expand Down Expand Up @@ -179,6 +189,42 @@ export class ConfigurationFile implements vscode.Disposable {
throw new HumanError(`Could not find node_modules above '${mocha}'`);
}

private async _resolveNvmRc(): Promise<string | undefined> {
// the .nvmrc file can be placed in any location up the directory tree, so we do the same
// starting from the mocha config file
// https://github.com/nvm-sh/nvm/blob/06413631029de32cd9af15b6b7f6ed77743cbd79/nvm.sh#L475-L491
try {
if (!(await isNvmInstalled())) {
return undefined;
}

let dir: string | undefined = path.dirname(this.uri.fsPath);

while (dir) {
const nvmrc = path.join(dir, '.nvmrc');
if (
await fs.promises
.access(nvmrc)
.then(() => true)
.catch(() => false)
) {
this.logChannel.debug(`Found .nvmrc at ${nvmrc}`);
return nvmrc;
}

const parent = path.dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
} catch (e) {
this.logChannel.error(e as Error, 'Error while searching for nvmrc');
}

return undefined;
}

private async _resolveLocalMochaBinPath(): Promise<string> {
try {
const packageJsonPath = await this._resolveLocalMochaPath('/package.json');
Expand All @@ -193,17 +239,21 @@ export class ConfigurationFile implements vscode.Disposable {
// ignore
}

this.logChannel.warn('Could not resolve mocha bin path from package.json, fallback to default');
this.logChannel.info('Could not resolve mocha bin path from package.json, fallback to default');
return await this._resolveLocalMochaPath('/bin/mocha.js');
}

private _resolveLocalMochaPath(suffix: string = ''): Promise<string> {
return this._resolve(`mocha${suffix}`);
}

private _resolve(request: string): Promise<string> {
return new Promise<string>((resolve, reject) => {
const dir = path.dirname(this.uri.fsPath);
this.logChannel.debug(`resolving 'mocha${suffix}' via ${dir}`);
this.getResolver().resolve({}, dir, 'mocha' + suffix, {}, (err, res) => {
this.logChannel.debug(`resolving '${request}' via ${dir}`);
this.getResolver().resolve({}, dir, request, {}, (err, res) => {
if (err) {
this.logChannel.error(`resolving 'mocha${suffix}' failed with error ${err}`);
this.logChannel.error(`resolving '${request}' failed with error ${err}`);
reject(
new HumanError(
`Could not find mocha in working directory '${path.dirname(
Expand All @@ -212,7 +262,7 @@ export class ConfigurationFile implements vscode.Disposable {
),
);
} else {
this.logChannel.debug(`'mocha${suffix}' resolved to '${res}'`);
this.logChannel.debug(`'${request}' resolved to '${res}'`);
resolve(res as string);
}
});
Expand Down
20 changes: 13 additions & 7 deletions src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
* https://opensource.org/licenses/MIT.
*/

import fs from 'fs';
import { homedir } from 'os';
import path from 'path';
import * as vscode from 'vscode';
import which from 'which';

export async function getPathTo(logChannel: vscode.LogOutputChannel, bin: string, name: string) {
async function getPathTo(logChannel: vscode.LogOutputChannel, bin: string, name: string) {
logChannel.debug(`Resolving ${name} executable`);
let pathToBin = await which(bin, { nothrow: true });
if (pathToBin) {
Expand All @@ -34,11 +37,14 @@ export async function getPathToNode(logChannel: vscode.LogOutputChannel) {
return pathToNode;
}

let pathToNpm: string | null = null;

export async function getPathToNpm(logChannel: vscode.LogOutputChannel) {
if (!pathToNpm) {
pathToNpm = await getPathTo(logChannel, 'npm', 'NPM');
export async function isNvmInstalled() {
// https://github.com/nvm-sh/nvm/blob/179d45050be0a71fd57591b0ed8aedf9b177ba10/install.sh#L27
const nvmDir = process.env.NVM_DIR || homedir();
// https://github.com/nvm-sh/nvm/blob/179d45050be0a71fd57591b0ed8aedf9b177ba10/install.sh#L143
try {
await fs.promises.access(path.join(nvmDir, '.nvm', '.git'));
return true;
} catch (e) {
return false;
}
return pathToNpm;
}
61 changes: 61 additions & 0 deletions src/test/integration/nvm.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Copyright (C) Daniel Kuschny (Danielku15) and contributors.
* Copyright (C) Microsoft Corporation. All rights reserved.
*
* Use of this source code is governed by an MIT-style
* license that can be found in the LICENSE file or at
* https://opensource.org/licenses/MIT.
*/

import { expect } from 'chai';
import fs from 'fs';
import os from 'os';
import path from 'path';
import * as vscode from 'vscode';
import { isNvmInstalled } from '../../node';
import { captureTestRun, expectTestTree, getController, integrationTestPrepare } from '../util';

describe('nvm', () => {
const workingDir = integrationTestPrepare('nvm');

it('discovers tests', async () => {
const c = await getController();

expectTestTree(c, [['nvm.test.js', [['nvm', [['ensure-version']]]]]]);
});

it('runs tests', async () => {
const c = await getController();
const profiles = c.profiles;
expect(profiles).to.have.lengthOf(2);

const run = await captureTestRun(
c,
new vscode.TestRunRequest(
undefined,
undefined,
profiles.find((p) => p.kind === vscode.TestRunProfileKind.Run),
),
);

run.expectStates({
'nvm.test.js/nvm/ensure-version': ['enqueued', 'started', 'passed'],
});

const expectedVersion = await fs.promises.readFile(path.join(workingDir, '.nvmrc'), 'utf-8');
const actualVersion = await fs.promises.readFile(
path.resolve(__dirname, '..', '..', '..', 'tmp', '.nvmrc-actual'),
'utf-8',
);

// nvm is only available on MacOS and Linux
// so we skip it on windows.
// also if NVM on local development we skip this test (for GITHUB_ACTIONS we expect it to be there).
const shouldRun =
os.platform() === 'linux' && ((await isNvmInstalled()) || process.env.GITHUB_ACTIONS);
console.log(`Expecting node ${expectedVersion}, ran in ${actualVersion}`);
if (shouldRun) {
expect(process.version).to.match(new RegExp(expectedVersion + '.*'));
}
});
});
3 changes: 3 additions & 0 deletions test-workspaces/nvm/.mocharc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
spec: '**/*.test.js'
};
1 change: 1 addition & 0 deletions test-workspaces/nvm/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v20
9 changes: 9 additions & 0 deletions test-workspaces/nvm/nvm.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const { writeFileSync, mkdirSync } = require('node:fs');
const { join } = require('node:path');

describe('nvm', () => {
it('ensure-version', () => {
mkdirSync(process.env.TEST_TEMP, { recursive: true });
writeFileSync(join(process.env.TEST_TEMP, '.nvmrc-actual'), process.version);
});
});