Skip to content

Commit 267df92

Browse files
authored
disposableFiles: fix edge cases again (#1163)
* disposableFiles: fix edge cases again PROBLEM: some tests implicitly depend on the global `ExtensionDisposableFiles.INSTANCE`, other tests clear it, but it is not correctly re-initialized. SOLUTION: use `clearInstance()`, and rearrange its logic to avoid a race condition. Hoist `TestExtensionDisposableFiles` into `FakeExtensionContext` so it can use `clearInstance()`. ANALYSIS: Previous attempt to fix this in f143bad was incomplete, and only "worked" by accident. Problem resurfaces in `feature/debugconfig` because it has more tests that depend on `ExtensionDisposableFiles.INSTANCE`. Rearrange `clearInstance()` so that ExtensionDisposableFiles.INSTANCE = undefined is the first step. Because `del.sync()` does file IO, node may switch to another context while it waits on the file IO. Fixes this error (among others): 1) localLambdaRunner "before all" hook in "localLambdaRunner": Error: ExtensionDisposableFiles already initialized at Function.<anonymous> (/Volumes/workplace/aws-toolkit-vscode/src/shared/utilities/disposableFiles.ts:78:19) at Generator.next (<anonymous>) at /Volumes/workplace/aws-toolkit-vscode/dist/src/shared/utilities/disposableFiles.js:12:71 at new Promise (<anonymous>) at /Volumes/workplace/aws-toolkit-vscode/dist/src/shared/utilities/disposableFiles.js:8:12 at Function.initialize (/Volumes/workplace/aws-toolkit-vscode/dist/src/shared/utilities/disposableFiles.js:79:16) at Function.<anonymous> (/Volumes/workplace/aws-toolkit-vscode/src/test/fakeExtensionContext.ts:72:44) at Generator.next (<anonymous>) at /Volumes/workplace/aws-toolkit-vscode/dist/src/test/fakeExtensionContext.js:12:71 at new Promise (<anonymous>) at /Volumes/workplace/aws-toolkit-vscode/dist/src/test/fakeExtensionContext.js:8:12 at Function.getNew (/Volumes/workplace/aws-toolkit-vscode/dist/src/test/fakeExtensionContext.js:54:16) at /Volumes/workplace/aws-toolkit-vscode/src/test/shared/codelens/localLambdaRunner.test.ts:22:36 at Generator.next (<anonymous>) at /Volumes/workplace/aws-toolkit-vscode/dist/src/test/shared/codelens/localLambdaRunner.test.js:12:71 at new Promise (<anonymous>) at /Volumes/workplace/aws-toolkit-vscode/dist/src/test/shared/codelens/localLambdaRunner.test.js:8:12 at Context.<anonymous> (/Volumes/workplace/aws-toolkit-vscode/src/test/shared/codelens/localLambdaRunner.test.ts:21:23) at processImmediate (internal/timers.js:439:21) at process.topLevelDomainCallback (domain.js:131:23) * fixup! disposableFiles: fix edge cases again
1 parent 5e12464 commit 267df92

File tree

3 files changed

+37
-25
lines changed

3 files changed

+37
-25
lines changed

src/shared/utilities/disposableFiles.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ export class DisposableFiles implements vscode.Disposable {
2626
return this
2727
}
2828

