Skip to content

Commit 05a4ec5

Browse files
committed
Use a task to build the GNATtest harness project
1 parent f606502 commit 05a4ec5

File tree

7 files changed

+138
-130
lines changed

7 files changed

+138
-130
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ section below it for the last release. -->
77
* Support single-line (`//`) and multi-line (`/* */`) comments in ALS JSON configuration files
88
* Fix the reporting of test results in the Testing view when the test tree is not expanded
99
* Fix sluggish completion while editing GPR files
10+
* Provide a task `ada: Build GNATtest test harness project` allowing to customize the build step of test execution in `tasks.json`
1011

1112
## 26.0.202412190
1213

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -291,14 +291,18 @@ For example, if the project defines a `main1.adb` and `main2.adb` located under
291291

292292
If you install GNATtest, the Ada & SPARK extension for VS Code will provide the following functionalities:
293293

294-
* The task `ada: Create/update test skeletons for the project` will call `gnattest` to create test skeletons for your project automatically. You can use standard VS Code task customization to configure command line arguments to your liking in a `tasks.json` file.
294+
* The task `ada: Create or update GNATtest test framework` will call `gnattest` to create a test harness and test skeletons for your project automatically. You can use standard VS Code task customization to configure command line arguments to your liking in a `tasks.json` file.
295+
296+
* Once the test harness project is created, the task `ada: Build GNATtest test harness project` is provided automatically for building it. Command line arguments can be customized by configuring the task in a `tasks.json` file.
295297

296298
* Tests created with GNATtest will be loaded in the VS Code **Testing** view as follows.
297299

298300
<img src="doc/gnattest-test-tree.png" width="650" alt="GNATtest Test Tree">
299301

300302
* Tests can be executed individually or in batch through the available buttons in the interface, or through the `Test: Run All Tests` command or related commands.
301303

304+
* Test execution always starts by executing the `ada: Build GNATtest test harness project` task to make sure that test executables are up to date.
305+
302306
* Test execution results are reflected in the test tree.
303307

304308
<img src="doc/gnattest-results.png" width="500" alt="GNATtest Test Results">
@@ -308,8 +312,6 @@ GNATtest support has the following known limitations:
308312
* The extension relies on the default conventions of GNATtest such as the naming, location and object directory of the test harness project.
309313
If those aspects are configured or altered manually, the features may no longer work.
310314

311-
* Test execution always starts with a `gprbuild` call on the test harness project. It is not possible to disable that call or customize its arguments. This limitation will be lifted in future releases.
312-
313315
* Language support such as navigation and auto-completion is limited when editing test sources. A workaround is to modify the `ada.projectFile` setting to point to the test harness project created by GNATtest. That should restore language support when developing tests.
314316

315317
* Sections of test sources delimited by `begin read only` and `end read only` comments are not really protected from inadvertant edits.

