Skip to content

Commit

Permalink
Merge pull request #23 from microsoft/connor4312/mocha-require-api
Browse files Browse the repository at this point in the history
fix: properly support mocha 
equire API
  • Loading branch information
connor4312 authored Feb 16, 2024
2 parents 9f939e3 + 8d22c57 commit 981f155
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 138 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default defineConfig([
workspaceFolder: `${__dirname}/sampleWorkspace`,
// Optional: additional mocha options to use:
mocha: {
preload: `./out/test-utils.js`,
require: `./out/test-utils.js`,
timeout: 20000,
},
},
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@vscode/test-cli",
"version": "0.0.6",
"version": "0.0.7",
"description": "Command-line runner for VS Code extension tests",
"scripts": {
"prepack": "npm run build",
Expand Down
6 changes: 3 additions & 3 deletions src/bin.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@

import * as chokidar from 'chokidar';
import { cliArgs, configFileDefault } from './cli/args.mjs';
import { Coverage } from './cli/coverage.mjs';
import { IPreparedRun, IRunContext, platforms } from './cli/platform/index.mjs';
import {
ResolvedTestConfiguration,
loadDefaultConfigFile,
tryLoadConfigFile,
} from './cli/resolver.mjs';
} from './cli/config.mjs';
import { Coverage } from './cli/coverage.mjs';
import { IPreparedRun, IRunContext, platforms } from './cli/platform/index.mjs';
import { TestConfiguration } from './config.cjs';

export const args = cliArgs.parseSync();
Expand Down
121 changes: 121 additions & 0 deletions src/cli/config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { existsSync, promises as fs } from 'fs';
import { dirname, isAbsolute, join } from 'path';
import { pathToFileURL } from 'url';
import {
IConfigurationWithGlobalOptions,
ICoverageConfiguration,
TestConfiguration,
} from '../config.cjs';
import { CliExpectedError } from './error.mjs';
import { mustResolve } from './resolver.mjs';
import { ensureArray } from './util.mjs';

type ConfigOrArray = IConfigurationWithGlobalOptions | TestConfiguration | TestConfiguration[];

const configFileRules: {
[ext: string]: (path: string) => Promise<ConfigOrArray | Promise<ConfigOrArray>>;
} = {
json: (path: string) => fs.readFile(path, 'utf8').then(JSON.parse),
js: (path) => import(pathToFileURL(path).toString()),
mjs: (path) => import(pathToFileURL(path).toString()),
};

/** Loads the default config based on the process working directory. */
export async function loadDefaultConfigFile(): Promise<ResolvedTestConfiguration> {
const base = '.vscode-test';

let dir = process.cwd();
while (true) {
for (const ext of Object.keys(configFileRules)) {
const candidate = join(dir, `${base}.${ext}`);
if (existsSync(candidate)) {
return tryLoadConfigFile(candidate);
}
}

const next = dirname(dir);
if (next === dir) {
break;
}

dir = next;
}

throw new CliExpectedError(
`Could not find a ${base} file in this directory or any parent. You can specify one with the --config option.`,
);
}

/** Loads a specific config file by the path, throwing if loading fails. */
export async function tryLoadConfigFile(path: string): Promise<ResolvedTestConfiguration> {
const ext = path.split('.').pop()!;
if (!configFileRules.hasOwnProperty(ext)) {
throw new CliExpectedError(
`I don't know how to load the extension '${ext}'. We can load: ${Object.keys(
configFileRules,
).join(', ')}`,
);
}

try {
let loaded = await configFileRules[ext](path);
if ('default' in loaded) {
// handle default es module exports
loaded = (loaded as { default: TestConfiguration }).default;
}
// allow returned promises to resolve:
loaded = await loaded;

if (typeof loaded === 'object' && 'tests' in loaded) {
return await ResolvedTestConfiguration.load(loaded, path);
}

return await ResolvedTestConfiguration.load({ tests: ensureArray(loaded) }, path);
} catch (e) {
throw new CliExpectedError(`Could not read config file ${path}: ${(e as Error).stack || e}`);
}
}

export class ResolvedTestConfiguration implements IConfigurationWithGlobalOptions {
public readonly tests: TestConfiguration[];
public readonly coverage: ICoverageConfiguration | undefined;
/** Directory name the configuration file resides in. */
public readonly dir: string;

public static async load(config: IConfigurationWithGlobalOptions, path: string) {
// Resolve all mocha `require` locations relative to the configuration file,
// since these are otherwise relative to the runner which is opaque to the user.
const dir = dirname(path);
for (const test of config.tests) {
if (test.mocha?.require) {
test.mocha.require = await Promise.all(
ensureArray(test.mocha.require).map((f) => mustResolve(dir, f)),
);
}
}

return new ResolvedTestConfiguration(config, path);
}

protected constructor(
config: IConfigurationWithGlobalOptions,
public readonly path: string,
) {
this.coverage = config.coverage;
this.tests = config.tests;
this.dir = dirname(path);
}

/**
* Gets the resolved extension development path for the test configuration.
*/
public extensionDevelopmentPath(test: TestConfiguration) {
return ensureArray(test.extensionDevelopmentPath?.slice() || this.dir).map((p) =>
isAbsolute(p) ? p : join(this.dir, p),
);
}
}
2 changes: 1 addition & 1 deletion src/cli/coverage.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { existsSync, promises as fs, mkdirSync } from 'fs';
import { tmpdir } from 'os';
import { join, resolve } from 'path';
import { CliArgs } from './args.mjs';
import { ResolvedTestConfiguration } from './config.mjs';
import { CliExpectedError } from './error.mjs';
import { ResolvedTestConfiguration } from './resolver.mjs';

const srcDirCandidates = ['src', 'lib', '.'];

Expand Down
32 changes: 4 additions & 28 deletions src/cli/platform/desktop.mts
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,17 @@
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import resolveCb from 'enhanced-resolve';
import { resolve as resolvePath } from 'path';
import supportsColor from 'supports-color';
import { fileURLToPath, pathToFileURL } from 'url';
import { promisify } from 'util';
import { IDesktopTestConfiguration } from '../../config.cjs';
import { CliArgs } from '../args.mjs';
import { CliExpectedError } from '../error.mjs';
import { ResolvedTestConfiguration } from '../config.mjs';
import { gatherFiles } from '../gatherFiles.mjs';
import { ResolvedTestConfiguration } from '../resolver.mjs';
import { mustResolve } from '../resolver.mjs';
import { ensureArray } from '../util.mjs';
import { IPlatform, IPrepareContext, IPreparedRun, IRunContext } from './index.mjs';

const resolveModule = promisify(resolveCb);

/**
* Resolves the module in context of the configuration.
*
* Only does traditional Node resolution without looking at the `exports` field
* or alternative extensions (cjs/mjs) to match what the VS Code loader does.
*/
const mustResolve = async (config: ResolvedTestConfiguration, moduleName: string) => {
const path = await resolveModule(config.dir, moduleName);
if (!path) {
let msg = `Could not resolve module "${moduleName}" in ${path}`;
if (!moduleName.startsWith('.')) {
msg += ' (you may need to install with `npm install`)';
}

throw new CliExpectedError(msg);
}

return path;
};

export class DesktopPlatform implements IPlatform {
/** @inheritdoc */
public async prepare({
Expand All @@ -60,7 +36,7 @@ export class DesktopPlatform implements IPlatform {

const preload = await Promise.all(
[...ensureArray(test.mocha?.preload || []), ...ensureArray(args.file || [])].map((p) =>
mustResolve(config, String(p)),
mustResolve(config.dir, String(p)),
),
);

Expand Down Expand Up @@ -98,7 +74,7 @@ class PreparedDesktopRun implements IPreparedRun {
) {}

private async importTestElectron() {
const electronPath = await mustResolve(this.config, '@vscode/test-electron');
const electronPath = await mustResolve(this.config.dir, '@vscode/test-electron');
const electron: typeof import('@vscode/test-electron') = await import(
pathToFileURL(electronPath).toString()
);
Expand Down
2 changes: 1 addition & 1 deletion src/cli/platform/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import { TestConfiguration } from '../../config.cjs';
import { CliArgs } from '../args.mjs';
import { ResolvedTestConfiguration } from '../resolver.mjs';
import { ResolvedTestConfiguration } from '../config.mjs';
import { DesktopPlatform } from './desktop.mjs';

export interface IPrepareContext {
Expand Down
118 changes: 19 additions & 99 deletions src/cli/resolver.mts
Original file line number Diff line number Diff line change
@@ -1,105 +1,25 @@
/*---------------------------------------------------------
* Copyright (C) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------*/

import { existsSync, promises as fs } from 'fs';
import { dirname, isAbsolute, join } from 'path';
import { pathToFileURL } from 'url';
import {
IConfigurationWithGlobalOptions,
ICoverageConfiguration,
TestConfiguration,
} from '../config.cjs';
import resolveCb from 'enhanced-resolve';
import { promisify } from 'util';
import { CliExpectedError } from './error.mjs';
import { ensureArray } from './util.mjs';

type ConfigOrArray = IConfigurationWithGlobalOptions | TestConfiguration | TestConfiguration[];

const configFileRules: {
[ext: string]: (path: string) => Promise<ConfigOrArray | Promise<ConfigOrArray>>;
} = {
json: (path: string) => fs.readFile(path, 'utf8').then(JSON.parse),
js: (path) => import(pathToFileURL(path).toString()),
mjs: (path) => import(pathToFileURL(path).toString()),
};

/** Loads the default config based on the process working directory. */
export async function loadDefaultConfigFile(): Promise<ResolvedTestConfiguration> {
const base = '.vscode-test';

let dir = process.cwd();
while (true) {
for (const ext of Object.keys(configFileRules)) {
const candidate = join(dir, `${base}.${ext}`);
if (existsSync(candidate)) {
return tryLoadConfigFile(candidate);
}
export const commonJsResolve = promisify(resolveCb);

/**
* Resolves the module in context of the configuration.
*
* Only does traditional Node resolution without looking at the `exports` field
* or alternative extensions (cjs/mjs) to match what the VS Code loader does.
*/
export const mustResolve = async (context: string, moduleName: string) => {
const path = await commonJsResolve(context, moduleName);
if (!path) {
let msg = `Could not resolve module "${moduleName}" in ${path}`;
if (!moduleName.startsWith('.')) {
msg += ' (you may need to install with `npm install`)';
}

const next = dirname(dir);
if (next === dir) {
break;
}

dir = next;
throw new CliExpectedError(msg);
}

throw new CliExpectedError(
`Could not find a ${base} file in this directory or any parent. You can specify one with the --config option.`,
);
}

/** Loads a specific config file by the path, throwing if loading fails. */
export async function tryLoadConfigFile(path: string): Promise<ResolvedTestConfiguration> {
const ext = path.split('.').pop()!;
if (!configFileRules.hasOwnProperty(ext)) {
throw new CliExpectedError(
`I don't know how to load the extension '${ext}'. We can load: ${Object.keys(
configFileRules,
).join(', ')}`,
);
}

try {
let loaded = await configFileRules[ext](path);
if ('default' in loaded) {
// handle default es module exports
loaded = (loaded as { default: TestConfiguration }).default;
}
// allow returned promises to resolve:
loaded = await loaded;

if (typeof loaded === 'object' && 'tests' in loaded) {
return new ResolvedTestConfiguration(loaded, path);
}

return new ResolvedTestConfiguration({ tests: ensureArray(loaded) }, path);
} catch (e) {
throw new CliExpectedError(`Could not read config file ${path}: ${(e as Error).stack || e}`);
}
}

export class ResolvedTestConfiguration implements IConfigurationWithGlobalOptions {
public readonly tests: TestConfiguration[];
public readonly coverage: ICoverageConfiguration | undefined;
/** Directory name the configuration file resides in. */
public readonly dir: string;

constructor(
config: IConfigurationWithGlobalOptions,
public readonly path: string,
) {
this.coverage = config.coverage;
this.tests = config.tests;
this.dir = dirname(path);
}

/**
* Gets the resolved extension development path for the test configuration.
*/
public extensionDevelopmentPath(test: TestConfiguration) {
return ensureArray(test.extensionDevelopmentPath?.slice() || this.dir).map((p) =>
isAbsolute(p) ? p : join(this.dir, p),
);
}
}
return path;
};
1 change: 1 addition & 0 deletions src/config.cts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface IBaseTestConfiguration {
mocha?: Mocha.MochaOptions & {
/**
* Specify file(s) to be loaded prior to root suite.
* @deprecated use `require` instead
*/
preload?: string | string[];
};
Expand Down
Loading

0 comments on commit 981f155

Please sign in to comment.