29+
public isDisposed(): boolean {
30+
return this._disposed
31+
}
32+
2933
public dispose(): void {
3034
const logger: Logger = getLogger()
3135
if (!this._disposed) {
@@ -70,7 +74,7 @@ export class ExtensionDisposableFiles extends DisposableFiles {
7074
}
7175

7276
public static async initialize(extensionContext: vscode.ExtensionContext): Promise<void> {
73-
if (!!ExtensionDisposableFiles.INSTANCE) {
77+
if (ExtensionDisposableFiles.INSTANCE && !ExtensionDisposableFiles.INSTANCE.isDisposed()) {
7478
throw new Error('ExtensionDisposableFiles already initialized')
7579
}
7680

@@ -82,7 +86,7 @@ export class ExtensionDisposableFiles extends DisposableFiles {
8286
}
8387

8488
public static getInstance(): ExtensionDisposableFiles {
85-
if (!ExtensionDisposableFiles.INSTANCE) {
89+
if (!ExtensionDisposableFiles.INSTANCE || ExtensionDisposableFiles.INSTANCE.isDisposed()) {
8690
throw new Error('ExtensionDisposableFiles not initialized')
8791
}
8892

src/test/fakeExtensionContext.ts

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import { ExtensionContext, Memento } from 'vscode'
6+
import * as vscode from 'vscode'
7+
import * as del from 'del'
78
import { ExtensionDisposableFiles } from '../shared/utilities/disposableFiles'
89

910
export interface FakeMementoStorage {
@@ -15,12 +16,12 @@ export interface FakeExtensionState {
1516
workspaceState?: FakeMementoStorage
1617
}
1718

18-
export class FakeExtensionContext implements ExtensionContext {
19+
export class FakeExtensionContext implements vscode.ExtensionContext {
1920
public subscriptions: {
2021
dispose(): any
2122
}[] = []
22-
public workspaceState: Memento = new FakeMemento()
23-
public globalState: Memento = new FakeMemento()
23+
public workspaceState: vscode.Memento = new FakeMemento()
24+
public globalState: vscode.Memento = new FakeMemento()
2425
public storagePath: string | undefined
2526
public globalStoragePath: string = '.'
2627
public logPath: string = ''
@@ -54,15 +55,37 @@ export class FakeExtensionContext implements ExtensionContext {
5455
public static async getNew(): Promise<FakeExtensionContext> {
5556
const ctx = new FakeExtensionContext()
5657
try {
57-
ExtensionDisposableFiles.getInstance().dispose()
58+
TestExtensionDisposableFiles.clearInstance()
5859
} catch {
59-
await ExtensionDisposableFiles.initialize(ctx)
60+
// Ignore.
6061
}
62+
await TestExtensionDisposableFiles.initialize(ctx)
6163
return ctx
6264
}
6365
}
6466

65-
class FakeMemento implements Memento {
67+
export class TestExtensionDisposableFiles extends ExtensionDisposableFiles {
68+
public static ORIGINAL_INSTANCE = ExtensionDisposableFiles.INSTANCE
69+
70+
public static clearInstance() {
71+
// XXX: INSTANCE=undefined is done first, to avoid a race:
72+
// 1. del.sync() does file IO
73+
// 2. the Node scheduler looks for pending handlers to execute while waiting on IO
74+
// 3. other async handlers may try to use ExtensionDisposableFiles.getInstance()
75+
const instance = ExtensionDisposableFiles.INSTANCE
76+
ExtensionDisposableFiles.INSTANCE = undefined
77+
if (instance) {
78+
del.sync([instance.toolkitTempFolder], { force: true })
79+
instance.dispose()
80+
}
81+
}
82+
83+
public static resetOriginalInstance() {
84+
ExtensionDisposableFiles.INSTANCE = TestExtensionDisposableFiles.ORIGINAL_INSTANCE
85+
}
86+
}
87+
88+
class FakeMemento implements vscode.Memento {
6689
public constructor(private readonly _storage: FakeMementoStorage = {}) {}
6790
public get<T>(key: string): T | undefined
6891
public get<T>(key: string, defaultValue: T): T

src/test/shared/utilities/disposableFiles.test.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import * as vscode from 'vscode'
1111
import { mkdir } from '../../../shared/filesystem'
1212
import { fileExists, makeTemporaryToolkitFolder } from '../../../shared/filesystemUtilities'
1313
import { DisposableFiles, ExtensionDisposableFiles } from '../../../shared/utilities/disposableFiles'
14+
import { TestExtensionDisposableFiles } from '../../fakeExtensionContext'
1415

1516
describe('DisposableFiles', async () => {
1617
let tempFolder: string
@@ -87,22 +88,6 @@ describe('DisposableFiles', async () => {
8788
})
8889

8990
describe('ExtensionDisposableFiles', async () => {
90-
class TestExtensionDisposableFiles extends ExtensionDisposableFiles {
91-
public static ORIGINAL_INSTANCE = ExtensionDisposableFiles.INSTANCE
92-
93-
public static clearInstance() {
94-
if (ExtensionDisposableFiles.INSTANCE) {
95-
del.sync([ExtensionDisposableFiles.INSTANCE.toolkitTempFolder], { force: true })
96-
}
97-
98-
ExtensionDisposableFiles.INSTANCE = undefined
99-
}
100-
101-
public static resetOriginalInstance() {
102-
ExtensionDisposableFiles.INSTANCE = TestExtensionDisposableFiles.ORIGINAL_INSTANCE
103-
}
104-
}
105-
10691
let extensionContext: vscode.ExtensionContext
10792

10893
beforeEach(() => {

0 commit comments

Comments
 (0)