diff --git a/src/VSCodeExtension/.vscode/launch.json b/src/VSCodeExtension/.vscode/launch.json index d44896ac68..34937e0f75 100644 --- a/src/VSCodeExtension/.vscode/launch.json +++ b/src/VSCodeExtension/.vscode/launch.json @@ -3,40 +3,29 @@ // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 { - "version": "0.2.0", - "configurations": [ - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" - }, - { - "name": "Extension", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}" - ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "npm: watch" - }, - { - "name": "Extension Tests", - "type": "extensionHost", - "request": "launch", - "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "--extensionTestsPath=${workspaceFolder}/out/test" - ], - "outFiles": [ - "${workspaceFolder}/out/test/**/*.js" - ], - "preLaunchTask": "npm: watch" - } - ] -} + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": ["--extensionDevelopmentPath=${workspaceFolder}"], + "outFiles": ["${workspaceFolder}/out/**/*.js"], + "preLaunchTask": "npm: watch" + }, + { + "name": "Run Extension Tests", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index", + "/Users/owner/Desktop/sampleQuantumProj" + ], + "outFiles": ["${workspaceFolder}/out/test/**/*.js"], + "preLaunchTask": "npm: watch" + } + ] +} \ No newline at end of file diff --git a/src/VSCodeExtension/package.json.v.template b/src/VSCodeExtension/package.json.v.template index b7115b20dc..f4db27ea7e 100644 --- a/src/VSCodeExtension/package.json.v.template +++ b/src/VSCodeExtension/package.json.v.template @@ -199,6 +199,7 @@ "@azure/ms-rest-azure-env": "^2.0.0", "@azure/ms-rest-nodeauth": "^3.1.1", "@types/fs-extra": "^8.0.0", + "@types/glob": "^7.2.0", "@vscode/extension-telemetry": "0.6.2", "decompress-zip": "^0.2.2", "dotnet": "^1.1.4", @@ -221,15 +222,19 @@ "yosay": "^2.0.1" }, "devDependencies": { + "@types/chai": "^4.3.3", + "@types/mocha": "^9.1.1", "@types/node": "^9.6.57", "@types/request": "^2.48.3", "@types/semver": "^6.0.0", + "@types/sinon": "^10.0.13", "@types/tmp": "0.0.33", "@types/vscode": "^1.52.0", "@types/which": "1.3.1", "@types/yeoman-environment": "2.3.3", "@types/yeoman-generator": "3.1.4", "@types/yosay": "0.0.29", + "@vscode/test-electron": "^2.1.5", "mocha": "^8.2.1", "tslint": "^5.8.0", "typescript": "^4.1.3" diff --git a/src/VSCodeExtension/src/configFileHelpers.ts b/src/VSCodeExtension/src/configFileHelpers.ts index 9aa4bb5af3..7dda3318b6 100644 --- a/src/VSCodeExtension/src/configFileHelpers.ts +++ b/src/VSCodeExtension/src/configFileHelpers.ts @@ -4,7 +4,7 @@ import {getWorkspaceFromUser} from "./quickPickWorkspace"; import {workspaceInfo, configFileInfo} from "./utils/types"; import {setupAuthorizedWorkspaceStatusButton} from "./workspaceStatusButtonHelpers"; import * as https from "https"; - +import * as glob from 'glob'; // If config not present, queries user for workspace information // If config present, verifies config. If verification fails, @@ -28,7 +28,7 @@ export async function setWorkspace(context:vscode.ExtensionContext, credential:a export async function handleUnauthorizedConfig(context:vscode.ExtensionContext, credential:any, workspaceStatusBarItem:vscode.StatusBarItem){ const userInput = await vscode.window.showErrorMessage("You do not have access to this workspace, or it doesn't exist.", {}, ...["Change Workspace"]); if (userInput === "Change Workspace"){ - const workspaceInfo = await getWorkspaceFromUser(context, credential, workspaceStatusBarItem, 3, true); + const workspaceInfo = await getWorkspaceFromUser(context, credential, workspaceStatusBarItem, 3); if(workspaceInfo){ return true; } @@ -111,7 +111,6 @@ export async function verifyConfig(context: vscode.ExtensionContext, credential: // returns an object with the workspaceInfo, if succ export async function getConfig(context:vscode.ExtensionContext, credential:any, workspaceStatusBarItem:vscode.StatusBarItem, validateFlag=true):Promise { - const configFileInfo:configFileInfo = { workspaceInfo:undefined, exitRequest:false @@ -127,26 +126,41 @@ export async function getConfig(context:vscode.ExtensionContext, credential:any, return configFileInfo; } + // using glob to search for config file. This is necessary to be test + // compatiable with mocha let workspaceInfo:workspaceInfo; - const configFile = await vscode.workspace.findFiles( - "**/azurequantumconfig.json" - ); + let rootFolder= ""; + let pullConfigFiles:any[] =[]; + if(vscode?.workspace?.workspaceFolders){ + rootFolder = vscode?.workspace?.workspaceFolders[0]?.uri.fsPath; + } + await new Promise(async (resolve)=>{ + await glob('**/azurequantumconfig.json', { cwd: rootFolder }, (err, files) => { + if (err) { + console.log(err); + resolve(); + } + pullConfigFiles = files; + resolve(); + }); + }); + // no config file present, but this is not function stopping as // the user will be queried for a workspace - if(configFile.length ===0){ + if(pullConfigFiles.length ===0){ return configFileInfo; } // If multiple config files are present, stop the function as more // than one config in a user's workspace is not permitted at this time. - if(configFile.length>1){ + if(pullConfigFiles.length>1){ configFileInfo.exitRequest = true; vscode.window.showWarningMessage("Only one azurequantumconfig.json file is allowed in a workspace."); return configFileInfo; } const workspaceInfoChunk: any = await vscode.workspace.fs.readFile( - configFile[0] + vscode.Uri.file(rootFolder+"/"+pullConfigFiles[0]) ); // try to pull subscription, resource groupm, workspace, and location diff --git a/src/VSCodeExtension/src/extension.ts b/src/VSCodeExtension/src/extension.ts index f88ddd4915..554ddb26df 100644 --- a/src/VSCodeExtension/src/extension.ts +++ b/src/VSCodeExtension/src/extension.ts @@ -17,8 +17,8 @@ import {getWorkspaceFromUser} from "./quickPickWorkspace"; import {getConfig, setWorkspace} from "./configFileHelpers"; import { AbortController} from "@azure/abort-controller"; import * as https from "https"; -import {MSA_ACCOUNT_TENANT, workspaceStatusEnum} from "./utils/constants" -import {checkForNesting} from "./checkForNesting" +import {MSA_ACCOUNT_TENANT, workspaceStatusEnum} from "./utils/constants"; +import {checkForNesting} from "./checkForNesting"; import {setupDefaultWorkspaceStatusButton, setupUnknownWorkspaceStatusButton} from "./workspaceStatusButtonHelpers"; const findPort = require('find-open-port'); @@ -434,13 +434,16 @@ export async function activate(context: vscode.ExtensionContext) { const oldStatus = context.workspaceState.get("workspaceStatus"); // Get current workspace if available to avoid clearing // local jobs submission panel if a user selects same workspace - // they are currently in - let {workspaceInfo:oldWorkspace} = await getConfig(context, credential, workspaceStatusBarItem, false); - const newWorkspaceInfo = await getWorkspaceFromUser(context, credential, workspaceStatusBarItem, 3, true); + // they are currently in. Pass false for validation flag as + // the user is in process of changing workspace and therefore + // does not need to be shown the error message that they are + // in an unauthorized workspace. + let {workspaceInfo:oldWorkspaceInfo} = await getConfig(context, credential, workspaceStatusBarItem, false); + const newWorkspaceInfo = await getWorkspaceFromUser(context, credential, workspaceStatusBarItem, 3, oldWorkspaceInfo); sendTelemetryEvent(EventNames.changeWorkspace, {},{}); // Only clear local jobs is user changes workspaces and has // a currently authorized workspace status - if((newWorkspaceInfo?.workspace!==oldWorkspace?.workspace)&& oldStatus === workspaceStatusEnum.AUTHORIZED){ + if((newWorkspaceInfo?.workspace!==oldWorkspaceInfo?.workspace)&& oldStatus === workspaceStatusEnum.AUTHORIZED){ context.workspaceState.update("locallySubmittedJobs", undefined); localSubmissionsProvider.refresh(context); } @@ -536,7 +539,7 @@ export async function activate(context: vscode.ExtensionContext) { } if(!workspaceInfo){ totalSteps = 4; - workspaceInfo = await getWorkspaceFromUser(context, credential, workspaceStatusBarItem, totalSteps) + workspaceInfo = await getWorkspaceFromUser(context, credential, workspaceStatusBarItem, totalSteps); } if(!workspaceInfo){ return; @@ -635,7 +638,6 @@ export async function activate(context: vscode.ExtensionContext) { ); return context; - } // this method is called when your extension is deactivated diff --git a/src/VSCodeExtension/src/quickPickWorkspace.ts b/src/VSCodeExtension/src/quickPickWorkspace.ts index e2f9d131cd..b8df4554c3 100644 --- a/src/VSCodeExtension/src/quickPickWorkspace.ts +++ b/src/VSCodeExtension/src/quickPickWorkspace.ts @@ -5,7 +5,6 @@ import { AccessToken } from "@azure/identity"; import { TextEncoder } from "util"; -import {getConfig} from "./configFileHelpers"; import {workspaceInfo} from "./utils/types"; // import fetch from 'node-fetch'; import * as https from "https"; @@ -23,7 +22,7 @@ export async function getWorkspaceFromUser( credential: InteractiveBrowserCredential | AzureCliCredential, workspaceStatusBarItem: vscode.StatusBarItem, totalSteps: number, - unauthorizedUser = false + existingWorkspace:workspaceInfo|undefined = undefined ) { // get access token let token: AccessToken; @@ -39,12 +38,6 @@ export async function getWorkspaceFromUser( }); return new Promise( async (resolve, reject)=>{ - let workspaceInfo: workspaceInfo|undefined; - - if(!unauthorizedUser){ - const configFileInfo = await getConfig(context, credential, workspaceStatusBarItem); - workspaceInfo = configFileInfo["workspaceInfo"]; -} const options:any = { headers: { Authorization: `Bearer ${token.token}`, @@ -64,7 +57,7 @@ export async function getWorkspaceFromUser( // if user is submitting job, total steps will be 7, otherwise 3 quickPick.totalSteps = totalSteps; - await setupSubscriptionIdQuickPick(quickPick, workspaceInfo, options); + await setupSubscriptionIdQuickPick(quickPick, existingWorkspace, options); quickPick.onDidAccept(async () => { const selection = quickPick.selectedItems[0]; // user selects subscription, now set up resource group selection @@ -76,7 +69,7 @@ export async function getWorkspaceFromUser( subscriptionId = selection["description"]; await setupResourceGroupQuickPick( quickPick, - workspaceInfo, + existingWorkspace, subscriptionId, options ); @@ -120,13 +113,13 @@ export async function getWorkspaceFromUser( quickPick.onDidTriggerButton(async (button) => { // resource group back button pressed, go back to subscription id if (quickPick.step === selectionStepEnum.RESOURCE_GROUP) { - await setupSubscriptionIdQuickPick(quickPick, workspaceInfo, options); + await setupSubscriptionIdQuickPick(quickPick, existingWorkspace, options); } // workspaces back button pressed, go back to resource group if (quickPick.step === selectionStepEnum.WORKSPACE) { await setupResourceGroupQuickPick( quickPick, - workspaceInfo, + existingWorkspace, subscriptionId, options ); @@ -147,7 +140,7 @@ export async function getWorkspaceFromUser( async function setupResourceGroupQuickPick( quickPick: vscode.QuickPick, - currentworkspaceInfo: workspaceInfo | undefined, + existingWorkspaceInfo: workspaceInfo | undefined, subscriptionId: string, options:any ) { @@ -193,7 +186,7 @@ export async function getWorkspaceFromUser( }); // Prefill if there is already a resource group quickPick.items = rgJSON.value.map((rg: any) => { - if (currentworkspaceInfo?.resourceGroup === rg.name && quickPick.step ===selectionStepEnum.RESOURCE_GROUP) { + if (existingWorkspaceInfo?.resourceGroup === rg.name && quickPick.step ===selectionStepEnum.RESOURCE_GROUP) { quickPick.value = rg.name; } return { label: rg.name }; @@ -207,7 +200,7 @@ export async function getWorkspaceFromUser( async function setupSubscriptionIdQuickPick( quickPick: vscode.QuickPick, - currentworkspaceInfo: workspaceInfo | undefined, + existingWorkspaceInfo: workspaceInfo | undefined, options: any ) { quickPick.placeholder = ""; @@ -249,7 +242,7 @@ export async function getWorkspaceFromUser( return rg1.displayName.localeCompare(rg2.displayName); }); quickPick.items = subscriptionsJSON.value.map((subscription: any) => { - if (currentworkspaceInfo?.subscriptionId === subscription.subscriptionId && quickPick.step ===selectionStepEnum.SUBSCRIPTION) { + if (existingWorkspaceInfo?.subscriptionId === subscription.subscriptionId && quickPick.step ===selectionStepEnum.SUBSCRIPTION) { quickPick.value = subscription.displayName; } return { diff --git a/src/VSCodeExtension/src/test/runTest.ts b/src/VSCodeExtension/src/test/runTest.ts new file mode 100644 index 0000000000..fa2fd890ae --- /dev/null +++ b/src/VSCodeExtension/src/test/runTest.ts @@ -0,0 +1,24 @@ +import * as path from 'path'; + +import { runTests } from '@vscode/test-electron'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to the extension test runner script + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error(err); + console.error('Failed to run tests'); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/src/VSCodeExtension/src/test/suite/extension.test.ts b/src/VSCodeExtension/src/test/suite/extension.test.ts new file mode 100644 index 0000000000..88dfe45b7d --- /dev/null +++ b/src/VSCodeExtension/src/test/suite/extension.test.ts @@ -0,0 +1,215 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { window, extensions, commands} from 'vscode'; +import * as vscode from "vscode"; +import { workspace_1, workspace_2 } from '../../utils/test-utils/workspaces'; +import { expectedResult_1, expectedDetails_1, mockLocalPanelJob, jobId_1, jobParameters} from '../../utils/test-utils/jobData'; +import { EventEmitter } from '../../utils/test-utils/events'; +import {describe, it} from "mocha"; +import {findWorkspace, eventuallyOk} from "./testHelpers"; + + +describe('Extension Test Suite', async() => { + window.showInformationMessage('Start all tests.'); + const started = extensions.getExtension("quantum.quantum-devkit-vscode"); + await started?.activate(); + let createQuickPick: sinon.SinonSpy; + let acceptQuickPick: EventEmitter; + + it("Should start quantum extension", async () => { + // activate the extension + assert.notEqual(started, undefined); + assert.equal(started?.isActive, true); + }); + + it("Should register all commands", async () => { + // get commands + const commandsList = await commands.getCommands(); + const quantumCommandsList = commandsList.filter(x => x.startsWith("quantum")); + //assert + assert.equal(quantumCommandsList.includes("quantum.newProject"), true ); + assert.equal(quantumCommandsList.includes("quantum.installTemplates"), true ); + assert.equal(quantumCommandsList.includes("quantum.openDocumentation"), true ); + assert.equal(quantumCommandsList.includes("quantum.installIQSharp"), true ); + assert.equal(quantumCommandsList.includes("quantum.installIQSharp"), true ); + assert.equal(quantumCommandsList.includes("quantum.connectToAzureAccount"), true ); + assert.equal(quantumCommandsList.includes("quantum.submitJob"), true ); + assert.equal(quantumCommandsList.includes("quantum.jobResultsPalette"), true ); + assert.equal(quantumCommandsList.includes("quantum.changeAzureAccount"), true ); + assert.equal(quantumCommandsList.includes("quantum.changeWorkspace"), true ); + }); + + it("Should connect to Azure Account", async () => { + // tester needs to be logged into Az Cli + const showInformationMessageStub = sinon.stub(vscode.window, "showInformationMessage"); + await commands.executeCommand("quantum.connectToAzureAccount"); + await new Promise(resolve => setTimeout(resolve, 3000)); + assert.ok(showInformationMessageStub.calledOnce); + assert(showInformationMessageStub.args[0], "Successfully connected to account."); + }); + + + it("Set workspace", async () => { + prepareStubsQuickPick(); + await commands.executeCommand("quantum.getWorkspace"); + await new Promise(resolve => setTimeout(resolve, 3000)); + + const typePicker = await eventuallyOk(() => { + expect(createQuickPick.callCount).to.equal(1); + const picker: vscode.QuickPick = + createQuickPick.getCall(0).returnValue; + return picker; + }); + typePicker.selectedItems = await typePicker.items.filter(i => i.label === workspace_1.subscriptionName); + acceptQuickPick.fire(); + + await new Promise(resolve => setTimeout(resolve, 2000)); + + typePicker.selectedItems = await typePicker.items.filter(i => i.label === workspace_1.resourceGroup); + acceptQuickPick.fire(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + typePicker.selectedItems = await typePicker.items.filter(i => i.label === workspace_1.workspace); + acceptQuickPick.fire(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + // pull workspace from azurequantumconfig.json and test against + // expected workspace details + const workspaceInfo = await findWorkspace(); + assert(workspaceInfo["subscriptionId"],workspace_1["subscriptionId"]); + assert(workspaceInfo["resourceGroup"],workspace_1["resourceGroup"]); + assert(workspaceInfo["workspace"],workspace_1["workspace"]); + assert(workspaceInfo["location"],workspace_1["location"]); + clearStubsQuickPick(); + }); + + + it("Change workspace", async () => { + prepareStubsQuickPick(); + await commands.executeCommand("quantum.changeWorkspace"); + await new Promise(resolve => setTimeout(resolve, 3000)); + const typePicker = await eventuallyOk(() => { + const picker: vscode.QuickPick = createQuickPick.getCall(0).returnValue; + return picker; + }, 2000); + typePicker.selectedItems = await typePicker.items.filter(i => i.label === workspace_2.subscriptionName); + acceptQuickPick.fire(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + typePicker.selectedItems = await typePicker.items.filter(i => i.label === workspace_2.resourceGroup); + acceptQuickPick.fire(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + typePicker.selectedItems = await typePicker.items.filter(i => i.label === workspace_2.workspace); + acceptQuickPick.fire(); + await new Promise(resolve => setTimeout(resolve, 2000)); + // pull workspace from azurequantumconfig.json and test against + // expected workspace details + const workspaceInfo = await findWorkspace(); + assert(workspaceInfo["subscriptionId"],workspace_2["subscriptionId"]); + assert(workspaceInfo["resourceGroup"],workspace_2["resourceGroup"]); + assert(workspaceInfo["workspace"],workspace_2["workspace"]); + assert(workspaceInfo["location"],workspace_2["location"]); + clearStubsQuickPick(); + }); + + it ("Get job results from button",async ()=>{ + // test the results button on the local tree view panel + await commands.executeCommand("quantum-jobs.jobResultsButton", mockLocalPanelJob); + await new Promise(resolve => setTimeout(resolve, 5000)); + const results = window.activeTextEditor?.document.getText(); + const testResults = JSON.stringify(expectedResult_1,null, 4); + assert.equal(results, testResults); + }); + + it ("Get job details",async ()=>{ + // test the details button on the local tree view panel + await commands.executeCommand("quantum-jobs.jobDetails", mockLocalPanelJob); + await new Promise(resolve => setTimeout(resolve, 5000)); + const results = window.activeTextEditor?.document.getText(); + if(!results){ + throw Error; + } + // remove inputDataUri and outputDataUri as links will differ + // between calls + const detailsJson = JSON.parse(results); + delete detailsJson.inputDataUri; + delete detailsJson.outputDataUri; + const detailsString = JSON.stringify(detailsJson,null, 4); + const expectedDetailsString = JSON.stringify(expectedDetails_1,null, 4); + + assert.equal(detailsString, expectedDetailsString); + }); + + it ("Get job results with Id",async ()=>{ + prepareStubsGetJobPalette(jobId_1); + // test the results command from the palette + await commands.executeCommand("quantum.jobResultsPalette"); + await new Promise(resolve => setTimeout(resolve, 5000)); + const results = window.activeTextEditor?.document.getText(); + const testResults = JSON.stringify(expectedResult_1,null, 4); + assert.equal(results, testResults); + clearStubsGetJobPalette(); + }); + + // TODO CAN ONLY ENTER QUCKPICK CHOICES FOR PROVIDER AND TARGET + // 1) IDEAL OPTION IS TO FIGURE OUT A WAY TO SET UP A SINON + // CREATEINPUTBOX FOR NAME AND ARGUMENTS INPUTS + // 2) IF CANT FIGURE OUT SINON, CONSIDER MOVING SUBMIT JOB + // FUNCTIONALITY (BUILDING AND RUNNING THE EXECUTABLE) WITH HARDCODED + // PAREMETERS + it("Submit job to Azure Quantum", async ()=>{ + prepareStubsQuickPick(); + await commands.executeCommand("quantum.submitJob"); + await new Promise(resolve => setTimeout(resolve, 2000)); + + const typePicker = await eventuallyOk(() => { + const picker: vscode.QuickPick = createQuickPick.getCall(0).returnValue; + return picker; + }, 2000); + + await new Promise(resolve => setTimeout(resolve, 3000)); + + typePicker.selectedItems = await typePicker.items.filter(i => i.label === jobParameters.provider); + acceptQuickPick.fire(); + await new Promise(resolve => setTimeout(resolve, 2000)); + + typePicker.selectedItems = await typePicker.items.filter(i => i.label === jobParameters.target); + acceptQuickPick.fire(); + await new Promise(resolve => setTimeout(resolve, 2000)); + clearStubsQuickPick(); + + // TODO CREATEINPUTBOX PARAMETERS FOR JOB NAME AND JOB ARGUMENTS + // BELOW CODE IS SIMPLIFIED VERSION OF WHAT IS NEEDED TO SUBMIT JOB + sinon.stub(window, 'createInputBox').resolves(jobParameters.name); + sinon.stub(window, 'createInputBox').resolves(jobParameters.additionalArgs); + + }); + + function prepareStubsGetJobPalette(jobId: string){ + // restore sinon + sinon.restore(); + // prepare stubs + sinon.stub(window, 'showInputBox').resolves(jobId); + } + + function clearStubsGetJobPalette(){ + (window['showInputBox'] as any).restore(); + } + + function prepareStubsQuickPick(){ + const originalQuickPick = vscode.window.createQuickPick; + createQuickPick = sinon.stub(vscode.window, 'createQuickPick').callsFake(() => { + const picker = originalQuickPick(); + acceptQuickPick = new EventEmitter(); + sinon.stub(picker, 'onDidAccept').callsFake(acceptQuickPick.event); + return picker; + }); + } + + function clearStubsQuickPick(){ + createQuickPick.restore(); + } + +}); \ No newline at end of file diff --git a/src/VSCodeExtension/src/test/suite/index.ts b/src/VSCodeExtension/src/test/suite/index.ts new file mode 100644 index 0000000000..5a150a9e39 --- /dev/null +++ b/src/VSCodeExtension/src/test/suite/index.ts @@ -0,0 +1,38 @@ +import * as path from 'path'; +import * as Mocha from 'mocha'; +import * as glob from 'glob'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true, + "timeout":60000 + }); + + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((c, e) => { + glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run(failures => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + e(err); + } + }); + }); +} \ No newline at end of file diff --git a/src/VSCodeExtension/src/test/suite/testHelpers.ts b/src/VSCodeExtension/src/test/suite/testHelpers.ts new file mode 100644 index 0000000000..dfe71cefbd --- /dev/null +++ b/src/VSCodeExtension/src/test/suite/testHelpers.ts @@ -0,0 +1,49 @@ +import * as vscode from "vscode"; +import * as glob from "glob"; + +export async function findWorkspace(){ + let rootFolder= ""; + let pullConfigFiles:any[] =[]; + if(vscode?.workspace?.workspaceFolders){ + rootFolder = vscode?.workspace?.workspaceFolders[0]?.uri.fsPath; + } + await new Promise(async (resolve)=>{ + await glob('**/azurequantumconfig.json', { cwd: rootFolder }, (err, files) => { + if (err) { + console.log(err); + resolve(); + } + pullConfigFiles = files; + resolve(); + }); + }); + + const workspaceInfoChunk: any = await vscode.workspace.fs.readFile( + vscode.Uri.file(rootFolder+"/"+pullConfigFiles[0]) + ); + return JSON.parse(String.fromCharCode(...workspaceInfoChunk)); +} + + +export const eventuallyOk = async ( + fn: () => Promise | T, + timeout = 10000, + wait = 500, + ): Promise => { + const deadline = Date.now() + timeout; + while (true) { + try { + return await fn(); + } catch (e) { + if (Date.now() + wait > deadline) { + throw e; + } + + await delay(wait); + } + } + }; + export const delay = (duration: number) => + isFinite(duration) + ? new Promise(resolve => setTimeout(resolve, duration)) + : new Promise(() => undefined); diff --git a/src/VSCodeExtension/src/utils/test-utils/events.ts b/src/VSCodeExtension/src/utils/test-utils/events.ts new file mode 100644 index 0000000000..d03dbf97ab --- /dev/null +++ b/src/VSCodeExtension/src/utils/test-utils/events.ts @@ -0,0 +1,139 @@ +/*--------------------------------------------------------- + * Copyright (C) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------*/ + +const unset = Symbol('unset'); + +export function once( + fn: (...args: Args) => T, + ): ((...args: Args) => T) & { value?: T; forget(): void } { + let value: T | typeof unset = unset; + const onced = (...args: Args) => { + if (value === unset) { + onced.value = value = fn(...args); + } + + return value; + }; + + onced.forget = () => { + value = unset; + onced.value = undefined; + }; + + onced.value = undefined as T | undefined; + + return onced; + } + +export interface IDisposable { + dispose(): void; +} + +export interface IEvent { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (listener: (e: T) => void, thisArg?: any, disposables?: IDisposable[]): IDisposable; +} + +type ListenerData = { + listener: (this: A, e: T) => void; + thisArg?: A; +}; + +export class EventEmitter implements IDisposable { + public event: IEvent; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _deliveryQueue?: { data: ListenerData; event: T }[]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _listeners = new Set>(); + + public get size() { + return this._listeners.size; + } + + constructor() { + this.event = ( + listener: (this: ThisArg, e: T) => void, + thisArg?: ThisArg, + disposables?: IDisposable[], + ) => { + const data: ListenerData = { listener, thisArg }; + this._listeners.add(data); + const result = { + dispose: () => { + result.dispose = () => { + /* no-op */ + }; + this._listeners.delete(data); + }, + }; + if (disposables) { + disposables.push(result); + } + return result; + }; + } + + fire(event: T): void { + const dispatch = !this._deliveryQueue; + if (!this._deliveryQueue) { + this._deliveryQueue = []; +} + for (const data of this._listeners) { + this._deliveryQueue.push({ data, event }); + } + if (!dispatch){ + return; + } + for (let index = 0; index < this._deliveryQueue.length; index++) { + const { data, event } = this._deliveryQueue[index]; + data.listener.call(data.thisArg, event); + } + this._deliveryQueue = undefined; + } + + dispose() { + this._listeners.clear(); + if (this._deliveryQueue){ + this._deliveryQueue = []; + } + } +} + +/** + * Map of listeners that deals with refcounting. + */ +export class ListenerMap { + private readonly map = new Map>(); + public readonly listeners: ReadonlyMap> = this.map; + + /** + * Adds a listener for the givne event. + */ + public listen(key: K, handler: (arg: V) => void): IDisposable { + let emitter = this.map.get(key); + if (!emitter) { + emitter = new EventEmitter(); + this.map.set(key, emitter); + } + + const listener = emitter.event(handler); + return { + dispose: once(() => { + listener.dispose(); + if (emitter?.size === 0) { + this.map.delete(key); + } + }), + }; + } + + /** + * Emits the event for the listener. + */ + public emit(event: K, value: V) { + this.listeners.get(event)?.fire(value); + } +} diff --git a/src/VSCodeExtension/src/utils/test-utils/jobData.ts b/src/VSCodeExtension/src/utils/test-utils/jobData.ts new file mode 100644 index 0000000000..057efec1bc --- /dev/null +++ b/src/VSCodeExtension/src/utils/test-utils/jobData.ts @@ -0,0 +1,84 @@ +export const jobParameters = { + csprojUrl: "/Users/owner/Desktop/sampleQuantumProj/proj1/ParallelQrng.csproj", + provider: "ionq", + target: "ionq.simulator", + name: "tester", + additionalArgs: "--n-qubits=2" +}; + +export const jobId_1 = "0d3a5a54-a09e-4428-a83e-f019cefc932a"; + +export const expectedResult_1 = { + "Histogram": { + "[0]": 0.5, + "[1]": 0.5 + } +}; + +export const mockLocalPanelJob = { + collapsibleState:0, + contextValue:'LocalSubmissionItem', + description:'tester', + fullId:'0d3a5a54-a09e-4428-a83e-f019cefc932a', + jobDetails:{ + jobId:'0d3a5a54-a09e-4428-a83e-f019cefc932a', + location:'eastus', + name:'tester', + programArguments:undefined, + provider:'ionq', + resourceGroup:'AzureQuantum', + submissionTime:'2022-08-20T20:45:43.055Z', + subscriptionId:'621181e5-3d0e-42c6-8287-d78d3c7f2629', + target:'ionq.simulator', + workspace:'monastest1' +}, +label:'2022-08-20, 20:45 | 0d3a5a54-a09e-4428-a83e-f019cefc932a', +tooltip:'Submitted from monastest1 to ionq.simulator', +}; + +export const expectedDetails_1 = { + "id": "0d3a5a54-a09e-4428-a83e-f019cefc932a", + "name": "tester", + "containerUri": "https://aqbcc64657985e447cbc096b.blob.core.windows.net/quantum-job-0d3a5a54-a09e-4428-a83e-f019cefc932a", + "inputDataFormat": "microsoft.ionq-ir.v3", + "inputParams": { + "shots": "500" + }, + "providerId": "ionq", + "target": "ionq.simulator", + "metadata": { + "entryPointInput": "{\"Qubits\":null}", + "outputMappingBlobUri": "https://aqbcc64657985e447cbc096b.blob.core.windows.net/quantum-job-0d3a5a54-a09e-4428-a83e-f019cefc932a/mappingData?sv=2019-02-02&sr=b&sig=s2udtP9a%2FcMvRWL%2ByRqjHepOK5pKxH7EEJEWG%2BiIABc%3D&se=2022-08-24T20%3A45%3A43Z&sp=rcw" + }, + "outputDataFormat": "microsoft.quantum-results.v1", + "status": "Succeeded", + "creationTime": "2022-08-20T20:45:42.943Z", + "beginExecutionTime": "2022-08-20T20:45:51.602Z", + "endExecutionTime": "2022-08-20T20:45:51.632Z", + "cancellationTime": null, + "errorData": null, + "costEstimate": { + "currencyCode": "USD", + "events": [ + { + "dimensionId": "gs1q", + "dimensionName": "1Q Gate Shot", + "measureUnit": "1q gate shot", + "amountBilled": 0, + "amountConsumed": 0, + "unitPrice": 0 + }, + { + "dimensionId": "gs2q", + "dimensionName": "2Q Gate Shot", + "measureUnit": "2q gate shot", + "amountBilled": 0, + "amountConsumed": 0, + "unitPrice": 0 + } + ], + "estimatedTotal": 0 + }, + "isCancelling": false, + "tags": [] +}; \ No newline at end of file diff --git a/src/VSCodeExtension/src/utils/test-utils/workspaces.ts b/src/VSCodeExtension/src/utils/test-utils/workspaces.ts new file mode 100644 index 0000000000..6d49aa0f09 --- /dev/null +++ b/src/VSCodeExtension/src/utils/test-utils/workspaces.ts @@ -0,0 +1,16 @@ +export const workspace_1= { + "subscriptionName":"Azure for Students", + "subscriptionId": "621181e5-3d0e-42c6-8287-d78d3c7f2629", + "resourceGroup": "AzureQuantum", + "workspace": "test2", + "location": "eastus" +}; +// would be better if the two testing workspaces were in different +// subscriptions +export const workspace_2= { + "subscriptionName":"Azure for Students", + "subscriptionId": "621181e5-3d0e-42c6-8287-d78d3c7f2629", + "resourceGroup": "AzureQuantum", + "workspace": "monastest1", + "location": "eastus" +};