Path: blob/main/src/vs/workbench/api/test/browser/extHostDiagnostics.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { URI, UriComponents } from '../../../../base/common/uri.js';7import { DiagnosticCollection, ExtHostDiagnostics } from '../../common/extHostDiagnostics.js';8import { Diagnostic, DiagnosticSeverity, Range, DiagnosticRelatedInformation, Location } from '../../common/extHostTypes.js';9import { MainThreadDiagnosticsShape, IMainContext } from '../../common/extHost.protocol.js';10import { IMarkerData, MarkerSeverity } from '../../../../platform/markers/common/markers.js';11import { mock } from '../../../../base/test/common/mock.js';12import { Emitter, Event } from '../../../../base/common/event.js';13import { NullLogService } from '../../../../platform/log/common/log.js';14import type * as vscode from 'vscode';15import { nullExtensionDescription } from '../../../services/extensions/common/extensions.js';16import { ExtUri, extUri } from '../../../../base/common/resources.js';17import { IExtHostFileSystemInfo } from '../../common/extHostFileSystemInfo.js';18import { runWithFakedTimers } from '../../../../base/test/common/timeTravelScheduler.js';19import { IExtHostDocumentsAndEditors } from '../../common/extHostDocumentsAndEditors.js';20import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';2122suite('ExtHostDiagnostics', () => {2324class DiagnosticsShape extends mock<MainThreadDiagnosticsShape>() {25override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void {26//27}28override $clear(owner: string): void {29//30}31}3233const fileSystemInfoService = new class extends mock<IExtHostFileSystemInfo>() {34override readonly extUri = extUri;35};3637const versionProvider = (uri: URI): number | undefined => {38return undefined;39};4041const store = ensureNoDisposablesAreLeakedInTestSuite();4243test('disposeCheck', () => {4445const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter());4647collection.dispose();48collection.dispose(); // that's OK49assert.throws(() => collection.name);50assert.throws(() => collection.clear());51assert.throws(() => collection.delete(URI.parse('aa:bb')));52assert.throws(() => collection.forEach(() => { }));53assert.throws(() => collection.get(URI.parse('aa:bb')));54assert.throws(() => collection.has(URI.parse('aa:bb')));55assert.throws(() => collection.set(URI.parse('aa:bb'), []));56assert.throws(() => collection.set(URI.parse('aa:bb'), undefined!));57});585960test('diagnostic collection, forEach, clear, has', function () {61let collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter());62assert.strictEqual(collection.name, 'test');63collection.dispose();64assert.throws(() => collection.name);6566let c = 0;67collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter());68collection.forEach(() => c++);69assert.strictEqual(c, 0);7071collection.set(URI.parse('foo:bar'), [72new Diagnostic(new Range(0, 0, 1, 1), 'message-1'),73new Diagnostic(new Range(0, 0, 1, 1), 'message-2')74]);75collection.forEach(() => c++);76assert.strictEqual(c, 1);7778c = 0;79collection.clear();80collection.forEach(() => c++);81assert.strictEqual(c, 0);8283collection.set(URI.parse('foo:bar1'), [84new Diagnostic(new Range(0, 0, 1, 1), 'message-1'),85new Diagnostic(new Range(0, 0, 1, 1), 'message-2')86]);87collection.set(URI.parse('foo:bar2'), [88new Diagnostic(new Range(0, 0, 1, 1), 'message-1'),89new Diagnostic(new Range(0, 0, 1, 1), 'message-2')90]);91collection.forEach(() => c++);92assert.strictEqual(c, 2);9394assert.ok(collection.has(URI.parse('foo:bar1')));95assert.ok(collection.has(URI.parse('foo:bar2')));96assert.ok(!collection.has(URI.parse('foo:bar3')));97collection.delete(URI.parse('foo:bar1'));98assert.ok(!collection.has(URI.parse('foo:bar1')));99100collection.dispose();101});102103test('diagnostic collection, immutable read', function () {104const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter());105collection.set(URI.parse('foo:bar'), [106new Diagnostic(new Range(0, 0, 1, 1), 'message-1'),107new Diagnostic(new Range(0, 0, 1, 1), 'message-2')108]);109110let array = collection.get(URI.parse('foo:bar')) as Diagnostic[];111assert.throws(() => array.length = 0);112assert.throws(() => array.pop());113assert.throws(() => array[0] = new Diagnostic(new Range(0, 0, 0, 0), 'evil'));114115collection.forEach((uri: URI, array: readonly vscode.Diagnostic[]): any => {116assert.throws(() => (array as Diagnostic[]).length = 0);117assert.throws(() => (array as Diagnostic[]).pop());118assert.throws(() => (array as Diagnostic[])[0] = new Diagnostic(new Range(0, 0, 0, 0), 'evil'));119});120121array = collection.get(URI.parse('foo:bar')) as Diagnostic[];122assert.strictEqual(array.length, 2);123124collection.dispose();125});126127128test('diagnostics collection, set with dupliclated tuples', function () {129const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter());130const uri = URI.parse('sc:hightower');131collection.set([132[uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-1')]],133[URI.parse('some:thing'), [new Diagnostic(new Range(0, 0, 1, 1), 'something')]],134[uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-2')]],135]);136137let array = collection.get(uri);138assert.strictEqual(array.length, 2);139let [first, second] = array;140assert.strictEqual(first.message, 'message-1');141assert.strictEqual(second.message, 'message-2');142143// clear144collection.delete(uri);145assert.ok(!collection.has(uri));146147// bad tuple clears 1/2148collection.set([149[uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-1')]],150[URI.parse('some:thing'), [new Diagnostic(new Range(0, 0, 1, 1), 'something')]],151[uri, undefined!]152]);153assert.ok(!collection.has(uri));154155// clear156collection.delete(uri);157assert.ok(!collection.has(uri));158159// bad tuple clears 2/2160collection.set([161[uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-1')]],162[URI.parse('some:thing'), [new Diagnostic(new Range(0, 0, 1, 1), 'something')]],163[uri, undefined!],164[uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-2')]],165[uri, [new Diagnostic(new Range(0, 0, 0, 1), 'message-3')]],166]);167168array = collection.get(uri);169assert.strictEqual(array.length, 2);170[first, second] = array;171assert.strictEqual(first.message, 'message-2');172assert.strictEqual(second.message, 'message-3');173174collection.dispose();175});176177test('diagnostics collection, set tuple overrides, #11547', function () {178179let lastEntries!: [UriComponents, IMarkerData[]][];180const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape {181override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void {182lastEntries = entries;183return super.$changeMany(owner, entries);184}185}, new Emitter());186const uri = URI.parse('sc:hightower');187188collection.set([[uri, [new Diagnostic(new Range(0, 0, 1, 1), 'error')]]]);189assert.strictEqual(collection.get(uri).length, 1);190assert.strictEqual(collection.get(uri)[0].message, 'error');191assert.strictEqual(lastEntries.length, 1);192const [[, data1]] = lastEntries;193assert.strictEqual(data1.length, 1);194assert.strictEqual(data1[0].message, 'error');195lastEntries = undefined!;196197collection.set([[uri, [new Diagnostic(new Range(0, 0, 1, 1), 'warning')]]]);198assert.strictEqual(collection.get(uri).length, 1);199assert.strictEqual(collection.get(uri)[0].message, 'warning');200assert.strictEqual(lastEntries.length, 1);201const [[, data2]] = lastEntries;202assert.strictEqual(data2.length, 1);203assert.strictEqual(data2[0].message, 'warning');204lastEntries = undefined!;205});206207test('do send message when not making a change', function () {208209let changeCount = 0;210let eventCount = 0;211212const emitter = new Emitter<any>();213store.add(emitter.event(_ => eventCount += 1));214const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape {215override $changeMany() {216changeCount += 1;217}218}, emitter);219220const uri = URI.parse('sc:hightower');221const diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff');222223collection.set(uri, [diag]);224assert.strictEqual(changeCount, 1);225assert.strictEqual(eventCount, 1);226227collection.set(uri, [diag]);228assert.strictEqual(changeCount, 2);229assert.strictEqual(eventCount, 2);230231});232233test('diagnostics collection, tuples and undefined (small array), #15585', function () {234235const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter());236const uri = URI.parse('sc:hightower');237const uri2 = URI.parse('sc:nomad');238const diag = new Diagnostic(new Range(0, 0, 0, 1), 'ffff');239240collection.set([241[uri, [diag, diag, diag]],242[uri, undefined!],243[uri, [diag]],244245[uri2, [diag, diag]],246[uri2, undefined!],247[uri2, [diag]],248]);249250assert.strictEqual(collection.get(uri).length, 1);251assert.strictEqual(collection.get(uri2).length, 1);252});253254test('diagnostics collection, tuples and undefined (large array), #15585', function () {255256const collection = new DiagnosticCollection('test', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), new Emitter());257const tuples: [URI, Diagnostic[]][] = [];258259for (let i = 0; i < 500; i++) {260const uri = URI.parse('sc:hightower#' + i);261const diag = new Diagnostic(new Range(0, 0, 0, 1), i.toString());262263tuples.push([uri, [diag, diag, diag]]);264tuples.push([uri, undefined!]);265tuples.push([uri, [diag]]);266}267268collection.set(tuples);269270for (let i = 0; i < 500; i++) {271const uri = URI.parse('sc:hightower#' + i);272assert.strictEqual(collection.has(uri), true);273assert.strictEqual(collection.get(uri).length, 1);274}275});276277test('diagnostic capping (max per file)', function () {278279let lastEntries!: [UriComponents, IMarkerData[]][];280const collection = new DiagnosticCollection('test', 'test', 100, 250, versionProvider, extUri, new class extends DiagnosticsShape {281override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void {282lastEntries = entries;283return super.$changeMany(owner, entries);284}285}, new Emitter());286const uri = URI.parse('aa:bb');287288const diagnostics: Diagnostic[] = [];289for (let i = 0; i < 500; i++) {290diagnostics.push(new Diagnostic(new Range(i, 0, i + 1, 0), `error#${i}`, i < 300291? DiagnosticSeverity.Warning292: DiagnosticSeverity.Error));293}294295collection.set(uri, diagnostics);296assert.strictEqual(collection.get(uri).length, 500);297assert.strictEqual(lastEntries.length, 1);298assert.strictEqual(lastEntries[0][1].length, 251);299assert.strictEqual(lastEntries[0][1][0].severity, MarkerSeverity.Error);300assert.strictEqual(lastEntries[0][1][200].severity, MarkerSeverity.Warning);301assert.strictEqual(lastEntries[0][1][250].severity, MarkerSeverity.Info);302});303304test('diagnostic capping (max files)', function () {305306let lastEntries!: [UriComponents, IMarkerData[]][];307const collection = new DiagnosticCollection('test', 'test', 2, 1, versionProvider, extUri, new class extends DiagnosticsShape {308override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]): void {309lastEntries = entries;310return super.$changeMany(owner, entries);311}312}, new Emitter());313314const diag = new Diagnostic(new Range(0, 0, 1, 1), 'Hello');315316317collection.set([318[URI.parse('aa:bb1'), [diag]],319[URI.parse('aa:bb2'), [diag]],320[URI.parse('aa:bb3'), [diag]],321[URI.parse('aa:bb4'), [diag]],322]);323assert.strictEqual(lastEntries.length, 3); // goes above the limit and then stops324});325326test('diagnostic eventing', async function () {327const emitter = new Emitter<readonly URI[]>();328const collection = new DiagnosticCollection('ddd', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), emitter);329330const diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1');331const diag2 = new Diagnostic(new Range(1, 1, 2, 3), 'diag2');332const diag3 = new Diagnostic(new Range(1, 1, 2, 3), 'diag3');333334let p = Event.toPromise(emitter.event).then(a => {335assert.strictEqual(a.length, 1);336assert.strictEqual(a[0].toString(), 'aa:bb');337assert.ok(URI.isUri(a[0]));338});339collection.set(URI.parse('aa:bb'), []);340await p;341342p = Event.toPromise(emitter.event).then(e => {343assert.strictEqual(e.length, 2);344assert.ok(URI.isUri(e[0]));345assert.ok(URI.isUri(e[1]));346assert.strictEqual(e[0].toString(), 'aa:bb');347assert.strictEqual(e[1].toString(), 'aa:cc');348});349collection.set([350[URI.parse('aa:bb'), [diag1]],351[URI.parse('aa:cc'), [diag2, diag3]],352]);353await p;354355p = Event.toPromise(emitter.event).then(e => {356assert.strictEqual(e.length, 2);357assert.ok(URI.isUri(e[0]));358assert.ok(URI.isUri(e[1]));359});360collection.clear();361await p;362});363364test('vscode.languages.onDidChangeDiagnostics Does Not Provide Document URI #49582', async function () {365const emitter = new Emitter<readonly URI[]>();366const collection = new DiagnosticCollection('ddd', 'test', 100, 100, versionProvider, extUri, new DiagnosticsShape(), emitter);367368const diag1 = new Diagnostic(new Range(1, 1, 2, 3), 'diag1');369370// delete371collection.set(URI.parse('aa:bb'), [diag1]);372let p = Event.toPromise(emitter.event).then(e => {373assert.strictEqual(e[0].toString(), 'aa:bb');374});375collection.delete(URI.parse('aa:bb'));376await p;377378// set->undefined (as delete)379collection.set(URI.parse('aa:bb'), [diag1]);380p = Event.toPromise(emitter.event).then(e => {381assert.strictEqual(e[0].toString(), 'aa:bb');382});383collection.set(URI.parse('aa:bb'), undefined!);384await p;385});386387test('diagnostics with related information', function (done) {388389const collection = new DiagnosticCollection('ddd', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape {390override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]) {391392const [[, data]] = entries;393assert.strictEqual(entries.length, 1);394assert.strictEqual(data.length, 1);395396const [diag] = data;397assert.strictEqual(diag.relatedInformation!.length, 2);398assert.strictEqual(diag.relatedInformation![0].message, 'more1');399assert.strictEqual(diag.relatedInformation![1].message, 'more2');400done();401}402}, new Emitter<any>());403404const diag = new Diagnostic(new Range(0, 0, 1, 1), 'Foo');405diag.relatedInformation = [406new DiagnosticRelatedInformation(new Location(URI.parse('cc:dd'), new Range(0, 0, 0, 0)), 'more1'),407new DiagnosticRelatedInformation(new Location(URI.parse('cc:ee'), new Range(0, 0, 0, 0)), 'more2')408];409410collection.set(URI.parse('aa:bb'), [diag]);411});412413test('vscode.languages.getDiagnostics appears to return old diagnostics in some circumstances #54359', function () {414const ownerHistory: string[] = [];415const diags = new ExtHostDiagnostics(new class implements IMainContext {416getProxy(id: any): any {417return new class DiagnosticsShape {418$clear(owner: string): void {419ownerHistory.push(owner);420}421};422}423set(): any {424return null;425}426dispose() { }427assertRegistered(): void {428429}430drain() {431return undefined!;432}433}, new NullLogService(), fileSystemInfoService, new class extends mock<IExtHostDocumentsAndEditors>() {434override getDocument() {435return undefined;436}437});438439const collection1 = diags.createDiagnosticCollection(nullExtensionDescription.identifier, 'foo');440const collection2 = diags.createDiagnosticCollection(nullExtensionDescription.identifier, 'foo'); // warns, uses a different owner441442collection1.clear();443collection2.clear();444445assert.strictEqual(ownerHistory.length, 2);446assert.strictEqual(ownerHistory[0], 'foo');447assert.strictEqual(ownerHistory[1], 'foo0');448});449450test('Error updating diagnostics from extension #60394', function () {451let callCount = 0;452const collection = new DiagnosticCollection('ddd', 'test', 100, 100, versionProvider, extUri, new class extends DiagnosticsShape {453override $changeMany(owner: string, entries: [UriComponents, IMarkerData[]][]) {454callCount += 1;455}456}, new Emitter<any>());457458const array: Diagnostic[] = [];459const diag1 = new Diagnostic(new Range(0, 0, 1, 1), 'Foo');460const diag2 = new Diagnostic(new Range(0, 0, 1, 1), 'Bar');461462array.push(diag1, diag2);463464collection.set(URI.parse('test:me'), array);465assert.strictEqual(callCount, 1);466467collection.set(URI.parse('test:me'), array);468assert.strictEqual(callCount, 2); // equal array469470array.push(diag2);471collection.set(URI.parse('test:me'), array);472assert.strictEqual(callCount, 3); // same but un-equal array473});474475test('Version id is set whenever possible', function () {476477const all: [UriComponents, IMarkerData[]][] = [];478479const collection = new DiagnosticCollection('ddd', 'test', 100, 100, uri => {480return 7;481}, extUri, new class extends DiagnosticsShape {482override $changeMany(_owner: string, entries: [UriComponents, IMarkerData[]][]) {483all.push(...entries);484}485}, new Emitter<any>());486487const array: Diagnostic[] = [];488const diag1 = new Diagnostic(new Range(0, 0, 1, 1), 'Foo');489const diag2 = new Diagnostic(new Range(0, 0, 1, 1), 'Bar');490491array.push(diag1, diag2);492493collection.set(URI.parse('test:one'), array);494collection.set(URI.parse('test:two'), [diag1]);495collection.set(URI.parse('test:three'), [diag2]);496497const allVersions = all.map(tuple => tuple[1].map(t => t.modelVersionId)).flat();498assert.deepStrictEqual(allVersions, [7, 7, 7, 7]);499});500501test('Diagnostics created by tasks aren\'t accessible to extensions #47292', async function () {502return runWithFakedTimers({}, async function () {503504const diags = new ExtHostDiagnostics(new class implements IMainContext {505getProxy(id: any): any {506return {};507}508set(): any {509return null;510}511dispose() { }512assertRegistered(): void {513514}515drain() {516return undefined!;517}518}, new NullLogService(), fileSystemInfoService, new class extends mock<IExtHostDocumentsAndEditors>() {519override getDocument() {520return undefined;521}522});523524525//526const uri = URI.parse('foo:bar');527const data: IMarkerData[] = [{528message: 'message',529startLineNumber: 1,530startColumn: 1,531endLineNumber: 1,532endColumn: 1,533severity: MarkerSeverity.Info534}];535536const p1 = Event.toPromise(diags.onDidChangeDiagnostics);537diags.$acceptMarkersChange([[uri, data]]);538await p1;539assert.strictEqual(diags.getDiagnostics(uri).length, 1);540541const p2 = Event.toPromise(diags.onDidChangeDiagnostics);542diags.$acceptMarkersChange([[uri, []]]);543await p2;544assert.strictEqual(diags.getDiagnostics(uri).length, 0);545});546});547548test('languages.getDiagnostics doesn\'t handle case insensitivity correctly #128198', function () {549550const diags = new ExtHostDiagnostics(new class implements IMainContext {551getProxy(id: any): any {552return new DiagnosticsShape();553}554set(): any {555return null;556}557dispose() { }558assertRegistered(): void {559560}561drain() {562return undefined!;563}564}, new NullLogService(), new class extends mock<IExtHostFileSystemInfo>() {565566override readonly extUri = new ExtUri(uri => uri.scheme === 'insensitive');567}, new class extends mock<IExtHostDocumentsAndEditors>() {568override getDocument() {569return undefined;570}571});572573const col = diags.createDiagnosticCollection(nullExtensionDescription.identifier);574575const uriSensitive = URI.from({ scheme: 'foo', path: '/SOME/path' });576const uriSensitiveCaseB = uriSensitive.with({ path: uriSensitive.path.toUpperCase() });577578const uriInSensitive = URI.from({ scheme: 'insensitive', path: '/SOME/path' });579const uriInSensitiveUpper = uriInSensitive.with({ path: uriInSensitive.path.toUpperCase() });580581col.set(uriSensitive, [new Diagnostic(new Range(0, 0, 0, 0), 'sensitive')]);582col.set(uriInSensitive, [new Diagnostic(new Range(0, 0, 0, 0), 'insensitive')]);583584// collection itself honours casing585assert.strictEqual(col.get(uriSensitive)?.length, 1);586assert.strictEqual(col.get(uriSensitiveCaseB)?.length, 0);587588assert.strictEqual(col.get(uriInSensitive)?.length, 1);589assert.strictEqual(col.get(uriInSensitiveUpper)?.length, 1);590591// languages.getDiagnostics honours casing592assert.strictEqual(diags.getDiagnostics(uriSensitive)?.length, 1);593assert.strictEqual(diags.getDiagnostics(uriSensitiveCaseB)?.length, 0);594595assert.strictEqual(diags.getDiagnostics(uriInSensitive)?.length, 1);596assert.strictEqual(diags.getDiagnostics(uriInSensitiveUpper)?.length, 1);597598599const fromForEach: URI[] = [];600col.forEach(uri => fromForEach.push(uri));601assert.strictEqual(fromForEach.length, 2);602assert.strictEqual(fromForEach[0].toString(), uriSensitive.toString());603assert.strictEqual(fromForEach[1].toString(), uriInSensitive.toString());604});605});606607608