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); } } }