diff --git a/src/Simulation/EntryPointDriver.Tests/Tests.fs b/src/Simulation/EntryPointDriver.Tests/Tests.fs index 5df89661f19..808b4b6b607 100644 --- a/src/Simulation/EntryPointDriver.Tests/Tests.fs +++ b/src/Simulation/EntryPointDriver.Tests/Tests.fs @@ -149,20 +149,20 @@ let private normalize s = Regex.Replace(s, @"\s+", " ").Trim() /// output. The standard error and out streams of the actual output are concatenated in that order. let private yields expected (assembly, args) = let out, error, exitCode = run assembly args - Assert.True (0 = exitCode, sprintf "Expected exit code 0, but got %d with:\n\n%s\n\n%s" exitCode out error) + Assert.True (0 = exitCode, sprintf "Expected exit code 0, but got %d with:\n\n%s\n\n%s" exitCode error out) Assert.Equal (normalize expected, normalize (error + out)) /// Asserts that running the entry point in the assembly with the given arguments fails. let private fails (assembly, args) = let out, error, exitCode = run assembly args - Assert.True (0 <> exitCode, sprintf "Expected non-zero exit code, but got 0 with:\n\n%s\n\n%s" out error) + Assert.True (0 <> exitCode, sprintf "Expected non-zero exit code, but got 0 with:\n\n%s\n\n%s" error out) -/// Asserts that running the entry point in the assembly with the given arguments fails and the error message starts -/// with the expected message. +/// Asserts that running the entry point in the assembly with the given arguments fails and the output starts with the +/// expected message. The standard error and out streams of the actual output are concatenated in that order. let private failsWith expected (assembly, args) = let out, error, exitCode = run assembly args - Assert.True (0 <> exitCode, sprintf "Expected non-zero exit code, but got 0 with:\n\n%s\n\n%s" out error) - Assert.StartsWith (normalize expected, normalize error) + Assert.True (0 <> exitCode, sprintf "Expected non-zero exit code, but got 0 with:\n\n%s\n\n%s" error out) + Assert.StartsWith (normalize expected, normalize (error + out)) /// A tuple of the test assembly and arguments using the standard default simulator. The tuple can be passed to yields /// or fails. @@ -176,6 +176,24 @@ let private testWith testNum defaultSimulator = let assembly = testAssembly testNum (Some defaultSimulator) fun args -> assembly, Array.ofList args +/// Standard command-line arguments for the "submit" command without specifying a target. +let private submitWithoutTarget = + [ "submit" + "--storage" + "myStorage" + "--subscription" + "mySubscription" + "--resource-group" + "myResourceGroup" + "--workspace" + "myWorkspace" ] + +/// Standard command-line arguments for the "submit" command using the "test.nothing" target. +let private submitWithNothingTarget = submitWithoutTarget @ ["--target"; "test.nothing"] + +/// Standard command-line arguments for the "submit" command using the "test.error" target. +let private submitWithErrorTarget = submitWithoutTarget @ ["--target"; "test.error"] + // No Option @@ -437,6 +455,22 @@ let ``Shadows --version`` () = let given = test 29 given ["--version"; "foo"] |> yields "foo" +[] +let ``Shadows --target`` () = + let given = test 30 + given ["--target"; "foo"] |> yields "foo" + given submitWithNothingTarget + |> failsWith "The required option --target conflicts with an entry point parameter name." + +[] +let ``Shadows --shots`` () = + let given = test 31 + given ["--shots"; "7"] |> yields "7" + given (submitWithNothingTarget @ ["--shots"; "7"]) + |> yields "Warning: Option --shots is overridden by an entry point parameter name. Using default value 500. + The friendly URI for viewing job results is not available yet. Showing the job ID instead. + 00000000-0000-0000-0000-0000000000000" + // Simulators @@ -454,30 +488,30 @@ let private resourceSummary = [] let ``Supports QuantumSimulator`` () = - let given = test 30 + let given = test 32 given ["--simulator"; AssemblyConstants.QuantumSimulator; "--use-h"; "false"] |> yields "Hello, World!" given ["--simulator"; AssemblyConstants.QuantumSimulator; "--use-h"; "true"] |> yields "Hello, World!" [] let ``Supports ToffoliSimulator`` () = - let given = test 30 + let given = test 32 given ["--simulator"; AssemblyConstants.ToffoliSimulator; "--use-h"; "false"] |> yields "Hello, World!" given ["--simulator"; AssemblyConstants.ToffoliSimulator; "--use-h"; "true"] |> fails [] let ``Supports ResourcesEstimator`` () = - let given = test 30 + let given = test 32 given ["--simulator"; AssemblyConstants.ResourcesEstimator; "--use-h"; "false"] |> yields resourceSummary given ["--simulator"; AssemblyConstants.ResourcesEstimator; "--use-h"; "true"] |> yields resourceSummary [] let ``Rejects unknown simulator`` () = - let given = test 30 + let given = test 32 given ["--simulator"; "FooSimulator"; "--use-h"; "false"] |> fails [] let ``Supports default standard simulator`` () = - let given = testWith 30 AssemblyConstants.ResourcesEstimator + let given = testWith 32 AssemblyConstants.ResourcesEstimator given ["--use-h"; "false"] |> yields resourceSummary given ["--simulator"; AssemblyConstants.QuantumSimulator; "--use-h"; "false"] |> yields "Hello, World!" @@ -485,7 +519,7 @@ let ``Supports default standard simulator`` () = let ``Supports default custom simulator`` () = // This is not really a "custom" simulator, but the driver does not recognize the fully-qualified name of the // standard simulators, so it is treated as one. - let given = testWith 30 typeof.FullName + let given = testWith 32 typeof.FullName given ["--use-h"; "false"] |> yields "Hello, World!" given ["--use-h"; "true"] |> fails given ["--simulator"; typeof.FullName; "--use-h"; "false"] |> yields "Hello, World!" @@ -498,39 +532,24 @@ let ``Supports default custom simulator`` () = // Azure Quantum Submission -/// Standard command-line arguments for the "submit" command without specifying a target. -let private submitWithoutTarget = - [ "submit" - "--storage" - "myStorage" - "--subscription" - "mySubscription" - "--resource-group" - "myResourceGroup" - "--workspace" - "myWorkspace" ] - -/// Standard command-line arguments for the "submit" command using the "nothing" target. -let private submitWithTestTarget = submitWithoutTarget @ ["--target"; "nothing"] - [] let ``Submit can submit a job`` () = let given = test 1 - given submitWithTestTarget + given submitWithNothingTarget |> yields "The friendly URI for viewing job results is not available yet. Showing the job ID instead. 00000000-0000-0000-0000-0000000000000" [] let ``Submit can show only the ID`` () = let given = test 1 - given (submitWithTestTarget @ ["--output"; "id"]) |> yields "00000000-0000-0000-0000-0000000000000" + given (submitWithNothingTarget @ ["--output"; "id"]) |> yields "00000000-0000-0000-0000-0000000000000" [] let ``Submit uses default values`` () = let given = test 1 - given (submitWithTestTarget @ ["--verbose"]) + given (submitWithNothingTarget @ ["--verbose"]) |> yields "The friendly URI for viewing job results is not available yet. Showing the job ID instead. - Target: nothing + Target: test.nothing Storage: myStorage Subscription: mySubscription Resource Group: myResourceGroup @@ -548,7 +567,7 @@ let ``Submit uses default values`` () = [] let ``Submit allows overriding default values`` () = let given = test 1 - given (submitWithTestTarget @ [ + given (submitWithNothingTarget @ [ "--verbose" "--aad-token" "myToken" @@ -560,7 +579,7 @@ let ``Submit allows overriding default values`` () = "750" ]) |> yields "The friendly URI for viewing job results is not available yet. Showing the job ID instead. - Target: nothing + Target: test.nothing Storage: myStorage Subscription: mySubscription Resource Group: myResourceGroup @@ -578,17 +597,55 @@ let ``Submit allows overriding default values`` () = [] let ``Submit requires a positive number of shots`` () = let given = test 1 - given (submitWithTestTarget @ ["--shots"; "1"]) + given (submitWithNothingTarget @ ["--shots"; "1"]) |> yields "The friendly URI for viewing job results is not available yet. Showing the job ID instead. 00000000-0000-0000-0000-0000000000000" - given (submitWithTestTarget @ ["--shots"; "0"]) |> fails - given (submitWithTestTarget @ ["--shots"; "-1"]) |> fails + given (submitWithNothingTarget @ ["--shots"; "0"]) |> fails + given (submitWithNothingTarget @ ["--shots"; "-1"]) |> fails [] let ``Submit fails with unknown target`` () = let given = test 1 given (submitWithoutTarget @ ["--target"; "foo"]) |> failsWith "The target 'foo' was not recognized." +[] +let ``Submit supports dry run option`` () = + let given = test 1 + given (submitWithNothingTarget @ ["--dry-run"]) |> yields "✔️ The program is valid!" + given (submitWithErrorTarget @ ["--dry-run"]) + |> failsWith "❌ The program is invalid. + + This quantum machine always has an error." + +[] +let ``Submit has required options`` () = + // Returns the "power set" of a list: every possible combination of elements in the list without changing the order. + let rec powerSet = function + | [] -> [[]] + | x :: xs -> + let next = powerSet xs + List.map (fun list -> x :: list) next @ next + let given = test 1 + + // Try every possible combination of arguments. The command should succeed only when all of the arguments are + // included. + let commandName = List.head submitWithNothingTarget + let allArgs = submitWithNothingTarget |> List.tail |> List.chunkBySize 2 + for args in powerSet allArgs do + given (commandName :: List.concat args) + |> if List.length args = List.length allArgs + then yields "The friendly URI for viewing job results is not available yet. Showing the job ID instead. + 00000000-0000-0000-0000-0000000000000" + else fails + +[] +let ``Submit catches exceptions`` () = + let given = test 1 + given submitWithErrorTarget + |> failsWith "Something went wrong when submitting the program to the Azure Quantum service. + + This quantum machine always has an error." + // Help @@ -614,7 +671,7 @@ let ``Uses documentation`` () = Commands: simulate (default) Run the program using a local simulator." - let given = test 31 + let given = test 33 given ["--help"] |> yields message given ["-h"] |> yields message given ["-?"] |> yields message @@ -645,5 +702,5 @@ let ``Shows help text for submit command`` () = --my-cool-bool (REQUIRED) A neat bit. -?, -h, --help Show help and usage information" - let given = test 31 + let given = test 33 given ["submit"; "--help"] |> yields message diff --git a/src/Simulation/EntryPointDriver.Tests/Tests.qs b/src/Simulation/EntryPointDriver.Tests/Tests.qs index f45080104db..bfe7a467c8d 100644 --- a/src/Simulation/EntryPointDriver.Tests/Tests.qs +++ b/src/Simulation/EntryPointDriver.Tests/Tests.qs @@ -284,6 +284,24 @@ namespace Microsoft.Quantum.CsharpGeneration.EntryPointDriver.Tests { // --- +namespace Microsoft.Quantum.CsharpGeneration.EntryPointDriver.Tests { + @EntryPoint() + operation ShadowTarget(target : String) : String { + return target; + } +} + +// --- + +namespace Microsoft.Quantum.CsharpGeneration.EntryPointDriver.Tests { + @EntryPoint() + operation ShadowShots(shots : Int) : Int { + return shots; + } +} + +// --- + // // Simulators // diff --git a/src/Simulation/EntryPointDriver/Azure.cs b/src/Simulation/EntryPointDriver/Azure.cs index 1ab7da8b1d9..48116911142 100644 --- a/src/Simulation/EntryPointDriver/Azure.cs +++ b/src/Simulation/EntryPointDriver/Azure.cs @@ -25,6 +25,7 @@ internal static class Azure /// The submission settings. /// The entry point's argument type. /// The entry point's return type. + /// The exit code. internal static async Task Submit( IEntryPoint entryPoint, ParseResult parseResult, AzureSettings settings) { @@ -43,53 +44,70 @@ internal static async Task Submit( } var input = entryPoint.CreateArgument(parseResult); - if (settings.DryRun) + return settings.DryRun + ? Validate(machine, entryPoint, input) + : await SubmitJob(machine, entryPoint, input, settings); + } + + /// + /// Submits a job to Azure Quantum. + /// + /// The quantum machine target. + /// The program entry point. + /// The program input. + /// The submission settings. + /// The input type. + /// The output type. + /// The exit code. + private static async Task SubmitJob( + IQuantumMachine machine, IEntryPoint entryPoint, TIn input, AzureSettings settings) + { + try { - var (isValid, message) = machine.Validate(entryPoint.Info, input); - Console.WriteLine(isValid ? "✔️ The program is valid!" : "❌ The program is invalid."); - if (!string.IsNullOrWhiteSpace(message)) + var job = await machine.SubmitAsync(entryPoint.Info, input, new SubmissionContext { - Console.WriteLine(); - Console.WriteLine(message); - } - return isValid ? 0 : 1; + FriendlyName = settings.JobName, + Shots = settings.Shots + }); + DisplayJob(job, settings.Output); + return 0; } - else + catch (AzureQuantumException ex) { - try - { - var job = await machine.SubmitAsync(entryPoint.Info, input, new SubmissionContext - { - FriendlyName = settings.JobName, - Shots = settings.Shots - }); - DisplayJob(job, settings.Output); - } - catch (AzureQuantumException azureQuantumEx) - { - DisplayWithColor( - ConsoleColor.Red, - Console.Error, - "Something went wrong when submitting the program to the Azure Quantum service."); - - Console.Error.WriteLine(); - Console.Error.WriteLine(azureQuantumEx.Message); - return 1; - } - catch (QuantumProcessorTranslationException translationEx) - { - DisplayWithColor( - ConsoleColor.Red, - Console.Error, - "Something went wrong when performing translation to the intermediate representation used by the target quantum machine."); - - Console.Error.WriteLine(); - Console.Error.WriteLine(translationEx.Message); - return 1; - } + DisplayError( + "Something went wrong when submitting the program to the Azure Quantum service.", + ex.Message); + return 1; + } + catch (QuantumProcessorTranslationException ex) + { + DisplayError( + "Something went wrong when performing translation to the intermediate representation used by the " + + "target quantum machine.", + ex.Message); + return 1; + } + } - return 0; + /// + /// Validates the program for the quantum machine target. + /// + /// The quantum machine target. + /// The program entry point. + /// The program input. + /// The input type. + /// The output type. + /// The exit code. + private static int Validate(IQuantumMachine machine, IEntryPoint entryPoint, TIn input) + { + var (isValid, message) = machine.Validate(entryPoint.Info, input); + Console.WriteLine(isValid ? "✔️ The program is valid!" : "❌ The program is invalid."); + if (!string.IsNullOrWhiteSpace(message)) + { + Console.WriteLine(); + Console.WriteLine(message); } + return isValid ? 0 : 1; } /// @@ -116,15 +134,29 @@ private static void DisplayJob(IQuantumMachineJob job, OutputFormat format) } } + /// + /// Displays an error to the console. + /// + /// A summary of the error. + /// The full error message. + private static void DisplayError(string summary, string message) + { + DisplayWithColor(ConsoleColor.Red, Console.Error, summary); + Console.Error.WriteLine(); + Console.Error.WriteLine(message); + } + /// /// Creates a quantum machine based on the Azure Quantum submission settings. /// /// The Azure Quantum submission settings. /// A quantum machine. - private static IQuantumMachine? CreateMachine(AzureSettings settings) => - settings.Target == "nothing" - ? new NothingMachine() - : QuantumMachineFactory.CreateMachine(settings.CreateWorkspace(), settings.Target, settings.Storage); + private static IQuantumMachine? CreateMachine(AzureSettings settings) => settings.Target switch + { + NothingMachine.TargetId => new NothingMachine(), + ErrorMachine.TargetId => new ErrorMachine(), + _ => QuantumMachineFactory.CreateMachine(settings.CreateWorkspace(), settings.Target, settings.Storage) + }; /// /// The quantum machine submission context. diff --git a/src/Simulation/EntryPointDriver/NothingMachine.cs b/src/Simulation/EntryPointDriver/TestMachines.cs similarity index 53% rename from src/Simulation/EntryPointDriver/NothingMachine.cs rename to src/Simulation/EntryPointDriver/TestMachines.cs index f4bf6942e99..905f6ee61cb 100644 --- a/src/Simulation/EntryPointDriver/NothingMachine.cs +++ b/src/Simulation/EntryPointDriver/TestMachines.cs @@ -4,6 +4,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Microsoft.Azure.Quantum.Exceptions; using Microsoft.Quantum.Runtime; using Microsoft.Quantum.Simulation.Core; @@ -14,9 +15,14 @@ namespace Microsoft.Quantum.CsharpGeneration.EntryPointDriver /// internal class NothingMachine : IQuantumMachine { + /// + /// The target ID for the nothing machine. + /// + internal const string TargetId = "test.nothing"; + public string ProviderId { get; } = nameof(NothingMachine); - public string Target { get; } = "Nothing"; + public string Target { get; } = TargetId; public Task> ExecuteAsync( EntryPointInfo info, TInput input) => @@ -106,4 +112,85 @@ public Task RefreshAsync(CancellationToken cancellationToken = default) => throw new NotSupportedException(); } } + + /// + /// A quantum machine that always has an error. + /// + internal class ErrorMachine : IQuantumMachine + { + /// + /// The target ID for the error machine. + /// + internal const string TargetId = "test.error"; + + public string ProviderId { get; } = nameof(ErrorMachine); + + public string Target { get; } = TargetId; + + public Task> ExecuteAsync( + EntryPointInfo info, TInput input) => + throw new NotSupportedException(); + + public Task> ExecuteAsync( + EntryPointInfo info, + TInput input, + IQuantumMachineSubmissionContext submissionContext) => + throw new NotSupportedException(); + + public Task> ExecuteAsync( + EntryPointInfo info, + TInput input, + IQuantumMachineSubmissionContext submissionContext, + IQuantumMachine.ConfigureJob configureJobCallback) => + throw new NotSupportedException(); + + public Task> ExecuteAsync( + EntryPointInfo info, + TInput input, + IQuantumMachineExecutionContext executionContext) => + throw new NotSupportedException(); + + public Task> ExecuteAsync( + EntryPointInfo info, + TInput input, + IQuantumMachineExecutionContext executionContext, + IQuantumMachine.ConfigureJob configureJobCallback) => + throw new NotSupportedException(); + + public Task> ExecuteAsync( + EntryPointInfo info, + TInput input, + IQuantumMachineSubmissionContext submissionContext, + IQuantumMachineExecutionContext executionContext) => + throw new NotSupportedException(); + + public Task> ExecuteAsync( + EntryPointInfo info, + TInput input, + IQuantumMachineSubmissionContext submissionContext, + IQuantumMachineExecutionContext executionContext, + IQuantumMachine.ConfigureJob configureJobCallback) => + throw new NotSupportedException(); + + public Task SubmitAsync( + EntryPointInfo info, TInput input) => + throw new AzureQuantumException("This quantum machine always has an error."); + + public Task SubmitAsync( + EntryPointInfo info, + TInput input, + IQuantumMachineSubmissionContext submissionContext) => + SubmitAsync(info, input); + + public Task SubmitAsync( + EntryPointInfo info, + TInput input, + IQuantumMachineSubmissionContext submissionContext, + IQuantumMachine.ConfigureJob configureJobCallback) => + SubmitAsync(info, input); + + public (bool IsValid, string Message) Validate( + EntryPointInfo info, TInput input) => + (false, "This quantum machine always has an error."); + } }