integration/vscode/ada/src/commands.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -656,16 +656,25 @@ async function buildAndDebugSpecifiedMain(main: vscode.Uri): Promise<void> {
656656
* for use with GPR-based tools.
657657
*/
658658
export async function gprProjectArgs(): Promise<string[]> {
659+
const scenarioArgs = gprScenarioArgs();
660+
661+
return ['-P', await getProjectFromConfigOrALS()].concat(scenarioArgs);
662+
}
663+
664+
export const PROJECT_FROM_CONFIG = '${config:ada.projectFile}';
665+
666+
/**
667+
* @returns an array of -X scenario command lines arguments for use with
668+
* GPR-based tools.
669+
*/
670+
export function gprScenarioArgs() {
659671
const vars: string[][] = Object.entries(
660672
vscode.workspace.getConfiguration('ada').get('scenarioVariables') ?? [],
661673
);
662-
return ['-P', await getProjectFromConfigOrALS()].concat(
663-
vars.map(([key, value]) => `-X${key}=${value}`),
664-
);
674+
const scenarioArgs = vars.map(([key, value]) => `-X${key}=${value}`);
675+
return scenarioArgs;
665676
}
666677

667-
export const PROJECT_FROM_CONFIG = '${config:ada.projectFile}';
668-
669678
/**
670679
* @returns `"$\{config:ada.projectFile\}"` if that setting has a value, or else
671680
* queries the ALS for the current project and returns the full path.

integration/vscode/ada/src/gnattest.ts

Lines changed: 29 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { TestItem } from 'vscode';
88
import { CancellationToken } from 'vscode-languageclient';
99
import { adaExtState } from './extension';
1010
import { escapeRegExp, exe, getObjectDir } from './helpers';
11+
import {
12+
findTaskByName,
13+
runTaskAndGetResult,
14+
TASK_BUILD_TEST_DRIVER,
15+
TASK_TYPE_ADA,
16+
} from './taskProviders';
1117

1218
export let controller: vscode.TestController;
1319
export let testRunProfile: vscode.TestRunProfile;
@@ -582,18 +588,22 @@ async function handleRunRequestedTests(request: vscode.TestRunRequest, token?: C
582588
* reported on those tests.
583589
*/
584590
async function buildTestDriverAndReportErrors(run: vscode.TestRun, testsToRun: vscode.TestItem[]) {
585-
try {
586-
await buildTestDriver(run);
587-
} catch (error) {
591+
const task = await findTaskByName(`${TASK_TYPE_ADA}: ${TASK_BUILD_TEST_DRIVER}`);
592+
const result = await runTaskAndGetResult(task);
593+
if (result != 0) {
594+
const msg =
595+
`Task '${TASK_BUILD_TEST_DRIVER}' failed.` +
596+
` Check the [Problems](command:workbench.panel.markers.view.focus) view` +
597+
` and the [Terminal](command:terminal.focus) view for more information.`;
598+
const md = new vscode.MarkdownString(msg);
599+
md.isTrusted = true;
600+
const testMsg = new vscode.TestMessage(md);
588601
/**
589-
* In case of failure, report all tests as errored.
602+
* Mark each test as errored, not failed, since the tests can't run
603+
* because of the build error.
590604
*/
591-
const md = getBuildErrorMessage();
592-
for (const test of testsToRun) {
593-
run.errored(test, new vscode.TestMessage(md));
594-
}
595-
run.end();
596-
throw error;
605+
testsToRun.forEach((test) => run.errored(test, testMsg));
606+
throw Error(msg);
597607
}
598608
}
599609

@@ -617,6 +627,15 @@ function prepareAndAppendOutput(run: vscode.TestRun, out: string) {
617627
async function handleRunAll(request: vscode.TestRunRequest, token?: CancellationToken) {
618628
const run = controller.createTestRun(request, undefined, false);
619629
try {
630+
/**
631+
* If the run request was created with the 'Tests: Run All Tests'
632+
* command before activating the Testing view, then the controller is
633+
* still empty. In that case let's refresh it to load the tests.
634+
*/
635+
if (controller.items.size == 0) {
636+
await controller.refreshHandler!(token ?? new vscode.CancellationTokenSource().token);
637+
}
638+
620639
/**
621640
* Collect all tests, i.e. all leafs of the TestItem tree.
622641
*/
@@ -673,54 +692,6 @@ async function handleRunAll(request: vscode.TestRunRequest, token?: Cancellation
673692
}
674693
}
675694

676-
/**
677-
* Failures to build the test driver are reported as test errors to make them
678-
* clearly visible.
679-
*
680-
* @returns the message to be used as a test failure when the test driver fails
681-
* to build.
682-
*/
683-
function getBuildErrorMessage() {
684-
const md = new vscode.MarkdownString(
685-
'Failed to build the test driver, [view output](command:testing.showMostRecentOutput)',
686-
);
687-
md.isTrusted = true;
688-
return md;
689-
}
690-
691-
/**
692-
* Invoke gprbuild on the test driver project and pipe the output into the given
693-
* TestRun object.
694-
*
695-
* @param run - the TestRun object hosting this execution
696-
* @throws an Error if the process ends with an error code
697-
*/
698-
async function buildTestDriver(run: vscode.TestRun) {
699-
/**
700-
* TODO replace this with a task invocation to capture problems
701-
*/
702-
const driverPrjPath = await getGnatTestDriverProjectPath();
703-
run.appendOutput(`Building the test harness project\r\n`);
704-
/**
705-
* The following arguments are passed directly to the spawned subprocess.
706-
* It not necessary nor appropriate to apply shell quoting here.
707-
*/
708-
const gprbuild = logAndRun(run, ['gprbuild', '-P', driverPrjPath, '-cargs:ada', '-gnatef']);
709-
710-
prepareAndAppendOutput(run, gprbuild.stdout.toLocaleString());
711-
prepareAndAppendOutput(run, gprbuild.stderr.toLocaleString());
712-
713-
if (gprbuild.status !== 0) {
714-
throw Error(
715-
'Error while building the test driver:\n' +
716-
gprbuild.output
717-
.filter((x) => x != null)
718-
.map((x) => x.toLocaleString())
719-
.join('\n'),
720-
);
721-
}
722-
}
723-
724695
/**
725696
* Decide the outcome of the given test based on the test driver output, and
726697
* report it to the TestRun object.

integration/vscode/ada/src/taskProviders.ts

Lines changed: 81 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616
----------------------------------------------------------------------------*/
1717

1818
import assert from 'assert';
19+
import { existsSync } from 'fs';
1920
import path, { basename } from 'path';
2021
import * as vscode from 'vscode';
2122
import { CMD_GPR_PROJECT_ARGS } from './commands';
2223
import { adaExtState, logger } from './extension';
24+
import { getGnatTestDriverProjectPath } from './gnattest';
2325
import { AdaMain, getAdaMains, showErrorMessageWithOpenLogButton } from './helpers';
2426

2527
export const TASK_TYPE_ADA = 'ada';
@@ -90,6 +92,7 @@ export const TASK_PROVE_SUPB_PLAIN_NAME = 'Prove subprogram';
9092
export const TASK_PROVE_REGION_PLAIN_NAME = 'Prove selected region';
9193
export const TASK_PROVE_LINE_PLAIN_NAME = 'Prove line';
9294
export const TASK_PROVE_FILE_PLAIN_NAME = 'Prove file';
95+
export const TASK_BUILD_TEST_DRIVER = 'Build GNATtest test harness project';
9396

9497
/**
9598
* Predefined tasks offered by the extension. Both 'ada' and 'spark' tasks are
@@ -230,7 +233,7 @@ const predefinedTasks: PredefinedTask[] = [
230233
problemMatchers: '',
231234
},
232235
{
233-
label: 'Create/update test skeletons for the project',
236+
label: 'Create or update GNATtest test framework',
234237
taskDef: {
235238
type: TASK_TYPE_ADA,
236239
command: 'gnattest',
@@ -454,6 +457,23 @@ export class SimpleTaskProvider implements vscode.TaskProvider {
454457
return [buildTask, runTask, buildAndRunTask];
455458
}),
456459
);
460+
461+
/**
462+
* If a test harness project exists, provide a task to build it.
463+
*/
464+
const harnessPrj = await getGnatTestDriverProjectPath();
465+
if (existsSync(harnessPrj)) {
466+
taskDeclsToOffer.push({
467+
label: TASK_BUILD_TEST_DRIVER,
468+
taskDef: {
469+
type: TASK_TYPE_ADA,
470+
command: 'gprbuild',
471+
args: ['-P', harnessPrj, "'-cargs:ada'", '-gnatef'],
472+
},
473+
problemMatchers: DEFAULT_PROBLEM_MATCHER,
474+
taskGroup: vscode.TaskGroup.Build,
475+
});
476+
}
457477
}
458478

459479
/**
@@ -1157,3 +1177,63 @@ function isAlire(command: string | vscode.ShellQuotedString): boolean {
11571177
const commandBasename = basename(value);
11581178
return commandBasename.match(/^alr(\.exe)?$/) != null;
11591179
}
1180+
1181+
/**
1182+
* Execute the given task, wait until it finishes and return the underlying
1183+
* process exit code.
1184+
*
1185+
* @param task - a {@link vscode.Task}
1186+
* @returns a Promise that resolves to the underlying process exit code when the
1187+
* task finishes execution.
1188+
*/
1189+
export async function runTaskAndGetResult(task: vscode.Task): Promise<number | undefined> {
1190+
return await new Promise<number | undefined>((resolve, reject) => {
1191+
let started = false;
1192+
1193+
const startDisposable = vscode.tasks.onDidStartTask((e) => {
1194+
if (e.execution.task == task) {
1195+
/**
1196+
* Task was started, let's listen to the end.
1197+
*/
1198+
started = true;
1199+
startDisposable.dispose();
1200+
}
1201+
});
1202+
1203+
const endDisposable = vscode.tasks.onDidEndTaskProcess((e) => {
1204+
if (e.execution.task == task) {
1205+
endDisposable.dispose();
1206+
resolve(e.exitCode);
1207+
}
1208+
});
1209+
1210+
setTimeout(() => {
1211+
/**
1212+
* If the task has not started within the timeout below, it means an
1213+
* error occured during startup. Reject the promise.
1214+
*/
1215+
if (!started) {
1216+
const msg = `The task '${getConventionalTaskLabel(
1217+
task,
1218+
)}' was not started, likely due to an error.\n`;
1219+
reject(Error(msg));
1220+
}
1221+
}, 3000);
1222+
1223+
void vscode.tasks.executeTask(task);
1224+
}).catch(async (reason) => {
1225+
if (reason instanceof Error) {
1226+
let msg = 'The current list of tasks is:\n';
1227+
msg += await vscode.tasks.fetchTasks({ type: task.definition.type }).then(
1228+
(list) => list.map(getConventionalTaskLabel).join('\n'),
1229+
1230+
(reason) => `fetchTasks promise was rejected: ${reason}`,
1231+
);
1232+
1233+
reason.message += '\n' + msg;
1234+
}
1235+
1236+
// eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors
1237+
return Promise.reject(reason);
1238+
});
1239+
}

integration/vscode/ada/test/general/tasks.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import {
2121
isCoreTask,
2222
isGNATSASTask,
2323
negate,
24-
runTaskAndGetResult,
2524
testTask,
2625
} from '../utils';
26+
import { runTaskAndGetResult } from '../../src/taskProviders';
2727

2828
suite('Task Providers', function () {
2929
this.timeout('15s');

0 commit comments

Comments
 (0)