mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-10 00:27:05 -06:00
eng: add support for snapshot tests (#190444)
* eng: add support for snapshot tests This adds Jest-like support for snapshot testing. Developers can do something like: ```js await assertSnapshot(myComplexObject) ``` The first time this is run, the snapshot expectation file is written to a `__snapshots__` directory beside the test file. Subsequent runs will compare the object to the snapshot, and fail if it doesn't match. You can see an example of this in the test for snapshots themselves! After a successful run, any unused snapshots are cleaned up. On a failed run, a gitignored `.actual` snapshot file is created beside the snapshot for easy processing and inspection. Shortly I will do some integration with the selfhost test extension to allow developers to easily update snapshots from the vscode UI. For #189680 cc @ulugbekna @hediet * fix async stacktraces getting clobbered * random fixes * comment out leak detector, for now * add option to snapshot file extension
This commit is contained in:
parent
ee823a18e4
commit
6a847ba6d1
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,3 +18,4 @@ vscode.db
|
||||
/cli/target
|
||||
/cli/openssl
|
||||
product.overrides.json
|
||||
*.snap.actual
|
||||
|
||||
@ -150,10 +150,12 @@ function hygiene(some, linting = true) {
|
||||
}
|
||||
|
||||
const productJsonFilter = filter('product.json', { restore: true });
|
||||
const snapshotFilter = filter(['**', '!**/*.snap', '!**/*.snap.actual']);
|
||||
const unicodeFilterStream = filter(unicodeFilter, { restore: true });
|
||||
|
||||
const result = input
|
||||
.pipe(filter((f) => !f.stat.isDirectory()))
|
||||
.pipe(snapshotFilter)
|
||||
.pipe(productJsonFilter)
|
||||
.pipe(process.env['BUILD_SOURCEVERSION'] ? es.through() : productJson)
|
||||
.pipe(productJsonFilter.restore)
|
||||
|
||||
@ -47,7 +47,7 @@ export namespace Iterable {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function find<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): T | undefined;
|
||||
export function find<T, R extends T>(iterable: Iterable<T>, predicate: (t: T) => t is R): R | undefined;
|
||||
export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined;
|
||||
export function find<T>(iterable: Iterable<T>, predicate: (t: T) => boolean): T | undefined {
|
||||
for (const element of iterable) {
|
||||
|
||||
185
src/vs/base/test/common/snapshot.ts
Normal file
185
src/vs/base/test/common/snapshot.ts
Normal file
@ -0,0 +1,185 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Lazy } from 'vs/base/common/lazy';
|
||||
import { FileAccess } from 'vs/base/common/network';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
declare const __readFileInTests: (path: string) => Promise<string>;
|
||||
declare const __writeFileInTests: (path: string, contents: string) => Promise<void>;
|
||||
declare const __readDirInTests: (path: string) => Promise<string[]>;
|
||||
declare const __unlinkInTests: (path: string) => Promise<void>;
|
||||
declare const __mkdirPInTests: (path: string) => Promise<void>;
|
||||
|
||||
// setup on import so assertSnapshot has the current context without explicit passing
|
||||
let context: Lazy<SnapshotContext> | undefined;
|
||||
const sanitizeName = (name: string) => name.replace(/[^a-z0-9_-]/gi, '_');
|
||||
const normalizeCrlf = (str: string) => str.replace(/\r\n/g, '\n');
|
||||
|
||||
export interface ISnapshotOptions {
|
||||
/** Name for snapshot file, rather than an incremented number */
|
||||
name?: string;
|
||||
/** Extension name of the snapshot file, defaults to `.snap` */
|
||||
extension?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is exported only for tests against the snapshotting itself! Use
|
||||
* {@link assertSnapshot} as a consumer!
|
||||
*/
|
||||
export class SnapshotContext {
|
||||
private nextIndex = 0;
|
||||
private readonly namePrefix: string;
|
||||
private readonly snapshotsDir: URI;
|
||||
private readonly usedNames = new Set();
|
||||
|
||||
constructor(private readonly test: Mocha.Test | undefined) {
|
||||
if (!test) {
|
||||
throw new Error('assertSnapshot can only be used in a test');
|
||||
}
|
||||
|
||||
if (!test.file) {
|
||||
throw new Error('currentTest.file is not set, please open an issue with the test you\'re trying to run');
|
||||
}
|
||||
|
||||
const src = FileAccess.asFileUri('');
|
||||
const parts = test.file.split(/[/\\]/g);
|
||||
|
||||
this.namePrefix = sanitizeName(test.fullTitle()) + '_';
|
||||
this.snapshotsDir = URI.joinPath(src, ...[...parts.slice(0, -1), '__snapshots__']);
|
||||
}
|
||||
|
||||
public async assert(value: any, options?: ISnapshotOptions) {
|
||||
const originalStack = new Error().stack!; // save to make the stack nicer on failure
|
||||
const nameOrIndex = (options?.name ? sanitizeName(options.name) : this.nextIndex++);
|
||||
const fileName = this.namePrefix + nameOrIndex + '.' + (options?.extension || 'snap');
|
||||
this.usedNames.add(fileName);
|
||||
|
||||
const fpath = URI.joinPath(this.snapshotsDir, fileName).fsPath;
|
||||
const actual = formatValue(value);
|
||||
let expected: string;
|
||||
try {
|
||||
expected = await __readFileInTests(fpath);
|
||||
} catch {
|
||||
console.info(`Creating new snapshot in: ${fpath}`);
|
||||
await __mkdirPInTests(this.snapshotsDir.fsPath);
|
||||
await __writeFileInTests(fpath, actual);
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizeCrlf(expected) !== normalizeCrlf(actual)) {
|
||||
await __writeFileInTests(fpath + '.actual', actual);
|
||||
const err: any = new Error(`Snapshot #${nameOrIndex} does not match expected output`);
|
||||
err.expected = expected;
|
||||
err.actual = actual;
|
||||
err.snapshotPath = fpath;
|
||||
err.stack = (err.stack as string)
|
||||
.split('\n')
|
||||
// remove all frames from the async stack and keep the original caller's frame
|
||||
.slice(0, 1)
|
||||
.concat(originalStack.split('\n').slice(3))
|
||||
.join('\n');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
public async removeOldSnapshots() {
|
||||
const contents = await __readDirInTests(this.snapshotsDir.fsPath);
|
||||
const toDelete = contents.filter(f => f.startsWith(this.namePrefix) && !this.usedNames.has(f));
|
||||
if (toDelete.length) {
|
||||
console.info(`Deleting ${toDelete.length} old snapshots for ${this.test?.fullTitle()}`);
|
||||
}
|
||||
|
||||
await Promise.all(toDelete.map(f => __unlinkInTests(URI.joinPath(this.snapshotsDir, f).fsPath)));
|
||||
}
|
||||
}
|
||||
|
||||
const debugDescriptionSymbol = Symbol.for('debug.description');
|
||||
|
||||
function formatValue(value: unknown, level = 0, seen: unknown[] = []): string {
|
||||
switch (typeof value) {
|
||||
case 'bigint':
|
||||
case 'boolean':
|
||||
case 'number':
|
||||
case 'symbol':
|
||||
case 'undefined':
|
||||
return String(value);
|
||||
case 'string':
|
||||
return level === 0 ? value : JSON.stringify(value);
|
||||
case 'function':
|
||||
return `[Function ${value.name}]`;
|
||||
case 'object': {
|
||||
if (value === null) {
|
||||
return 'null';
|
||||
}
|
||||
if (value instanceof RegExp) {
|
||||
return String(value);
|
||||
}
|
||||
if (seen.includes(value)) {
|
||||
return '[Circular]';
|
||||
}
|
||||
if (debugDescriptionSymbol in value && typeof (value as any)[debugDescriptionSymbol] === 'function') {
|
||||
return (value as any)[debugDescriptionSymbol]();
|
||||
}
|
||||
const oi = ' '.repeat(level);
|
||||
const ci = ' '.repeat(level + 1);
|
||||
if (Array.isArray(value)) {
|
||||
const children = value.map(v => formatValue(v, level + 1, [...seen, value]));
|
||||
const multiline = children.some(c => c.includes('\n')) || children.join(', ').length > 80;
|
||||
return multiline ? `[\n${ci}${children.join(`,\n${ci}`)}\n${oi}]` : `[ ${children.join(', ')} ]`;
|
||||
}
|
||||
|
||||
let entries;
|
||||
let prefix = '';
|
||||
if (value instanceof Map) {
|
||||
prefix = 'Map ';
|
||||
entries = [...value.entries()];
|
||||
} else if (value instanceof Set) {
|
||||
prefix = 'Set ';
|
||||
entries = [...value.entries()];
|
||||
} else {
|
||||
entries = Object.entries(value);
|
||||
}
|
||||
|
||||
const lines = entries.map(([k, v]) => `${k}: ${formatValue(v, level + 1, [...seen, value])}`);
|
||||
return prefix + (lines.length > 1
|
||||
? `{\n${ci}${lines.join(`,\n${ci}`)}\n${oi}}`
|
||||
: `{ ${lines.join(',\n')} }`);
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown type ${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
setup(function () {
|
||||
const currentTest = this.currentTest;
|
||||
context = new Lazy(() => new SnapshotContext(currentTest));
|
||||
});
|
||||
teardown(async function () {
|
||||
if (this.currentTest?.state === 'passed') {
|
||||
await context?.rawValue?.removeOldSnapshots();
|
||||
}
|
||||
context = undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* Implements a snapshot testing utility. ⚠️ This is async! ⚠️
|
||||
*
|
||||
* The first time a snapshot test is run, it'll record the value it's called
|
||||
* with as the expected value. Subsequent runs will fail if the value differs,
|
||||
* but the snapshot can be regenerated by hand or using the Selfhost Test
|
||||
* Provider Extension which'll offer to update it.
|
||||
*
|
||||
* The snapshot will be associated with the currently running test and stored
|
||||
* in a `__snapshots__` directory next to the test file, which is expected to
|
||||
* be the first `.test.js` file in the callstack.
|
||||
*/
|
||||
export function assertSnapshot(value: any, options?: ISnapshotOptions): Promise<void> {
|
||||
if (!context) {
|
||||
throw new Error('assertSnapshot can only be used in a test');
|
||||
}
|
||||
|
||||
return context.value.assert(value, options);
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
hello_world__0.snap:
|
||||
{ cool: true }
|
||||
hello_world__1.snap:
|
||||
{ nifty: true }
|
||||
hello_world__fourthTest.snap:
|
||||
{ customName: 2 }
|
||||
hello_world__thirdTest.txt:
|
||||
{ customName: 1 }
|
||||
@ -0,0 +1,4 @@
|
||||
hello_world__0.snap:
|
||||
{ cool: true }
|
||||
hello_world__thirdTest.snap:
|
||||
{ customName: 1 }
|
||||
@ -0,0 +1,2 @@
|
||||
hello_world__0.snap:
|
||||
{ cool: true }
|
||||
@ -0,0 +1,35 @@
|
||||
[
|
||||
1,
|
||||
true,
|
||||
undefined,
|
||||
null,
|
||||
123,
|
||||
Symbol(heyo),
|
||||
"hello",
|
||||
{ hello: "world" },
|
||||
{ a: [Circular] },
|
||||
Map {
|
||||
hello: 1,
|
||||
goodbye: 2
|
||||
},
|
||||
Set {
|
||||
1: 1,
|
||||
2: 2,
|
||||
3: 3
|
||||
},
|
||||
[Function helloWorld],
|
||||
/hello/g,
|
||||
[
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string",
|
||||
"long stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong stringlong string"
|
||||
],
|
||||
Range [1 -> 5]
|
||||
]
|
||||
143
src/vs/base/test/node/snapshot.test.ts
Normal file
143
src/vs/base/test/node/snapshot.test.ts
Normal file
@ -0,0 +1,143 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { tmpdir } from 'os';
|
||||
import { getRandomTestPath } from 'vs/base/test/node/testUtils';
|
||||
import { Promises } from 'vs/base/node/pfs';
|
||||
import { SnapshotContext, assertSnapshot } from 'vs/base/test/common/snapshot';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import path = require('path');
|
||||
import { assertThrowsAsync } from 'vs/base/test/common/utils';
|
||||
|
||||
// tests for snapshot are in Node so that we can use native FS operations to
|
||||
// set up and validate things.
|
||||
//
|
||||
// Uses snapshots for testing snapshots. It's snapception!
|
||||
|
||||
suite('snapshot', () => {
|
||||
let testDir: string;
|
||||
|
||||
setup(function () {
|
||||
testDir = getRandomTestPath(tmpdir(), 'vsctests', 'snapshot');
|
||||
return Promises.mkdir(testDir, { recursive: true });
|
||||
});
|
||||
|
||||
teardown(function () {
|
||||
return Promises.rm(testDir);
|
||||
});
|
||||
|
||||
const makeContext = (test: Partial<Mocha.Test> | undefined) => {
|
||||
const ctx = new SnapshotContext(test as Mocha.Test);
|
||||
(ctx as any as { snapshotsDir: URI }).snapshotsDir = URI.file(testDir);
|
||||
return ctx;
|
||||
};
|
||||
|
||||
const snapshotFileTree = async () => {
|
||||
let str = '';
|
||||
|
||||
const printDir = async (dir: string, indent: number) => {
|
||||
const children = await Promises.readdir(dir);
|
||||
for (const child of children) {
|
||||
const p = path.join(dir, child);
|
||||
if ((await Promises.stat(p)).isFile()) {
|
||||
const content = await Promises.readFile(p, 'utf-8');
|
||||
str += `${' '.repeat(indent)}${child}:\n`;
|
||||
for (const line of content.split('\n')) {
|
||||
str += `${' '.repeat(indent + 2)}${line}\n`;
|
||||
}
|
||||
} else {
|
||||
str += `${' '.repeat(indent)}${child}/\n`;
|
||||
await printDir(p, indent + 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await printDir(testDir, 0);
|
||||
await assertSnapshot(str);
|
||||
};
|
||||
|
||||
test('creates a snapshot', async () => {
|
||||
const ctx = makeContext({
|
||||
file: 'foo/bar',
|
||||
fullTitle: () => 'hello world!'
|
||||
});
|
||||
|
||||
await ctx.assert({ cool: true });
|
||||
await snapshotFileTree();
|
||||
});
|
||||
|
||||
test('validates a snapshot', async () => {
|
||||
const ctx1 = makeContext({
|
||||
file: 'foo/bar',
|
||||
fullTitle: () => 'hello world!'
|
||||
});
|
||||
|
||||
await ctx1.assert({ cool: true });
|
||||
|
||||
const ctx2 = makeContext({
|
||||
file: 'foo/bar',
|
||||
fullTitle: () => 'hello world!'
|
||||
});
|
||||
|
||||
// should pass:
|
||||
await ctx2.assert({ cool: true });
|
||||
|
||||
const ctx3 = makeContext({
|
||||
file: 'foo/bar',
|
||||
fullTitle: () => 'hello world!'
|
||||
});
|
||||
|
||||
// should fail:
|
||||
await assertThrowsAsync(() => ctx3.assert({ cool: false }));
|
||||
});
|
||||
|
||||
test('cleans up old snapshots', async () => {
|
||||
const ctx1 = makeContext({
|
||||
file: 'foo/bar',
|
||||
fullTitle: () => 'hello world!'
|
||||
});
|
||||
|
||||
await ctx1.assert({ cool: true });
|
||||
await ctx1.assert({ nifty: true });
|
||||
await ctx1.assert({ customName: 1 }, { name: 'thirdTest', extension: 'txt' });
|
||||
await ctx1.assert({ customName: 2 }, { name: 'fourthTest' });
|
||||
|
||||
await snapshotFileTree();
|
||||
|
||||
const ctx2 = makeContext({
|
||||
file: 'foo/bar',
|
||||
fullTitle: () => 'hello world!'
|
||||
});
|
||||
|
||||
await ctx2.assert({ cool: true });
|
||||
await ctx2.assert({ customName: 1 }, { name: 'thirdTest' });
|
||||
await ctx2.removeOldSnapshots();
|
||||
|
||||
await snapshotFileTree();
|
||||
});
|
||||
|
||||
test('formats object nicely', async () => {
|
||||
const circular: any = {};
|
||||
circular.a = circular;
|
||||
|
||||
await assertSnapshot([
|
||||
1,
|
||||
true,
|
||||
undefined,
|
||||
null,
|
||||
123n,
|
||||
Symbol('heyo'),
|
||||
'hello',
|
||||
{ hello: 'world' },
|
||||
circular,
|
||||
new Map([['hello', 1], ['goodbye', 2]]),
|
||||
new Set([1, 2, 3]),
|
||||
function helloWorld() { },
|
||||
/hello/g,
|
||||
new Array(10).fill('long string'.repeat(10)),
|
||||
{ [Symbol.for('debug.description')]() { return `Range [1 -> 5]`; } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -12,9 +12,9 @@ import { instantiateTextModel } from 'vs/editor/test/common/testTextModel';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { ILanguageService } from 'vs/editor/common/languages/language';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
|
||||
suite('bracket matching', () => {
|
||||
|
||||
let disposables: DisposableStore;
|
||||
let instantiationService: TestInstantiationService;
|
||||
let languageConfigurationService: ILanguageConfigurationService;
|
||||
@ -31,6 +31,8 @@ suite('bracket matching', () => {
|
||||
disposables.dispose();
|
||||
});
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
function createTextModelWithBrackets(text: string) {
|
||||
const languageId = 'bracketMode';
|
||||
disposables.add(languageService.registerLanguage({ id: languageId }));
|
||||
|
||||
@ -12,10 +12,10 @@ import { CompletionItemKind, CompletionItemProvider } from 'vs/editor/common/lan
|
||||
import { CompletionOptions, provideSuggestionItems, SnippetSortOrder } from 'vs/editor/contrib/suggest/browser/suggest';
|
||||
import { createTextModel } from 'vs/editor/test/common/testTextModel';
|
||||
import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
|
||||
|
||||
suite('Suggest', function () {
|
||||
|
||||
let model: TextModel;
|
||||
let registration: IDisposable;
|
||||
let registry: LanguageFeatureRegistry<CompletionItemProvider>;
|
||||
@ -54,37 +54,43 @@ suite('Suggest', function () {
|
||||
model.dispose();
|
||||
});
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('sort - snippet inline', async function () {
|
||||
const { items } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Inline));
|
||||
const { items, disposable } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Inline));
|
||||
assert.strictEqual(items.length, 3);
|
||||
assert.strictEqual(items[0].completion.label, 'aaa');
|
||||
assert.strictEqual(items[1].completion.label, 'fff');
|
||||
assert.strictEqual(items[2].completion.label, 'zzz');
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('sort - snippet top', async function () {
|
||||
const { items } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Top));
|
||||
const { items, disposable } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Top));
|
||||
assert.strictEqual(items.length, 3);
|
||||
assert.strictEqual(items[0].completion.label, 'aaa');
|
||||
assert.strictEqual(items[1].completion.label, 'zzz');
|
||||
assert.strictEqual(items[2].completion.label, 'fff');
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('sort - snippet bottom', async function () {
|
||||
const { items } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Bottom));
|
||||
const { items, disposable } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(SnippetSortOrder.Bottom));
|
||||
assert.strictEqual(items.length, 3);
|
||||
assert.strictEqual(items[0].completion.label, 'fff');
|
||||
assert.strictEqual(items[1].completion.label, 'aaa');
|
||||
assert.strictEqual(items[2].completion.label, 'zzz');
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('sort - snippet none', async function () {
|
||||
const { items } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(undefined, new Set<CompletionItemKind>().add(CompletionItemKind.Snippet)));
|
||||
const { items, disposable } = await provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(undefined, new Set<CompletionItemKind>().add(CompletionItemKind.Snippet)));
|
||||
assert.strictEqual(items.length, 1);
|
||||
assert.strictEqual(items[0].completion.label, 'fff');
|
||||
disposable.dispose();
|
||||
});
|
||||
|
||||
test('only from', function () {
|
||||
test('only from', function (callback) {
|
||||
|
||||
const foo: any = {
|
||||
triggerCharacters: [],
|
||||
@ -102,11 +108,13 @@ suite('Suggest', function () {
|
||||
};
|
||||
const registration = registry.register({ pattern: 'bar/path', scheme: 'foo' }, foo);
|
||||
|
||||
provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(undefined, undefined, new Set<CompletionItemProvider>().add(foo))).then(({ items }) => {
|
||||
provideSuggestionItems(registry, model, new Position(1, 1), new CompletionOptions(undefined, undefined, new Set<CompletionItemProvider>().add(foo))).then(({ items, disposable }) => {
|
||||
registration.dispose();
|
||||
|
||||
assert.strictEqual(items.length, 1);
|
||||
assert.ok(items[0].provider === foo);
|
||||
disposable.dispose();
|
||||
callback();
|
||||
});
|
||||
});
|
||||
|
||||
@ -142,7 +150,7 @@ suite('Suggest', function () {
|
||||
};
|
||||
|
||||
const registration = registry.register({ pattern: 'bar/path', scheme: 'foo' }, foo);
|
||||
const { items } = await provideSuggestionItems(registry, model, new Position(0, 0), new CompletionOptions(undefined, undefined, new Set<CompletionItemProvider>().add(foo)));
|
||||
const { items, disposable } = await provideSuggestionItems(registry, model, new Position(0, 0), new CompletionOptions(undefined, undefined, new Set<CompletionItemProvider>().add(foo)));
|
||||
registration.dispose();
|
||||
|
||||
assert.strictEqual(items.length, 2);
|
||||
@ -152,5 +160,6 @@ suite('Suggest', function () {
|
||||
assert.strictEqual(a.isInvalid, false);
|
||||
assert.strictEqual(b.completion.label, 'two');
|
||||
assert.strictEqual(b.isInvalid, true);
|
||||
disposable.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
@ -5,12 +5,15 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
import { LanguageAgnosticBracketTokens } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/brackets';
|
||||
import { SmallImmutableSet, DenseKeyProvider } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/smallImmutableSet';
|
||||
import { Token, TokenKind } from 'vs/editor/common/model/bracketPairsTextModelPart/bracketPairsTree/tokenizer';
|
||||
import { TestLanguageConfigurationService } from 'vs/editor/test/common/modes/testLanguageConfigurationService';
|
||||
|
||||
suite('Bracket Pair Colorizer - Brackets', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('Basic', () => {
|
||||
const languageId = 'testMode1';
|
||||
const denseKeyProvider = new DenseKeyProvider<string>();
|
||||
@ -24,7 +27,7 @@ suite('Bracket Pair Colorizer - Brackets', () => {
|
||||
};
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const languageConfigService = new TestLanguageConfigurationService();
|
||||
const languageConfigService = disposableStore.add(new TestLanguageConfigurationService());
|
||||
disposableStore.add(languageConfigService.register(languageId, {
|
||||
brackets: [
|
||||
['{', '}'], ['[', ']'], ['(', ')'],
|
||||
|
||||
@ -97,8 +97,8 @@ export abstract class ErrorHandler {
|
||||
return _prepareStackTrace;
|
||||
},
|
||||
set(v) {
|
||||
if (v === prepareStackTraceAndFindExtension || v[_wasWrapped]) {
|
||||
_prepareStackTrace = v;
|
||||
if (v === prepareStackTraceAndFindExtension || !v || v[_wasWrapped]) {
|
||||
_prepareStackTrace = v || prepareStackTraceAndFindExtension;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -63,6 +63,7 @@ suite('ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slo
|
||||
}]
|
||||
);
|
||||
|
||||
const originalPrepareStackTrace = Error.prepareStackTrace;
|
||||
const insta = new InstantiationService(collection, false);
|
||||
|
||||
let existingErrorHandler: (e: any) => void;
|
||||
@ -81,6 +82,10 @@ suite('ExtensionHostMain#ErrorHandler - Wrapping prepareStackTrace can cause slo
|
||||
findSubstrCount = 0;
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
Error.prepareStackTrace = originalPrepareStackTrace;
|
||||
});
|
||||
|
||||
test('basics', function () {
|
||||
|
||||
const err = new Error('test1');
|
||||
|
||||
@ -7,7 +7,6 @@ import * as assert from 'assert';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IReader, transaction } from 'vs/base/common/observable';
|
||||
import { isDefined } from 'vs/base/common/types';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
import { Range } from 'vs/editor/common/core/range';
|
||||
import { linesDiffComputers } from 'vs/editor/common/diff/linesDiffComputers';
|
||||
import { EndOfLinePreference, ITextModel } from 'vs/editor/common/model';
|
||||
@ -20,7 +19,8 @@ import { MergeEditorModel } from 'vs/workbench/contrib/mergeEditor/browser/model
|
||||
import { MergeEditorTelemetry } from 'vs/workbench/contrib/mergeEditor/browser/telemetry';
|
||||
|
||||
suite('merge editor model', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
// todo: renable when failing case is found https://github.com/microsoft/vscode/pull/190444#issuecomment-1678151428
|
||||
// ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('prepend line', async () => {
|
||||
await testMergeModel(
|
||||
|
||||
@ -25,6 +25,7 @@ import { createTextModel } from 'vs/editor/test/common/testTextModel';
|
||||
import { IThemeService } from 'vs/platform/theme/common/themeService';
|
||||
import { TestThemeService } from 'vs/platform/theme/test/common/testThemeService';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
|
||||
|
||||
suite('Editor - Range decorations', () => {
|
||||
|
||||
@ -43,13 +44,13 @@ suite('Editor - Range decorations', () => {
|
||||
instantiationService.stub(ILanguageService, LanguageService);
|
||||
instantiationService.stub(IModelService, stubModelService(instantiationService));
|
||||
text = 'LINE1' + '\n' + 'LINE2' + '\n' + 'LINE3' + '\n' + 'LINE4' + '\r\n' + 'LINE5';
|
||||
model = aModel(URI.file('some_file'));
|
||||
codeEditor = createTestCodeEditor(model);
|
||||
model = disposables.add(aModel(URI.file('some_file')));
|
||||
codeEditor = disposables.add(createTestCodeEditor(model));
|
||||
|
||||
instantiationService.stub(IEditorService, 'activeEditor', { get resource() { return codeEditor.getModel()!.uri; } });
|
||||
instantiationService.stub(IEditorService, 'activeTextEditorControl', codeEditor);
|
||||
|
||||
testObject = instantiationService.createInstance(RangeHighlightDecorations);
|
||||
testObject = disposables.add(instantiationService.createInstance(RangeHighlightDecorations));
|
||||
});
|
||||
|
||||
teardown(() => {
|
||||
@ -58,6 +59,8 @@ suite('Editor - Range decorations', () => {
|
||||
disposables.dispose();
|
||||
});
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('highlight range for the resource if it is an active editor', function () {
|
||||
const range: IRange = new Range(1, 1, 1, 1);
|
||||
testObject.highlightRange({ resource: model.uri, range });
|
||||
|
||||
@ -5,7 +5,6 @@
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { LanguagesRegistry } from 'vs/editor/common/services/languagesRegistry';
|
||||
// import { LanguageService } from 'vs/editor/common/services/languageServiceImpl';
|
||||
|
||||
/**
|
||||
* This function is called before test running and also again at the end of test running
|
||||
|
||||
@ -14,6 +14,7 @@ const createStatsCollector = require('../../../node_modules/mocha/lib/stats-coll
|
||||
const MochaJUnitReporter = require('mocha-junit-reporter');
|
||||
const url = require('url');
|
||||
const minimatch = require('minimatch');
|
||||
const fs = require('fs');
|
||||
const playwright = require('@playwright/test');
|
||||
const { applyReporter } = require('../reporter');
|
||||
|
||||
@ -141,9 +142,18 @@ async function runTestsInBrowser(testModules, browserType) {
|
||||
}
|
||||
|
||||
const emitter = new events.EventEmitter();
|
||||
await page.exposeFunction('mocha_report', (type, data1, data2) => {
|
||||
emitter.emit(type, data1, data2);
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
page.exposeFunction('mocha_report', (type, data1, data2) => {
|
||||
emitter.emit(type, data1, data2);
|
||||
}),
|
||||
// Test file operations that are common across platforms. Used for test infra, namely snapshot tests
|
||||
page.exposeFunction('__readFileInTests', (path) => fs.promises.readFile(path, 'utf-8')),
|
||||
page.exposeFunction('__writeFileInTests', (path, contents) => fs.promises.writeFile(path, contents)),
|
||||
page.exposeFunction('__readDirInTests', (path) => fs.promises.readdir(path)),
|
||||
page.exposeFunction('__unlinkInTests', (path) => fs.promises.unlink(path)),
|
||||
page.exposeFunction('__mkdirPInTests', (path) => fs.promises.mkdir(path, { recursive: true })),
|
||||
]);
|
||||
|
||||
await page.goto(target.href);
|
||||
|
||||
|
||||
@ -116,15 +116,22 @@
|
||||
runner.on('pending', test => window.mocha_report('pending', serializeRunnable(test)));
|
||||
};
|
||||
|
||||
|
||||
async function loadModules(modules) {
|
||||
for (const file of modules) {
|
||||
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha);
|
||||
const m = await new Promise((resolve, reject) => require([file], resolve, err => {
|
||||
console.log("BAD " + file + JSON.stringify(err, undefined, '\t'));
|
||||
resolve({});
|
||||
}));
|
||||
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, mocha);
|
||||
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha);
|
||||
}
|
||||
}
|
||||
|
||||
window.loadAndRun = async function loadAndRun({ modules, grep }, manual = false) {
|
||||
// load
|
||||
await Promise.all(modules.map(module => new Promise((resolve, reject) => {
|
||||
require([module], resolve, err => {
|
||||
console.log("BAD " + module + JSON.stringify(err, undefined, '\t'));
|
||||
// console.log(module);
|
||||
resolve({});
|
||||
});
|
||||
})));
|
||||
await loadModules(modules);
|
||||
// await new Promise((resolve, reject) => {
|
||||
// require(modules, resolve, err => {
|
||||
// console.log(err);
|
||||
|
||||
@ -5,8 +5,9 @@
|
||||
|
||||
/*eslint-env mocha*/
|
||||
|
||||
const fs = require('fs');
|
||||
|
||||
(function () {
|
||||
const fs = require('fs');
|
||||
const originals = {};
|
||||
let logging = false;
|
||||
let withStacks = false;
|
||||
@ -79,6 +80,15 @@ globalThis._VSCODE_NODE_MODULES = new Proxy(Object.create(null), { get: (_target
|
||||
globalThis._VSCODE_PRODUCT_JSON = (require.__$__nodeRequire ?? require)('../../../product.json');
|
||||
globalThis._VSCODE_PACKAGE_JSON = (require.__$__nodeRequire ?? require)('../../../package.json');
|
||||
|
||||
// Test file operations that are common across platforms. Used for test infra, namely snapshot tests
|
||||
Object.assign(globalThis, {
|
||||
__readFileInTests: path => fs.promises.readFile(path, 'utf-8'),
|
||||
__writeFileInTests: (path, contents) => fs.promises.writeFile(path, contents),
|
||||
__readDirInTests: path => fs.promises.readdir(path),
|
||||
__unlinkInTests: path => fs.promises.unlink(path),
|
||||
__mkdirPInTests: path => fs.promises.mkdir(path, { recursive: true }),
|
||||
});
|
||||
|
||||
const _tests_glob = '**/test/**/*.test.js';
|
||||
let loader;
|
||||
let _out;
|
||||
@ -121,6 +131,15 @@ function loadWorkbenchTestingUtilsModule() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadModules(modules) {
|
||||
for (const file of modules) {
|
||||
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, mocha);
|
||||
const m = await new Promise((resolve, reject) => loader.require([file], resolve, reject));
|
||||
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, mocha);
|
||||
mocha.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, mocha);
|
||||
}
|
||||
}
|
||||
|
||||
function loadTestModules(opts) {
|
||||
|
||||
if (opts.run) {
|
||||
@ -130,9 +149,7 @@ function loadTestModules(opts) {
|
||||
file = file.replace(/\.ts$/, '.js');
|
||||
return path.relative(_out, file).replace(/\.js$/, '');
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
loader.require(modules, resolve, reject);
|
||||
});
|
||||
return loadModules(modules);
|
||||
}
|
||||
|
||||
const pattern = opts.runGlob || _tests_glob;
|
||||
@ -146,11 +163,7 @@ function loadTestModules(opts) {
|
||||
const modules = files.map(file => file.replace(/\.js$/, ''));
|
||||
resolve(modules);
|
||||
});
|
||||
}).then(modules => {
|
||||
return new Promise((resolve, reject) => {
|
||||
loader.require(modules, resolve, reject);
|
||||
});
|
||||
});
|
||||
}).then(loadModules);
|
||||
}
|
||||
|
||||
function loadTests(opts) {
|
||||
@ -239,6 +252,7 @@ function serializeError(err) {
|
||||
return {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
snapshotPath: err.snapshotPath,
|
||||
actual: safeStringify({ value: err.actual }),
|
||||
expected: safeStringify({ value: err.expected }),
|
||||
uncaught: err.uncaught,
|
||||
|
||||
@ -37,6 +37,7 @@ module.exports = class FullJsonStreamReporter extends BaseRunner {
|
||||
test.expected = err.expected;
|
||||
test.actualJSON = err.actualJSON;
|
||||
test.expectedJSON = err.expectedJSON;
|
||||
test.snapshotPath = err.snapshotPath;
|
||||
test.err = err.message;
|
||||
test.stack = err.stack || null;
|
||||
writeEvent(['fail', test]);
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
process.env.MOCHA_COLORS = '1'; // Force colors (note that this must come before any mocha imports)
|
||||
|
||||
const assert = require('assert');
|
||||
const mocha = require('mocha');
|
||||
const Mocha = require('mocha');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const glob = require('glob');
|
||||
@ -64,6 +64,14 @@ function main() {
|
||||
globalThis._VSCODE_PRODUCT_JSON = require(`${REPO_ROOT}/product.json`);
|
||||
globalThis._VSCODE_PACKAGE_JSON = require(`${REPO_ROOT}/package.json`);
|
||||
|
||||
// Test file operations that are common across platforms. Used for test infra, namely snapshot tests
|
||||
Object.assign(globalThis, {
|
||||
__readFileInTests: (/** @type {string} */ path) => fs.promises.readFile(path, 'utf-8'),
|
||||
__writeFileInTests: (/** @type {string} */ path, /** @type {BufferEncoding} */ contents) => fs.promises.writeFile(path, contents),
|
||||
__readDirInTests: (/** @type {string} */ path) => fs.promises.readdir(path),
|
||||
__unlinkInTests: (/** @type {string} */ path) => fs.promises.unlink(path),
|
||||
__mkdirPInTests: (/** @type {string} */ path) => fs.promises.mkdir(path, { recursive: true }),
|
||||
});
|
||||
|
||||
process.on('uncaughtException', function (e) {
|
||||
console.error(e.stack || e);
|
||||
@ -127,7 +135,24 @@ function main() {
|
||||
return write.apply(process.stderr, args);
|
||||
};
|
||||
|
||||
/** @type { (callback:(err:any)=>void)=>void } */
|
||||
|
||||
const runner = new Mocha({
|
||||
ui: 'tdd'
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {string[]} modules
|
||||
*/
|
||||
async function loadModules(modules) {
|
||||
for (const file of modules) {
|
||||
runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_PRE_REQUIRE, globalThis, file, runner);
|
||||
const m = await new Promise((resolve, reject) => loader([file], resolve, reject));
|
||||
runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_REQUIRE, m, file, runner);
|
||||
runner.suite.emit(Mocha.Suite.constants.EVENT_FILE_POST_REQUIRE, globalThis, file, runner);
|
||||
}
|
||||
}
|
||||
|
||||
/** @type { null|((callback:(err:any)=>void)=>void) } */
|
||||
let loadFunc = null;
|
||||
|
||||
if (argv.runGlob) {
|
||||
@ -140,7 +165,7 @@ function main() {
|
||||
|
||||
return test.replace(/(\.js)|(\.d\.ts)|(\.js\.map)$/, '');
|
||||
});
|
||||
loader(modulesToLoad, () => cb(null), cb);
|
||||
loadModules(modulesToLoad).then(() => cb(null), cb);
|
||||
};
|
||||
|
||||
glob(argv.runGlob, { cwd: src }, function (err, files) { doRun(files); });
|
||||
@ -153,7 +178,7 @@ function main() {
|
||||
return path.relative(src, path.resolve(test)).replace(/(\.js)|(\.js\.map)$/, '').replace(/\\/g, '/');
|
||||
});
|
||||
loadFunc = (cb) => {
|
||||
loader(modulesToLoad, () => cb(null), cb);
|
||||
loadModules(modulesToLoad).then(() => cb(null), cb);
|
||||
};
|
||||
} else {
|
||||
loadFunc = (cb) => {
|
||||
@ -165,7 +190,7 @@ function main() {
|
||||
modules.push(file.replace(/\.js$/, ''));
|
||||
}
|
||||
}
|
||||
loader(modules, function () { cb(null); }, cb);
|
||||
loadModules(modules).then(() => cb(null), cb);
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -180,7 +205,7 @@ function main() {
|
||||
|
||||
if (!argv.run && !argv.runGlob) {
|
||||
// set up last test
|
||||
mocha.suite('Loader', function () {
|
||||
Mocha.suite('Loader', function () {
|
||||
test('should not explode while loading', function () {
|
||||
assert.ok(!didErr, 'should not explode while loading');
|
||||
});
|
||||
@ -189,7 +214,7 @@ function main() {
|
||||
|
||||
// report failing test for every unexpected error during any of the tests
|
||||
const unexpectedErrors = [];
|
||||
mocha.suite('Errors', function () {
|
||||
Mocha.suite('Errors', function () {
|
||||
test('should not have unexpected errors in tests', function () {
|
||||
if (unexpectedErrors.length) {
|
||||
unexpectedErrors.forEach(function (stack) {
|
||||
@ -210,7 +235,7 @@ function main() {
|
||||
});
|
||||
|
||||
// fire up mocha
|
||||
mocha.run();
|
||||
runner.run(failures => process.exit(failures ? 1 : 0));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user