Change:
I used blame to find the owner of inline completions. Who should this be assigned to?
Looks like you did that wrong <img width="826" alt="Screen Shot 2021-07-12 at 12 53 21" src="https://user-"
From slack: I found the real issue of the flaky unit test. Apparently, some test that runs before the inline suggestion test (which fakes /) calls . spawns a (5 seconds) that calls itself (basically an endless loop over time). When the time is faked, my inline completion test waits for all scheduled timers to finish until the queue of scheduled timers is empty (assuming I am in control of all scheduled timers when my test is run). However, the loop in then causes an endless loop. My test gives up after running 1000 scheduled timers and I guess then things are in an inconsistent state. This only happens when my tests are run right before is about to run. If my tests a run in the 5s interval in which is not called, nothing bad happens. As a quick fix, I will change my tests to not wait for the timer queue to get empty. As a long term fix, I think tests should be isolated better - at least they should make really sure they clean up everything (such as disposables/timers/global handlers/etc.). Joh tracked this down to this leaking disposable:

Discussion:
import { GhostTextContext, MockInlineCompletionsProvider } from 'vs/editor/contrib/inlineCompletions/test/utils'; import { ITestCodeEditor, TestCodeEditorCreationOptions, withAsyncTestCodeEditor } from 'vs/editor/test/browser/testCodeEditor'; import { createTextModel } from 'vs/editor/test/common/editorTestUtils'; <del> import sinon = require('sinon'); </del> import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils'; <ins> import { runWithFakedTimers } from 'vs/editor/contrib/inlineCompletions/test/timeTravelScheduler'; </ins> suite('Inline Completions', () => { ensureNoDisposablesAreLeakedInTestSuite();

}); }); <del> async function withAsyncTestCodeEditorAndInlineCompletionsModel( </del> <ins> async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>( </ins> text: string, options: TestCodeEditorCreationOptions & { provider?: InlineCompletionsProvider, fakeClock?: boolean }, <del> callback: (args: { editor: ITestCodeEditor, editorViewModel: ViewModel, model: InlineCompletionsModel, context: GhostTextContext }) => Promise<void> ): Promise<void> { const disposableStore = new DisposableStore(); if (options.provider) { const d = InlineCompletionsProviderRegistry.register({ pattern: '**' }, options.provider); disposableStore.add(d); } let clock: sinon.SinonFakeTimers | undefined; if (options.fakeClock) { clock = sinon.useFakeTimers(); } try { const p = withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => { const model = instantiationService.createInstance(InlineCompletionsModel, editor); const context = new GhostTextContext(model, editor); await callback({ editor, editorViewModel, model, context }); context.dispose(); model.dispose(); }); </del> <ins> callback: (args: { editor: ITestCodeEditor, editorViewModel: ViewModel, model: InlineCompletionsModel, context: GhostTextContext }) => Promise<T> ): Promise<T> { return await runWithFakedTimers({ useFakeTimers: options.fakeClock, }, async () => { const disposableStore = new DisposableStore(); try { if (options.provider) { const d = InlineCompletionsProviderRegistry.register({ pattern: '**' }, options.provider); disposableStore.add(d); } </ins> <del> const p2 = clock?.runAllAsync(); </del> <ins> let result: T; await withAsyncTestCodeEditor(text, options, async (editor, editorViewModel, instantiationService) => { const model = instantiationService.createInstance(InlineCompletionsModel, editor); const context = new GhostTextContext(model, editor); result = await callback({ editor, editorViewModel, model, context }); context.dispose(); model.dispose(); }); </ins> <del> await p; await p2; </del> <ins> if (options.provider instanceof MockInlineCompletionsProvider) { options.provider.assertNotCalledTwiceWithin50ms(); } </ins> <del> if (options.provider instanceof MockInlineCompletionsProvider) { options.provider.assertNotCalledTwiceWithin50ms(); </del> <ins> return result!; } finally { disposableStore.dispose(); </ins> } <del> } finally { clock?.restore(); disposableStore.dispose(); } </del> <ins> }); </ins> }

import { IMenuService, IMenu } from 'vs/platform/actions/common/actions'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; <del> import sinon = require('sinon'); </del> import { timeout } from 'vs/base/common/async'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { CompletionItemKind, CompletionItemProvider, CompletionProviderRegistry } from 'vs/editor/common/modes';

import assert = require('assert'); import { GhostTextContext } from 'vs/editor/contrib/inlineCompletions/test/utils'; import { Range } from 'vs/editor/common/core/range'; <ins> import { runWithFakedTimers } from 'vs/editor/contrib/inlineCompletions/test/timeTravelScheduler'; </ins> suite('Suggest Widget Model', () => { test('Active', async () => { await withAsyncTestCodeEditorAndInlineCompletionsModel('', { fakeClock: true, provider, },

options: TestCodeEditorCreationOptions & { provider?: CompletionItemProvider, fakeClock?: boolean, serviceCollection?: never }, callback: (args: { editor: ITestCodeEditor, editorViewModel: ViewModel, model: SuggestWidgetAdapterModel, context: GhostTextContext }) => Promise<void> ): Promise<void> { <del> const serviceCollection = new ServiceCollection( [ITelemetryService, NullTelemetryService], [ILogService, new NullLogService()], [IStorageService, new InMemoryStorageService()], [IKeybindingService, new MockKeybindingService()], [IEditorWorkerService, new class extends mock<IEditorWorkerService>() { override computeWordRanges() { return Promise.resolve({}); } }], [ISuggestMemoryService, new class extends mock<ISuggestMemoryService>() { override memorize(): void { } override select(): number { return 0; } }], [IMenuService, new class extends mock<IMenuService>() { override createMenu() { return new class extends mock<IMenu>() { override onDidChange = Event.None; override dispose() { } }; </del> <ins> await runWithFakedTimers({ useFakeTimers: options.fakeClock }, async () => { const disposableStore = new DisposableStore(); try { const serviceCollection = new ServiceCollection( [ITelemetryService, NullTelemetryService], [ILogService, new NullLogService()], [IStorageService, new InMemoryStorageService()], [IKeybindingService, new MockKeybindingService()], [IEditorWorkerService, new class extends mock<IEditorWorkerService>() { override computeWordRanges() { return Promise.resolve({}); } }], [ISuggestMemoryService, new class extends mock<ISuggestMemoryService>() { override memorize(): void { } override select(): number { return 0; } }], [IMenuService, new class extends mock<IMenuService>() { override createMenu() { return new class extends mock<IMenu>() { override onDidChange = Event.None; override dispose() { } }; } }] ); if (options.provider) { const d = CompletionProviderRegistry.register({ pattern: '**' }, options.provider); disposableStore.add(d); </ins> } <del> }] ); const disposableStore = new DisposableStore(); if (options.provider) { const d = CompletionProviderRegistry.register({ pattern: '**' }, options.provider); disposableStore.add(d); } let clock: sinon.SinonFakeTimers | undefined; if (options.fakeClock) { clock = sinon.useFakeTimers(); } try { const p = withAsyncTestCodeEditor(text, { ...options, serviceCollection }, async (editor, editorViewModel, instantiationService) => { editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController); const model = instantiationService.createInstance(SuggestWidgetAdapterModel, editor); const context = new GhostTextContext(model, editor); await callback({ editor, editorViewModel, model, context }); model.dispose(); }); const p2 = clock?.runAllAsync(); await p; await p2; } finally { clock?.restore(); disposableStore.dispose(); } </del> <ins> await withAsyncTestCodeEditor(text, { ...options, serviceCollection }, async (editor, editorViewModel, instantiationService) => { editor.registerAndInstantiateContribution(SnippetController2.ID, SnippetController2); editor.registerAndInstantiateContribution(SuggestController.ID, SuggestController); const model = instantiationService.createInstance(SuggestWidgetAdapterModel, editor); const context = new GhostTextContext(model, editor); await callback({ editor, editorViewModel, model, context }); model.dispose(); }); } finally { disposableStore.dispose(); } }); </ins> }

<ins> /*--------------------------------------------------------------------------------------------- *  Copyright (c) Microsoft Corporation. All rights reserved. *  Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; interface PriorityQueue<T> { length: number; add(value: T): void; remove(value: T): void; removeMin(): T | undefined; toSortedArray(): T[]; } class SimplePriorityQueue<T> implements PriorityQueue<T> { private isSorted = false; private items: T[]; constructor(items: T[], private readonly compare: (a: T, b: T) => number) { this.items = items; } get length(): number { return this.items.length; } add(value: T): void { this.items.push(value); this.isSorted = false; } remove(value: T): void { this.items.splice(this.items.indexOf(value), 1); this.isSorted = false; } removeMin(): T | undefined { this.ensureSorted(); return this.items.shift(); } getMin(): T | undefined { this.ensureSorted(); return this.items[0]; } toSortedArray(): T[] { this.ensureSorted(); return [...this.items]; } private ensureSorted() { if (!this.isSorted) { this.items.sort(this.compare); this.isSorted = true; } } } export type TimeOffset = number; export interface Scheduler { schedule(task: ScheduledTask): IDisposable; get now(): TimeOffset; } export interface ScheduledTask { readonly time: TimeOffset; readonly source: ScheduledTaskSource; run(): void; } export interface ScheduledTaskSource { toString(): string; readonly stackTrace: string | undefined; } interface ExtendedScheduledTask extends ScheduledTask { id: number; } function compareScheduledTasks(a: ExtendedScheduledTask, b: ExtendedScheduledTask): number { if (a.time !== b.time) { // Prefer lower time return a.time - b.time; } if (a.id !== b.id) { // Prefer lower id return a.id - b.id; } return 0; } export class TimeTravelScheduler implements Scheduler { private taskCounter = 0; private _now: TimeOffset = 0; private readonly queue: PriorityQueue<ExtendedScheduledTask> = new SimplePriorityQueue([], compareScheduledTasks); private readonly taskScheduledEmitter = new Emitter<{ task: ScheduledTask }>(); public readonly onTaskScheduled = this.taskScheduledEmitter.event; schedule(task: ScheduledTask): IDisposable { if (task.time < this._now) { throw new Error(`Scheduled time (${task.time}) must be equal to or greater than the current time (${this._now}).`); } const extendedTask: ExtendedScheduledTask = { ...task, id: this.taskCounter++ }; this.queue.add(extendedTask); this.taskScheduledEmitter.fire({ task }); return { dispose: () => this.queue.remove(extendedTask) }; } get now(): TimeOffset { return this._now; } get hasScheduledTasks(): boolean { return this.queue.length > 0; } getScheduledTasks(): readonly ScheduledTask[] { return this.queue.toSortedArray(); } runNext(): ScheduledTask | undefined { const task = this.queue.removeMin(); if (task) { this._now = task.time; task.run(); } return task; } installGlobally(): IDisposable { return overwriteGlobals(this); } } export class AsyncSchedulerProcessor extends Disposable { private isProcessing = false; private readonly _history = new Array<ScheduledTask>(); public get history(): readonly ScheduledTask[] { return this._history; } private readonly maxTaskCount: number; private readonly queueEmptyEmitter = new Emitter<void>(); public readonly onTaskQueueEmpty = this.queueEmptyEmitter.event; private lastError: Error | undefined; constructor(private readonly scheduler: TimeTravelScheduler, options?: { maxTaskCount?: number }) { super(); this.maxTaskCount = options && options.maxTaskCount ? options.maxTaskCount : 100; this._register(scheduler.onTaskScheduled(() => { if (this.isProcessing) { return; } else { this.isProcessing = true; this.schedule(); } })); } private schedule() { // This allows promises created by a previous task to settle and schedule tasks before the next task is run. // Tasks scheduled in those promises might have to run before the current next task. Promise.resolve().then(() => { originalGlobalValues.setTimeout(() => this.process()); }); } private process() { const executedTask = this.scheduler.runNext(); if (executedTask) { this._history.push(executedTask); if (history.length >= this.maxTaskCount && this.scheduler.hasScheduledTasks) { const lastTasks = this._history.slice(Math.max(0, history.length - 10)).map(h => `${h.source.toString()}: ${h.source.stackTrace}`); let e = new Error(`Queue did not get empty after processing ${history.length} items. These are the last ${lastTasks.length} scheduled tasks:n${lastTasks.join('nnn')}`); this.lastError = e; throw e; } } if (this.scheduler.hasScheduledTasks) { this.schedule(); } else { this.isProcessing = false; this.queueEmptyEmitter.fire(); } } waitForEmptyQueue(): Promise<void> { if (this.lastError) { const error = this.lastError; this.lastError = undefined; throw error; } if (!this.isProcessing) { return Promise.resolve(); } else { return Event.toPromise(this.onTaskQueueEmpty).then(() => { if (this.lastError) { throw this.lastError; } }); } } } export async function runWithFakedTimers<T>(options: { useFakeTimers?: boolean, maxTaskCount?: number }, fn: () => Promise<T>): Promise<T> { const useFakeTimers = options.useFakeTimers === undefined ? true : options.useFakeTimers; if (!useFakeTimers) { return fn(); } const scheduler = new TimeTravelScheduler(); const schedulerProcessor = new AsyncSchedulerProcessor(scheduler, { maxTaskCount: options.maxTaskCount }); const globalInstallDisposable = scheduler.installGlobally(); let result: T; try { result = await fn(); } finally { globalInstallDisposable.dispose(); } try { // We process the remaining scheduled tasks. // The global override is no longer active, so during this, no more tasks will be scheduled. await schedulerProcessor.waitForEmptyQueue(); } finally { schedulerProcessor.dispose(); } return result; } export const originalGlobalValues = { setTimeout: globalThis.setTimeout.bind(globalThis), clearTimeout: globalThis.clearTimeout.bind(globalThis), setInterval: globalThis.setInterval.bind(globalThis), clearInterval: globalThis.clearInterval.bind(globalThis), setImmediate: globalThis.setImmediate?.bind(globalThis), clearImmediate: globalThis.clearImmediate?.bind(globalThis), requestAnimationFrame: globalThis.requestAnimationFrame?.bind(globalThis), cancelAnimationFrame: globalThis.cancelAnimationFrame?.bind(globalThis), Date: globalThis.Date, }; function setTimeout(scheduler: Scheduler, handler: TimerHandler, timeout: number): IDisposable { if (typeof handler === 'string') { throw new Error('String handler args should not be used and are not supported'); } return scheduler.schedule({ time: scheduler.now + timeout, run: () => { handler(); }, source: { toString() { return 'setTimeout'; }, stackTrace: new Error().stack, } }); } function setInterval(scheduler: Scheduler, handler: TimerHandler, interval: number): IDisposable { if (typeof handler === 'string') { throw new Error('String handler args should not be used and are not supported'); } const validatedHandler = handler; let iterCount = 0; const stackTrace = new Error().stack; let disposed = false; let lastDisposable: IDisposable; function schedule(): void { iterCount++; const curIter = iterCount; lastDisposable = scheduler.schedule({ time: scheduler.now + interval, run() { if (!disposed) { schedule(); validatedHandler(); } }, source: { toString() { return `setInterval (iteration ${curIter})`; }, stackTrace, } }); } schedule(); return { dispose: () => { if (disposed) { return; } disposed = true; lastDisposable.dispose(); } }; } function overwriteGlobals(scheduler: Scheduler): IDisposable { globalThis.setTimeout = ((handler: TimerHandler, timeout: number) => setTimeout(scheduler, handler, timeout)) as any; globalThis.clearTimeout = (timeoutId: any) => { if (typeof timeoutId === 'object' && timeoutId && 'dispose' in timeoutId) { timeoutId.dispose(); } else { originalGlobalValues.clearTimeout(timeoutId); } }; globalThis.setInterval = ((handler: TimerHandler, timeout: number) => setInterval(scheduler, handler, timeout)) as any; globalThis.clearInterval = (timeoutId: any) => { if (typeof timeoutId === 'object' && timeoutId && 'dispose' in timeoutId) { timeoutId.dispose(); } else { originalGlobalValues.clearInterval(timeoutId); } }; globalThis.Date = createDateClass(scheduler); return { dispose: () => { Object.assign(globalThis, originalGlobalValues); } }; } function createDateClass(scheduler: Scheduler): DateConstructor { const OriginalDate = originalGlobalValues.Date; function SchedulerDate(this: any, ...args: any): any { // the Date constructor called as a function, ref Ecma-262 Edition 5.1, section 15.9.2. // This remains so in the 10th edition of 2019 as well. if (!(this instanceof SchedulerDate)) { return new OriginalDate(scheduler.now).toString(); } // if Date is called as a constructor with 'new' keyword if (args.length === 0) { return new OriginalDate(scheduler.now); } return new (OriginalDate as any)(...args); } for (let prop in OriginalDate) { if (OriginalDate.hasOwnProperty(prop)) { (SchedulerDate as any)[prop] = (OriginalDate as any)[prop]; } } SchedulerDate.now = function now() { return scheduler.now; }; SchedulerDate.toString = function toString() { return OriginalDate.toString(); }; SchedulerDate.prototype = OriginalDate.prototype; SchedulerDate.parse = OriginalDate.parse; SchedulerDate.UTC = OriginalDate.UTC; SchedulerDate.prototype.toUTCString = OriginalDate.prototype.toUTCString; return SchedulerDate as any; } </ins>