From aff2d8557e1ed97a5f92b37991b715c7675cd916 Mon Sep 17 00:00:00 2001 From: Jack Monas Date: Mon, 22 Aug 2022 04:21:23 -0500 Subject: [PATCH 1/4] 1) change config file searcher to use glob so its compatible with mocha testing. 2) slight modification to getWorkSpaceFromUser. Eliminating call to getConfig as it would have already been called in the flow. Instead just pass the config file results if relevant. This allows prefilling of quickpick boxes in select cases. --- src/VSCodeExtension/src/configFileHelpers.ts | 32 +++++++++++++------ src/VSCodeExtension/src/extension.ts | 17 ++++++---- src/VSCodeExtension/src/quickPickWorkspace.ts | 25 ++++++--------- 3 files changed, 42 insertions(+), 32 deletions(-) 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..25b252e5ef 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; 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 { From b12cf545f055ac2c71f182ad982ec9e47e2629de Mon Sep 17 00:00:00 2001 From: Jack Monas Date: Mon, 22 Aug 2022 04:26:59 -0500 Subject: [PATCH 2/4] integration testing --- src/VSCodeExtension/.vscode/launch.json | 63 +++-- src/VSCodeExtension/src/extension.ts | 40 ++-- src/VSCodeExtension/src/test/runTest.ts | 24 ++ .../src/test/suite/extension.test.ts | 215 ++++++++++++++++++ src/VSCodeExtension/src/test/suite/index.ts | 38 ++++ .../src/test/suite/testHelpers.ts | 49 ++++ .../src/utils/test-utils/events.ts | 139 +++++++++++ .../src/utils/test-utils/jobData.ts | 84 +++++++ .../src/utils/test-utils/workspaces.ts | 16 ++ 9 files changed, 611 insertions(+), 57 deletions(-) create mode 100644 src/VSCodeExtension/src/test/runTest.ts create mode 100644 src/VSCodeExtension/src/test/suite/extension.test.ts create mode 100644 src/VSCodeExtension/src/test/suite/index.ts create mode 100644 src/VSCodeExtension/src/test/suite/testHelpers.ts create mode 100644 src/VSCodeExtension/src/utils/test-utils/events.ts create mode 100644 src/VSCodeExtension/src/utils/test-utils/jobData.ts create mode 100644 src/VSCodeExtension/src/utils/test-utils/workspaces.ts 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/src/extension.ts b/src/VSCodeExtension/src/extension.ts index 25b252e5ef..c9e802fe68 100644 --- a/src/VSCodeExtension/src/extension.ts +++ b/src/VSCodeExtension/src/extension.ts @@ -616,26 +616,26 @@ export async function activate(context: vscode.ExtensionContext) { let rootFolder = findRootFolder(); - // Start the language server client. - let languageServer = new LanguageServer(context, rootFolder); - await languageServer - .start() - .catch( - err => { - console.log(`[qsharp-lsp] Language server failed to start: ${err}`); - let reportFeedbackItem = "Report feedback..."; - vscode.window.showErrorMessage( - `Language server failed to start: ${err}`, - reportFeedbackItem - ).then( - item => { - vscode.env.openExternal(vscode.Uri.parse( - "https://github.com/microsoft/qsharp-compiler/issues/new?assignees=&labels=bug,Area-IDE&template=bug_report.md&title=" - )); - } - ); - } - ); + // // Start the language server client. + // let languageServer = new LanguageServer(context, rootFolder); + // await languageServer + // .start() + // .catch( + // err => { + // console.log(`[qsharp-lsp] Language server failed to start: ${err}`); + // let reportFeedbackItem = "Report feedback..."; + // vscode.window.showErrorMessage( + // `Language server failed to start: ${err}`, + // reportFeedbackItem + // ).then( + // item => { + // vscode.env.openExternal(vscode.Uri.parse( + // "https://github.com/microsoft/qsharp-compiler/issues/new?assignees=&labels=bug,Area-IDE&template=bug_report.md&title=" + // )); + // } + // ); + // } + // ); return context; 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" +}; From 46e243e8c61dea9ae1de3b8be0672b0ac16551ce Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Mon, 22 Aug 2022 16:33:59 -0700 Subject: [PATCH 3/4] Uncomment language server code --- src/VSCodeExtension/src/extension.ts | 41 ++++++++++++++-------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/src/VSCodeExtension/src/extension.ts b/src/VSCodeExtension/src/extension.ts index c9e802fe68..554ddb26df 100644 --- a/src/VSCodeExtension/src/extension.ts +++ b/src/VSCodeExtension/src/extension.ts @@ -616,29 +616,28 @@ export async function activate(context: vscode.ExtensionContext) { let rootFolder = findRootFolder(); - // // Start the language server client. - // let languageServer = new LanguageServer(context, rootFolder); - // await languageServer - // .start() - // .catch( - // err => { - // console.log(`[qsharp-lsp] Language server failed to start: ${err}`); - // let reportFeedbackItem = "Report feedback..."; - // vscode.window.showErrorMessage( - // `Language server failed to start: ${err}`, - // reportFeedbackItem - // ).then( - // item => { - // vscode.env.openExternal(vscode.Uri.parse( - // "https://github.com/microsoft/qsharp-compiler/issues/new?assignees=&labels=bug,Area-IDE&template=bug_report.md&title=" - // )); - // } - // ); - // } - // ); + // Start the language server client. + let languageServer = new LanguageServer(context, rootFolder); + await languageServer + .start() + .catch( + err => { + console.log(`[qsharp-lsp] Language server failed to start: ${err}`); + let reportFeedbackItem = "Report feedback..."; + vscode.window.showErrorMessage( + `Language server failed to start: ${err}`, + reportFeedbackItem + ).then( + item => { + vscode.env.openExternal(vscode.Uri.parse( + "https://github.com/microsoft/qsharp-compiler/issues/new?assignees=&labels=bug,Area-IDE&template=bug_report.md&title=" + )); + } + ); + } + ); return context; - } // this method is called when your extension is deactivated From 4700415d5cd03cd184f233234f1f1d90a1eef822 Mon Sep 17 00:00:00 2001 From: Mahmut Burak Senol Date: Mon, 22 Aug 2022 17:01:15 -0700 Subject: [PATCH 4/4] Adding missing packages --- src/VSCodeExtension/package.json.v.template | 5 +++++ 1 file changed, 5 insertions(+) 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"