diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs
index c03ad4a61f..ed059d99f9 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
@@ -20,12 +20,14 @@ 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 IMetadataController MetadataController { get; }
+ private bool IsPythonUserAgent => MetadataController?.UserAgent?.StartsWith("qsharp.py") ?? false;
private string ConnectionString { get; set; } = string.Empty;
private AzureExecutionTarget? ActiveTarget { get; set; }
- private IAzureWorkspace? ActiveWorkspace { get; set; }
private string MostRecentJobId { get; set; } = string.Empty;
private IEnumerable? AvailableProviders { get; set; }
private IEnumerable? AvailableTargets => AvailableProviders?.SelectMany(provider => provider.Targets);
@@ -39,11 +41,13 @@ public AzureClient(
IExecutionEngine engine,
IReferences references,
IEntryPointGenerator entryPointGenerator,
+ IMetadataController metadataController,
ILogger logger,
IEventService eventService)
{
References = references;
EntryPointGenerator = entryPointGenerator;
+ MetadataController = metadataController;
Logger = logger;
eventService?.TriggerServiceInitialized(this);
@@ -55,6 +59,8 @@ public AzureClient(
baseEngine.RegisterDisplayEncoder(new TargetStatusToTextEncoder());
baseEngine.RegisterDisplayEncoder(new HistogramToHtmlEncoder());
baseEngine.RegisterDisplayEncoder(new HistogramToTextEncoder());
+ baseEngine.RegisterDisplayEncoder(new AzureClientErrorToHtmlEncoder());
+ baseEngine.RegisterDisplayEncoder(new AzureClientErrorToTextEncoder());
}
}
@@ -83,6 +89,11 @@ public async Task ConnectAsync(IChannel channel,
channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}.");
+ if (ValidExecutionTargets.Count() == 0)
+ {
+ channel.Stderr($"No valid Q# execution targets found in Azure Quantum workspace {ActiveWorkspace.Name}.");
+ }
+
return ValidExecutionTargets.ToExecutionResult();
}
@@ -103,20 +114,19 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, Az
{
if (ActiveWorkspace == null)
{
- channel.Stderr("Please call %azure.connect before submitting a job.");
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before submitting a job.");
return AzureClientError.NotConnected.ToExecutionResult();
}
if (ActiveTarget == null)
{
- channel.Stderr("Please call %azure.target before submitting a job.");
+ channel.Stderr($"Please call {GetCommandDisplayName("target")} before submitting a job.");
return AzureClientError.NoTarget.ToExecutionResult();
}
if (string.IsNullOrEmpty(submissionContext.OperationName))
{
- var commandName = execute ? "%azure.execute" : "%azure.submit";
- channel.Stderr($"Please pass a valid Q# operation name to {commandName}.");
+ channel.Stderr($"Please pass a valid Q# operation name to {GetCommandDisplayName(execute ? "execute" : "submit")}.");
return AzureClientError.NoOperationName.ToExecutionResult();
}
@@ -206,20 +216,21 @@ public async Task GetActiveTargetAsync(IChannel channel)
{
if (AvailableProviders == null)
{
- channel.Stderr("Please call %azure.connect before getting the execution target.");
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting the execution target.");
return AzureClientError.NotConnected.ToExecutionResult();
}
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, call {GetCommandDisplayName("target")} with the target ID.");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return AzureClientError.NoTarget.ToExecutionResult();
}
channel.Stdout($"Current execution target: {ActiveTarget.TargetId}");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
- return ActiveTarget.TargetId.ToExecutionResult();
+
+ return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult();
}
///
@@ -227,7 +238,7 @@ public async Task SetActiveTargetAsync(IChannel channel, string
{
if (AvailableProviders == null)
{
- channel.Stderr("Please call %azure.connect before setting an execution target.");
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before setting an execution target.");
return AzureClientError.NotConnected.ToExecutionResult();
}
@@ -254,7 +265,9 @@ public async Task SetActiveTargetAsync(IChannel channel, string
channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies...");
await References.AddPackage(ActiveTarget.PackageName);
- return $"Active target is now {ActiveTarget.TargetId}".ToExecutionResult();
+ channel.Stdout($"Active target is now {ActiveTarget.TargetId}");
+
+ return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult();
}
///
@@ -262,7 +275,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo
{
if (ActiveWorkspace == null)
{
- channel.Stderr("Please call %azure.connect before getting job results.");
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job results.");
return AzureClientError.NotConnected.ToExecutionResult();
}
@@ -286,7 +299,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo
if (!job.Succeeded || string.IsNullOrEmpty(job.Details.OutputDataUri))
{
- channel.Stderr($"Job ID {jobId} has not completed. To check the status, use:\n %azure.status {jobId}");
+ channel.Stderr($"Job ID {jobId} has not completed. To check the status, call {GetCommandDisplayName("status")} with the job ID.");
return AzureClientError.JobNotCompleted.ToExecutionResult();
}
@@ -309,7 +322,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo
{
if (ActiveWorkspace == null)
{
- channel.Stderr("Please call %azure.connect before getting job status.");
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job status.");
return AzureClientError.NotConnected.ToExecutionResult();
}
@@ -339,18 +352,20 @@ public async Task GetJobListAsync(IChannel channel)
{
if (ActiveWorkspace == null)
{
- channel.Stderr("Please call %azure.connect before listing jobs.");
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before listing jobs.");
return AzureClientError.NotConnected.ToExecutionResult();
}
- var jobs = await ActiveWorkspace.ListJobsAsync();
- 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();
}
-
+
return jobs.ToExecutionResult();
}
+
+ private string GetCommandDisplayName(string commandName) =>
+ IsPythonUserAgent ? $"qsharp.azure.{commandName}()" : $"%azure.{commandName}";
}
}
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 729a56ef59..66dc1de62d 100644
--- a/src/AzureClient/AzureEnvironment.cs
+++ b/src/AzureClient/AzureEnvironment.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -16,10 +16,11 @@
namespace Microsoft.Quantum.IQSharp.AzureClient
{
- internal enum AzureEnvironmentType { Production, Canary, Dogfood };
+ internal enum AzureEnvironmentType { Production, Canary, Dogfood, Mock };
internal class AzureEnvironment
{
+ public static string EnvironmentVariableName => "AZURE_QUANTUM_ENV";
public AzureEnvironmentType Type { get; private set; }
private string SubscriptionId { get; set; } = string.Empty;
@@ -34,8 +35,7 @@ private AzureEnvironment()
public static AzureEnvironment Create(string subscriptionId)
{
- var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV";
- var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName);
+ var azureEnvironmentName = System.Environment.GetEnvironmentVariable(EnvironmentVariableName);
if (Enum.TryParse(azureEnvironmentName, true, out AzureEnvironmentType environmentType))
{
@@ -47,6 +47,8 @@ public static AzureEnvironment Create(string subscriptionId)
return Canary(subscriptionId);
case AzureEnvironmentType.Dogfood:
return Dogfood(subscriptionId);
+ case AzureEnvironmentType.Mock:
+ return Mock();
default:
throw new InvalidOperationException("Unexpected EnvironmentType value.");
}
@@ -57,6 +59,12 @@ public static AzureEnvironment Create(string subscriptionId)
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);
@@ -154,6 +162,9 @@ private static AzureEnvironment Canary(string subscriptionId)
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 d56064efa5..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,7 +11,7 @@ internal enum AzureProvider { IonQ, Honeywell, QCI }
internal class AzureExecutionTarget
{
- public string TargetId { get; private set; }
+ public string TargetId { get; private set; } = string.Empty;
public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetId)}";
public static bool IsValid(string targetId) => GetProvider(targetId) != null;
diff --git a/src/AzureClient/AzureSubmissionContext.cs b/src/AzureClient/AzureSubmissionContext.cs
index bfe023e9fc..f299d90694 100644
--- a/src/AzureClient/AzureSubmissionContext.cs
+++ b/src/AzureClient/AzureSubmissionContext.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/AzureWorkspace.cs b/src/AzureClient/AzureWorkspace.cs
index 54fb782abc..a5083c442b 100644
--- a/src/AzureClient/AzureWorkspace.cs
+++ b/src/AzureClient/AzureWorkspace.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs
index 1fda56014e..6e4cc96c56 100644
--- a/src/AzureClient/Extensions.cs
+++ b/src/AzureClient/Extensions.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -37,24 +37,9 @@ internal static ExecutionResult ToExecutionResult(this AzureClientError azureCli
new ExecutionResult
{
Status = ExecuteStatus.Error,
- Output = azureClientError.ToDescription()
+ Output = azureClientError,
};
- ///
- /// Returns the string value of the for the given
- /// enumeration value.
- ///
- ///
- ///
- internal static string ToDescription(this AzureClientError azureClientError)
- {
- var attributes = azureClientError
- .GetType()
- .GetField(azureClientError.ToString())
- .GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];
- return attributes?.Length > 0 ? attributes[0].Description : string.Empty;
- }
-
///
/// Encapsulates a given as the result of an execution.
///
diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs
index d6c67678ff..441abc7bff 100644
--- a/src/AzureClient/IAzureClient.cs
+++ b/src/AzureClient/IAzureClient.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -10,90 +10,6 @@
namespace Microsoft.Quantum.IQSharp.AzureClient
{
- ///
- /// Describes possible error results from methods.
- ///
- public enum AzureClientError
- {
- ///
- /// Method completed with an unknown error.
- ///
- [Description(Resources.AzureClientErrorUnknownError)]
- UnknownError,
-
- ///
- /// 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,
- }
-
///
/// This service is capable of connecting to Azure Quantum workspaces
/// and submitting jobs.
diff --git a/src/AzureClient/IAzureWorkspace.cs b/src/AzureClient/IAzureWorkspace.cs
index a08152b3b7..1c2c71b813 100644
--- a/src/AzureClient/IAzureWorkspace.cs
+++ b/src/AzureClient/IAzureWorkspace.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -13,11 +13,11 @@ namespace Microsoft.Quantum.IQSharp.AzureClient
{
internal interface IAzureWorkspace
{
- public string Name { get; }
+ public string? Name { get; }
- public Task> GetProvidersAsync();
- public Task GetJobAsync(string jobId);
- public Task> ListJobsAsync();
+ public Task?> GetProvidersAsync();
+ public Task GetJobAsync(string jobId);
+ public Task?> ListJobsAsync();
public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString);
}
}
\ No newline at end of file
diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs
index f7eac3b5ce..823554c48f 100644
--- a/src/AzureClient/Magic/AzureClientMagicBase.cs
+++ b/src/AzureClient/Magic/AzureClientMagicBase.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs
index 559c04e3e7..ffdceff925 100644
--- a/src/AzureClient/Magic/ConnectMagic.cs
+++ b/src/AzureClient/Magic/ConnectMagic.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs
index 6a3080d274..122882abe5 100644
--- a/src/AzureClient/Magic/ExecuteMagic.cs
+++ b/src/AzureClient/Magic/ExecuteMagic.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Magic/JobsMagic.cs b/src/AzureClient/Magic/JobsMagic.cs
index f23708d9ea..590e77b418 100644
--- a/src/AzureClient/Magic/JobsMagic.cs
+++ b/src/AzureClient/Magic/JobsMagic.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Magic/OutputMagic.cs b/src/AzureClient/Magic/OutputMagic.cs
index 17ca257a46..dd542d7329 100644
--- a/src/AzureClient/Magic/OutputMagic.cs
+++ b/src/AzureClient/Magic/OutputMagic.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs
index e7eaa56d6c..80a2a85733 100644
--- a/src/AzureClient/Magic/StatusMagic.cs
+++ b/src/AzureClient/Magic/StatusMagic.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs
index 516646382e..3c7bcfccdd 100644
--- a/src/AzureClient/Magic/SubmitMagic.cs
+++ b/src/AzureClient/Magic/SubmitMagic.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs
index 778f88ab74..d0c93be7e1 100644
--- a/src/AzureClient/Magic/TargetMagic.cs
+++ b/src/AzureClient/Magic/TargetMagic.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Mocks/MockAzureWorkspace.cs b/src/AzureClient/Mocks/MockAzureWorkspace.cs
new file mode 100644
index 0000000000..212d605714
--- /dev/null
+++ b/src/AzureClient/Mocks/MockAzureWorkspace.cs
@@ -0,0 +1,63 @@
+// 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.Azure.Quantum;
+using Microsoft.Azure.Quantum.Client.Models;
+using Microsoft.Quantum.Runtime;
+
+namespace Microsoft.Quantum.IQSharp.AzureClient
+{
+ internal class MockAzureWorkspace : IAzureWorkspace
+ {
+ public const string NameWithMockProviders = "WorkspaceNameWithMockProviders";
+
+ public string Name { get; private set; }
+
+ public List Providers { get; } = new List();
+
+ public List Jobs { get; } = new List();
+
+ public MockAzureWorkspace(string workspaceName)
+ {
+ Name = workspaceName;
+ if (Name == NameWithMockProviders)
+ {
+ // add a mock target for each provider: "ionq.mock", "honeywell.mock", etc.
+ AddMockTargets(
+ Enum.GetNames(typeof(AzureProvider))
+ .Select(provider => $"{provider.ToLowerInvariant()}.mock")
+ .ToArray());
+ }
+ }
+
+ public async Task GetJobAsync(string jobId) => Jobs.FirstOrDefault(job => job.Id == jobId);
+
+ public async Task?> GetProvidersAsync() => Providers;
+
+ public async Task?> ListJobsAsync() => Jobs;
+
+ public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString) => new MockQuantumMachine(this);
+
+ public void AddMockJobs(params string[] jobIds)
+ {
+ foreach (var jobId in jobIds)
+ {
+ var mockJob = new MockCloudJob();
+ mockJob.Details.Id = jobId;
+ Jobs.Add(mockJob);
+ }
+ }
+
+ public void AddMockTargets(params string[] targetIds)
+ {
+ var targets = targetIds.Select(id => new TargetStatus(id)).ToList();
+ Providers.Add(new ProviderStatus(null, null, targets));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AzureClient/Mocks/MockCloudJob.cs b/src/AzureClient/Mocks/MockCloudJob.cs
new file mode 100644
index 0000000000..73d6cc789d
--- /dev/null
+++ b/src/AzureClient/Mocks/MockCloudJob.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using Microsoft.Azure.Quantum;
+using Microsoft.Azure.Quantum.Client.Models;
+using System;
+using System.IO;
+
+namespace Microsoft.Quantum.IQSharp.AzureClient
+{
+ internal class MockCloudJob : CloudJob
+ {
+ public MockCloudJob()
+ : base(
+ new Azure.Quantum.Workspace("mockSubscriptionId", "mockResourceGroupName", "mockWorkspaceName"),
+ new JobDetails(
+ containerUri: null,
+ inputDataFormat: null,
+ providerId: null,
+ target: null,
+ id: Guid.NewGuid().ToString(),
+ status: "Succeeded",
+ outputDataUri: CreateMockOutputFileUri()
+ ))
+ {
+ }
+
+ private static string CreateMockOutputFileUri()
+ {
+ var tempFilePath = Path.GetTempFileName();
+ using var outputFile = new StreamWriter(tempFilePath);
+ outputFile.WriteLine(@"{'Histogram':['0',0.5,'1',0.5]}");
+ return new Uri(tempFilePath).AbsoluteUri;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/AzureClient/Mocks/MockQuantumMachine.cs b/src/AzureClient/Mocks/MockQuantumMachine.cs
new file mode 100644
index 0000000000..9e6c28411f
--- /dev/null
+++ b/src/AzureClient/Mocks/MockQuantumMachine.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Quantum.Runtime;
+using Microsoft.Quantum.Simulation.Core;
+
+namespace Microsoft.Quantum.IQSharp.AzureClient
+{
+ internal class MockQuantumMachine : IQuantumMachine
+ {
+ public string ProviderId => throw new NotImplementedException();
+
+ public string Target => throw new NotImplementedException();
+
+ private MockAzureWorkspace? Workspace { get; }
+
+ public MockQuantumMachine(MockAzureWorkspace? workspace = null) => Workspace = workspace;
+
+ public Task> ExecuteAsync(EntryPointInfo info, TInput input)
+ => ExecuteAsync(info, input, null as IQuantumMachineSubmissionContext);
+
+ public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext? submissionContext)
+ => ExecuteAsync(info, input, submissionContext, null as IQuantumMachine.ConfigureJob);
+
+ public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext? submissionContext, IQuantumMachine.ConfigureJob? configureJobCallback)
+ => ExecuteAsync(info, input, submissionContext, null, configureJobCallback);
+
+ public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext? executionContext)
+ => ExecuteAsync(info, input, executionContext, null);
+
+ public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext? executionContext, IQuantumMachine.ConfigureJob? configureJobCallback)
+ => ExecuteAsync(info, input, null, executionContext);
+
+ public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext? submissionContext, IQuantumMachineExecutionContext? executionContext)
+ => ExecuteAsync(info, input, submissionContext, executionContext, null);
+
+ public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext? submissionContext, IQuantumMachineExecutionContext? executionContext, IQuantumMachine.ConfigureJob? configureJobCallback)
+ => throw new NotImplementedException();
+
+ public Task SubmitAsync(EntryPointInfo info, TInput input)
+ => SubmitAsync(info, input, null);
+
+ public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext? submissionContext)
+ => SubmitAsync(info, input, submissionContext, null);
+
+ public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext? submissionContext, IQuantumMachine.ConfigureJob? configureJobCallback)
+ {
+ var job = new MockCloudJob();
+ Workspace?.AddMockJobs(job.Id);
+ return Task.FromResult(job as IQuantumMachineJob);
+ }
+
+ public (bool IsValid, string Message) Validate(EntryPointInfo info, TInput input)
+ => throw new NotImplementedException();
+ }
+}
\ No newline at end of file
diff --git a/src/AzureClient/Visualization/AzureClientErrorEncoders.cs b/src/AzureClient/Visualization/AzureClientErrorEncoders.cs
new file mode 100644
index 0000000000..4848d7a8a3
--- /dev/null
+++ b/src/AzureClient/Visualization/AzureClientErrorEncoders.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using Microsoft.Jupyter.Core;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.Quantum.IQSharp.AzureClient
+{
+ internal static class AzureClientErrorExtensions
+ {
+ ///
+ /// Returns the string value of the for the given
+ /// enumeration value.
+ ///
+ internal static string ToDescription(this AzureClientError error)
+ {
+ var attributes = error
+ .GetType()
+ .GetField(error.ToString())
+ .GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[];
+ return attributes?.Length > 0 ? attributes[0].Description : string.Empty;
+ }
+
+ ///
+ /// Returns a dictionary representing the properties of the .
+ ///
+ internal static Dictionary ToDictionary(this AzureClientError error) =>
+ new Dictionary()
+ {
+ ["error_code"] = System.Convert.ToInt32(error),
+ ["error_name"] = error.ToString(),
+ ["error_description"] = error.ToDescription(),
+ };
+ }
+
+ public class AzureClientErrorToHtmlEncoder : IResultEncoder
+ {
+ public string MimeType => MimeTypes.Html;
+
+ public EncodedData? Encode(object displayable) => (displayable as AzureClientError?)?.ToDescription().ToEncodedData();
+ }
+
+ public class AzureClientErrorToTextEncoder : IResultEncoder
+ {
+ public string MimeType => MimeTypes.PlainText;
+
+ public EncodedData? Encode(object displayable) => (displayable as AzureClientError?)?.ToDescription().ToEncodedData();
+ }
+
+ public class AzureClientErrorJsonConverter : JsonConverter
+ {
+ public override AzureClientError ReadJson(JsonReader reader, Type objectType, AzureClientError existingValue, bool hasExistingValue, JsonSerializer serializer) =>
+ throw new NotImplementedException();
+
+ public override void WriteJson(JsonWriter writer, AzureClientError value, JsonSerializer serializer) =>
+ JToken.FromObject(value.ToDictionary()).WriteTo(writer);
+ }
+}
diff --git a/src/AzureClient/Visualization/CloudJobEncoders.cs b/src/AzureClient/Visualization/CloudJobEncoders.cs
index 8e5f9b5610..4d8d9c8a3a 100644
--- a/src/AzureClient/Visualization/CloudJobEncoders.cs
+++ b/src/AzureClient/Visualization/CloudJobEncoders.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -22,8 +22,8 @@ internal static class CloudJobExtensions
? dateTime
: null as DateTime?;
- internal static Dictionary ToDictionary(this CloudJob cloudJob) =>
- new Dictionary()
+ internal static Dictionary ToDictionary(this CloudJob cloudJob) =>
+ new Dictionary()
{
// TODO: add cloudJob.Uri after https://github.com/microsoft/qsharp-runtime/issues/236 is fixed.
["id"] = cloudJob.Id,
@@ -31,9 +31,9 @@ internal static Dictionary ToDictionary(this CloudJob cloudJob)
["status"] = cloudJob.Status,
["provider"] = cloudJob.Details.ProviderId,
["target"] = cloudJob.Details.Target,
- ["creationTime"] = cloudJob.Details.CreationTime.ToDateTime()?.ToUniversalTime(),
- ["beginExecutionTime"] = cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToUniversalTime(),
- ["endExecutionTime"] = cloudJob.Details.EndExecutionTime.ToDateTime()?.ToUniversalTime(),
+ ["creation_time"] = cloudJob.Details.CreationTime.ToDateTime()?.ToUniversalTime(),
+ ["begin_execution_time"] = cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToUniversalTime(),
+ ["end_execution_time"] = cloudJob.Details.EndExecutionTime.ToDateTime()?.ToUniversalTime(),
};
internal static Table ToJupyterTable(this IEnumerable jobsList) =>
@@ -47,11 +47,11 @@ internal static Table ToJupyterTable(this IEnumerable jobsLi
("Job Status", cloudJob => cloudJob.Status),
("Provider", cloudJob => cloudJob.Details.ProviderId),
("Target", cloudJob => cloudJob.Details.Target),
- ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString()),
- ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString()),
- ("End Execution Time", cloudJob => cloudJob.Details.EndExecutionTime.ToDateTime()?.ToString()),
+ ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString() ?? string.Empty),
+ ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString() ?? string.Empty),
+ ("End Execution Time", cloudJob => cloudJob.Details.EndExecutionTime.ToDateTime()?.ToString() ?? string.Empty),
},
- Rows = jobsList.OrderByDescending(job => job.Details.CreationTime).ToList()
+ Rows = jobsList.OrderByDescending(job => job.Details.CreationTime).ToList(),
};
}
diff --git a/src/AzureClient/Visualization/HistogramEncoders.cs b/src/AzureClient/Visualization/HistogramEncoders.cs
index befbbf095b..41f3f0698b 100644
--- a/src/AzureClient/Visualization/HistogramEncoders.cs
+++ b/src/AzureClient/Visualization/HistogramEncoders.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
diff --git a/src/AzureClient/Visualization/JsonConverters.cs b/src/AzureClient/Visualization/JsonConverters.cs
index 24f26f2515..394370155a 100644
--- a/src/AzureClient/Visualization/JsonConverters.cs
+++ b/src/AzureClient/Visualization/JsonConverters.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -19,7 +19,8 @@ public static class JsonConverters
new CloudJobJsonConverter(),
new CloudJobListJsonConverter(),
new TargetStatusJsonConverter(),
- new TargetStatusListJsonConverter()
+ new TargetStatusListJsonConverter(),
+ new AzureClientErrorJsonConverter()
);
public static JsonConverter[] AllConverters => allConverters.ToArray();
diff --git a/src/AzureClient/Visualization/TargetStatusEncoders.cs b/src/AzureClient/Visualization/TargetStatusEncoders.cs
index 1de24e7bc0..6925163fcb 100644
--- a/src/AzureClient/Visualization/TargetStatusEncoders.cs
+++ b/src/AzureClient/Visualization/TargetStatusEncoders.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Microsoft Corporation. All rights reserved.
+// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
#nullable enable
@@ -20,8 +20,8 @@ internal static Dictionary ToDictionary(this TargetStatus target
new Dictionary()
{
["id"] = target.Id,
- ["currentAvailability"] = target.CurrentAvailability,
- ["averageQueueTime"] = target.AverageQueueTime,
+ ["current_availability"] = target.CurrentAvailability,
+ ["average_queue_time"] = target.AverageQueueTime,
};
internal static Table ToJupyterTable(this IEnumerable targets) =>
diff --git a/src/Kernel/Magic/ConfigMagic.cs b/src/Kernel/Magic/ConfigMagic.cs
index 6a020af780..586a8a6cbb 100644
--- a/src/Kernel/Magic/ConfigMagic.cs
+++ b/src/Kernel/Magic/ConfigMagic.cs
@@ -45,7 +45,7 @@ save those options to a JSON file in the current working
dump.basisStateLabelingConvention ""BigEndian""
dump.truncateSmallAmplitudes true
```
- ",
+ ".Dedent(),
@"
Configure the `DumpMachine` and `DumpRegister` callables
diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py
index f69fb83a00..afbe53c3fb 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,108 @@
'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 __repr__(self) -> str:
+ return self.__dict__.__repr__()
+
+ def __eq__(self, other) -> bool:
+ 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 __repr__(self) -> str:
+ return self.__dict__.__repr__()
+
+ def __eq__(self, other) -> bool:
+ if not isinstance(other, AzureJob):
+ # don't attempt to compare against unrelated types
+ return NotImplemented
+ return self.__dict__ == other.__dict__
+
+class AzureError(Exception):
+ """
+ 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 __repr__(self) -> str:
+ return self.__dict__.__repr__()
+
+ def __eq__(self, other) -> bool:
+ 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) -> List[AzureTarget]:
+ result = qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params)
+ if "error_code" in result: raise AzureError(result)
+ return [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) -> AzureTarget:
+ result = qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params)
+ if "error_code" in result: raise AzureError(result)
+ return 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) -> AzureJob:
+ result = qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params)
+ if "error_code" in result: raise AzureError(result)
+ return 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) -> Dict:
+ result = qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params)
+ if "error_code" in result: raise AzureError(result)
+ return 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) -> AzureJob:
+ result = qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params)
+ if "error_code" in result: raise AzureError(result)
+ return 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) -> Dict:
+ result = qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params)
+ if "error_code" in result: raise AzureError(result)
+ return result
-def jobs(**params) -> Any:
- return qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params)
+def jobs(**params) -> List[AzureJob]:
+ result = qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params)
+ if "error_code" in result: raise AzureError(result)
+ return [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..f626a759c6
--- /dev/null
+++ b/src/Python/qsharp/tests/test_azure.py
@@ -0,0 +1,109 @@
+#!/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():
+ """
+ Tests behavior of a mock workspace with no providers.
+ """
+ with pytest.raises(AzureError) as exception_info:
+ qsharp.azure.target()
+ assert exception_info.value.error_name == "NotConnected"
+
+ targets = qsharp.azure.connect(
+ storageAccountConnectionString="test",
+ subscriptionId="test",
+ resourceGroupName="test",
+ workspaceName="test"
+ )
+ assert targets == []
+
+ with pytest.raises(AzureError) as exception_info:
+ qsharp.azure.target("invalid.target")
+ assert exception_info.value.error_name == "InvalidTarget"
+
+ jobs = qsharp.azure.jobs()
+ assert jobs == []
+
+def test_workspace_with_providers():
+ """
+ Tests behavior of a mock workspace with mock providers.
+ """
+ targets = qsharp.azure.connect(
+ storageAccountConnectionString="test",
+ subscriptionId="test",
+ resourceGroupName="test",
+ workspaceName="WorkspaceNameWithMockProviders"
+ )
+ assert isinstance(targets, list)
+ assert len(targets) > 0
+
+ with pytest.raises(AzureError) as exception_info:
+ qsharp.azure.target()
+ assert exception_info.value.error_name == "NoTarget"
+
+ 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)
+
+ with pytest.raises(AzureError) as exception_info:
+ qsharp.azure.execute(op)
+ assert exception_info.value.error_name == "JobSubmissionFailed"
+
+ 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
index 48d0416c61..a4a121c994 100644
--- a/src/Tests/AzureClientEntryPointTests.cs
+++ b/src/Tests/AzureClientEntryPointTests.cs
@@ -132,63 +132,4 @@ public async Task InvalidEntryPointOperation()
entryPointGenerator.Generate("InvalidEntryPoint", null));
}
}
-
- public class MockQuantumMachine : IQuantumMachine
- {
- public string ProviderId => throw new NotImplementedException();
-
- public string Target => throw new NotImplementedException();
-
- public Task> ExecuteAsync(EntryPointInfo info, TInput input)
- => ExecuteAsync(info, input, null as IQuantumMachineSubmissionContext);
-
- public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext)
- => ExecuteAsync(info, input, submissionContext, null as IQuantumMachine.ConfigureJob);
-
- public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback)
- => ExecuteAsync(info, input, submissionContext, null, configureJobCallback);
-
- public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext)
- => ExecuteAsync(info, input, executionContext, null);
-
- public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback)
- => ExecuteAsync(info, input, null, executionContext);
-
- public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachineExecutionContext executionContext)
- => ExecuteAsync(info, input, submissionContext, executionContext, null);
-
- public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback)
- => throw new NotImplementedException();
-
- public Task SubmitAsync(EntryPointInfo info, TInput input)
- => SubmitAsync(info, input, null);
-
- public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext)
- => SubmitAsync(info, input, submissionContext, null);
-
- public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback)
- => Task.FromResult(new MockQuantumMachineJob() as IQuantumMachineJob);
-
- public (bool IsValid, string Message) Validate(EntryPointInfo info, TInput input)
- => throw new NotImplementedException();
- }
-
- public class MockQuantumMachineJob : IQuantumMachineJob
- {
- public bool Failed => throw new NotImplementedException();
-
- public string Id => throw new NotImplementedException();
-
- public bool InProgress => throw new NotImplementedException();
-
- public string Status => throw new NotImplementedException();
-
- public bool Succeeded => throw new NotImplementedException();
-
- public Uri Uri => throw new NotImplementedException();
-
- public Task CancelAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
-
- public Task RefreshAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException();
- }
}
diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs
index a115085104..099c251268 100644
--- a/src/Tests/AzureClientTests.cs
+++ b/src/Tests/AzureClientTests.cs
@@ -3,47 +3,84 @@
#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 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 ID, but not yet connected
- var result = azureClient.SetActiveTargetAsync(new MockChannel(), "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 ID
- result = azureClient.SetActiveTargetAsync(new MockChannel(), "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]
@@ -55,21 +92,177 @@ public void TestAzureExecutionTarget()
targetId = "ionq.targetId";
executionTarget = AzureExecutionTarget.Create(targetId);
- Assert.IsNotNull(executionTarget);
- Assert.AreEqual(executionTarget.TargetId, targetId);
- Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.IonQ");
+ Assert.AreEqual(targetId, executionTarget?.TargetId);
+ Assert.AreEqual("Microsoft.Quantum.Providers.IonQ", executionTarget?.PackageName);
targetId = "HonEYWEll.targetId";
executionTarget = AzureExecutionTarget.Create(targetId);
- Assert.IsNotNull(executionTarget);
- Assert.AreEqual(executionTarget.TargetId, targetId);
- Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.Honeywell");
+ Assert.AreEqual(targetId, executionTarget?.TargetId);
+ Assert.AreEqual("Microsoft.Quantum.Providers.Honeywell", executionTarget?.PackageName);
targetId = "qci.target.name.qpu";
executionTarget = AzureExecutionTarget.Create(targetId);
- Assert.IsNotNull(executionTarget);
- Assert.AreEqual(executionTarget.TargetId, targetId);
- Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.QCI");
+ 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);
}
}
}