Path: blob/main/src/vs/base/test/common/lifecycle.test.ts
5250 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 { Emitter } from '../../common/event.js';7import { DisposableSet, DisposableStore, dispose, IDisposable, markAsSingleton, ReferenceCollection, thenIfNotDisposed, toDisposable } from '../../common/lifecycle.js';8import { ensureNoDisposablesAreLeakedInTestSuite, throwIfDisposablesAreLeaked } from './utils.js';910class Disposable implements IDisposable {11isDisposed = false;12dispose() { this.isDisposed = true; }13}1415// Leaks are allowed here since we test lifecycle stuff:16// eslint-disable-next-line local/code-ensure-no-disposables-leak-in-test17suite('Lifecycle', () => {18test('dispose single disposable', () => {19const disposable = new Disposable();2021assert(!disposable.isDisposed);2223dispose(disposable);2425assert(disposable.isDisposed);26});2728test('dispose disposable array', () => {29const disposable = new Disposable();30const disposable2 = new Disposable();3132assert(!disposable.isDisposed);33assert(!disposable2.isDisposed);3435dispose([disposable, disposable2]);3637assert(disposable.isDisposed);38assert(disposable2.isDisposed);39});4041test('dispose disposables', () => {42const disposable = new Disposable();43const disposable2 = new Disposable();4445assert(!disposable.isDisposed);46assert(!disposable2.isDisposed);4748dispose(disposable);49dispose(disposable2);5051assert(disposable.isDisposed);52assert(disposable2.isDisposed);53});5455test('dispose array should dispose all if a child throws on dispose', () => {56const disposedValues = new Set<number>();5758let thrownError: any;59try {60dispose([61toDisposable(() => { disposedValues.add(1); }),62toDisposable(() => { throw new Error('I am error'); }),63toDisposable(() => { disposedValues.add(3); }),64]);65} catch (e) {66thrownError = e;67}6869assert.ok(disposedValues.has(1));70assert.ok(disposedValues.has(3));71assert.strictEqual(thrownError.message, 'I am error');72});7374test('dispose array should rethrow composite error if multiple entries throw on dispose', () => {75const disposedValues = new Set<number>();7677let thrownError: any;78try {79dispose([80toDisposable(() => { disposedValues.add(1); }),81toDisposable(() => { throw new Error('I am error 1'); }),82toDisposable(() => { throw new Error('I am error 2'); }),83toDisposable(() => { disposedValues.add(4); }),84]);85} catch (e) {86thrownError = e;87}8889assert.ok(disposedValues.has(1));90assert.ok(disposedValues.has(4));91assert.ok(thrownError instanceof AggregateError);92assert.strictEqual((thrownError as AggregateError).errors.length, 2);93assert.strictEqual((thrownError as AggregateError).errors[0].message, 'I am error 1');94assert.strictEqual((thrownError as AggregateError).errors[1].message, 'I am error 2');95});9697test('Action bar has broken accessibility #100273', function () {98const array = [{ dispose() { } }, { dispose() { } }];99const array2 = dispose(array);100101assert.strictEqual(array.length, 2);102assert.strictEqual(array2.length, 0);103assert.ok(array !== array2);104105const set = new Set<IDisposable>([{ dispose() { } }, { dispose() { } }]);106const setValues = set.values();107const setValues2 = dispose(setValues);108assert.ok(setValues === setValues2);109});110});111112suite('DisposableStore', () => {113ensureNoDisposablesAreLeakedInTestSuite();114115test('dispose should call all child disposes even if a child throws on dispose', () => {116const disposedValues = new Set<number>();117118const store = new DisposableStore();119store.add(toDisposable(() => { disposedValues.add(1); }));120store.add(toDisposable(() => { throw new Error('I am error'); }));121store.add(toDisposable(() => { disposedValues.add(3); }));122123let thrownError: any;124try {125store.dispose();126} catch (e) {127thrownError = e;128}129130assert.ok(disposedValues.has(1));131assert.ok(disposedValues.has(3));132assert.strictEqual(thrownError.message, 'I am error');133});134135test('dispose should throw composite error if multiple children throw on dispose', () => {136const disposedValues = new Set<number>();137138const store = new DisposableStore();139store.add(toDisposable(() => { disposedValues.add(1); }));140store.add(toDisposable(() => { throw new Error('I am error 1'); }));141store.add(toDisposable(() => { throw new Error('I am error 2'); }));142store.add(toDisposable(() => { disposedValues.add(4); }));143144let thrownError: any;145try {146store.dispose();147} catch (e) {148thrownError = e;149}150151assert.ok(disposedValues.has(1));152assert.ok(disposedValues.has(4));153assert.ok(thrownError instanceof AggregateError);154assert.strictEqual((thrownError as AggregateError).errors.length, 2);155assert.strictEqual((thrownError as AggregateError).errors[0].message, 'I am error 1');156assert.strictEqual((thrownError as AggregateError).errors[1].message, 'I am error 2');157});158159test('delete should evict and dispose of the disposables', () => {160const disposedValues = new Set<number>();161const disposables: IDisposable[] = [162toDisposable(() => { disposedValues.add(1); }),163toDisposable(() => { disposedValues.add(2); })164];165166const store = new DisposableStore();167store.add(disposables[0]);168store.add(disposables[1]);169170store.delete(disposables[0]);171172assert.ok(disposedValues.has(1));173assert.ok(!disposedValues.has(2));174175store.dispose();176177assert.ok(disposedValues.has(1));178assert.ok(disposedValues.has(2));179});180181test('deleteAndLeak should evict and not dispose of the disposables', () => {182const disposedValues = new Set<number>();183const disposables: IDisposable[] = [184toDisposable(() => { disposedValues.add(1); }),185toDisposable(() => { disposedValues.add(2); })186];187188const store = new DisposableStore();189store.add(disposables[0]);190store.add(disposables[1]);191192store.deleteAndLeak(disposables[0]);193194assert.ok(!disposedValues.has(1));195assert.ok(!disposedValues.has(2));196197store.dispose();198199assert.ok(!disposedValues.has(1));200assert.ok(disposedValues.has(2));201202disposables[0].dispose();203});204});205206suite('DisposableSet', () => {207ensureNoDisposablesAreLeakedInTestSuite();208209test('dispose should dispose all values and mark as disposed', () => {210const disposedValues = new Set<number>();211212const set = new DisposableSet<IDisposable>();213set.add(toDisposable(() => { disposedValues.add(1); }));214set.add(toDisposable(() => { disposedValues.add(2); }));215set.add(toDisposable(() => { disposedValues.add(3); }));216217assert.strictEqual(set.size, 3);218219set.dispose();220221assert.ok(disposedValues.has(1));222assert.ok(disposedValues.has(2));223assert.ok(disposedValues.has(3));224assert.strictEqual(set.size, 0);225});226227test('dispose should call all child disposes even if a child throws on dispose', () => {228const disposedValues = new Set<number>();229230const set = new DisposableSet<IDisposable>();231set.add(toDisposable(() => { disposedValues.add(1); }));232set.add(toDisposable(() => { throw new Error('I am error'); }));233set.add(toDisposable(() => { disposedValues.add(3); }));234235let thrownError: any;236try {237set.dispose();238} catch (e) {239thrownError = e;240}241242assert.ok(disposedValues.has(1));243assert.ok(disposedValues.has(3));244assert.strictEqual(thrownError.message, 'I am error');245});246247test('clearAndDisposeAll should dispose values but not mark as disposed', () => {248const disposedValues = new Set<number>();249250const set = new DisposableSet<IDisposable>();251const d1 = toDisposable(() => { disposedValues.add(1); });252set.add(d1);253254set.clearAndDisposeAll();255256assert.ok(disposedValues.has(1));257assert.strictEqual(set.size, 0);258259// Can still add new values260const d2 = toDisposable(() => { disposedValues.add(2); });261set.add(d2);262assert.strictEqual(set.size, 1);263264set.dispose();265assert.ok(disposedValues.has(2));266});267268test('has should return true if value exists', () => {269const set = new DisposableSet<IDisposable>();270const d = toDisposable(() => { });271set.add(d);272273const other = toDisposable(() => { });274assert.ok(set.has(d));275assert.ok(!set.has(other));276277set.dispose();278other.dispose();279});280281test('deleteAndDispose should remove and dispose the value', () => {282const disposedValues = new Set<number>();283284const set = new DisposableSet<IDisposable>();285const d1 = toDisposable(() => { disposedValues.add(1); });286const d2 = toDisposable(() => { disposedValues.add(2); });287set.add(d1);288set.add(d2);289290set.deleteAndDispose(d1);291292assert.ok(disposedValues.has(1));293assert.ok(!disposedValues.has(2));294assert.strictEqual(set.size, 1);295assert.ok(!set.has(d1));296assert.ok(set.has(d2));297298set.dispose();299assert.ok(disposedValues.has(2));300});301302test('deleteAndLeak should remove but not dispose the value', () => {303const disposedValues = new Set<number>();304305const set = new DisposableSet<IDisposable>();306const d1 = toDisposable(() => { disposedValues.add(1); });307const d2 = toDisposable(() => { disposedValues.add(2); });308set.add(d1);309set.add(d2);310311const leaked = set.deleteAndLeak(d1);312313assert.strictEqual(leaked, d1);314assert.ok(!disposedValues.has(1));315assert.ok(!disposedValues.has(2));316assert.strictEqual(set.size, 1);317318set.dispose();319320assert.ok(!disposedValues.has(1));321assert.ok(disposedValues.has(2));322323// Caller is responsible for disposing324d1.dispose();325});326327test('deleteAndLeak should return undefined if value not in set', () => {328const set = new DisposableSet<IDisposable>();329const d = toDisposable(() => { });330331const leaked = set.deleteAndLeak(d);332333assert.strictEqual(leaked, undefined);334335set.dispose();336d.dispose();337});338339test('values should iterate over all values', () => {340const set = new DisposableSet<IDisposable>();341const d1 = toDisposable(() => { });342const d2 = toDisposable(() => { });343set.add(d1);344set.add(d2);345346const values = [...set.values()];347assert.strictEqual(values.length, 2);348assert.ok(values.includes(d1));349assert.ok(values.includes(d2));350351set.dispose();352});353354test('Symbol.iterator should allow for-of iteration', () => {355const set = new DisposableSet<IDisposable>();356const d1 = toDisposable(() => { });357const d2 = toDisposable(() => { });358set.add(d1);359set.add(d2);360361const values: IDisposable[] = [];362for (const v of set) {363values.push(v);364}365366assert.strictEqual(values.length, 2);367assert.ok(values.includes(d1));368assert.ok(values.includes(d2));369370set.dispose();371});372});373374suite('Reference Collection', () => {375ensureNoDisposablesAreLeakedInTestSuite();376377class Collection extends ReferenceCollection<number> {378private _count = 0;379get count() { return this._count; }380protected createReferencedObject(key: string): number { this._count++; return key.length; }381protected destroyReferencedObject(key: string, object: number): void { this._count--; }382}383384test('simple', () => {385const collection = new Collection();386387const ref1 = collection.acquire('test');388assert(ref1);389assert.strictEqual(ref1.object, 4);390assert.strictEqual(collection.count, 1);391ref1.dispose();392assert.strictEqual(collection.count, 0);393394const ref2 = collection.acquire('test');395const ref3 = collection.acquire('test');396assert.strictEqual(ref2.object, ref3.object);397assert.strictEqual(collection.count, 1);398399const ref4 = collection.acquire('monkey');400assert.strictEqual(ref4.object, 6);401assert.strictEqual(collection.count, 2);402403ref2.dispose();404assert.strictEqual(collection.count, 2);405406ref3.dispose();407assert.strictEqual(collection.count, 1);408409ref4.dispose();410assert.strictEqual(collection.count, 0);411});412});413414function assertThrows(fn: () => void, test: (error: any) => void) {415try {416fn();417assert.fail('Expected function to throw, but it did not.');418} catch (e) {419assert.ok(test(e));420}421}422423suite('No Leakage Utilities', () => {424suite('throwIfDisposablesAreLeaked', () => {425test('throws if an event subscription is not cleaned up', () => {426const eventEmitter = new Emitter();427428assertThrows(() => {429throwIfDisposablesAreLeaked(() => {430eventEmitter.event(() => {431// noop432});433}, false);434}, e => e.message.indexOf('undisposed disposables') !== -1);435});436437test('throws if a disposable is not disposed', () => {438assertThrows(() => {439throwIfDisposablesAreLeaked(() => {440new DisposableStore();441}, false);442}, e => e.message.indexOf('undisposed disposables') !== -1);443});444445test('does not throw if all event subscriptions are cleaned up', () => {446const eventEmitter = new Emitter();447throwIfDisposablesAreLeaked(() => {448eventEmitter.event(() => {449// noop450}).dispose();451});452});453454test('does not throw if all disposables are disposed', () => {455// This disposable is reported before the test and not tracked.456toDisposable(() => { });457458throwIfDisposablesAreLeaked(() => {459// This disposable is marked as singleton460markAsSingleton(toDisposable(() => { }));461462// These disposables are also marked as singleton463const disposableStore = new DisposableStore();464disposableStore.add(toDisposable(() => { }));465markAsSingleton(disposableStore);466467toDisposable(() => { }).dispose();468});469});470});471472suite('ensureNoDisposablesAreLeakedInTest', () => {473ensureNoDisposablesAreLeakedInTestSuite();474475test('Basic Test', () => {476toDisposable(() => { }).dispose();477});478});479480suite('thenIfNotDisposed', () => {481const store = ensureNoDisposablesAreLeakedInTestSuite();482483test('normal case', async () => {484let called = false;485store.add(thenIfNotDisposed(Promise.resolve(123), (result: number) => {486assert.strictEqual(result, 123);487called = true;488}));489490await new Promise(resolve => setTimeout(resolve, 0));491assert.strictEqual(called, true);492});493494test('disposed before promise resolves', async () => {495let called = false;496const disposable = thenIfNotDisposed(Promise.resolve(123), () => {497called = true;498});499500disposable.dispose();501await new Promise(resolve => setTimeout(resolve, 0));502assert.strictEqual(called, false);503});504});505});506507508