Skip to content

Commit 9dfb140

Browse files
authored
Restore previously loaded test cases (#52)
This change will persist the prior list of test cases as part of the workspace state, and restore this data when the test explorer relaunches. This avoids a wait for the full BSP build target request before test cases become visible: - Update the test case store to cache the build targets result and each target sources result. When a refresh runs, the latest version of these get stored. - Before registering the handlers, there is now an additional step to pull these stored test cases and re-add them back to the test explorer. - These get processed through the usual test case creation logic, so that a test explorer tree created from a restored result or a new one from the build server will behave the same way. - The refresh run always clears out any cached values, so these are only used when restoring the window, and an actual refresh run is always guaranteed to contain the latest results from the build server.
1 parent 422aa20 commit 9dfb140

File tree

6 files changed

+515
-31
lines changed

6 files changed

+515
-31
lines changed

src/test-explorer/resolver.ts

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,86 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
3939

4040
onModuleInit() {
4141
this.ctx.subscriptions.push(this)
42+
this.restorePriorTests()
43+
.catch(e => {
44+
this.outputChannel.appendLine(`Error restoring prior tests: ${e}`)
45+
})
46+
.finally(() => {
47+
this.registerHandlers()
48+
})
49+
}
50+
51+
registerHandlers() {
4252
this.store.testController.resolveHandler = this.resolveHandler.bind(this)
4353
this.store.testController.refreshHandler = this.refreshHandler.bind(this)
4454
}
4555

56+
private async restorePriorTests() {
57+
await this.resolveRoot()
58+
const cachedBuildTargetsResult = this.store.getCachedBuildTargetsResult()
59+
60+
// Restore the root test item and build targets.
61+
let addedTargets = false
62+
if (cachedBuildTargetsResult) {
63+
this.outputChannel.appendLine('Restoring prior test cases')
64+
const root = this.store.testController.items.get('root')
65+
if (root) {
66+
try {
67+
this.outputChannel.appendLine('Restoring Source Files for Root')
68+
await this.processWorkspaceBuildTargetsResult(
69+
root,
70+
cachedBuildTargetsResult
71+
)
72+
root.canResolveChildren = false
73+
addedTargets = true
74+
} catch (e) {
75+
this.outputChannel.appendLine(
76+
`Error restoring source files for root: ${e}`
77+
)
78+
}
79+
}
80+
}
81+
82+
// Restore cached source files for targets that have them.
83+
const promises: Promise<void>[] = []
84+
for (const [key, testItem] of this.store.targetIdentifiers.entries()) {
85+
let cachedSourceFiles: bsp.SourcesResult | undefined
86+
try {
87+
const target: bsp.BuildTargetIdentifier = JSON.parse(key)
88+
const sourceParams: bsp.SourcesParams = {
89+
targets: [target],
90+
}
91+
cachedSourceFiles = this.store.getCachedSourcesResult(sourceParams)
92+
} catch (e) {
93+
this.outputChannel.appendLine(
94+
`Invalid key '${key}' when restoring source files: ${e}`
95+
)
96+
}
97+
98+
if (cachedSourceFiles) {
99+
this.outputChannel.appendLine(
100+
`Restoring source files for target: ${key}`
101+
)
102+
try {
103+
testItem.canResolveChildren = false
104+
const promise = this.processTargetSourcesResult(
105+
testItem,
106+
cachedSourceFiles
107+
)
108+
promises.push(promise)
109+
} catch (e) {
110+
this.outputChannel.appendLine(
111+
`Error restoring source files for target: ${key}`
112+
)
113+
}
114+
}
115+
}
116+
await Promise.all(promises)
117+
118+
// Kick off discovery of test cases in other open files that may not have been discovered yet.
119+
if (addedTargets) await this.resolveOpenSourceFiles()
120+
}
121+
46122
dispose() {}
47123

48124
private async resolveHandler(
@@ -74,7 +150,6 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
74150
if (parentTest === undefined) {
75151
try {
76152
await this.buildServer.getConnection()
77-
this.resolveRoot()
78153
} catch (e) {
79154
this.outputChannel.appendLine(
80155
'Test explorer disabled due to lack of available build server.'
@@ -102,6 +177,7 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
102177
'Fetching test targets from build server ([progress](command:bazelbsp.showServerOutput))',
103178
})
104179
await this.resolveTargets(parentTest, combinedToken)
180+
await this.resolveOpenSourceFiles()
105181
break
106182
case TestItemType.BazelTarget:
107183
progress.report({
@@ -124,6 +200,7 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
124200
* @param token Cancellation token tied to the refresh button on the VS Code UI.
125201
*/
126202
private async refreshHandler(token: vscode.CancellationToken) {
203+
this.store.clearCache()
127204
const promises: Promise<void>[] = []
128205
this.store.testController.items.forEach(async item => {
129206
promises.push(this.resolveHandler(item, token))
@@ -173,6 +250,7 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
173250
bsp.WorkspaceBuildTargets.type,
174251
cancellationToken
175252
)
253+
this.store.cacheBuildTargetsResult(result)
176254
} catch (e) {
177255
if (e.code === CANCEL_ERROR_CODE) {
178256
updateDescription(
@@ -184,7 +262,13 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
184262
updateDescription(parentTest, 'Error: unable to fetch targets')
185263
throw e
186264
}
265+
await this.processWorkspaceBuildTargetsResult(parentTest, result)
266+
}
187267

268+
private async processWorkspaceBuildTargetsResult(
269+
parentTest: vscode.TestItem,
270+
result: bsp.WorkspaceBuildTargetsResult
271+
) {
188272
updateDescription(parentTest, 'Loading: processing target results')
189273

190274
// Process the returned targets, create new test items, and store their metadata.
@@ -243,8 +327,6 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
243327
// Replace all children with the newly returned test cases.
244328
this.condenseTestItems(parentTest)
245329
updateDescription(parentTest)
246-
247-
await this.resolveOpenSourceFiles()
248330
}
249331

250332
/**
@@ -357,6 +439,17 @@ export class TestResolver implements OnModuleInit, vscode.Disposable {
357439
params,
358440
cancellationToken
359441
)
442+
this.store.cacheSourcesResult(params, result)
443+
await this.processTargetSourcesResult(parentTest, result)
444+
}
445+
446+
private async processTargetSourcesResult(
447+
parentTest: vscode.TestItem,
448+
result: bsp.SourcesResult,
449+
cancellationToken?: vscode.CancellationToken
450+
) {
451+
const parentTarget = this.store.testCaseMetadata.get(parentTest)?.target
452+
if (!parentTarget) return
360453

361454
const directories = new Map<string, vscode.TestItem>()
362455
parentTest.children.replace([])

src/test-explorer/store.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as vscode from 'vscode'
2+
import * as bsp from '../bsp/bsp'
23
import {Inject, OnModuleInit} from '@nestjs/common'
34

45
import {
@@ -8,6 +9,9 @@ import {
89
import {TestCaseInfo} from '../test-info/test-info'
910
import {BuildTargetIdentifier} from 'src/bsp/bsp'
1011

12+
const CONTEXT_KEY_TARGETS_RESULT = 'testExplorerBuildTargetsResult'
13+
const CONTEXT_KEY_SOURCES_RESULT = 'testExplorerSourcesResult'
14+
1115
export class TestCaseStore implements OnModuleInit, vscode.Disposable {
1216
@Inject(EXTENSION_CONTEXT_TOKEN) private readonly ctx: vscode.ExtensionContext
1317
@Inject(TEST_CONTROLLER_TOKEN) readonly testController: vscode.TestController
@@ -17,7 +21,7 @@ export class TestCaseStore implements OnModuleInit, vscode.Disposable {
1721
// Watcher to update a test item's children. Key corresponds to the test item ID.
1822
testItemWatchers: Map<string, vscode.FileSystemWatcher>
1923
knownFiles: Set<string>
20-
private targetIdentifiers: Map<string, vscode.TestItem>
24+
targetIdentifiers: Map<string, vscode.TestItem>
2125

2226
constructor() {
2327
this.testCaseMetadata = new WeakMap<vscode.TestItem, TestCaseInfo>()
@@ -83,4 +87,41 @@ export class TestCaseStore implements OnModuleInit, vscode.Disposable {
8387
clearTargetIdentifiers() {
8488
this.targetIdentifiers.clear()
8589
}
90+
91+
cacheBuildTargetsResult(result: bsp.WorkspaceBuildTargetsResult) {
92+
this.ctx.workspaceState.update(CONTEXT_KEY_TARGETS_RESULT, result)
93+
}
94+
95+
getCachedBuildTargetsResult(): bsp.WorkspaceBuildTargetsResult | undefined {
96+
return this.ctx.workspaceState.get<bsp.WorkspaceBuildTargetsResult>(
97+
CONTEXT_KEY_TARGETS_RESULT
98+
)
99+
}
100+
101+
cacheSourcesResult(params: bsp.SourcesParams, result: bsp.SourcesResult) {
102+
const key = JSON.stringify(params)
103+
const allResults =
104+
this.ctx.workspaceState.get<Record<string, bsp.SourcesResult>>(
105+
CONTEXT_KEY_SOURCES_RESULT
106+
) || {}
107+
108+
// Add or update the result for this params key
109+
allResults[key] = result
110+
this.ctx.workspaceState.update(CONTEXT_KEY_SOURCES_RESULT, allResults)
111+
}
112+
113+
getCachedSourcesResult(
114+
params: bsp.SourcesParams
115+
): bsp.SourcesResult | undefined {
116+
const key = JSON.stringify(params)
117+
const allResults = this.ctx.workspaceState.get<
118+
Record<string, bsp.SourcesResult>
119+
>(CONTEXT_KEY_SOURCES_RESULT)
120+
return allResults?.[key]
121+
}
122+
123+
clearCache() {
124+
this.ctx.workspaceState.update(CONTEXT_KEY_TARGETS_RESULT, undefined)
125+
this.ctx.workspaceState.update(CONTEXT_KEY_SOURCES_RESULT, undefined)
126+
}
86127
}

0 commit comments

Comments
 (0)