Skip to content

Commit

Permalink
feat: Add full file evaluation mode (#61)
Browse files Browse the repository at this point in the history
  • Loading branch information
Danielku15 authored May 20, 2024
1 parent e224a88 commit 9b72080
Show file tree
Hide file tree
Showing 26 changed files with 596 additions and 104 deletions.
34 changes: 34 additions & 0 deletions .vscode-ci-test-reporter.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const BaseReporter = require('mocha/lib/reporters/base');
const SpecReporter = require('mocha/lib/reporters/spec');
const JsonReporter = require('mocha/lib/reporters/json');
const Mocha = require('mocha');

module.exports = class MultiReporter extends BaseReporter {
reporters;
Expand All @@ -14,6 +15,39 @@ module.exports = class MultiReporter extends BaseReporter {
new JsonReporter(runner, {
reporterOption: options.reporterOption.jsonReporterOption,
}),
new (class TestExecutionLogReporter {
constructor(runner) {
let indent = 0;
function log(txt) {
console.log(' '.repeat(indent * 2) + txt)
}

runner.once(Mocha.Runner.constants.EVENT_RUN_BEGIN, () => {
log('Begin Run')
indent++;
});
runner.once(Mocha.Runner.constants.EVENT_RUN_END, () => {
indent--;
log('End Run')
});
runner.once(Mocha.Runner.constants.EVENT_SUITE_BEGIN, (suite) => {
log(`Begin Suite '${suite.titlePath()}'`)
indent++;
});
runner.once(Mocha.Runner.constants.EVENT_SUITE_END, (suite) => {
indent--;
log(`End Suite '${suite.titlePath()}'`)
});
runner.once(Mocha.Runner.constants.EVENT_TEST_BEGIN, (test) => {
log(`Begin Test '${test.title}'`)
indent++;
});
runner.once(Mocha.Runner.constants.EVENT_TEST_END, (test) => {
indent--;
log(`End Test '${test.title}'`)
});
}
})(runner),
];
}
};
5 changes: 4 additions & 1 deletion .vscode-test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ let createCommonOptions = (label) => {
version: vsCodeVersion,
env: {
MOCHA_COLORS: 'true',
MOCHA_VSCODE_TEST: 'true'
},
mocha: {
ui: 'bdd',
Expand All @@ -34,7 +35,9 @@ let createCommonOptions = (label) => {
return {
platform: vsCodePlatform,
version: vsCodeVersion,

env: {
MOCHA_VSCODE_TEST: 'true'
},
mocha: {
ui: 'bdd',
timeout: 60_000,
Expand Down
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ This extension automatically discovers and works with the `.mocharc.js/cjs/yaml/

## Configuration

- `mocha-vscode.extractSettings`: configures how tests get extracted. You can configure:
- `mocha-vscode.extractSettings`: Configures how tests get extracted. You can configure:

- The `extractWith` mode, that specifies if tests are extracted via evaluation or syntax-tree parsing. Evaluation is likely to lead to better results, but may have side-effects. Defaults to `evaluation`.
- The `extractWith` mode, that specifies if tests are extracted.
- `evaluation-cjs` (default) Translate the test file to CommonJS and evaluate it with all dependencies mocked.
- `evaluation-cjs-full` Translate the test file to CommonJS and fully evaluate it with all dependencies.
- `syntax` Parse the file and try to extract the tests from the syntax tree.
- The `extractTimeout` limiting how long the extraction of tests for a single file is allowed to take.
- The `test` and `suite` identifiers the process extracts. Defaults to `["it", "test"]` and `["describe", "suite"]` respectively, covering Mocha's common interfaces.

Expand Down
9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
"title": "Mocha for VS Code",
"properties": {
"mocha-vscode.extractSettings": {
"markdownDescription": "Configures how tests get extracted. You can configure:\n\n- The `extractWith` mode, that specifies if tests are extracted via evaluation or syntax-tree parsing.\n- The `extractTimeout` limiting how long the extraction of tests for a single file is allowed to take.\n- The `test` and `suite` identifiers the process extracts.",
"markdownDescription": "Configures how tests get extracted. You can configure:\n\n- The `extractWith` mode, that specifies if tests are extracted.\n - `evaluation-cjs` (default) Translate the test file to CommonJS and evaluate it with all dependencies mocked.\n - `evaluation-cjs-full` Translate the test file to CommonJS and fully evaluate it with all dependencies.\n - `syntax` Parse the file and try to extract the tests from the syntax tree.\n- The `extractTimeout` limiting how long the extraction of tests for a single file is allowed to take.\n- The `test` and `suite` identifiers the process extracts. Defaults to `[\"it\", \"test\"]` and `[\"describe\", \"suite\"]` respectively, covering Mocha's common interfaces.\n\n- `mocha-vscode.debugOptions`: options, normally found in the launch.json, to pass when debugging the extension. See [the docs](https://code.visualstudio.com/docs/nodejs/nodejs-debugging#_launch-configuration-attributes) for a complete list of options.",
"type": "object",
"properties": {
"suite": {
Expand All @@ -56,7 +56,8 @@
"extractWith": {
"type": "string",
"enum": [
"evaluation",
"evaluation-cjs",
"evaluation-cjs-full",
"syntax"
]
},
Expand All @@ -73,7 +74,7 @@
"it",
"test"
],
"extractWith": "evaluation",
"extractWith": "evaluation-cjs",
"extractTimeout": 10000
},
"required": [
Expand All @@ -94,7 +95,7 @@
},
"activationEvents": [
"workspaceContains:**/.mocharc.{js,cjs,yaml,yml,json,jsonc}",
"onCommand:mocha-vscode.get-controllers-for-test"
"onCommand:mocha-vscode.getControllersForTest"
],
"repository": {
"type": "git",
Expand Down
4 changes: 2 additions & 2 deletions src/configValue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

import * as vscode from 'vscode';

const sectionName = 'extension-test-runner';
const sectionName = 'mocha-vscode';

export class ConfigValue<T> {
private readonly changeEmitter = new vscode.EventEmitter<T>();
Expand Down Expand Up @@ -49,7 +49,7 @@ export class ConfigValue<T> {
this.changeEmitter.dispose();
}

private setValue(value: T) {
public setValue(value: T) {
this._value = value;
this.changeEmitter.fire(this._value);
}
Expand Down
70 changes: 70 additions & 0 deletions src/consoleLogChannel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* 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 type { Event, LogLevel, LogOutputChannel, ViewColumn } from 'vscode';

export class ConsoleOuputChannel implements LogOutputChannel {
constructor(private inner: LogOutputChannel) {}

get logLevel(): LogLevel {
return this.inner.logLevel;
}

get onDidChangeLogLevel(): Event<LogLevel> {
return this.inner.onDidChangeLogLevel;
}
trace(message: string, ...args: any[]): void {
this.inner.trace(message, ...args);
console.trace(`[Mocha VS Code] ${message}`, ...args);
}
debug(message: string, ...args: any[]): void {
this.inner.debug(message, ...args);
console.debug(`[Mocha VS Code] ${message}`, ...args);
}
info(message: string, ...args: any[]): void {
this.inner.info(message, ...args);
console.info(`[Mocha VS Code] ${message}`, ...args);
}
warn(message: string, ...args: any[]): void {
this.inner.warn(message, ...args);
console.warn(`[Mocha VS Code] ${message}`, ...args);
}
error(error: string | Error, ...args: any[]): void {
this.inner.error(error, ...args);
console.error(`[Mocha VS Code] ${error}`, ...args);
}
get name(): string {
return this.inner.name;
}
append(value: string): void {
this.inner.append(value);
}
appendLine(value: string): void {
this.inner.appendLine(value);
}
replace(value: string): void {
this.inner.replace(value);
}
clear(): void {
this.inner.clear();
}
show(columnOrPreserveFocus?: ViewColumn | boolean, preserveFocus?: boolean): void {
if (typeof columnOrPreserveFocus === 'boolean') {
this.inner.show(columnOrPreserveFocus);
} else {
this.inner.show(columnOrPreserveFocus, preserveFocus);
}
}
hide(): void {
this.inner.hide();
}
dispose(): void {
this.inner.dispose();
}
}
4 changes: 2 additions & 2 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ export const configFilePattern = '**/.mocharc.{js,cjs,yaml,yml,json,jsonc}';
export const defaultTestSymbols: IExtensionSettings = {
suite: ['describe', 'suite'],
test: ['it', 'test'],
extractWith: 'evaluation',
extractWith: 'evaluation-cjs',
extractTimeout: 10_000,
};

export const showConfigErrorCommand = 'mocha-vscode.showConfigError';
export const getControllersForTestCommand = 'mocha-vscode.get-controllers-for-test';
export const getControllersForTestCommand = 'mocha-vscode.getControllersForTest';

function equalsIgnoreCase(a: string, b: string) {
return a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0;
Expand Down
24 changes: 17 additions & 7 deletions src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,12 @@ export class Controller {
/** Fired when the file associated with the controller is deleted. */
public readonly onDidDelete: vscode.Event<void>;

private readonly settings = this.disposables.add(
public readonly settings = this.disposables.add(
new ConfigValue('extractSettings', defaultTestSymbols),
);
private readonly watcher = this.disposables.add(new MutableDisposable());
private readonly didChangeEmitter = new vscode.EventEmitter<void>();
private readonly scanCompleteEmitter = new vscode.EventEmitter<void>();
private runProfiles = new Map<string, vscode.TestRunProfile[]>();

/** Error item shown in the tree, if any. */
Expand All @@ -102,8 +103,9 @@ export class Controller {
}
>();

/** Change emitter used for testing, to pick up when the file watcher detects a chagne */
/** Change emitter used for testing, to pick up when the file watcher detects a change */
public readonly onDidChange = this.didChangeEmitter.event;
public readonly onScanComplete = this.scanCompleteEmitter.event;
private tsconfigStore?: TsConfigStore;

/** Gets run profiles the controller has registerd. */
Expand All @@ -130,10 +132,14 @@ export class Controller {

this.recreateDiscoverer();

const rescan = (reason: string) => {
logChannel.info(`Rescan of tests triggered (${reason})`);
this.recreateDiscoverer();
this.scanFiles();
const rescan = async (reason: string) => {
try {
logChannel.info(`Rescan of tests triggered (${reason})`);
this.recreateDiscoverer();
await this.scanFiles();
} catch (e) {
this.logChannel.error(e as Error, 'Failed to rescan tests');
}
};
this.disposables.add(this.configFile.onDidChange(() => rescan('mocharc changed')));
this.disposables.add(this.settings.onDidChange(() => rescan('settings changed')));
Expand Down Expand Up @@ -161,7 +167,7 @@ export class Controller {

this.discoverer = new SettingsBasedFallbackTestDiscoverer(
this.logChannel,
this.settings.value,
this.settings,
this.tsconfigStore!,
);
}
Expand Down Expand Up @@ -438,12 +444,14 @@ export class Controller {
}

if (!this.watcher.value) {
this.logChannel.trace('Missing watcher, creating it');
// starting the watcher will call this again
return this.startWatchingWorkspace();
}

const configs = await this.readConfig();
if (!configs) {
this.logChannel.trace('Skipping scan, no configs');
return;
}

Expand Down Expand Up @@ -480,6 +488,8 @@ export class Controller {
if (this.testsInFiles.size === 0) {
this.watcher.clear(); // stop watching if there are no tests discovered
}

this.scanCompleteEmitter.fire();
}

/** Gets the test collection for a file of the given URI, descending from the root. */
Expand Down
72 changes: 72 additions & 0 deletions src/discoverer/evaluate-full.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* 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 { TraceMap } from '@jridgewell/trace-mapping';
import { build as esbuildBuild } from 'esbuild';
import { createRequire } from 'module';
import * as vm from 'vm';
import * as vscode from 'vscode';
import { ConfigValue } from '../configValue';
import { isEsm, isTypeScript } from '../constants';
import { TsConfigStore } from '../tsconfig-store';
import { EvaluationTestDiscoverer } from './evaluate';
import { IExtensionSettings } from './types';

export class FullEvaluationTestDiscoverer extends EvaluationTestDiscoverer {
constructor(
logChannel: vscode.LogOutputChannel | undefined,
settings: ConfigValue<IExtensionSettings>,
tsconfigStore: TsConfigStore,
) {
super(logChannel, settings, tsconfigStore);
}

protected evaluate(contextObj: vm.Context, filePath: string, code: string) {
contextObj['require'] = createRequire(filePath);
return super.evaluate(contextObj, filePath, code);
}

override async transpileCode(
filePath: string,
code: string,
): Promise<[string, TraceMap | undefined]> {
let sourceMap: TraceMap | undefined;
const needsTranspile = isTypeScript(filePath) || isEsm(filePath, code);

if (needsTranspile) {
const result = await esbuildBuild({
...this.esbuildCommonOptions(filePath),
entryPoints: [filePath],
bundle: true,
sourcemap: 'external', // need source map for correct test positions
write: false,
outfile: 'tests.js',
});

const jsFile = result.outputFiles.find((f) => f.path.endsWith('.js'));
const mapFile = result.outputFiles.find((f) => f.path.endsWith('.js.map'));

if (jsFile && mapFile) {
code = jsFile.text;
try {
sourceMap = new TraceMap(mapFile.text, filePath);
} catch (e) {
this.logChannel?.error('Error parsing source map of TypeScript output', e);
}
}
}

return [code, sourceMap];
}

protected buildDynamicModules(): Map<string, Set<string>> {
// no dynamic modules, only real ones.
return new Map<string, Set<string>>();
}
}
Loading

0 comments on commit 9b72080

Please sign in to comment.