diff --git a/build/test.ps1 b/build/test.ps1
index 88bf8583a3..b259698eca 100644
--- a/build/test.ps1
+++ b/build/test.ps1
@@ -41,7 +41,7 @@ function Test-Python {
Write-Host "##[info]Testing Python inside $testFolder"
Push-Location (Join-Path $PSScriptRoot $testFolder)
python --version
- pytest --log-level=DEBUG
+ pytest -v --log-level=DEBUG
Pop-Location
if ($LastExitCode -ne 0) {
diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs
index ee639ec3e8..9c63cf334a 100644
--- a/src/AzureClient/AzureClient.cs
+++ b/src/AzureClient/AzureClient.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -6,41 +6,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.IO;
+using System.Net;
using System.Threading.Tasks;
-using Microsoft.Azure.Quantum.Client;
-using Microsoft.Identity.Client;
-using Microsoft.Identity.Client.Extensions.Msal;
-using Microsoft.Jupyter.Core;
-using Microsoft.Quantum.Simulation.Core;
-using Microsoft.Rest.Azure;
-using Microsoft.Azure.Quantum.Client.Models;
-using Microsoft.Azure.Quantum.Storage;
using Microsoft.Azure.Quantum;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
+using Microsoft.Azure.Quantum.Client.Models;
+using Microsoft.Extensions.Logging;
+using Microsoft.Jupyter.Core;
+using Microsoft.Quantum.IQSharp.Common;
+using Microsoft.Quantum.Simulation.Common;
namespace Microsoft.Quantum.IQSharp.AzureClient
{
///
public class AzureClient : IAzureClient
{
+ internal IAzureWorkspace? ActiveWorkspace { get; private set; }
+ private ILogger Logger { get; }
+ private IReferences References { get; }
+ private IEntryPointGenerator EntryPointGenerator { get; }
private string ConnectionString { get; set; } = string.Empty;
private AzureExecutionTarget? ActiveTarget { get; set; }
- private AuthenticationResult? AuthenticationResult { get; set; }
- private IQuantumClient? QuantumClient { get; set; }
- private Azure.Quantum.IWorkspace? ActiveWorkspace { get; set; }
private string MostRecentJobId { get; set; } = string.Empty;
- private IPage? AvailableProviders { get; set; }
- private IEnumerable? AvailableTargets { get => AvailableProviders?.SelectMany(provider => provider.Targets); }
- private IEnumerable? ValidExecutionTargets { get => AvailableTargets?.Where(target => AzureExecutionTarget.IsValid(target.Id)); }
- private string ValidExecutionTargetsDisplayText
+ private IEnumerable? AvailableProviders { get; set; }
+ private IEnumerable? AvailableTargets => AvailableProviders?.SelectMany(provider => provider.Targets);
+ private IEnumerable? ValidExecutionTargets => AvailableTargets?.Where(target => AzureExecutionTarget.IsValid(target.Id));
+ private string ValidExecutionTargetsDisplayText =>
+ ValidExecutionTargets == null
+ ? "(no execution targets available)"
+ : string.Join(", ", ValidExecutionTargets.Select(target => target.Id));
+
+ public AzureClient(
+ IExecutionEngine engine,
+ IReferences references,
+ IEntryPointGenerator entryPointGenerator,
+ ILogger logger,
+ IEventService eventService)
{
- get => ValidExecutionTargets == null
- ? "(no execution targets available)"
- : string.Join(", ", ValidExecutionTargets.Select(target => target.Id));
- }
+ References = references;
+ EntryPointGenerator = entryPointGenerator;
+ Logger = logger;
+ eventService?.TriggerServiceInitialized(this);
+ if (engine is BaseEngine baseEngine)
+ {
+ baseEngine.RegisterDisplayEncoder(new CloudJobToHtmlEncoder());
+ baseEngine.RegisterDisplayEncoder(new CloudJobToTextEncoder());
+ baseEngine.RegisterDisplayEncoder(new TargetStatusToHtmlEncoder());
+ baseEngine.RegisterDisplayEncoder(new TargetStatusToTextEncoder());
+ baseEngine.RegisterDisplayEncoder(new HistogramToHtmlEncoder());
+ baseEngine.RegisterDisplayEncoder(new HistogramToTextEncoder());
+ baseEngine.RegisterDisplayEncoder(new AzureClientErrorToHtmlEncoder());
+ baseEngine.RegisterDisplayEncoder(new AzureClientErrorToTextEncoder());
+ }
+ }
///
public async Task ConnectAsync(
@@ -53,109 +71,43 @@ public async Task ConnectAsync(
{
ConnectionString = storageAccountConnectionString;
- var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV";
- var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName);
- var azureEnvironment = AzureEnvironment.Create(azureEnvironmentName, subscriptionId);
-
- var msalApp = PublicClientApplicationBuilder
- .Create(azureEnvironment.ClientId)
- .WithAuthority(azureEnvironment.Authority)
- .Build();
-
- // Register the token cache for serialization
- var cacheFileName = "aad.bin";
- var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE";
- var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName);
- if (string.IsNullOrEmpty(cacheDirectory))
- {
- cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum");
- }
-
- var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, azureEnvironment.ClientId).Build();
- var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties);
- cacheHelper.RegisterCache(msalApp.UserTokenCache);
-
- bool shouldShowLoginPrompt = refreshCredentials;
- if (!shouldShowLoginPrompt)
- {
- try
- {
- var accounts = await msalApp.GetAccountsAsync();
- AuthenticationResult = await msalApp.AcquireTokenSilent(
- azureEnvironment.Scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync();
- }
- catch (MsalUiRequiredException)
- {
- shouldShowLoginPrompt = true;
- }
- }
-
- if (shouldShowLoginPrompt)
+ var azureEnvironment = AzureEnvironment.Create(subscriptionId);
+ ActiveWorkspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials);
+ if (ActiveWorkspace == null)
{
- AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode(
- azureEnvironment.Scopes,
- deviceCodeResult =>
- {
- channel.Stdout(deviceCodeResult.Message);
- return Task.FromResult(0);
- }).WithAuthority(msalApp.Authority).ExecuteAsync();
+ return AzureClientError.AuthenticationFailed.ToExecutionResult();
}
- if (AuthenticationResult == null)
+ AvailableProviders = await ActiveWorkspace.GetProvidersAsync();
+ if (AvailableProviders == null)
{
- return AzureClientError.AuthenticationFailed.ToExecutionResult();
+ return AzureClientError.WorkspaceNotFound.ToExecutionResult();
}
- var credentials = new Rest.TokenCredentials(AuthenticationResult.AccessToken);
- QuantumClient = new QuantumClient(credentials)
- {
- SubscriptionId = subscriptionId,
- ResourceGroupName = resourceGroupName,
- WorkspaceName = workspaceName,
- BaseUri = azureEnvironment.BaseUri,
- };
- ActiveWorkspace = new Azure.Quantum.Workspace(
- QuantumClient.SubscriptionId,
- QuantumClient.ResourceGroupName,
- QuantumClient.WorkspaceName,
- AuthenticationResult?.AccessToken,
- azureEnvironment.BaseUri);
+ channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}.");
- try
+ if (ValidExecutionTargets.Count() == 0)
{
- AvailableProviders = await QuantumClient.Providers.GetStatusAsync();
+ channel.Stderr($"No valid Q# execution targets found in Azure Quantum workspace {ActiveWorkspace.Name}.");
}
- catch (Exception e)
- {
- channel.Stderr(e.ToString());
- return AzureClientError.WorkspaceNotFound.ToExecutionResult();
- }
-
- channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}.");
- // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly.
- return ValidExecutionTargets.ToJupyterTable().ToExecutionResult();
+ return ValidExecutionTargets.ToExecutionResult();
}
///
public async Task GetConnectionStatusAsync(IChannel channel)
{
- if (QuantumClient == null || AvailableProviders == null)
+ if (ActiveWorkspace == null || AvailableProviders == null)
{
return AzureClientError.NotConnected.ToExecutionResult();
}
- channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}.");
+ channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}.");
- // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly.
- return ValidExecutionTargets.ToJupyterTable().ToExecutionResult();
+ return ValidExecutionTargets.ToExecutionResult();
}
- private async Task SubmitOrExecuteJobAsync(
- IChannel channel,
- IOperationResolver operationResolver,
- string operationName,
- bool execute)
+ private async Task SubmitOrExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext, bool execute)
{
if (ActiveWorkspace == null)
{
@@ -169,64 +121,96 @@ private async Task SubmitOrExecuteJobAsync(
return AzureClientError.NoTarget.ToExecutionResult();
}
- if (string.IsNullOrEmpty(operationName))
+ if (string.IsNullOrEmpty(submissionContext.OperationName))
{
var commandName = execute ? "%azure.execute" : "%azure.submit";
channel.Stderr($"Please pass a valid Q# operation name to {commandName}.");
return AzureClientError.NoOperationName.ToExecutionResult();
}
- var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString);
+ var machine = ActiveWorkspace.CreateQuantumMachine(ActiveTarget.TargetId, ConnectionString);
if (machine == null)
{
// We should never get here, since ActiveTarget should have already been validated at the time it was set.
- channel.Stderr($"Unexpected error while preparing job for execution on target {ActiveTarget.TargetName}.");
+ channel.Stderr($"Unexpected error while preparing job for execution on target {ActiveTarget.TargetId}.");
return AzureClientError.InvalidTarget.ToExecutionResult();
}
- var operationInfo = operationResolver.Resolve(operationName);
- var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType);
- var entryPointInput = QVoid.Instance;
+ channel.Stdout($"Submitting {submissionContext.OperationName} to target {ActiveTarget.TargetId}...");
- if (execute)
+ IEntryPoint? entryPoint = null;
+ try
{
- channel.Stdout($"Executing {operationName} on target {ActiveTarget.TargetName}...");
- var output = await machine.ExecuteAsync(entryPointInfo, entryPointInput);
- MostRecentJobId = output.Job.Id;
-
- // TODO: Add encoder to visualize IEnumerable>
- return output.Histogram.ToExecutionResult();
+ entryPoint = EntryPointGenerator.Generate(submissionContext.OperationName, ActiveTarget.TargetId);
+ }
+ catch (UnsupportedOperationException e)
+ {
+ channel.Stderr($"{submissionContext.OperationName} is not a recognized Q# operation name.");
+ return AzureClientError.UnrecognizedOperationName.ToExecutionResult();
}
- else
+ catch (CompilationErrorsException e)
{
- channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}...");
- var job = await machine.SubmitAsync(entryPointInfo, entryPointInput);
- channel.Stdout($"Job {job.Id} submitted successfully.");
+ channel.Stderr($"The Q# operation {submissionContext.OperationName} could not be compiled as an entry point for job execution.");
+ foreach (var message in e.Errors) channel.Stderr(message);
+ return AzureClientError.InvalidEntryPoint.ToExecutionResult();
+ }
+ try
+ {
+ var job = await entryPoint.SubmitAsync(machine, submissionContext);
+ channel.Stdout($"Job successfully submitted for {submissionContext.Shots} shots.");
+ channel.Stdout($" Job name: {submissionContext.FriendlyName}");
+ channel.Stdout($" Job ID: {job.Id}");
MostRecentJobId = job.Id;
+ }
+ catch (ArgumentException e)
+ {
+ channel.Stderr($"Failed to parse all expected parameters for Q# operation {submissionContext.OperationName}.");
+ channel.Stderr(e.Message);
+ return AzureClientError.JobSubmissionFailed.ToExecutionResult();
+ }
+ catch (Exception e)
+ {
+ channel.Stderr($"Failed to submit Q# operation {submissionContext.OperationName} for execution.");
+ channel.Stderr(e.InnerException?.Message ?? e.Message);
+ return AzureClientError.JobSubmissionFailed.ToExecutionResult();
+ }
- // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here.
- return job.ToJupyterTable().ToExecutionResult();
+ if (!execute)
+ {
+ return await GetJobStatusAsync(channel, MostRecentJobId);
}
+
+ channel.Stdout($"Waiting up to {submissionContext.ExecutionTimeout} seconds for Azure Quantum job to complete...");
+
+ using (var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(submissionContext.ExecutionTimeout)))
+ {
+ CloudJob? cloudJob = null;
+ do
+ {
+ // TODO: Once jupyter-core supports interrupt requests (https://github.com/microsoft/jupyter-core/issues/55),
+ // handle Jupyter kernel interrupt here and break out of this loop
+ await Task.Delay(TimeSpan.FromSeconds(submissionContext.ExecutionPollingInterval));
+ if (cts.IsCancellationRequested) break;
+ cloudJob = await ActiveWorkspace.GetJobAsync(MostRecentJobId);
+ channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}");
+ }
+ while (cloudJob == null || cloudJob.InProgress);
+ }
+
+ return await GetJobResultAsync(channel, MostRecentJobId);
}
///
- public async Task SubmitJobAsync(
- IChannel channel,
- IOperationResolver operationResolver,
- string operationName) =>
- await SubmitOrExecuteJobAsync(channel, operationResolver, operationName, execute: false);
+ public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext) =>
+ await SubmitOrExecuteJobAsync(channel, submissionContext, execute: false);
///
- public async Task ExecuteJobAsync(
- IChannel channel,
- IOperationResolver operationResolver,
- string operationName) =>
- await SubmitOrExecuteJobAsync(channel, operationResolver, operationName, execute: true);
+ public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext) =>
+ await SubmitOrExecuteJobAsync(channel, submissionContext, execute: true);
///
- public async Task GetActiveTargetAsync(
- IChannel channel)
+ public async Task GetActiveTargetAsync(IChannel channel)
{
if (AvailableProviders == null)
{
@@ -236,21 +220,19 @@ public async Task GetActiveTargetAsync(
if (ActiveTarget == null)
{
- channel.Stderr("No execution target has been specified. To specify one, run:\n%azure.target ");
+ channel.Stderr("No execution target has been specified. To specify one, run:\n%azure.target ");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return AzureClientError.NoTarget.ToExecutionResult();
}
- channel.Stdout($"Current execution target: {ActiveTarget.TargetName}");
+ channel.Stdout($"Current execution target: {ActiveTarget.TargetId}");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
- return ActiveTarget.TargetName.ToExecutionResult();
+
+ return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult();
}
///
- public async Task SetActiveTargetAsync(
- IChannel channel,
- IReferences references,
- string targetName)
+ public async Task SetActiveTargetAsync(IChannel channel, string targetId)
{
if (AvailableProviders == null)
{
@@ -258,19 +240,19 @@ public async Task SetActiveTargetAsync(
return AzureClientError.NotConnected.ToExecutionResult();
}
- // Validate that this target name is valid in the workspace.
- if (!AvailableTargets.Any(target => targetName == target.Id))
+ // Validate that this target is valid in the workspace.
+ if (!AvailableTargets.Any(target => targetId == target.Id))
{
- channel.Stderr($"Target name {targetName} is not available in the current Azure Quantum workspace.");
+ channel.Stderr($"Target {targetId} is not available in the current Azure Quantum workspace.");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return AzureClientError.InvalidTarget.ToExecutionResult();
}
- // Validate that we know which package to load for this target name.
- var executionTarget = AzureExecutionTarget.Create(targetName);
+ // Validate that we know which package to load for this target.
+ var executionTarget = AzureExecutionTarget.Create(targetId);
if (executionTarget == null)
{
- channel.Stderr($"Target name {targetName} does not support executing Q# jobs.");
+ channel.Stderr($"Target {targetId} does not support executing Q# jobs.");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return AzureClientError.InvalidTarget.ToExecutionResult();
}
@@ -279,15 +261,15 @@ public async Task SetActiveTargetAsync(
ActiveTarget = executionTarget;
channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies...");
- await references.AddPackage(ActiveTarget.PackageName);
+ await References.AddPackage(ActiveTarget.PackageName);
- return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult();
+ channel.Stdout($"Active target is now {ActiveTarget.TargetId}");
+
+ return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult();
}
///
- public async Task GetJobResultAsync(
- IChannel channel,
- string jobId)
+ public async Task GetJobResultAsync(IChannel channel, string jobId)
{
if (ActiveWorkspace == null)
{
@@ -306,7 +288,7 @@ public async Task GetJobResultAsync(
jobId = MostRecentJobId;
}
- var job = ActiveWorkspace.GetJob(jobId);
+ var job = await ActiveWorkspace.GetJobAsync(jobId);
if (job == null)
{
channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace.");
@@ -319,25 +301,22 @@ public async Task GetJobResultAsync(
return AzureClientError.JobNotCompleted.ToExecutionResult();
}
- var stream = new MemoryStream();
- await new JobStorageHelper(ConnectionString).DownloadJobOutputAsync(jobId, stream);
- stream.Seek(0, SeekOrigin.Begin);
- var output = new StreamReader(stream).ReadToEnd();
- var deserializedOutput = JsonConvert.DeserializeObject>(output);
- var histogram = new Dictionary();
- foreach (var entry in deserializedOutput["histogram"] as JObject)
+ try
{
- histogram[entry.Key] = entry.Value.ToObject();
+ var request = WebRequest.Create(job.Details.OutputDataUri);
+ using var responseStream = request.GetResponse().GetResponseStream();
+ return responseStream.ToHistogram().ToExecutionResult();
+ }
+ catch (Exception e)
+ {
+ channel.Stderr($"Failed to retrieve results for job ID {jobId}.");
+ Logger?.LogError(e, $"Failed to download the job output for the specified Azure Quantum job: {e.Message}");
+ return AzureClientError.JobOutputDownloadFailed.ToExecutionResult();
}
-
- // TODO: Add encoder to visualize IEnumerable>
- return histogram.ToExecutionResult();
}
///
- public async Task GetJobStatusAsync(
- IChannel channel,
- string jobId)
+ public async Task GetJobStatusAsync(IChannel channel, string jobId)
{
if (ActiveWorkspace == null)
{
@@ -356,20 +335,18 @@ public async Task GetJobStatusAsync(
jobId = MostRecentJobId;
}
- var job = ActiveWorkspace.GetJob(jobId);
+ var job = await ActiveWorkspace.GetJobAsync(jobId);
if (job == null)
{
channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace.");
return AzureClientError.JobNotFound.ToExecutionResult();
}
- // TODO: Add encoder for CloudJob which calls ToJupyterTable() for display.
- return job.Details.ToExecutionResult();
+ return job.ToExecutionResult();
}
///
- public async Task GetJobListAsync(
- IChannel channel)
+ public async Task GetJobListAsync(IChannel channel)
{
if (ActiveWorkspace == null)
{
@@ -377,15 +354,13 @@ public async Task GetJobListAsync(
return AzureClientError.NotConnected.ToExecutionResult();
}
- var jobs = ActiveWorkspace.ListJobs();
- if (jobs == null || jobs.Count() == 0)
+ var jobs = await ActiveWorkspace.ListJobsAsync() ?? new List();
+ if (jobs.Count() == 0)
{
channel.Stderr("No jobs found in current Azure Quantum workspace.");
- return AzureClientError.JobNotFound.ToExecutionResult();
}
-
- // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly.
- return jobs.Select(job => job.Details).ToJupyterTable().ToExecutionResult();
+
+ return jobs.ToExecutionResult();
}
}
}
diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj
index b2fc4b4554..995ae372bb 100644
--- a/src/AzureClient/AzureClient.csproj
+++ b/src/AzureClient/AzureClient.csproj
@@ -13,7 +13,7 @@
-
+
diff --git a/src/AzureClient/AzureClientError.cs b/src/AzureClient/AzureClientError.cs
new file mode 100644
index 0000000000..a5ac286870
--- /dev/null
+++ b/src/AzureClient/AzureClientError.cs
@@ -0,0 +1,93 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System.ComponentModel;
+
+namespace Microsoft.Quantum.IQSharp.AzureClient
+{
+ ///
+ /// Describes possible error results from methods.
+ ///
+ public enum AzureClientError
+ {
+ ///
+ /// Method completed with an unknown error.
+ ///
+ [Description(Resources.AzureClientErrorUnknownError)]
+ UnknownError = 1000,
+
+ ///
+ /// No connection has been made to any Azure Quantum workspace.
+ ///
+ [Description(Resources.AzureClientErrorNotConnected)]
+ NotConnected,
+
+ ///
+ /// A target has not yet been configured for job submission.
+ ///
+ [Description(Resources.AzureClientErrorNoTarget)]
+ NoTarget,
+
+ ///
+ /// The specified target is not valid for job submission.
+ ///
+ [Description(Resources.AzureClientErrorInvalidTarget)]
+ InvalidTarget,
+
+ ///
+ /// A job meeting the specified criteria was not found.
+ ///
+ [Description(Resources.AzureClientErrorJobNotFound)]
+ JobNotFound,
+
+ ///
+ /// The result of a job was requested, but the job has not yet completed.
+ ///
+ [Description(Resources.AzureClientErrorJobNotCompleted)]
+ JobNotCompleted,
+
+ ///
+ /// The job output failed to be downloaded from the Azure storage location.
+ ///
+ [Description(Resources.AzureClientErrorJobOutputDownloadFailed)]
+ JobOutputDownloadFailed,
+
+ ///
+ /// No Q# operation name was provided where one was required.
+ ///
+ [Description(Resources.AzureClientErrorNoOperationName)]
+ NoOperationName,
+
+ ///
+ /// The specified Q# operation name is not recognized.
+ ///
+ [Description(Resources.AzureClientErrorUnrecognizedOperationName)]
+ UnrecognizedOperationName,
+
+ ///
+ /// The specified Q# operation cannot be used as an entry point.
+ ///
+ [Description(Resources.AzureClientErrorInvalidEntryPoint)]
+ InvalidEntryPoint,
+
+ ///
+ /// The Azure Quantum job submission failed.
+ ///
+ [Description(Resources.AzureClientErrorJobSubmissionFailed)]
+ JobSubmissionFailed,
+
+ ///
+ /// Authentication with the Azure service failed.
+ ///
+ [Description(Resources.AzureClientErrorAuthenticationFailed)]
+ AuthenticationFailed,
+
+ ///
+ /// A workspace meeting the specified criteria was not found.
+ ///
+ [Description(Resources.AzureClientErrorWorkspaceNotFound)]
+ WorkspaceNotFound,
+ }
+}
diff --git a/src/AzureClient/AzureEnvironment.cs b/src/AzureClient/AzureEnvironment.cs
index 20db5920ae..66dc1de62d 100644
--- a/src/AzureClient/AzureEnvironment.cs
+++ b/src/AzureClient/AzureEnvironment.cs
@@ -1,74 +1,170 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Net;
-using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Azure.Quantum.Client;
+using Microsoft.Identity.Client;
+using Microsoft.Identity.Client.Extensions.Msal;
+using Microsoft.Jupyter.Core;
namespace Microsoft.Quantum.IQSharp.AzureClient
{
- internal enum AzureEnvironmentType { Production, Canary, Dogfood };
+ internal enum AzureEnvironmentType { Production, Canary, Dogfood, Mock };
internal class AzureEnvironment
{
- public string ClientId { get; private set; } = string.Empty;
- public string Authority { get; private set; } = string.Empty;
- public List Scopes { get; private set; } = new List();
- public Uri? BaseUri { get; private set; }
+ public static string EnvironmentVariableName => "AZURE_QUANTUM_ENV";
+ public AzureEnvironmentType Type { get; private set; }
+
+ private string SubscriptionId { get; set; } = string.Empty;
+ private string ClientId { get; set; } = string.Empty;
+ private string Authority { get; set; } = string.Empty;
+ private List Scopes { get; set; } = new List();
+ private Uri? BaseUri { get; set; }
private AzureEnvironment()
{
}
- public static AzureEnvironment Create(string environment, string subscriptionId)
+ public static AzureEnvironment Create(string subscriptionId)
{
- if (Enum.TryParse(environment, true, out AzureEnvironmentType environmentType))
+ var azureEnvironmentName = System.Environment.GetEnvironmentVariable(EnvironmentVariableName);
+
+ if (Enum.TryParse(azureEnvironmentName, true, out AzureEnvironmentType environmentType))
{
switch (environmentType)
{
case AzureEnvironmentType.Production:
- return Production();
+ return Production(subscriptionId);
case AzureEnvironmentType.Canary:
- return Canary();
+ return Canary(subscriptionId);
case AzureEnvironmentType.Dogfood:
return Dogfood(subscriptionId);
+ case AzureEnvironmentType.Mock:
+ return Mock();
default:
throw new InvalidOperationException("Unexpected EnvironmentType value.");
}
}
- return Production();
+ return Production(subscriptionId);
}
- private static AzureEnvironment Production() =>
+ public async Task GetAuthenticatedWorkspaceAsync(IChannel channel, string resourceGroupName, string workspaceName, bool refreshCredentials)
+ {
+ if (Type == AzureEnvironmentType.Mock)
+ {
+ channel.Stdout("AZURE_QUANTUM_ENV set to Mock. Using mock Azure workspace rather than connecting to the real service.");
+ return new MockAzureWorkspace(workspaceName);
+ }
+
+ // Find the token cache folder
+ var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE";
+ var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName);
+ if (string.IsNullOrEmpty(cacheDirectory))
+ {
+ cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum");
+ }
+
+ // Register the token cache for serialization
+ var cacheFileName = "aad.bin";
+ var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, ClientId).Build();
+ var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties);
+ var msalApp = PublicClientApplicationBuilder.Create(ClientId).WithAuthority(Authority).Build();
+ cacheHelper.RegisterCache(msalApp.UserTokenCache);
+
+ // Perform the authentication
+ bool shouldShowLoginPrompt = refreshCredentials;
+ AuthenticationResult? authenticationResult = null;
+ if (!shouldShowLoginPrompt)
+ {
+ try
+ {
+ var accounts = await msalApp.GetAccountsAsync();
+ authenticationResult = await msalApp.AcquireTokenSilent(
+ Scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync();
+ }
+ catch (MsalUiRequiredException)
+ {
+ shouldShowLoginPrompt = true;
+ }
+ }
+
+ if (shouldShowLoginPrompt)
+ {
+ authenticationResult = await msalApp.AcquireTokenWithDeviceCode(
+ Scopes,
+ deviceCodeResult =>
+ {
+ channel.Stdout(deviceCodeResult.Message);
+ return Task.FromResult(0);
+ }).WithAuthority(msalApp.Authority).ExecuteAsync();
+ }
+
+ if (authenticationResult == null)
+ {
+ return null;
+ }
+
+ // Construct and return the AzureWorkspace object
+ var credentials = new Rest.TokenCredentials(authenticationResult.AccessToken);
+ var azureQuantumClient = new QuantumClient(credentials)
+ {
+ SubscriptionId = SubscriptionId,
+ ResourceGroupName = resourceGroupName,
+ WorkspaceName = workspaceName,
+ BaseUri = BaseUri,
+ };
+ var azureQuantumWorkspace = new Azure.Quantum.Workspace(
+ azureQuantumClient.SubscriptionId,
+ azureQuantumClient.ResourceGroupName,
+ azureQuantumClient.WorkspaceName,
+ authenticationResult?.AccessToken,
+ BaseUri);
+
+ return new AzureWorkspace(azureQuantumClient, azureQuantumWorkspace);
+ }
+
+ private static AzureEnvironment Production(string subscriptionId) =>
new AzureEnvironment()
{
+ Type = AzureEnvironmentType.Production,
ClientId = "84ba0947-6c53-4dd2-9ca9-b3694761521b", // QDK client ID
Authority = "https://login.microsoftonline.com/common",
Scopes = new List() { "https://quantum.microsoft.com/Jobs.ReadWrite" },
BaseUri = new Uri("https://app-jobscheduler-prod.azurewebsites.net/"),
+ SubscriptionId = subscriptionId,
};
private static AzureEnvironment Dogfood(string subscriptionId) =>
new AzureEnvironment()
{
+ Type = AzureEnvironmentType.Dogfood,
ClientId = "46a998aa-43d0-4281-9cbb-5709a507ac36", // QDK dogfood client ID
Authority = GetDogfoodAuthority(subscriptionId),
Scopes = new List() { "api://dogfood.azure-quantum/Jobs.ReadWrite" },
BaseUri = new Uri("https://app-jobscheduler-test.azurewebsites.net/"),
+ SubscriptionId = subscriptionId,
};
- private static AzureEnvironment Canary()
+ private static AzureEnvironment Canary(string subscriptionId)
{
- var canary = Production();
+ var canary = Production(subscriptionId);
+ canary.Type = AzureEnvironmentType.Canary;
canary.BaseUri = new Uri("https://app-jobs-canarysouthcentralus.azurewebsites.net/");
return canary;
}
+ private static AzureEnvironment Mock() =>
+ new AzureEnvironment() { Type = AzureEnvironmentType.Mock };
+
private static string GetDogfoodAuthority(string subscriptionId)
{
try
diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs
index 2f0d5c89d1..f2cebf24f0 100644
--- a/src/AzureClient/AzureExecutionTarget.cs
+++ b/src/AzureClient/AzureExecutionTarget.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -11,28 +11,28 @@ internal enum AzureProvider { IonQ, Honeywell, QCI }
internal class AzureExecutionTarget
{
- public string TargetName { get; private set; }
- public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetName)}";
+ public string TargetId { get; private set; } = string.Empty;
+ public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetId)}";
- public static bool IsValid(string targetName) => GetProvider(targetName) != null;
+ public static bool IsValid(string targetId) => GetProvider(targetId) != null;
- public static AzureExecutionTarget? Create(string targetName) =>
- IsValid(targetName)
- ? new AzureExecutionTarget() { TargetName = targetName }
+ public static AzureExecutionTarget? Create(string targetId) =>
+ IsValid(targetId)
+ ? new AzureExecutionTarget() { TargetId = targetId }
: null;
///
/// Gets the Azure Quantum provider corresponding to the given execution target.
///
- /// The Azure Quantum execution target name.
+ /// The Azure Quantum execution target ID.
/// The enum value representing the provider.
///
- /// Valid target names are structured as "provider.target".
+ /// Valid target IDs are structured as "provider.target".
/// For example, "ionq.simulator" or "honeywell.qpu".
///
- private static AzureProvider? GetProvider(string targetName)
+ private static AzureProvider? GetProvider(string targetId)
{
- var parts = targetName.Split('.', 2);
+ var parts = targetId.Split('.', 2);
if (Enum.TryParse(parts[0], true, out AzureProvider provider))
{
return provider;
diff --git a/src/AzureClient/AzureSubmissionContext.cs b/src/AzureClient/AzureSubmissionContext.cs
new file mode 100644
index 0000000000..f299d90694
--- /dev/null
+++ b/src/AzureClient/AzureSubmissionContext.cs
@@ -0,0 +1,92 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Quantum.IQSharp.Jupyter;
+using Microsoft.Quantum.Runtime;
+
+namespace Microsoft.Quantum.IQSharp.AzureClient
+{
+ ///
+ /// Represents the configuration settings for a job submission to Azure Quantum.
+ ///
+ public sealed class AzureSubmissionContext : IQuantumMachineSubmissionContext
+ {
+ private static readonly int DefaultShots = 500;
+ private static readonly int DefaultExecutionTimeoutInSeconds = 30;
+ private static readonly int DefaultExecutionPollingIntervalInSeconds = 5;
+
+ internal static readonly string ParameterNameOperationName = "__operationName__";
+ internal static readonly string ParameterNameJobName = "jobName";
+ internal static readonly string ParameterNameShots = "shots";
+ internal static readonly string ParameterNameTimeout = "timeout";
+ internal static readonly string ParameterNamePollingInterval = "poll";
+
+ ///
+ public string FriendlyName { get; set; } = string.Empty;
+
+ ///
+ public int Shots { get; set; } = DefaultShots;
+
+ ///
+ /// The Q# operation name to be executed as part of this job.
+ ///
+ public string OperationName { get; set; } = string.Empty;
+
+ ///
+ /// The input parameters to be provided to the specified Q# operation.
+ ///
+ public Dictionary InputParameters { get; set; } = new Dictionary();
+
+ ///
+ /// The execution timeout for the job, expressed in seconds.
+ ///
+ ///
+ /// This setting only applies to %azure.execute. It is ignored for %azure.submit.
+ /// The timeout determines how long the IQ# kernel will wait for the job to complete;
+ /// the Azure Quantum job itself will continue to execute until it is completed.
+ ///
+ public int ExecutionTimeout { get; set; } = DefaultExecutionTimeoutInSeconds;
+
+ ///
+ /// The polling interval, in seconds, to check for job status updates
+ /// while waiting for an Azure Quantum job to complete execution.
+ ///
+ ///
+ /// This setting only applies to %azure.execute. It is ignored for %azure.submit.
+ ///
+ public int ExecutionPollingInterval { get; set; } = DefaultExecutionPollingIntervalInSeconds;
+
+ ///
+ /// Parses the input from a magic command into an object
+ /// suitable for job submission via .
+ ///
+ public static AzureSubmissionContext Parse(string inputCommand)
+ {
+ var inputParameters = AbstractMagic.ParseInputParameters(inputCommand, firstParameterInferredName: ParameterNameOperationName);
+ var operationName = inputParameters.DecodeParameter(ParameterNameOperationName);
+ var jobName = inputParameters.DecodeParameter(ParameterNameJobName, defaultValue: operationName);
+ var shots = inputParameters.DecodeParameter(ParameterNameShots, defaultValue: DefaultShots);
+ var timeout = inputParameters.DecodeParameter(ParameterNameTimeout, defaultValue: DefaultExecutionTimeoutInSeconds);
+ var pollingInterval = inputParameters.DecodeParameter(ParameterNamePollingInterval, defaultValue: DefaultExecutionPollingIntervalInSeconds);
+
+ var decodedParameters = inputParameters.ToDictionary(
+ item => item.Key,
+ item => inputParameters.DecodeParameter(item.Key));
+
+ return new AzureSubmissionContext()
+ {
+ FriendlyName = jobName,
+ Shots = shots,
+ OperationName = operationName,
+ InputParameters = decodedParameters,
+ ExecutionTimeout = timeout,
+ ExecutionPollingInterval = pollingInterval,
+ };
+ }
+ }
+}
diff --git a/src/AzureClient/AzureWorkspace.cs b/src/AzureClient/AzureWorkspace.cs
new file mode 100644
index 0000000000..a5083c442b
--- /dev/null
+++ b/src/AzureClient/AzureWorkspace.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Microsoft.Azure.Quantum;
+using Microsoft.Azure.Quantum.Client;
+using Microsoft.Azure.Quantum.Client.Models;
+using Microsoft.Extensions.Logging;
+using Microsoft.Quantum.Runtime;
+
+namespace Microsoft.Quantum.IQSharp.AzureClient
+{
+ internal class AzureWorkspace : IAzureWorkspace
+ {
+ public string? Name => AzureQuantumClient?.WorkspaceName;
+
+ private Azure.Quantum.IWorkspace AzureQuantumWorkspace { get; set; }
+ private QuantumClient AzureQuantumClient { get; set; }
+ private ILogger Logger { get; } = new LoggerFactory().CreateLogger();
+
+ public AzureWorkspace(QuantumClient azureQuantumClient, Azure.Quantum.Workspace azureQuantumWorkspace)
+ {
+ AzureQuantumClient = azureQuantumClient;
+ AzureQuantumWorkspace = azureQuantumWorkspace;
+ }
+
+ public async Task?> GetProvidersAsync()
+ {
+ try
+ {
+ return await AzureQuantumClient.Providers.GetStatusAsync();
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, $"Failed to retrieve the providers list from the Azure Quantum workspace: {e.Message}");
+ }
+
+ return null;
+ }
+
+ public async Task GetJobAsync(string jobId)
+ {
+ try
+ {
+ return await AzureQuantumWorkspace.GetJobAsync(jobId);
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, $"Failed to retrieve the specified Azure Quantum job: {e.Message}");
+ }
+
+ return null;
+ }
+
+ public async Task?> ListJobsAsync()
+ {
+ try
+ {
+ return await AzureQuantumWorkspace.ListJobsAsync();
+ }
+ catch (Exception e)
+ {
+ Logger.LogError(e, $"Failed to retrieve the list of jobs from the Azure Quantum workspace: {e.Message}");
+ }
+
+ return null;
+ }
+
+ public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString)
+ {
+ return QuantumMachineFactory.CreateMachine(AzureQuantumWorkspace, targetId, storageAccountConnectionString);
+ }
+ }
+}
diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs
new file mode 100644
index 0000000000..17d10806ed
--- /dev/null
+++ b/src/AzureClient/EntryPoint/EntryPoint.cs
@@ -0,0 +1,91 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Quantum.Runtime;
+using Microsoft.Quantum.Simulation.Core;
+
+namespace Microsoft.Quantum.IQSharp.AzureClient
+{
+ ///
+ internal class EntryPoint : IEntryPoint
+ {
+ private object EntryPointInfo { get; }
+ private Type InputType { get; }
+ private Type OutputType { get; }
+ private OperationInfo OperationInfo { get; }
+
+ ///
+ /// Creates an object used to submit jobs to Azure Quantum.
+ ///
+ /// Must be an object with type
+ /// parameters specified by the types in the entryPointInputbeginWords argument.
+ /// Specifies the input parameter type for the
+ /// object provided as the entryPointInfo argument.
+ /// Specifies the output parameter type for the
+ /// object provided as the entryPointInfo argument.
+ /// Information about the Q# operation to be used as the entry point.
+ public EntryPoint(object entryPointInfo, Type inputType, Type outputType, OperationInfo operationInfo)
+ {
+ EntryPointInfo = entryPointInfo;
+ InputType = inputType;
+ OutputType = outputType;
+ OperationInfo = operationInfo;
+ }
+
+ ///
+ public Task SubmitAsync(IQuantumMachine machine, AzureSubmissionContext submissionContext)
+ {
+ var parameterTypes = new List();
+ var parameterValues = new List
diff --git a/src/Core/Loggers/QsharpLogger.cs b/src/Core/Loggers/QsharpLogger.cs
index 247227fc72..6afbb7ebb9 100644
--- a/src/Core/Loggers/QsharpLogger.cs
+++ b/src/Core/Loggers/QsharpLogger.cs
@@ -22,13 +22,12 @@ public class QSharpLogger : QsCompiler.Diagnostics.LogTracker
public List Logs { get; }
- public List ErrorCodesToIgnore { get; }
+ public List ErrorCodesToIgnore { get; } = new List();
- public QSharpLogger(ILogger logger, List errorCodesToIgnore = null)
+ public QSharpLogger(ILogger logger)
{
this.Logger = logger;
this.Logs = new List();
- this.ErrorCodesToIgnore = errorCodesToIgnore ?? new List();
}
public static LogLevel MapLevel(LSP.DiagnosticSeverity original)
diff --git a/src/Core/OperationInfo.cs b/src/Core/OperationInfo.cs
index caaeddbd5b..ebb9141f04 100644
--- a/src/Core/OperationInfo.cs
+++ b/src/Core/OperationInfo.cs
@@ -26,12 +26,14 @@ public class OperationInfo
{
private Lazy> _params;
private Lazy _roslynParams;
+ private Lazy _returnType;
internal OperationInfo(Type roslynType, CallableDeclarationHeader header)
{
this.Header = header ?? throw new ArgumentNullException(nameof(header));
RoslynType = roslynType;
_roslynParams = new Lazy(() => RoslynType?.GetMethod("Run").GetParameters().Skip(1).ToArray());
+ _returnType = new Lazy(() => RoslynType?.GetMethod("Run").ReturnType.GenericTypeArguments.Single());
_params = new Lazy>(() => RoslynParameters?.ToDictionary(p => p.Name, p => p.ParameterType.Name));
}
@@ -60,6 +62,12 @@ internal OperationInfo(Type roslynType, CallableDeclarationHeader header)
[JsonIgnore]
public ParameterInfo[] RoslynParameters => _roslynParams.Value;
+ ///
+ /// The return type for the underlying compiled .NET Type for this Q# operation
+ ///
+ [JsonIgnore]
+ public Type ReturnType => _returnType.Value;
+
public override string ToString() => FullName;
}
diff --git a/src/Core/Resolver/OperationResolver.cs b/src/Core/Resolver/OperationResolver.cs
index 8be04f2933..2bf52f5b16 100644
--- a/src/Core/Resolver/OperationResolver.cs
+++ b/src/Core/Resolver/OperationResolver.cs
@@ -71,11 +71,12 @@ private IEnumerable RelevantAssemblies()
/// Symbol names without a dot are resolved to the first symbol
/// whose base name matches the given name.
///
- public OperationInfo Resolve(string name)
+ public OperationInfo Resolve(string name) => ResolveFromAssemblies(name, RelevantAssemblies());
+
+ public static OperationInfo ResolveFromAssemblies(string name, IEnumerable assemblies)
{
var isQualified = name.Contains('.');
- var relevant = RelevantAssemblies();
- foreach (var operation in relevant.SelectMany(asm => asm.Operations))
+ foreach (var operation in assemblies.SelectMany(asm => asm.Operations))
{
if (name == (isQualified ? operation.FullName : operation.Header.QualifiedName.Name.Value))
{
diff --git a/src/Core/Snippets/ISnippets.cs b/src/Core/Snippets/ISnippets.cs
index f8393cb6b3..60e52fb2ea 100644
--- a/src/Core/Snippets/ISnippets.cs
+++ b/src/Core/Snippets/ISnippets.cs
@@ -55,6 +55,11 @@ public interface ISnippets
///
AssemblyInfo AssemblyInfo { get; }
+ ///
+ /// The list of currently available snippets.
+ ///
+ IEnumerable Items { get; set; }
+
///
/// Adds or updates a snippet of code. If successful, this updates the AssemblyInfo
/// with the new operations found in the Snippet and returns a new Snippet
diff --git a/src/Core/Snippets/Snippets.cs b/src/Core/Snippets/Snippets.cs
index bf3f0cf189..f6afa05204 100644
--- a/src/Core/Snippets/Snippets.cs
+++ b/src/Core/Snippets/Snippets.cs
@@ -104,7 +104,7 @@ private void OnWorkspaceReloaded(object sender, ReloadedEventArgs e)
///
/// The list of currently available snippets.
///
- internal IEnumerable Items { get; set; }
+ public IEnumerable Items { get; set; }
///
/// The list of Q# operations available across all snippets.
@@ -144,11 +144,7 @@ public Snippet Compile(string code)
if (string.IsNullOrWhiteSpace(code)) throw new ArgumentNullException(nameof(code));
var duration = Stopwatch.StartNew();
- var errorCodesToIgnore = new List()
- {
- QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary, // Ignore any @EntryPoint() attributes found in snippets.
- };
- var logger = new QSharpLogger(Logger, errorCodesToIgnore);
+ var logger = new QSharpLogger(Logger);
try
{
diff --git a/src/Core/Workspace/IWorkspace.cs b/src/Core/Workspace/IWorkspace.cs
index 4bc0e806ca..f950f1ca79 100644
--- a/src/Core/Workspace/IWorkspace.cs
+++ b/src/Core/Workspace/IWorkspace.cs
@@ -64,6 +64,11 @@ public interface IWorkspace
///
string Root { get; }
+ ///
+ /// Gets the source files to be built for this Workspace.
+ ///
+ public IEnumerable SourceFiles { get; }
+
///
/// The folder where the assembly is permanently saved for cache.
///
diff --git a/src/Jupyter/Extensions.cs b/src/Jupyter/Extensions.cs
index b016d9c328..7b2c1bb9bf 100644
--- a/src/Jupyter/Extensions.cs
+++ b/src/Jupyter/Extensions.cs
@@ -187,7 +187,7 @@ public static T DecodeParameter(this Dictionary parameters, s
{
return defaultValue;
}
- return (T)(JsonConvert.DeserializeObject(parameterValue)) ?? defaultValue;
+ return (T)System.Convert.ChangeType(JsonConvert.DeserializeObject(parameterValue), typeof(T)) ?? defaultValue;
}
}
}
diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs
index 1b0cd24c3a..09a3df31b8 100644
--- a/src/Jupyter/Magic/AbstractMagic.cs
+++ b/src/Jupyter/Magic/AbstractMagic.cs
@@ -93,7 +93,7 @@ public static Dictionary JsonToDict(string input) =>
/// Dictionary with the names and values of the parameters,
/// where the values of the Dictionary are JSON-serialized objects.
///
- public Dictionary ParseInputParameters(string input, string firstParameterInferredName = "")
+ public static Dictionary ParseInputParameters(string input, string firstParameterInferredName = "")
{
Dictionary inputParameters = new Dictionary();
diff --git a/src/Kernel/IQSharpEngine.cs b/src/Kernel/IQSharpEngine.cs
index 759b89114b..88122f5271 100644
--- a/src/Kernel/IQSharpEngine.cs
+++ b/src/Kernel/IQSharpEngine.cs
@@ -60,7 +60,11 @@ IMagicSymbolResolver magicSymbolResolver
RegisterDisplayEncoder(new DataTableToTextEncoder());
RegisterDisplayEncoder(new DisplayableExceptionToHtmlEncoder());
RegisterDisplayEncoder(new DisplayableExceptionToTextEncoder());
- RegisterJsonEncoder(JsonConverters.AllConverters);
+
+ RegisterJsonEncoder(
+ JsonConverters.AllConverters
+ .Concat(AzureClient.JsonConverters.AllConverters)
+ .ToArray());
RegisterSymbolResolver(this.SymbolsResolver);
RegisterSymbolResolver(this.MagicResolver);
diff --git a/src/Kernel/Magic/EstimateMagic.cs b/src/Kernel/Magic/EstimateMagic.cs
index 7e227003b7..54e58f81d0 100644
--- a/src/Kernel/Magic/EstimateMagic.cs
+++ b/src/Kernel/Magic/EstimateMagic.cs
@@ -19,6 +19,8 @@ namespace Microsoft.Quantum.IQSharp.Kernel
///
public class EstimateMagic : AbstractMagic
{
+ private const string ParameterNameOperationName = "__operationName__";
+
///
/// Given a symbol resolver that can be used to locate operations,
/// constructs a new magic command that performs resource estimation
@@ -52,15 +54,16 @@ public override ExecutionResult Run(string input, IChannel channel) =>
///
public async Task RunAsync(string input, IChannel channel)
{
- var (name, args) = ParseInput(input);
+ var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName);
+ var name = inputParameters.DecodeParameter(ParameterNameOperationName);
var symbol = SymbolResolver.Resolve(name) as IQSharpSymbol;
if (symbol == null) throw new InvalidOperationException($"Invalid operation name: {name}");
var qsim = new ResourcesEstimator().WithStackTraceDisplay(channel);
qsim.DisableLogToConsole();
- await symbol.Operation.RunAsync(qsim, args);
+ await symbol.Operation.RunAsync(qsim, inputParameters);
return qsim.Data.ToExecutionResult();
}
diff --git a/src/Kernel/Magic/Simulate.cs b/src/Kernel/Magic/Simulate.cs
index 2b94eb9b06..5e552541b8 100644
--- a/src/Kernel/Magic/Simulate.cs
+++ b/src/Kernel/Magic/Simulate.cs
@@ -18,8 +18,7 @@ namespace Microsoft.Quantum.IQSharp.Kernel
///
public class SimulateMagic : AbstractMagic
{
- private const string
- ParameterNameOperationName = "operationName";
+ private const string ParameterNameOperationName = "__operationName__";
///
/// Constructs a new magic command given a resolver used to find
diff --git a/src/Kernel/Magic/ToffoliMagic.cs b/src/Kernel/Magic/ToffoliMagic.cs
index bb46a50e8f..48160c2bef 100644
--- a/src/Kernel/Magic/ToffoliMagic.cs
+++ b/src/Kernel/Magic/ToffoliMagic.cs
@@ -16,6 +16,8 @@ namespace Microsoft.Quantum.IQSharp.Kernel
///
public class ToffoliMagic : AbstractMagic
{
+ private const string ParameterNameOperationName = "__operationName__";
+
///
/// Default constructor.
///
@@ -43,8 +45,9 @@ public override ExecutionResult Run(string input, IChannel channel) =>
///
public async Task RunAsync(string input, IChannel channel)
{
- var (name, args) = ParseInput(input);
+ var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName);
+ var name = inputParameters.DecodeParameter(ParameterNameOperationName);
var symbol = SymbolResolver.Resolve(name) as IQSharpSymbol;
if (symbol == null) throw new InvalidOperationException($"Invalid operation name: {name}");
@@ -52,7 +55,7 @@ public async Task RunAsync(string input, IChannel channel)
qsim.DisableLogToConsole();
qsim.OnLog += channel.Stdout;
- var value = await symbol.Operation.RunAsync(qsim, args);
+ var value = await symbol.Operation.RunAsync(qsim, inputParameters);
return value.ToExecutionResult();
}
diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py
index f69fb83a00..e9c9abe633 100644
--- a/src/Python/qsharp/azure.py
+++ b/src/Python/qsharp/azure.py
@@ -11,11 +11,7 @@
import qsharp
import json
-import typing
-from typing import List, Dict, Callable, Any
-
-from qsharp.serialization import map_tuples
-from typing import List, Tuple, Dict, Iterable
+from typing import List, Dict, Callable, Any, Union
from enum import Enum
## LOGGING ##
@@ -33,27 +29,92 @@
'status',
'output',
'jobs'
+ 'AzureTarget',
+ 'AzureJob',
+ 'AzureError'
]
+## CLASSES ##
+
+class AzureTarget(object):
+ """
+ Represents an instance of an Azure Quantum execution target for Q# job submission.
+ """
+ def __init__(self, data: Dict):
+ self.__dict__ = data
+ self.id = data["id"]
+ self.current_availability = data["current_availability"]
+ self.average_queue_time = data["average_queue_time"]
+
+ def __eq__(self, other):
+ if not isinstance(other, AzureTarget):
+ # don't attempt to compare against unrelated types
+ return NotImplemented
+ return self.__dict__ == other.__dict__
+
+class AzureJob(object):
+ """
+ Represents an instance of an Azure Quantum job.
+ """
+ def __init__(self, data: Dict):
+ self.__dict__ = data
+ self.id = data["id"]
+ self.name = data["name"]
+ self.status = data["status"]
+ self.provider = data["provider"]
+ self.target = data["target"]
+ self.creation_time = data["creation_time"]
+ self.begin_execution_time = data["begin_execution_time"]
+ self.end_execution_time = data["end_execution_time"]
+
+ def __eq__(self, other):
+ if not isinstance(other, AzureJob):
+ # don't attempt to compare against unrelated types
+ return NotImplemented
+ return self.__dict__ == other.__dict__
+
+class AzureError(object):
+ """
+ Contains error information resulting from an attempt to interact with Azure.
+ """
+ def __init__(self, data: Dict):
+ self.__dict__ = data
+ self.error_code = data["error_code"]
+ self.error_name = data["error_name"]
+ self.error_description = data["error_description"]
+
+ def __eq__(self, other):
+ if not isinstance(other, AzureError):
+ # don't attempt to compare against unrelated types
+ return NotImplemented
+ return self.__dict__ == other.__dict__
+
## FUNCTIONS ##
-def connect(**params) -> Any:
- return qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params)
+def connect(**params) -> Union[List[AzureTarget], AzureError]:
+ result = qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params)
+ return AzureError(result) if "error_code" in result else [AzureTarget(target) for target in result]
-def target(name : str = '', **params) -> Any:
- return qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params)
+def target(name : str = '', **params) -> Union[AzureTarget, AzureError]:
+ result = qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params)
+ return AzureError(result) if "error_code" in result else AzureTarget(result)
-def submit(op, **params) -> Any:
- return qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params)
+def submit(op, **params) -> Union[AzureJob, AzureError]:
+ result = qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params)
+ return AzureError(result) if "error_code" in result else AzureJob(result)
-def execute(op, **params) -> Any:
- return qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params)
+def execute(op, **params) -> Union[Dict, AzureError]:
+ result = qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params)
+ return AzureError(result) if "error_code" in result else result
-def status(jobId : str = '', **params) -> Any:
- return qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params)
+def status(jobId : str = '', **params) -> Union[AzureJob, AzureError]:
+ result = qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params)
+ return AzureError(result) if "error_code" in result else AzureJob(result)
-def output(jobId : str = '', **params) -> Any:
- return qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params)
+def output(jobId : str = '', **params) -> Union[Dict, AzureError]:
+ result = qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params)
+ return AzureError(result) if "error_code" in result else result
-def jobs(**params) -> Any:
- return qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params)
+def jobs(**params) -> Union[List[AzureJob], AzureError]:
+ result = qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params)
+ return AzureError(result) if "error_code" in result else [AzureJob(job) for job in result]
diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py
new file mode 100644
index 0000000000..4e195e4f88
--- /dev/null
+++ b/src/Python/qsharp/tests/test_azure.py
@@ -0,0 +1,102 @@
+#!/bin/env python
+# -*- coding: utf-8 -*-
+##
+# test_azure.py: Tests Azure Quantum functionality against a mock workspace.
+##
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+##
+
+## IMPORTS ##
+
+import importlib
+import os
+import pytest
+import qsharp
+from qsharp.azure import AzureError, AzureJob, AzureTarget
+import sys
+
+## SETUP ##
+
+@pytest.fixture(scope="session", autouse=True)
+def set_environment_variables():
+ # Need to restart the IQ# kernel after setting the environment variable
+ os.environ["AZURE_QUANTUM_ENV"] = "mock"
+ importlib.reload(qsharp)
+ if "qsharp.chemistry" in sys.modules:
+ importlib.reload(qsharp.chemistry)
+
+## TESTS ##
+
+def test_empty_workspace(monkeypatch):
+ """
+ Tests behavior of a mock workspace with no providers.
+ """
+ targets = qsharp.azure.connect(
+ storageAccountConnectionString="test",
+ subscriptionId="test",
+ resourceGroupName="test",
+ workspaceName="test"
+ )
+ assert targets == []
+
+ result = qsharp.azure.target("invalid.target")
+ assert isinstance(result, AzureError)
+
+ jobs = qsharp.azure.jobs()
+ assert jobs == []
+
+def test_workspace_with_providers():
+ """
+ Tests behavior of a mock workspace with mock providers.
+ """
+ result = qsharp.azure.target()
+ assert isinstance(result, AzureError)
+
+ targets = qsharp.azure.connect(
+ storageAccountConnectionString="test",
+ subscriptionId="test",
+ resourceGroupName="test",
+ workspaceName="WorkspaceNameWithMockProviders"
+ )
+ assert isinstance(targets, list)
+ assert len(targets) > 0
+
+ for target in targets:
+ active_target = qsharp.azure.target(target.id)
+ assert isinstance(active_target, AzureTarget)
+ assert active_target == target
+
+ # Submit a snippet operation without parameters
+ op = qsharp.compile("""
+ operation HelloQ() : Result
+ {
+ Message($"Hello from quantum world!");
+ return Zero;
+ }
+ """)
+
+ job = qsharp.azure.submit(op)
+ assert isinstance(job, AzureJob)
+
+ retrieved_job = qsharp.azure.status(job.id)
+ assert isinstance(retrieved_job, AzureJob)
+ assert job.id == retrieved_job.id
+
+ # Execute a workspace operation with parameters
+ op = qsharp.QSharpCallable("Microsoft.Quantum.SanityTests.HelloAgain", None)
+
+ result = qsharp.azure.execute(op) # missing parameters
+ assert isinstance(result, AzureError)
+
+ histogram = qsharp.azure.execute(op, count=3, name="test")
+ assert isinstance(histogram, dict)
+
+ retrieved_histogram = qsharp.azure.output()
+ assert isinstance(retrieved_histogram, dict)
+ assert histogram == retrieved_histogram
+
+ # Check that both submitted jobs exist in the workspace
+ jobs = qsharp.azure.jobs()
+ assert isinstance(jobs, list)
+ assert len(jobs) == 2
diff --git a/src/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs
new file mode 100644
index 0000000000..a4a121c994
--- /dev/null
+++ b/src/Tests/AzureClientEntryPointTests.cs
@@ -0,0 +1,135 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Quantum.IQSharp;
+using Microsoft.Quantum.IQSharp.AzureClient;
+using Microsoft.Quantum.IQSharp.Common;
+using Microsoft.Quantum.Runtime;
+using Microsoft.Quantum.Simulation.Common;
+using Microsoft.Quantum.Simulation.Core;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+namespace Tests.IQSharp
+{
+ [TestClass]
+ public class AzureClientEntryPointTests
+ {
+ private IEntryPointGenerator Init(string workspace, IEnumerable? codeSnippets = null)
+ {
+ var services = Startup.CreateServiceProvider(workspace);
+
+ if (codeSnippets != null)
+ {
+ var snippets = services.GetService();
+ snippets.Items = codeSnippets.Select(codeSnippet => new Snippet() { code = codeSnippet });
+ }
+
+ return services.GetService();
+ }
+
+ [TestMethod]
+ public async Task FromSnippet()
+ {
+ var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.HelloQ });
+ var entryPoint = entryPointGenerator.Generate("HelloQ", null);
+ Assert.IsNotNull(entryPoint);
+
+ var job = await entryPoint.SubmitAsync(
+ new MockQuantumMachine(),
+ new AzureSubmissionContext());
+ Assert.IsNotNull(job);
+ }
+
+ [TestMethod]
+ public async Task FromBrokenSnippet()
+ {
+ var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.TwoErrors });
+ Assert.ThrowsException(() =>
+ entryPointGenerator.Generate("TwoErrors", null));
+ }
+
+ [TestMethod]
+ public async Task FromWorkspace()
+ {
+ var entryPointGenerator = Init("Workspace");
+ var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null);
+ Assert.IsNotNull(entryPoint);
+
+ var job = await entryPoint.SubmitAsync(
+ new MockQuantumMachine(),
+ new AzureSubmissionContext() { InputParameters = new Dictionary() { ["count"] = "2", ["name"] = "test" } });
+ Assert.IsNotNull(job);
+ }
+
+ [TestMethod]
+ public async Task FromWorkspaceMissingArgument()
+ {
+ var entryPointGenerator = Init("Workspace");
+ var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null);
+ Assert.IsNotNull(entryPoint);
+
+ Assert.ThrowsException(() =>
+ entryPoint.SubmitAsync(
+ new MockQuantumMachine(),
+ new AzureSubmissionContext() { InputParameters = new Dictionary() { ["count"] = "2" } }));
+ }
+
+ [TestMethod]
+ public async Task FromWorkspaceIncorrectArgumentType()
+ {
+ var entryPointGenerator = Init("Workspace");
+ var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null);
+ Assert.IsNotNull(entryPoint);
+
+ Assert.ThrowsException(() =>
+ entryPoint.SubmitAsync(
+ new MockQuantumMachine(),
+ new AzureSubmissionContext() { InputParameters = new Dictionary() { ["count"] = "NaN", ["name"] = "test" } }));
+ }
+
+ [TestMethod]
+ public async Task FromBrokenWorkspace()
+ {
+ var entryPointGenerator = Init("Workspace.Broken");
+ Assert.ThrowsException(() =>
+ entryPointGenerator.Generate("Tests.qss.HelloAgain", null));
+ }
+
+ [TestMethod]
+ public async Task FromSnippetDependsOnWorkspace()
+ {
+ var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.DependsOnWorkspace });
+ var entryPoint = entryPointGenerator.Generate("DependsOnWorkspace", null);
+ Assert.IsNotNull(entryPoint);
+
+ var job = await entryPoint.SubmitAsync(
+ new MockQuantumMachine(),
+ new AzureSubmissionContext());
+ Assert.IsNotNull(job);
+ }
+
+ [TestMethod]
+ public async Task InvalidOperationName()
+ {
+ var entryPointGenerator = Init("Workspace");
+ Assert.ThrowsException(() =>
+ entryPointGenerator.Generate("InvalidOperationName", null));
+ }
+
+ [TestMethod]
+ public async Task InvalidEntryPointOperation()
+ {
+ var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.InvalidEntryPoint });
+ Assert.ThrowsException(() =>
+ entryPointGenerator.Generate("InvalidEntryPoint", null));
+ }
+ }
+}
diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs
index fbbc880253..b06833d336 100644
--- a/src/Tests/AzureClientMagicTests.cs
+++ b/src/Tests/AzureClientMagicTests.cs
@@ -32,7 +32,7 @@ public class AzureClientMagicTests
private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING";
private readonly string jobId = "TEST_JOB_ID";
private readonly string operationName = "TEST_OPERATION_NAME";
- private readonly string targetName = "TEST_TARGET_NAME";
+ private readonly string targetId = "TEST_TARGET_ID";
[TestMethod]
public void TestConnectMagic()
@@ -85,8 +85,7 @@ public void TestSubmitMagic()
{
// no arguments
var azureClient = new MockAzureClient();
- var operationResolver = new MockOperationResolver();
- var submitMagic = new SubmitMagic(operationResolver, azureClient);
+ var submitMagic = new SubmitMagic(azureClient);
submitMagic.Test(string.Empty);
Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob);
@@ -101,8 +100,7 @@ public void TestExecuteMagic()
{
// no arguments
var azureClient = new MockAzureClient();
- var operationResolver = new MockOperationResolver();
- var executeMagic = new ExecuteMagic(operationResolver, azureClient);
+ var executeMagic = new ExecuteMagic(azureClient);
executeMagic.Test(string.Empty);
Assert.AreEqual(azureClient.LastAction, AzureClientAction.ExecuteJob);
@@ -141,19 +139,15 @@ public void TestJobsMagic()
[TestMethod]
public void TestTargetMagic()
{
- var workspace = "Workspace";
- var services = Startup.CreateServiceProvider(workspace);
- var references = services.GetService();
-
// single argument - should set active target
var azureClient = new MockAzureClient();
- var targetMagic = new TargetMagic(azureClient, references);
- targetMagic.Test(targetName);
+ var targetMagic = new TargetMagic(azureClient);
+ targetMagic.Test(targetId);
Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget);
// no arguments - should print active target
azureClient = new MockAzureClient();
- targetMagic = new TargetMagic(azureClient, references);
+ targetMagic = new TargetMagic(azureClient);
targetMagic.Test(string.Empty);
Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetActiveTarget);
}
@@ -178,33 +172,33 @@ public class MockAzureClient : IAzureClient
internal AzureClientAction LastAction = AzureClientAction.None;
internal string ConnectionString = string.Empty;
internal bool RefreshCredentials = false;
- internal string ActiveTargetName = string.Empty;
+ internal string ActiveTargetId = string.Empty;
internal List SubmittedJobs = new List();
internal List ExecutedJobs = new List();
- public async Task SetActiveTargetAsync(IChannel channel, IReferences references, string targetName)
+ public async Task SetActiveTargetAsync(IChannel channel, string targetId)
{
LastAction = AzureClientAction.SetActiveTarget;
- ActiveTargetName = targetName;
+ ActiveTargetId = targetId;
return ExecuteStatus.Ok.ToExecutionResult();
}
public async Task GetActiveTargetAsync(IChannel channel)
{
LastAction = AzureClientAction.GetActiveTarget;
- return ActiveTargetName.ToExecutionResult();
+ return ActiveTargetId.ToExecutionResult();
}
- public async Task SubmitJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName)
+ public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext)
{
LastAction = AzureClientAction.SubmitJob;
- SubmittedJobs.Add(operationName);
+ SubmittedJobs.Add(submissionContext.OperationName);
return ExecuteStatus.Ok.ToExecutionResult();
}
- public async Task ExecuteJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName)
+ public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext)
{
LastAction = AzureClientAction.ExecuteJob;
- ExecutedJobs.Add(operationName);
+ ExecutedJobs.Add(submissionContext.OperationName);
return ExecuteStatus.Ok.ToExecutionResult();
}
diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs
index af75ade1b8..099c251268 100644
--- a/src/Tests/AzureClientTests.cs
+++ b/src/Tests/AzureClientTests.cs
@@ -3,74 +3,266 @@
#nullable enable
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Collections.Generic;
using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Azure.Quantum;
+using Microsoft.Azure.Quantum.Client.Models;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Jupyter.Core;
-using Microsoft.Quantum.IQSharp;
using Microsoft.Quantum.IQSharp.AzureClient;
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Tests.IQSharp
{
- public static class AzureClientTestExtensions
- {
- }
-
[TestClass]
public class AzureClientTests
{
- private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID";
- private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME";
- private readonly string workspaceName = "TEST_WORKSPACE_NAME";
- private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING";
- private readonly string jobId = "TEST_JOB_ID";
- private readonly string operationName = "TEST_OPERATION_NAME";
+ private string originalEnvironmentName = string.Empty;
+
+ [TestInitialize]
+ public void SetMockEnvironment()
+ {
+ originalEnvironmentName = Environment.GetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName) ?? string.Empty;
+ Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Mock.ToString());
+ }
+
+ [TestCleanup]
+ public void RestoreEnvironment()
+ {
+ Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, originalEnvironmentName);
+ }
+
+ private T ExpectSuccess(Task task)
+ {
+ var result = task.GetAwaiter().GetResult();
+ Assert.AreEqual(ExecuteStatus.Ok, result.Status);
+ Assert.IsInstanceOfType(result.Output, typeof(T));
+ return (T)result.Output;
+ }
+
+ private void ExpectError(AzureClientError expectedError, Task task)
+ {
+ var result = task.GetAwaiter().GetResult();
+ Assert.AreEqual(ExecuteStatus.Error, result.Status);
+ Assert.IsInstanceOfType(result.Output, typeof(AzureClientError));
+ Assert.AreEqual(expectedError, (AzureClientError)result.Output);
+ }
+
+ private Task ConnectToWorkspaceAsync(IAzureClient azureClient, string workspaceName = "TEST_WORKSPACE_NAME")
+ {
+ return azureClient.ConnectAsync(
+ new MockChannel(),
+ "TEST_SUBSCRIPTION_ID",
+ "TEST_RESOURCE_GROUP_NAME",
+ workspaceName,
+ "TEST_CONNECTION_STRING");
+ }
[TestMethod]
- public void TestTargets()
+ public void TestAzureEnvironment()
{
- var workspace = "Workspace";
- var services = Startup.CreateServiceProvider(workspace);
- var references = services.GetService();
- var azureClient = services.GetService();
+ // Production environment
+ Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Production.ToString());
+ var environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID");
+ Assert.AreEqual(AzureEnvironmentType.Production, environment.Type);
- // SetActiveTargetAsync with recognized target name, but not yet connected
- var result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "ionq.simulator").GetAwaiter().GetResult();
- Assert.IsTrue(result.Status == ExecuteStatus.Error);
+ // Dogfood environment cannot be created in test because it requires a service call
+ Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Dogfood.ToString());
+ Assert.ThrowsException(() => AzureEnvironment.Create("TEST_SUBSCRIPTION_ID"));
- // SetActiveTargetAsync with unrecognized target name
- result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "contoso.qpu").GetAwaiter().GetResult();
- Assert.IsTrue(result.Status == ExecuteStatus.Error);
+ // Canary environment
+ Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Canary.ToString());
+ environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID");
+ Assert.AreEqual(AzureEnvironmentType.Canary, environment.Type);
- // GetActiveTargetAsync, but not yet connected
- result = azureClient.GetActiveTargetAsync(new MockChannel()).GetAwaiter().GetResult();
- Assert.IsTrue(result.Status == ExecuteStatus.Error);
+ // Mock environment
+ Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Mock.ToString());
+ environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID");
+ Assert.AreEqual(AzureEnvironmentType.Mock, environment.Type);
}
[TestMethod]
public void TestAzureExecutionTarget()
{
- var targetName = "invalidname";
- var executionTarget = AzureExecutionTarget.Create(targetName);
+ var targetId = "invalidname";
+ var executionTarget = AzureExecutionTarget.Create(targetId);
Assert.IsNull(executionTarget);
- targetName = "ionq.targetname";
- executionTarget = AzureExecutionTarget.Create(targetName);
- Assert.IsNotNull(executionTarget);
- Assert.AreEqual(executionTarget.TargetName, targetName);
- Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.IonQ");
-
- targetName = "HonEYWEll.targetname";
- executionTarget = AzureExecutionTarget.Create(targetName);
- Assert.IsNotNull(executionTarget);
- Assert.AreEqual(executionTarget.TargetName, targetName);
- Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.Honeywell");
-
- targetName = "qci.target.name.qpu";
- executionTarget = AzureExecutionTarget.Create(targetName);
- Assert.IsNotNull(executionTarget);
- Assert.AreEqual(executionTarget.TargetName, targetName);
- Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.QCI");
+ targetId = "ionq.targetId";
+ executionTarget = AzureExecutionTarget.Create(targetId);
+ Assert.AreEqual(targetId, executionTarget?.TargetId);
+ Assert.AreEqual("Microsoft.Quantum.Providers.IonQ", executionTarget?.PackageName);
+
+ targetId = "HonEYWEll.targetId";
+ executionTarget = AzureExecutionTarget.Create(targetId);
+ Assert.AreEqual(targetId, executionTarget?.TargetId);
+ Assert.AreEqual("Microsoft.Quantum.Providers.Honeywell", executionTarget?.PackageName);
+
+ targetId = "qci.target.name.qpu";
+ executionTarget = AzureExecutionTarget.Create(targetId);
+ Assert.AreEqual(targetId, executionTarget?.TargetId);
+ Assert.AreEqual("Microsoft.Quantum.Providers.QCI", executionTarget?.PackageName);
+ }
+
+ [TestMethod]
+ public void TestJobStatus()
+ {
+ var services = Startup.CreateServiceProvider("Workspace");
+ var azureClient = (AzureClient)services.GetService();
+
+ // not connected
+ ExpectError(AzureClientError.NotConnected, azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_1"));
+
+ // connect
+ var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient));
+ Assert.IsFalse(targets.Any());
+
+ // set up the mock workspace
+ var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace;
+ Assert.IsNotNull(azureWorkspace);
+ azureWorkspace?.AddMockJobs("JOB_ID_1", "JOB_ID_2");
+
+ // valid job ID
+ var job = ExpectSuccess(azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_1"));
+ Assert.AreEqual("JOB_ID_1", job.Id);
+
+ // invalid job ID
+ ExpectError(AzureClientError.JobNotFound, azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_3"));
+
+ // jobs list
+ var jobs = ExpectSuccess>(azureClient.GetJobListAsync(new MockChannel()));
+ Assert.AreEqual(2, jobs.Count());
+ }
+
+ [TestMethod]
+ public void TestManualTargets()
+ {
+ var services = Startup.CreateServiceProvider("Workspace");
+ var azureClient = (AzureClient)services.GetService();
+
+ // SetActiveTargetAsync with recognized target ID, but not yet connected
+ ExpectError(AzureClientError.NotConnected, azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator"));
+
+ // GetActiveTargetAsync, but not yet connected
+ ExpectError(AzureClientError.NotConnected, azureClient.GetActiveTargetAsync(new MockChannel()));
+
+ // connect
+ var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient));
+ Assert.IsFalse(targets.Any());
+
+ // set up the mock workspace
+ var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace;
+ Assert.IsNotNull(azureWorkspace);
+ azureWorkspace?.AddMockTargets("ionq.simulator", "honeywell.qpu", "unrecognized.target");
+
+ // get connection status to verify list of targets
+ targets = ExpectSuccess>(azureClient.GetConnectionStatusAsync(new MockChannel()));
+ Assert.AreEqual(2, targets.Count()); // only 2 valid quantum execution targets
+
+ // GetActiveTargetAsync, but no active target set yet
+ ExpectError(AzureClientError.NoTarget, azureClient.GetActiveTargetAsync(new MockChannel()));
+
+ // SetActiveTargetAsync with target ID not valid for quantum execution
+ ExpectError(AzureClientError.InvalidTarget, azureClient.SetActiveTargetAsync(new MockChannel(), "unrecognized.target"));
+
+ // SetActiveTargetAsync with valid target ID
+ var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator"));
+ Assert.AreEqual("ionq.simulator", target.Id);
+
+ // GetActiveTargetAsync
+ target = ExpectSuccess(azureClient.GetActiveTargetAsync(new MockChannel()));
+ Assert.AreEqual("ionq.simulator", target.Id);
+ }
+
+ [TestMethod]
+ public void TestAllTargets()
+ {
+ var services = Startup.CreateServiceProvider("Workspace");
+ var azureClient = (AzureClient)services.GetService();
+
+ // connect to mock workspace with all providers
+ var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient, MockAzureWorkspace.NameWithMockProviders));
+ Assert.AreEqual(Enum.GetNames(typeof(AzureProvider)).Length, targets.Count());
+
+ // set each target, which will load the corresponding package
+ foreach (var target in targets)
+ {
+ var returnedTarget = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), target.Id));
+ Assert.AreEqual(target.Id, returnedTarget.Id);
+ }
+ }
+
+ [TestMethod]
+ public void TestJobSubmission()
+ {
+ var services = Startup.CreateServiceProvider("Workspace");
+ var azureClient = (AzureClient)services.GetService();
+ var submissionContext = new AzureSubmissionContext();
+
+ // not yet connected
+ ExpectError(AzureClientError.NotConnected, azureClient.SubmitJobAsync(new MockChannel(), submissionContext));
+
+ // connect
+ var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient));
+ Assert.IsFalse(targets.Any());
+
+ // no target yet
+ ExpectError(AzureClientError.NoTarget, azureClient.SubmitJobAsync(new MockChannel(), submissionContext));
+
+ // add a target
+ var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace;
+ Assert.IsNotNull(azureWorkspace);
+ azureWorkspace?.AddMockTargets("ionq.simulator");
+
+ // set the active target
+ var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator"));
+ Assert.AreEqual("ionq.simulator", target.Id);
+
+ // no operation name specified
+ ExpectError(AzureClientError.NoOperationName, azureClient.SubmitJobAsync(new MockChannel(), submissionContext));
+
+ // specify an operation name, but have missing parameters
+ submissionContext.OperationName = "Tests.qss.HelloAgain";
+ ExpectError(AzureClientError.JobSubmissionFailed, azureClient.SubmitJobAsync(new MockChannel(), submissionContext));
+
+ // specify input parameters and verify that the job was submitted
+ submissionContext.InputParameters = new Dictionary() { ["count"] = "3", ["name"] = "testing" };
+ var job = ExpectSuccess(azureClient.SubmitJobAsync(new MockChannel(), submissionContext));
+ var retrievedJob = ExpectSuccess(azureClient.GetJobStatusAsync(new MockChannel(), job.Id));
+ Assert.AreEqual(job.Id, retrievedJob.Id);
+ }
+
+ [TestMethod]
+ public void TestJobExecution()
+ {
+ var services = Startup.CreateServiceProvider("Workspace");
+ var azureClient = (AzureClient)services.GetService();
+
+ // connect
+ var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient));
+ Assert.IsFalse(targets.Any());
+
+ // add a target
+ var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace;
+ Assert.IsNotNull(azureWorkspace);
+ azureWorkspace?.AddMockTargets("ionq.simulator");
+
+ // set the active target
+ var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator"));
+ Assert.AreEqual("ionq.simulator", target.Id);
+
+ // execute the job and verify that the results are retrieved successfully
+ var submissionContext = new AzureSubmissionContext()
+ {
+ OperationName = "Tests.qss.HelloAgain",
+ InputParameters = new Dictionary() { ["count"] = "3", ["name"] = "testing" },
+ ExecutionTimeout = 5,
+ ExecutionPollingInterval = 1,
+ };
+ var histogram = ExpectSuccess(azureClient.ExecuteJobAsync(new MockChannel(), submissionContext));
+ Assert.IsNotNull(histogram);
}
}
}
diff --git a/src/Tests/SNIPPETS.cs b/src/Tests/SNIPPETS.cs
index 8aff3f3214..ef29cf6628 100644
--- a/src/Tests/SNIPPETS.cs
+++ b/src/Tests/SNIPPETS.cs
@@ -207,6 +207,15 @@ operation InvalidFunctor(q: Qubit) : Unit {
}
";
+ public static string InvalidEntryPoint =
+@"
+ /// # Summary
+ /// This script has an operation that is not valid to be marked as an entry point.
+ operation InvalidEntryPoint(q : Qubit) : Unit {
+ H(q);
+ }
+";
+
public static string Reverse =
@"
/// # Summary
diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json
index 3edf302854..f6975e9c6c 100644
--- a/src/Tool/appsettings.json
+++ b/src/Tool/appsettings.json
@@ -6,24 +6,24 @@
},
"AllowedHosts": "*",
"DefaultPackageVersions": [
- "Microsoft.Quantum.Compiler::0.11.2006.207",
+ "Microsoft.Quantum.Compiler::0.11.2006.403",
- "Microsoft.Quantum.CsharpGeneration::0.11.2006.207",
- "Microsoft.Quantum.Development.Kit::0.11.2006.207",
- "Microsoft.Quantum.Simulators::0.11.2006.207",
- "Microsoft.Quantum.Xunit::0.11.2006.207",
+ "Microsoft.Quantum.CsharpGeneration::0.11.2006.403",
+ "Microsoft.Quantum.Development.Kit::0.11.2006.403",
+ "Microsoft.Quantum.Simulators::0.11.2006.403",
+ "Microsoft.Quantum.Xunit::0.11.2006.403",
- "Microsoft.Quantum.Standard::0.11.2006.207",
- "Microsoft.Quantum.Chemistry::0.11.2006.207",
- "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.207",
- "Microsoft.Quantum.Numerics::0.11.2006.207",
+ "Microsoft.Quantum.Standard::0.11.2006.403",
+ "Microsoft.Quantum.Chemistry::0.11.2006.403",
+ "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.403",
+ "Microsoft.Quantum.Numerics::0.11.2006.403",
- "Microsoft.Quantum.Katas::0.11.2006.207",
+ "Microsoft.Quantum.Katas::0.11.2006.403",
- "Microsoft.Quantum.Research::0.11.2006.207",
+ "Microsoft.Quantum.Research::0.11.2006.403",
- "Microsoft.Quantum.Providers.IonQ::0.11.2006.207",
- "Microsoft.Quantum.Providers.Honeywell::0.11.2006.207",
- "Microsoft.Quantum.Providers.QCI::0.11.2006.207",
+ "Microsoft.Quantum.Providers.IonQ::0.11.2006.403",
+ "Microsoft.Quantum.Providers.Honeywell::0.11.2006.403",
+ "Microsoft.Quantum.Providers.QCI::0.11.2006.403",
]
}