Skip to content

Commit 7fa7a27

Browse files
committed
Consider GNATtest project attributes in VS Code integration
1 parent 70dc26f commit 7fa7a27

File tree

2 files changed

+126
-45
lines changed

2 files changed

+126
-45
lines changed

integration/vscode/ada/src/ExtensionState.ts

Lines changed: 60 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { existsSync } from 'fs';
2+
import path, { isAbsolute } from 'path';
13
import * as vscode from 'vscode';
24
import {
35
Disposable,
@@ -6,14 +8,19 @@ import {
68
} from 'vscode-languageclient/node';
79
import { AdaCodeLensProvider } from './AdaCodeLensProvider';
810
import { AdaLanguageClient, createClient } from './clients';
11+
import {
12+
CMD_RELOAD_PROJECT,
13+
CMD_RESTART_LANG_SERVERS,
14+
CMD_SHOW_ADA_LS_OUTPUT,
15+
CMD_SHOW_EXTENSION_LOGS,
16+
CMD_SHOW_GPR_LS_OUTPUT,
17+
} from './commands';
918
import { AdaInitialDebugConfigProvider, initializeDebugging } from './debugConfigProvider';
1019
import { logger } from './extension';
11-
import { existsSync } from 'fs';
12-
import path from 'path';
1320
import { GnatTaskProvider } from './gnatTaskProvider';
1421
import { initializeTesting } from './gnattest';
1522
import { GprTaskProvider } from './gprTaskProvider';
16-
import { getArgValue, TERMINAL_ENV_SETTING_NAME, exe, getEvaluatedTerminalEnv } from './helpers';
23+
import { TERMINAL_ENV_SETTING_NAME, exe, getArgValue, getEvaluatedTerminalEnv } from './helpers';
1724
import {
1825
SimpleTaskDef,
1926
SimpleTaskProvider,
@@ -22,14 +29,6 @@ import {
2229
createAdaTaskProvider,
2330
createSparkTaskProvider,
2431
} from './taskProviders';
25-
import { isAbsolute } from 'path';
26-
import {
27-
CMD_SHOW_EXTENSION_LOGS,
28-
CMD_SHOW_ADA_LS_OUTPUT,
29-
CMD_SHOW_GPR_LS_OUTPUT,
30-
CMD_RELOAD_PROJECT,
31-
CMD_RESTART_LANG_SERVERS,
32-
} from './commands';
3332

3433
/**
3534
* Return type of the 'als-source-dirs' LSP request.
@@ -78,6 +77,7 @@ export class ExtensionState {
7877
cachedAlireTomls: vscode.Uri[] | undefined;
7978
cachedDebugServerAddress: string | undefined | null;
8079
cachedGdb: string | undefined | null = undefined;
80+
projectAttributeCache: Map<string, Promise<string | string[]>> = new Map();
8181

8282
private adaTaskProvider?: SimpleTaskProvider;
8383
private sparkTaskProvider?: SimpleTaskProvider;
@@ -92,6 +92,7 @@ export class ExtensionState {
9292
this.cachedAlireTomls = undefined;
9393
this.cachedDebugServerAddress = undefined;
9494
this.cachedGdb = undefined;
95+
this.projectAttributeCache.clear();
9596
}
9697

9798
constructor(context: vscode.ExtensionContext) {
@@ -367,35 +368,60 @@ export class ExtensionState {
367368
* an exception when the queried attribute is not known
368369
* (i.e: not registered in GPR2's knowledge database).
369370
*
371+
* Query results are cached by default so that querying the same attribute
372+
* multiple times will result in only one request to the server.
373+
*
370374
* @param attribute - The name of the project attribute (e.g: 'Target')
371375
* @param pkg - The name of the attribute's package (e.g: 'Compiler').
372376
* Can be empty for project-level attributes.
373377
* @param index - Attribute's index, if any. Can be a file or a language
374378
* (e.g: 'for Runtime ("Ada") use...').
379+
* @param useCache - whether to use cached result from previous query. If
380+
* set to false, the request will be sent to the server and the result will
381+
* be used to update the cache. This allows refreshing a particular project
382+
* attribute in the cache.
375383
* @returns the value of the queried project attribute. Can be either a string or a
376384
* list of strings, depending on the attribute itself (e.g: 'Main' attribute is
377385
* specified as a list of strings while 'Target' as a string)
378386
*/
379-
public async getProjectAttributeValue(
387+
public getProjectAttributeValue(
380388
attribute: string,
381389
pkg: string = '',
382390
index: string = '',
391+
useCache = true,
383392
): Promise<string | string[]> {
384-
const params: ExecuteCommandParams = {
385-
command: 'als-get-project-attribute-value',
386-
arguments: [
387-
{
388-
attribute: attribute,
389-
pkg: pkg,
390-
index: index,
391-
},
392-
],
393+
const queryArgs = {
394+
attribute: attribute,
395+
pkg: pkg,
396+
index: index,
393397
};
394398

395-
const attrValue = (await this.adaClient.sendRequest(ExecuteCommandRequest.type, params)) as
396-
| string
397-
| string[];
398-
return attrValue;
399+
/**
400+
* In the JS Map class, keys are compared using something equivalent to
401+
* ===. So two distinct objects with the same deep content will
402+
* constitute different keys. A common way of using objects as Map keys
403+
* is to stringify them.
404+
*/
405+
const mapKey = JSON.stringify(queryArgs);
406+
const cachedPromise = useCache ? this.projectAttributeCache.get(mapKey) : undefined;
407+
408+
if (cachedPromise === undefined) {
409+
const params: ExecuteCommandParams = {
410+
command: 'als-get-project-attribute-value',
411+
arguments: [queryArgs],
412+
};
413+
414+
const queryPromise = this.adaClient.sendRequest(
415+
ExecuteCommandRequest.type,
416+
params,
417+
) as Promise<string | string[]>;
418+
419+
this.projectAttributeCache.set(mapKey, queryPromise);
420+
421+
return queryPromise;
422+
} else {
423+
return cachedPromise;
424+
}
399425
}
400426

401427
/**
@@ -648,6 +674,15 @@ export class ExtensionState {
648674
public getAdaTaskProvider() {
649675
return this.adaTaskProvider;
650676
}
677+
678+
/**
679+
*
680+
* @returns path under the project object dir where the VS Code extension can
681+
* store temporary state
682+
*/
683+
public async getVSCodeObjectSubdir(): Promise<string> {
684+
return path.join(await this.getObjectDir(), 'vscode-ada');
685+
}
651686
}
652687

653688
/**

integration/vscode/ada/src/gnattest.ts

Lines changed: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,10 @@ import { CancellationToken } from 'vscode-languageclient';
99
import { adaExtState } from './extension';
1010
import { addCoverageData, GnatcovFileCoverage } from './gnatcov';
1111
import { getScenarioArgs } from './gnatTaskProvider';
12-
import { escapeRegExp, exe, getObjectDir, setTerminalEnvironment, slugify } from './helpers';
12+
import { escapeRegExp, exe, setTerminalEnvironment, slugify } from './helpers';
1313
import {
1414
DEFAULT_PROBLEM_MATCHER,
1515
findTaskByName,
16-
runTaskAndGetResult,
1716
runTaskSequence,
1817
SimpleTaskDef,
1918
TASK_BUILD_TEST_DRIVER,
@@ -160,19 +159,29 @@ export async function refreshTestItemTree() {
160159
/**
161160
* @returns the full path to the GNATtest XML file.
162161
*/
163-
async function getGnatTestXmlPath(): Promise<string> {
164-
const objDir = await getObjectDir();
165-
const gnatTestXmlPath = path.join(objDir, 'gnattest', 'harness', 'gnattest.xml');
162+
export async function getGnatTestXmlPath(): Promise<string> {
163+
const gnatTestXmlPath = path.join(await getHarnessDir(), 'gnattest.xml');
166164
return gnatTestXmlPath;
167165
}
168166

167+
export async function getHarnessDir() {
168+
return await adaExtState
169+
.getProjectAttributeValue('Harness_Dir', 'Gnattest')
170+
.catch(
171+
/**
172+
* default to gnattest/harness if Harness_Dir is unspecified
173+
*/
174+
() => path.join('gnattest', 'harness'),
175+
)
176+
.then(async (value) => path.join(await adaExtState.getObjectDir(), value as string));
177+
}
178+
169179
/**
170180
*
171181
* @returns the full path to the GNATtest test driver GPR project.
172182
*/
173183
export async function getGnatTestDriverProjectPath(): Promise<string> {
174-
const objDir = await getObjectDir();
175-
const testDriverPath = path.join(objDir, 'gnattest', 'harness', 'test_driver.gpr');
184+
const testDriverPath = path.join(await getHarnessDir(), 'test_driver.gpr');
176185
return testDriverPath;
177186
}
178187

@@ -181,11 +190,26 @@ export async function getGnatTestDriverProjectPath(): Promise<string> {
181190
* @returns the full path to the GNATtest test driver executable.
182191
*/
183192
export async function getGnatTestDriverExecPath(): Promise<string> {
184-
const objDir = await getObjectDir();
185-
const testDriverPath = path.join(objDir, 'gnattest', 'harness', 'test_runner' + exe);
193+
const testDriverPath = path.join(await getHarnessDir(), 'test_runner' + exe);
186194
return testDriverPath;
187195
}
188196

197+
/**
198+
*
199+
* @returns path where GNATcoverage execution traces will be created during a test run
200+
*/
201+
async function getTracesDir() {
202+
return path.join(await adaExtState.getVSCodeObjectSubdir(), 'test-traces');
203+
}
204+
205+
/**
206+
*
207+
* @returns path where GNATcoverage report will be created after a test run
208+
*/
209+
async function getGnatCovXMLReportDir() {
210+
return path.join(await adaExtState.getVSCodeObjectSubdir(), 'cov-xml');
211+
}
212+
189213
/**
190214
* Parse the GNATtest XML file and create the top-level TestItems in the test
191215
* controller for later lazy resolution.
@@ -330,6 +354,7 @@ async function addTestCaseItem(parentItem: vscode.TestItem, testCase: TestCase)
330354
const testFileBasename = test['@_file'];
331355
const pos = new vscode.Position(parseInt(test['@_line']), parseInt(test['@_column']) - 1);
332356
const range = new vscode.Range(pos, pos);
357+
333358
const testUri = await findFileInWorkspace(testFileBasename);
334359

335360
// The name of the source file of the tested subprogram is not part of the
@@ -557,7 +582,16 @@ async function handleRunRequestedTests(
557582
requestedRootTests.push(...request.include);
558583
} else {
559584
/**
560-
* Consider all tests as included
585+
* If the Testing view hasn't been opened yet, the tests may not
586+
* have been loaded yet. Check for that and load the tests in that
587+
* case.
588+
*/
589+
if (controller.items.size == 0) {
590+
await refreshTestItemTree();
591+
}
592+
593+
/**
594+
* Consider all tests as included.
561595
*/
562596
controller.items.forEach((i) => requestedRootTests.push(i));
563597
}
@@ -606,7 +640,11 @@ async function handleRunRequestedTests(
606640
* Invoke the test driver for each test
607641
*/
608642
const execPath = await getGnatTestDriverExecPath();
609-
const tracesDir = path.dirname(execPath);
643+
const tracesDir = await getTracesDir();
644+
645+
if (coverage) {
646+
fs.mkdirSync(tracesDir, { recursive: true });
647+
}
610648

611649
function getTracePath(test: TestItem): string {
612650
return path.join(tracesDir, slugify(test.id) + '.srctrace');
@@ -684,7 +722,7 @@ async function handleRunRequestedTests(
684722
/**
685723
* Produce a GNATcov XML report
686724
*/
687-
const outputDir = path.join(await adaExtState.getObjectDir(), 'cov-xml');
725+
const outputDir = await getGnatCovXMLReportDir();
688726
const adaTP = adaExtState.getAdaTaskProvider()!;
689727
const gnatcovReportTask = (await adaTP.resolveTask(
690728
new vscode.Task(
@@ -708,7 +746,7 @@ async function handleRunRequestedTests(
708746
),
709747
))!;
710748
gnatcovReportTask.presentationOptions.reveal = vscode.TaskRevealKind.Never;
711-
const result = await runTaskAndGetResult(gnatcovReportTask);
749+
const result = await runTaskSequence([gnatcovReportTask], new WriteEmitter(run));
712750
if (result != 0) {
713751
const msg =
714752
`Error while running coverage analysis.` +
@@ -726,6 +764,20 @@ async function handleRunRequestedTests(
726764
}
727765
}
728766

767+
/**
768+
* An EventEmitter that forwards data to a a TestRun given at construction
769+
* time.
770+
*/
771+
class WriteEmitter extends vscode.EventEmitter<string> {
772+
constructor(private run: vscode.TestRun) {
773+
super();
774+
}
775+
776+
override fire(data: string): void {
777+
this.run.appendOutput(data);
778+
}
779+
}
780+
729781
/**
730782
* Build the test driver and report build failure as errors on the tests
731783
* requested for execution.
@@ -739,12 +791,6 @@ async function buildTestDriverAndReportErrors(
739791
testsToRun: vscode.TestItem[],
740792
coverage: boolean,
741793
) {
742-
class WriteEmitter extends vscode.EventEmitter<string> {
743-
override fire(data: string): void {
744-
run.appendOutput(data);
745-
}
746-
}
747-
748794
const buildTasks = [];
749795
if (coverage) {
750796
const adaTP = adaExtState.getAdaTaskProvider();
@@ -836,7 +882,7 @@ async function buildTestDriverAndReportErrors(
836882
buildTasks.push(task);
837883
}
838884

839-
const result = await runTaskSequence(buildTasks, new WriteEmitter());
885+
const result = await runTaskSequence(buildTasks, new WriteEmitter(run));
840886

841887
if (result != 0) {
842888
let msg =

0 commit comments

Comments
 (0)