Skip to content

Commit b674451

Browse files
authored
feat(vscode): Add edit and view results for test files (#4411)
* Add fspath and commands to file tests * Add logic to handle TestItem node in edit unit test * Add logic to handle open unit test results * Add run unit test to command * Add logic to get the results from runs * Add information message for when unit test has been run * Update strings * Update comments
1 parent 78500a8 commit b674451

File tree

11 files changed

+195
-146
lines changed

11 files changed

+195
-146
lines changed

apps/vs-code-designer/src/app/commands/workflows/unitTest/editUnitTest.ts

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,11 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55
import { developmentDirectoryName, testsDirectoryName, workflowFileName } from '../../../../constants';
6-
import { localize } from '../../../../localize';
7-
import { getUnitTestInLocalProject, getUnitTestName } from '../../../utils/unitTests';
6+
import { getUnitTestName, pickUnitTest } from '../../../utils/unitTests';
87
import { tryGetLogicAppProjectRoot } from '../../../utils/verifyIsProject';
98
import { getWorkflowNode, getWorkspaceFolder } from '../../../utils/workspace';
109
import { type IAzureConnectorsContext } from '../azureConnectorWizard';
1110
import OpenDesignerForLocalProject from '../openDesigner/openDesignerForLocalProject';
12-
import { type IAzureQuickPickItem, type IActionContext } from '@microsoft/vscode-azext-utils';
1311
import { readFileSync } from 'fs';
1412
import * as path from 'path';
1513
import * as vscode from 'vscode';
@@ -20,13 +18,15 @@ import * as vscode from 'vscode';
2018
* @param {vscode.Uri} node - The URI of the unit test file to edit. If not provided, the user will be prompted to select a unit test file.
2119
* @returns A Promise that resolves when the unit test has been edited.
2220
*/
23-
export async function editUnitTest(context: IAzureConnectorsContext, node: vscode.Uri): Promise<void> {
21+
export async function editUnitTest(context: IAzureConnectorsContext, node: vscode.Uri | vscode.TestItem): Promise<void> {
2422
let unitTestNode: vscode.Uri;
2523
const workspaceFolder = await getWorkspaceFolder(context);
2624
const projectPath = await tryGetLogicAppProjectRoot(context, workspaceFolder);
2725

28-
if (node) {
26+
if (node && node instanceof vscode.Uri) {
2927
unitTestNode = getWorkflowNode(node) as vscode.Uri;
28+
} else if (node && !(node instanceof vscode.Uri) && node.uri instanceof vscode.Uri) {
29+
unitTestNode = node.uri;
3030
} else {
3131
const unitTest = await pickUnitTest(context, path.join(projectPath, developmentDirectoryName, testsDirectoryName));
3232
unitTestNode = vscode.Uri.file(unitTest.data) as vscode.Uri;
@@ -41,29 +41,3 @@ export async function editUnitTest(context: IAzureConnectorsContext, node: vscod
4141
const openDesignerObj = new OpenDesignerForLocalProject(context, workflowNode, unitTestName, unitTestDefinition);
4242
await openDesignerObj?.createPanel();
4343
}
44-
45-
/**
46-
* Prompts the user to select a unit test to edit.
47-
* @param {IActionContext} context - The action context.
48-
* @param {string} projectPath - The path of the project.
49-
* @returns A promise that resolves to the selected unit test.
50-
*/
51-
const pickUnitTest = async (context: IActionContext, projectPath: string) => {
52-
const placeHolder: string = localize('selectUnitTest', 'Select unit test to edit');
53-
return await context.ui.showQuickPick(getUnitTestPick(projectPath), { placeHolder });
54-
};
55-
56-
/**
57-
* Retrieves a list of unit tests in the local project.
58-
* @param {string} projectPath - The path to the project.
59-
* @returns A promise that resolves to an array of unit test picks.
60-
*/
61-
const getUnitTestPick = async (projectPath: string) => {
62-
const listOfUnitTest = await getUnitTestInLocalProject(projectPath);
63-
const picks: IAzureQuickPickItem<string>[] = Array.from(Object.keys(listOfUnitTest)).map((unitTestName) => {
64-
return { label: unitTestName, data: listOfUnitTest[unitTestName] };
65-
});
66-
67-
picks.sort((a, b) => a.label.localeCompare(b.label));
68-
return picks;
69-
};

apps/vs-code-designer/src/app/commands/workflows/unitTest/openUnitTestResults.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,44 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
5-
import { getUnitTestName } from '../../../utils/unitTests';
6-
import { getWorkflowNode } from '../../../utils/workspace';
5+
import { developmentDirectoryName, testsDirectoryName, workflowFileName } from '../../../../constants';
6+
import { ext } from '../../../../extensionVariables';
7+
import { localize } from '../../../../localize';
8+
import { getUnitTestName, pickUnitTest } from '../../../utils/unitTests';
9+
import { tryGetLogicAppProjectRoot } from '../../../utils/verifyIsProject';
10+
import { getWorkflowNode, getWorkspaceFolder } from '../../../utils/workspace';
711
import { type IAzureConnectorsContext } from '../azureConnectorWizard';
812
import OpenDesignerForLocalProject from '../openDesigner/openDesignerForLocalProject';
9-
import type * as vscode from 'vscode';
13+
import { readFileSync } from 'fs';
14+
import * as path from 'path';
15+
import { type TestItem, Uri, window } from 'vscode';
1016

11-
export async function openUnitTestResults(context: IAzureConnectorsContext, node: vscode.Uri): Promise<void> {
12-
const unitTestName = getUnitTestName(node.fsPath);
13-
const workflowNode = getWorkflowNode(node) as vscode.Uri;
14-
const openDesignerObj = new OpenDesignerForLocalProject(context, workflowNode, unitTestName);
15-
await openDesignerObj?.createPanel();
17+
export async function openUnitTestResults(context: IAzureConnectorsContext, node: Uri | TestItem): Promise<void> {
18+
let unitTestNode: Uri;
19+
const workspaceFolder = await getWorkspaceFolder(context);
20+
const projectPath = await tryGetLogicAppProjectRoot(context, workspaceFolder);
21+
22+
if (node && node instanceof Uri) {
23+
unitTestNode = getWorkflowNode(node) as Uri;
24+
} else if (node && !(node instanceof Uri) && node.uri instanceof Uri) {
25+
unitTestNode = node.uri;
26+
} else {
27+
const unitTest = await pickUnitTest(context, path.join(projectPath, developmentDirectoryName, testsDirectoryName));
28+
unitTestNode = Uri.file(unitTest.data) as Uri;
29+
}
30+
const unitTestName = getUnitTestName(unitTestNode.fsPath);
31+
32+
if (ext.testRuns.has(unitTestNode.fsPath)) {
33+
const workflowName = path.basename(path.dirname(unitTestNode.fsPath));
34+
const workflowPath = path.join(projectPath, workflowName, workflowFileName);
35+
const workflowNode = Uri.file(workflowPath);
36+
const unitTestDefinition = JSON.parse(readFileSync(unitTestNode.fsPath, 'utf8'));
37+
38+
const openDesignerObj = new OpenDesignerForLocalProject(context, workflowNode, unitTestName, unitTestDefinition);
39+
await openDesignerObj?.createPanel();
40+
}
41+
42+
window.showInformationMessage(
43+
localize('noRunForUnitTest', 'There is no run for the selected unit test. Make sure to run the unit test for "{0}"', unitTestName)
44+
);
1645
}

apps/vs-code-designer/src/app/commands/workflows/unitTest/runUnitTest.ts

Lines changed: 48 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,70 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55
import { runUnitTestEvent } from '../../../../constants';
6+
import { ext } from '../../../../extensionVariables';
67
import { localize } from '../../../../localize';
7-
import { getLogicAppProjectRoot } from '../../../utils/codeless/connection';
8-
import { type IAzureConnectorsContext } from '../azureConnectorWizard';
9-
import { callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
10-
import * as child from 'child_process';
11-
import * as path from 'path';
8+
import { type UnitTestResult } from '../../../utils/unitTests';
9+
import { type IActionContext, callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
1210
import * as vscode from 'vscode';
1311

14-
export async function runUnitTest(context: IAzureConnectorsContext, node: vscode.Uri): Promise<void> {
15-
await callWithTelemetryAndErrorHandling(runUnitTestEvent, async () => {
12+
/**
13+
* Runs a unit test for a given node in the Logic Apps designer.
14+
* @param {IActionContext} context - The action context.
15+
* @param {vscode.Uri | vscode.TestItem} node - The URI or TestItem representing the node to run the unit test for.
16+
* @returns A Promise that resolves to the UnitTestResult object.
17+
*/
18+
export async function runUnitTest(context: IActionContext, node: vscode.Uri | vscode.TestItem): Promise<UnitTestResult> {
19+
return await callWithTelemetryAndErrorHandling(runUnitTestEvent, async () => {
1620
const options: vscode.ProgressOptions = {
1721
location: vscode.ProgressLocation.Notification,
1822
title: localize('azureFunctions.runUnitTest', 'Running Unit Test...'),
1923
};
2024

21-
await vscode.window.withProgress(options, async () => {
22-
const pathToUnitTest = node.fsPath;
23-
const projectPath: string | undefined = await getLogicAppProjectRoot(this.context, pathToUnitTest);
24-
const workflowName = path.basename(path.dirname(pathToUnitTest));
25+
return await vscode.window.withProgress(options, async () => {
26+
// This is where we are going to run the unit test from extension bundle
27+
// Just put a random decision here in the meantime
28+
try {
29+
const runId = node instanceof vscode.Uri ? node.fsPath : node.uri.fsPath;
30+
const start = Date.now();
31+
await new Promise((resolve) => setTimeout(resolve, 1000 + Math.random() * 1000));
32+
const duration = Date.now() - start;
2533

26-
const UNIT_TEST_EXE_NAME = 'LogicAppsTest.exe';
27-
const pathToExe = path.join(projectPath, UNIT_TEST_EXE_NAME);
34+
const testResult = {
35+
isSuccessful: start % 2 === 0,
36+
assertions: [],
37+
duration,
38+
};
2839

29-
try {
30-
const res = child.spawn(pathToExe, ['-PathToRoot', projectPath, '-workflowName', workflowName, '-pathToUnitTest', pathToUnitTest]);
40+
ext.testRuns.set(runId, {
41+
runId,
42+
results: testResult,
43+
});
3144

32-
for await (const chunk of res.stdout) {
33-
vscode.window.showInformationMessage(`${chunk}`);
34-
}
45+
return testResult;
3546
} catch (error) {
3647
vscode.window.showErrorMessage(`${localize('runFailure', 'Error Running Unit Test.')} ${error.message}`, localize('OK', 'OK'));
3748
context.telemetry.properties.errorMessage = error.message;
3849
throw error;
3950
}
51+
52+
// const pathToUnitTest = node.fsPath;
53+
// const projectPath: string | undefined = await getLogicAppProjectRoot(this.context, pathToUnitTest);
54+
// const workflowName = path.basename(path.dirname(pathToUnitTest));
55+
56+
// const UNIT_TEST_EXE_NAME = 'LogicAppsTest.exe';
57+
// const pathToExe = path.join(projectPath, UNIT_TEST_EXE_NAME);
58+
59+
// try {
60+
// const res = child.spawn(pathToExe, ['-PathToRoot', projectPath, '-workflowName', workflowName, '-pathToUnitTest', pathToUnitTest]);
61+
62+
// for await (const chunk of res.stdout) {
63+
// vscode.window.showInformationMessage(`${chunk}`);
64+
// }
65+
// } catch (error) {
66+
// vscode.window.showErrorMessage(`${localize('runFailure', 'Error Running Unit Test.')} ${error.message}`, localize('OK', 'OK'));
67+
// context.telemetry.properties.errorMessage = error.message;
68+
// throw error;
69+
// }
4070
});
4171
});
4272
}

apps/vs-code-designer/src/app/tree/unitTestTree/index.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { TestWorkflow } from './testWorkflow';
1010
import { TestWorkspace } from './testWorkspace';
1111
import { isEmptyString } from '@microsoft/utils-logic-apps';
1212
import { type IActionContext, callWithTelemetryAndErrorHandling } from '@microsoft/vscode-azext-utils';
13+
import * as path from 'path';
1314
import {
1415
RelativePattern,
1516
type TestController,
@@ -30,8 +31,9 @@ export type TestData = TestWorkspace | TestWorkflow | TestFile;
3031
/**
3132
* Prepares the test explorer for unit tests.
3233
* @param {ExtensionContext} context - The extension context.
34+
* @param {IActionContext} activateContext - Command activate context.
3335
*/
34-
export const prepareTestExplorer = async (context: ExtensionContext) => {
36+
export const prepareTestExplorer = async (context: ExtensionContext, activateContext: IActionContext) => {
3537
callWithTelemetryAndErrorHandling(unitTestExplorer, async (actionContext: IActionContext) => {
3638
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
3739
const isLogicAppProject = await hasLogicAppProject(actionContext);
@@ -51,7 +53,7 @@ export const prepareTestExplorer = async (context: ExtensionContext) => {
5153
unitTestController.createRunProfile(
5254
'Run logic apps standard unit tests',
5355
TestRunProfileKind.Run,
54-
(request, cancellation) => runHandler(request, cancellation, unitTestController),
56+
(request, cancellation) => runHandler(request, cancellation, unitTestController, activateContext),
5557
true,
5658
undefined
5759
);
@@ -91,10 +93,16 @@ export const getWorkspaceTestPatterns = () => {
9193
* Runs the test handler based on the provided request and cancellation token.
9294
* @param {TestRunRequest} request - The test run request.
9395
* @param {CancellationToken} cancellation - The cancellation token.
96+
* @param {IActionContext} activateContext - Command activate context.
9497
*/
95-
export const runHandler = (request: TestRunRequest, cancellation: CancellationToken, unitTestController: TestController) => {
98+
export const runHandler = (
99+
request: TestRunRequest,
100+
cancellation: CancellationToken,
101+
unitTestController: TestController,
102+
activateContext: IActionContext
103+
) => {
96104
cancellation.onCancellationRequested(() => request.include.forEach((item) => ext.watchingTests.delete(item)));
97-
return startTestRun(request, unitTestController);
105+
return startTestRun(request, unitTestController, activateContext);
98106
};
99107

100108
/**
@@ -133,10 +141,6 @@ const testsWorkspaceWatcher = (controller: TestController, fileChangedEmitter: E
133141
fileChangedEmitter.fire(uri);
134142
});
135143
watcher.onDidChange(async (uri) => {
136-
const { data } = await getOrCreateFile(controller, uri);
137-
if (data instanceof TestFile) {
138-
await data.parseUnitTest();
139-
}
140144
fileChangedEmitter.fire(uri);
141145
});
142146
watcher.onDidDelete((uri) => controller.items.delete(uri.toString()));
@@ -156,7 +160,7 @@ const findInitialFiles = async (controller: TestController, pattern: RelativePat
156160
const unitTestFiles = await workspace.findFiles(pattern);
157161

158162
const workspacesTestFiles = unitTestFiles.reduce((acc, file: Uri) => {
159-
const workspaceName = file.path.split('/').slice(-5)[0];
163+
const workspaceName = file.fsPath.split('/').slice(-5)[0];
160164

161165
if (!acc[workspaceName]) {
162166
acc[workspaceName] = [];
@@ -182,7 +186,7 @@ const getOrCreateWorkspace = (controller: TestController, workspaceName: string,
182186
if (existing) {
183187
return { file: existing, data: ext.testData.get(existing) as TestWorkspace };
184188
}
185-
const filePath = files.length > 0 ? files[0].path : '';
189+
const filePath = files.length > 0 ? files[0].fsPath : '';
186190
const workspaceUri = isEmptyString(filePath) ? undefined : Uri.file(filePath.split('/').slice(0, -4).join('/'));
187191

188192
const workspaceTestItem = controller.createTestItem(workspaceName, workspaceName, workspaceUri);
@@ -202,9 +206,9 @@ const getOrCreateWorkspace = (controller: TestController, workspaceName: string,
202206
* @returns An object containing the file test and its associated data, if it exists.
203207
*/
204208
const getOrCreateFile = async (controller: TestController, uri: Uri) => {
205-
const workspaceName = uri.path.split('/').slice(-5)[0];
206-
const testName = uri.path.split('/').slice(-1)[0];
207-
const workflowName = uri.path.split('/').slice(-2)[0];
209+
const workspaceName = uri.fsPath.split('/').slice(-5)[0];
210+
const testName = path.basename(uri.fsPath);
211+
const workflowName = path.basename(path.dirname(uri.fsPath));
208212

209213
const existingWorkspaceTest = controller.items.get(workspaceName);
210214

@@ -225,7 +229,7 @@ const getOrCreateFile = async (controller: TestController, uri: Uri) => {
225229
existingWorkspaceTest.children.add(workflowTestItem);
226230
}
227231
} else {
228-
const workspaceUri = Uri.file(uri.path.split('/').slice(0, -4).join('/'));
232+
const workspaceUri = Uri.file(uri.fsPath.split('/').slice(0, -4).join('/'));
229233
const workspaceTestItem = controller.createTestItem(workspaceName, workspaceName, workspaceUri);
230234
workspaceTestItem.canResolveChildren = true;
231235
controller.items.add(workspaceTestItem);
@@ -242,8 +246,9 @@ const getOrCreateFile = async (controller: TestController, uri: Uri) => {
242246
* Starts a test run based on the provided request and unit test controller.
243247
* @param {TestRunRequest} request - The test run request.
244248
* @param {TestController} unitTestController - The unit test controller.
249+
* @param {IActionContext} activateContext - Command activate context.
245250
*/
246-
const startTestRun = (request: TestRunRequest, unitTestController: TestController) => {
251+
const startTestRun = (request: TestRunRequest, unitTestController: TestController, activateContext: IActionContext) => {
247252
const queue: { test: TestItem; data: TestFile }[] = [];
248253
const run = unitTestController.createTestRun(request);
249254

@@ -278,15 +283,15 @@ const startTestRun = (request: TestRunRequest, unitTestController: TestControlle
278283
*/
279284
const runTestQueue = async () => {
280285
for (const { test, data } of queue) {
281-
run.appendOutput(`Running ${test.id}\r\n`);
286+
run.appendOutput(`Running ${test.label}\r\n`);
282287
if (run.token.isCancellationRequested) {
283288
run.skipped(test);
284289
} else {
285290
run.started(test);
286-
await data.run(test, run);
291+
await data.run(test, run, activateContext);
287292
}
288293

289-
run.appendOutput(`Completed ${test.id}\r\n`);
294+
run.appendOutput(`Completed ${test.label}\r\n`);
290295
}
291296

292297
run.end();

0 commit comments

Comments
 (0)