diff --git a/.gitignore b/.gitignore index 5b33b48524e..7127ca52075 100644 --- a/.gitignore +++ b/.gitignore @@ -339,3 +339,6 @@ ASALocalRun/ /src/Simulation/Simulators.Tests/TestProjects/QSharpExe/built /src/Simulation/Simulators.Tests/TestProjects/TargetedExe/built dbw_test + +# Controller test artifacts +/src/Qir/Controller/test-artifacts/ \ No newline at end of file diff --git a/Simulation.sln b/Simulation.sln index f888733494b..7f2fab57378 100644 --- a/Simulation.sln +++ b/Simulation.sln @@ -117,6 +117,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Controller", "Controller", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QirController", "src\Qir\Controller\QirController.csproj", "{A77E6661-D143-4E3E-BCD1-8E321A966829}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.QirController", "src\Qir\Controller\Tests.QirController\Tests.QirController.csproj", "{2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -769,6 +771,22 @@ Global {A77E6661-D143-4E3E-BCD1-8E321A966829}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU {A77E6661-D143-4E3E-BCD1-8E321A966829}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU {A77E6661-D143-4E3E-BCD1-8E321A966829}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Debug|x64.ActiveCfg = Debug|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Debug|x64.Build.0 = Debug|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.MinSizeRel|x64.Build.0 = Debug|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Release|Any CPU.Build.0 = Release|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Release|x64.ActiveCfg = Release|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.Release|x64.Build.0 = Release|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68}.RelWithDebInfo|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -824,6 +842,7 @@ Global {D7D34736-A719-4B45-A33F-2723F59EC29D} = {A7DB7367-9FD6-4164-8263-A05077BE54AB} {4E07F247-ED93-4497-8B58-022314308E67} = {F6C2D4C0-12DC-40E3-9C86-FA5308D9B567} {A77E6661-D143-4E3E-BCD1-8E321A966829} = {4E07F247-ED93-4497-8B58-022314308E67} + {2E4B9604-A5CD-4B49-B1D4-A7AC8ABAEF68} = {4E07F247-ED93-4497-8B58-022314308E67} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {929C0464-86D8-4F70-8835-0A5EAF930821} diff --git a/src/Qir/Controller/Constant.cs b/src/Qir/Controller/Constant.cs new file mode 100644 index 00000000000..23d5a8ba351 --- /dev/null +++ b/src/Qir/Controller/Constant.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Microsoft.Quantum.Qir +{ + public static class Constant + { + // TODO: errors will be added as dependencies are implemented. + public static class ErrorCode + { + public const string InternalError = "InternalError"; + } + } +} diff --git a/src/Qir/Controller/Controller.cs b/src/Qir/Controller/Controller.cs index 0f613d5fc0c..5f9cc2b65cc 100644 --- a/src/Qir/Controller/Controller.cs +++ b/src/Qir/Controller/Controller.cs @@ -1,26 +1,97 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.IO; -using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Quantum.Qir.Driver; +using Microsoft.Quantum.Qir.Executable; +using Microsoft.Quantum.Qir.Model; +using Microsoft.Quantum.Qir.Utility; +using QirExecutionWrapperSerialization = Microsoft.Quantum.QsCompiler.BondSchemas.QirExecutionWrapper.Protocols; namespace Microsoft.Quantum.Qir { - internal static class Controller + public static class Controller { - internal static void Execute( - FileInfo input, - FileInfo output, - FileInfo error) + private const string SourceDirectoryPath = "src"; + private const string BinaryDirectoryPath = "bin"; + private const string ExecutableName = "simulation.exe"; + + public static async Task ExecuteAsync( + FileInfo inputFile, + FileInfo outputFile, + DirectoryInfo libraryDirectory, + DirectoryInfo includeDirectory, + FileInfo errorFile, + IQirDriverGenerator driverGenerator, + IQirExecutableGenerator executableGenerator, + IQuantumExecutableRunner executableRunner, + ILogger logger) { - var outputFileStream = output.Exists ? output.OpenWrite() : output.Create(); - outputFileStream.Write(new UTF8Encoding().GetBytes("output")); - outputFileStream.Flush(); - outputFileStream.Close(); - var errorFileStream = error.Exists ? error.OpenWrite() : error.Create(); - errorFileStream.Write(new UTF8Encoding().GetBytes("error")); - errorFileStream.Flush(); - errorFileStream.Close(); + try + { + // Step 1: Parse input. + logger.LogInfo("Parsing input."); + using var inputFileStream = inputFile.OpenRead(); + var input = QirExecutionWrapperSerialization.DeserializeFromFastBinary(inputFileStream); + + // Step 32: Create driver. + logger.LogInfo("Creating driver file."); + var sourceDirectory = new DirectoryInfo(SourceDirectoryPath); + await driverGenerator.GenerateQirDriverCppAsync(sourceDirectory, input.EntryPoint, input.QirBytecode); + + // Step 3: Create executable. + logger.LogInfo("Compiling and linking executable."); + var binaryDirectory = new DirectoryInfo(BinaryDirectoryPath); + var executableFile = new FileInfo(Path.Combine(BinaryDirectoryPath, ExecutableName)); + await executableGenerator.GenerateExecutableAsync(executableFile, sourceDirectory, libraryDirectory, includeDirectory); + + // Step 4: Run executable. + logger.LogInfo("Running executable."); + using var outputFileStream = outputFile.OpenWrite(); + await executableRunner.RunExecutableAsync(executableFile, input.EntryPoint, outputFile); + } + catch (Exception e) + { + logger.LogError("An error has been encountered. Will write an error to the error file and delete any output that has been generated."); + logger.LogException(e); + await WriteExceptionToFileAsync(e, errorFile); + } + } + + private static async Task WriteExceptionToFileAsync(Exception e, FileInfo errorFile) + { + // Create the error object. + Error error; + if (e is ControllerException controllerException) + { + error = new Error + { + Code = controllerException.Code, + Message = controllerException.Message, + }; + } + else + { + error = new Error + { + Code = Constant.ErrorCode.InternalError, + Message = ErrorMessages.InternalError, + }; + } + + // Serialize the error to JSON. + var errorJson = JsonSerializer.Serialize(error, new JsonSerializerOptions + { + WriteIndented = true, + }); + + // Write the error to the error file. + using var errorFileStream = errorFile.OpenWrite(); + using var streamWriter = new StreamWriter(errorFileStream); + await streamWriter.WriteAsync(errorJson); } } } diff --git a/src/Qir/Controller/ControllerException.cs b/src/Qir/Controller/ControllerException.cs new file mode 100644 index 00000000000..05cbd872cf9 --- /dev/null +++ b/src/Qir/Controller/ControllerException.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Quantum.Qir +{ + /// + /// Exception that represents an error that can be written to an error file. + /// + public class ControllerException : Exception + { + public ControllerException(string message, string code) + : base(message) + { + Code = code; + } + + public string Code { get; } + } +} diff --git a/src/Qir/Controller/Driver/IQirDriverGenerator.cs b/src/Qir/Controller/Driver/IQirDriverGenerator.cs new file mode 100644 index 00000000000..e4dc2a6172c --- /dev/null +++ b/src/Qir/Controller/Driver/IQirDriverGenerator.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint; + +namespace Microsoft.Quantum.Qir.Driver +{ + public interface IQirDriverGenerator + { + /// + /// Generates the C++ driver source file and writes the bytecode to a file. + /// + /// Directory to which driver and bytecode will be written. + /// Entry point information. + /// The QIR bytecode. + /// + Task GenerateQirDriverCppAsync(DirectoryInfo sourceDirectory, EntryPointOperation entryPointOperation, ArraySegment bytecode); + } +} diff --git a/src/Qir/Controller/Driver/QirDriverGenerator.cs b/src/Qir/Controller/Driver/QirDriverGenerator.cs new file mode 100644 index 00000000000..e169ea12be4 --- /dev/null +++ b/src/Qir/Controller/Driver/QirDriverGenerator.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Quantum.Qir.Utility; +using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint; + +namespace Microsoft.Quantum.Qir.Driver +{ + public class QirDriverGenerator : IQirDriverGenerator + { + private readonly ILogger logger; + + public QirDriverGenerator(ILogger logger) + { + this.logger = logger; + } + + public Task GenerateQirDriverCppAsync(DirectoryInfo sourceDirectory, EntryPointOperation entryPointOperation, ArraySegment bytecode) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Qir/Controller/ErrorMessages.Designer.cs b/src/Qir/Controller/ErrorMessages.Designer.cs new file mode 100644 index 00000000000..dbd8ce8ba5e --- /dev/null +++ b/src/Qir/Controller/ErrorMessages.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.Quantum.Qir { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class ErrorMessages { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ErrorMessages() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.Quantum.Qir.ErrorMessages", typeof(ErrorMessages).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to An internal error occurred.. + /// + public static string InternalError { + get { + return ResourceManager.GetString("InternalError", resourceCulture); + } + } + } +} diff --git a/src/Qir/Controller/ErrorMessages.resx b/src/Qir/Controller/ErrorMessages.resx new file mode 100644 index 00000000000..6cfb82bccc5 --- /dev/null +++ b/src/Qir/Controller/ErrorMessages.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + An internal error occurred. + + \ No newline at end of file diff --git a/src/Qir/Controller/Executable/ClangClient.cs b/src/Qir/Controller/Executable/ClangClient.cs new file mode 100644 index 00000000000..72fc903d4e8 --- /dev/null +++ b/src/Qir/Controller/Executable/ClangClient.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.CommandLine.Invocation; +using System.Threading.Tasks; +using Microsoft.Quantum.Qir.Utility; + +namespace Microsoft.Quantum.Qir.Executable +{ + public class ClangClient : IClangClient + { + private const string LinkFlag = " -l "; + private readonly ILogger logger; + + public ClangClient(ILogger logger) + { + this.logger = logger; + } + + public async Task CreateExecutableAsync(string[] inputFiles, string[] libraries, string libraryPath, string includePath, string outputPath) + { + var inputsArg = string.Join(' ', inputFiles); + + // string.Join does not automatically prepend the delimiter, so it is included again in the string here. + var librariesArg = $"{LinkFlag} {string.Join(LinkFlag, libraries)}"; + var arguments = $"{inputsArg} -I {includePath} -L {libraryPath} {librariesArg} -o {outputPath}"; + logger.LogInfo($"Invoking clang with the following arguments: {arguments}"); + var result = await Process.ExecuteAsync( + "clang", + arguments, + stdOut: s => { logger.LogInfo("clang: " + s); }, + stdErr: s => { logger.LogError("clang: " + s); }); + } + } +} diff --git a/src/Qir/Controller/Executable/IClangClient.cs b/src/Qir/Controller/Executable/IClangClient.cs new file mode 100644 index 00000000000..7e7a8e1c95d --- /dev/null +++ b/src/Qir/Controller/Executable/IClangClient.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; + +namespace Microsoft.Quantum.Qir.Executable +{ + /// + /// Wraps the 'clang' tool used for compilation. + /// + public interface IClangClient + { + Task CreateExecutableAsync(string[] inputFiles, string[] libraries, string libraryPath, string includePath, string outputPath); + } +} diff --git a/src/Qir/Controller/Executable/IQirExecutableGenerator.cs b/src/Qir/Controller/Executable/IQirExecutableGenerator.cs new file mode 100644 index 00000000000..e8ce44f7b7a --- /dev/null +++ b/src/Qir/Controller/Executable/IQirExecutableGenerator.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Threading.Tasks; + +namespace Microsoft.Quantum.Qir.Executable +{ + public interface IQirExecutableGenerator + { + /// + /// Generates a quantum simulation program executable. + /// + /// File path to create the executable at. Dependencies will be copied to its directory. + /// Location of the source files. + /// Location of the libraries that must be linked. + /// Location of the headers that must be included. + /// + public Task GenerateExecutableAsync(FileInfo executableFile, DirectoryInfo sourceDirectory, DirectoryInfo libraryDirectory, DirectoryInfo includeDirectory); + } +} diff --git a/src/Qir/Controller/Executable/IQuantumExecutableRunner.cs b/src/Qir/Controller/Executable/IQuantumExecutableRunner.cs new file mode 100644 index 00000000000..135fbbbfb4f --- /dev/null +++ b/src/Qir/Controller/Executable/IQuantumExecutableRunner.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint; + +namespace Microsoft.Quantum.Qir.Executable +{ + public interface IQuantumExecutableRunner + { + /// + /// Runs a quantum program executable with the given arguments. + /// + /// Location of the executable to run. + /// Entry point and arguments to pass. + /// Location to write program output. + /// + Task RunExecutableAsync(FileInfo executableFile, EntryPointOperation entryPointOperation, FileInfo outputFile); + } +} diff --git a/src/Qir/Controller/Executable/QirExecutableGenerator.cs b/src/Qir/Controller/Executable/QirExecutableGenerator.cs new file mode 100644 index 00000000000..c09a7a0fa67 --- /dev/null +++ b/src/Qir/Controller/Executable/QirExecutableGenerator.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.Quantum.Qir.Utility; + +namespace Microsoft.Quantum.Qir.Executable +{ + public class QirExecutableGenerator : IQirExecutableGenerator + { + private readonly IClangClient clangClient; + private readonly ILogger logger; + + public QirExecutableGenerator(IClangClient clangClient, ILogger logger) + { + this.clangClient = clangClient; + this.logger = logger; + } + + public Task GenerateExecutableAsync(FileInfo executableFile, DirectoryInfo sourceDirectory, DirectoryInfo libraryDirectory, DirectoryInfo includeDirectory) + { + // TODO: Compile and link libraries- "Microsoft.Quantum.Qir.Runtime", "Microsoft.Quantum.Qir.QSharp.Foundation", "Microsoft.Quantum.Qir.QSharp.Core" + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Qir/Controller/Executable/QuantumExecutableRunner.cs b/src/Qir/Controller/Executable/QuantumExecutableRunner.cs new file mode 100644 index 00000000000..774aa0cf517 --- /dev/null +++ b/src/Qir/Controller/Executable/QuantumExecutableRunner.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using System.Threading.Tasks; +using Microsoft.Quantum.Qir.Utility; +using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint; + +namespace Microsoft.Quantum.Qir.Executable +{ + public class QuantumExecutableRunner : IQuantumExecutableRunner + { + private readonly ILogger logger; + + public QuantumExecutableRunner(ILogger logger) + { + this.logger = logger; + } + + public Task RunExecutableAsync(FileInfo executableFile, EntryPointOperation entryPointOperation, FileInfo outputFile) + { + throw new System.NotImplementedException(); + } + } +} diff --git a/src/Qir/Controller/Model/Error.cs b/src/Qir/Controller/Model/Error.cs new file mode 100644 index 00000000000..ee96bff5348 --- /dev/null +++ b/src/Qir/Controller/Model/Error.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Microsoft.Quantum.Qir.Model +{ + public class Error + { + [JsonPropertyName("code")] + public string Code { get; set; } + + [JsonPropertyName("message")] + public string Message { get; set; } + } +} diff --git a/src/Qir/Controller/Program.cs b/src/Qir/Controller/Program.cs index e7fec1e6fd0..88677d0d1ce 100644 --- a/src/Qir/Controller/Program.cs +++ b/src/Qir/Controller/Program.cs @@ -4,6 +4,9 @@ using System.CommandLine; using System.CommandLine.Invocation; using System.IO; +using Microsoft.Quantum.Qir.Driver; +using Microsoft.Quantum.Qir.Executable; +using Microsoft.Quantum.Qir.Utility; namespace Microsoft.Quantum.Qir { @@ -11,6 +14,12 @@ class Program { static void Main(string[] args) { + var logger = new Logger(new Clock()); + var execGenerator = new QirExecutableGenerator(new ClangClient(logger), logger); + var driverGenerator = new QirDriverGenerator(logger); + var execRunner = new QuantumExecutableRunner(logger); + logger.LogInfo("QIR controller beginning."); + var rootCommand = new RootCommand( description: "Builds and runs QIR executable."); @@ -31,6 +40,23 @@ static void Main(string[] args) }; rootCommand.AddOption(outputOption); + + var libraryDirectoryOption = new Option( + aliases: new string[] { "--libraryDirectory" }) + { + Description = "Path to the directory containing the libraries that must be linked to the driver executable.", + IsRequired = true + }; + + rootCommand.AddOption(libraryDirectoryOption); + var includeDirectoryOption = new Option( + aliases: new string[] { "--includeDirectory" }) + { + Description = "Path to the directory containing headers that must be included by the C++ driver.", + IsRequired = true + }; + + rootCommand.AddOption(includeDirectoryOption); var errorOption = new Option( aliases: new string[] { "--error",}) { @@ -41,7 +67,9 @@ static void Main(string[] args) rootCommand.AddOption(errorOption); // Bind to a handler and invoke. - rootCommand.Handler = CommandHandler.Create((input, output, error) => Controller.Execute(input, output, error)); + rootCommand.Handler = CommandHandler.Create( + async (input, output, libraryDirectory, includeDirectory, error) => + await Controller.ExecuteAsync(input, output, libraryDirectory, includeDirectory, error, driverGenerator, execGenerator, execRunner, logger)); rootCommand.Invoke(args); } } diff --git a/src/Qir/Controller/QirController.csproj b/src/Qir/Controller/QirController.csproj index 0c21b5914f9..4570cafa210 100644 --- a/src/Qir/Controller/QirController.csproj +++ b/src/Qir/Controller/QirController.csproj @@ -1,12 +1,40 @@ - + Exe netcoreapp3.1 + Microsoft.Quantum.Qir + + + + + + + + + + + + + + + True + True + ErrorMessages.resx + + + + + + PublicResXFileCodeGenerator + ErrorMessages.Designer.cs + + + diff --git a/src/Qir/Controller/Tests.QirController/ControllerTests.cs b/src/Qir/Controller/Tests.QirController/ControllerTests.cs new file mode 100644 index 00000000000..5da9e861d37 --- /dev/null +++ b/src/Qir/Controller/Tests.QirController/ControllerTests.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Quantum.Qir; +using Microsoft.Quantum.Qir.Driver; +using Microsoft.Quantum.Qir.Executable; +using Microsoft.Quantum.Qir.Model; +using Microsoft.Quantum.Qir.Utility; +using Microsoft.Quantum.QsCompiler.BondSchemas.EntryPoint; +using Microsoft.Quantum.QsCompiler.BondSchemas.QirExecutionWrapper; +using Moq; +using Xunit; +using QirExecutionWrapperSerialization = Microsoft.Quantum.QsCompiler.BondSchemas.QirExecutionWrapper.Protocols; + +namespace Tests.QirController +{ + public class ControllerTests : IDisposable + { + private Mock driverGeneratorMock; + private Mock executableGeneratorMock; + private Mock executableRunnerMock; + private Mock loggerMock; + private FileInfo inputFile; + private FileInfo bytecodeFile; + private FileInfo errorFile; + private FileInfo outputFile; + private QirExecutionWrapper input; + + public ControllerTests() + { + driverGeneratorMock = new Mock(); + executableGeneratorMock = new Mock(); + executableRunnerMock = new Mock(); + inputFile = new FileInfo($"{Guid.NewGuid()}-input"); + bytecodeFile = new FileInfo($"{Guid.NewGuid()}-bytecode"); + errorFile = new FileInfo($"{Guid.NewGuid()}-error"); + outputFile = new FileInfo($"{Guid.NewGuid()}-output"); + loggerMock = new Mock(); + + // Create a QirExecutableWrapper to be used by the tests. + byte[] bytecode = { 1, 2, 3, 4, 5 }; + input = new QirExecutionWrapper() + { + EntryPoint = new EntryPointOperation() + { + Arguments = new List + { + new Argument() + { + Position = 0, + Name = "argname", + Values = new List { new ArgumentValue { String = "argvalue" } }, + } + } + }, + QirBytecode = new ArraySegment(bytecode, 1, 3), + }; + using var fileStream = inputFile.OpenWrite(); + QirExecutionWrapperSerialization.SerializeToFastBinary(input, fileStream); + } + + public void Dispose() + { + inputFile.Delete(); + bytecodeFile.Delete(); + errorFile.Delete(); + outputFile.Delete(); + } + + [Fact] + public async Task TestExecute() + { + var libraryDirectory = new DirectoryInfo("libraries"); + var includeDirectory = new DirectoryInfo("includes"); + FileInfo actualExecutableFile = null; + Action generateExecutableCallback = async (executableFile, srcDir, libDir, inclDir) => + { + actualExecutableFile = executableFile; + await Task.CompletedTask; + }; + executableGeneratorMock.Setup(obj => obj.GenerateExecutableAsync( + It.IsAny(), + It.IsAny(), + It.Is(actualLibraryDirectory => actualLibraryDirectory.FullName == libraryDirectory.FullName), + It.Is(actualIncludeDirectory => actualIncludeDirectory.FullName == includeDirectory.FullName))).Callback(generateExecutableCallback); + + await Controller.ExecuteAsync( + inputFile, + outputFile, + libraryDirectory, + includeDirectory, + errorFile, + driverGeneratorMock.Object, + executableGeneratorMock.Object, + executableRunnerMock.Object, + loggerMock.Object); + + // Verify driver was created. + driverGeneratorMock.Verify(obj => obj.GenerateQirDriverCppAsync( + It.IsAny(), + It.Is(entryPoint => EntryPointsAreEqual(entryPoint, input.EntryPoint)), + It.Is>(bytecode => BytecodesAreEqual(bytecode, input.QirBytecode)))); + + // Verify executable was generated. + executableGeneratorMock.Verify(obj => obj.GenerateExecutableAsync( + It.IsAny(), + It.IsAny(), + It.Is(actualLibraryDirectory => actualLibraryDirectory.FullName == libraryDirectory.FullName), + It.Is(actualIncludeDirectory => actualIncludeDirectory.FullName == includeDirectory.FullName))); + Assert.NotNull(actualExecutableFile); + + // Verify executable was run. + executableRunnerMock.Verify(obj => obj.RunExecutableAsync( + It.Is(executableFile => actualExecutableFile.FullName == executableFile.FullName), + It.Is(entryPoint => EntryPointsAreEqual(entryPoint, input.EntryPoint)), + It.Is(actualOutputFile => actualOutputFile.FullName == outputFile.FullName))); + } + + [Fact] + public async Task TestExecuteEncountersGenericException() + { + var libraryDirectory = new DirectoryInfo("libraries"); + var includeDirectory = new DirectoryInfo("includes"); + executableGeneratorMock.Setup(obj => obj.GenerateExecutableAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new Exception("exception message")); + + // Execute controller. + await Controller.ExecuteAsync( + inputFile, + outputFile, + libraryDirectory, + includeDirectory, + errorFile, + driverGeneratorMock.Object, + executableGeneratorMock.Object, + executableRunnerMock.Object, + loggerMock.Object); + + // Verify error file was created and contains the error. + Assert.True(errorFile.Exists); + using var errorFileStream = errorFile.OpenRead(); + using var streamReader = new StreamReader(errorFileStream); + var errorFileContents = await streamReader.ReadToEndAsync(); + var error = JsonSerializer.Deserialize(errorFileContents); + Assert.Equal(ErrorMessages.InternalError, error.Message); + Assert.Equal(Constant.ErrorCode.InternalError, error.Code); + } + + [Fact] + public async Task TestExecuteEncountersControllerException() + { + var exceptionMessage = "exception message"; + var errorCode = "error code"; + var libraryDirectory = new DirectoryInfo("libraries"); + var includeDirectory = new DirectoryInfo("includes"); + executableGeneratorMock.Setup(obj => obj.GenerateExecutableAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .ThrowsAsync(new ControllerException(exceptionMessage, errorCode)); + + // Execute controller. + await Controller.ExecuteAsync( + inputFile, + outputFile, + libraryDirectory, + includeDirectory, + errorFile, + driverGeneratorMock.Object, + executableGeneratorMock.Object, + executableRunnerMock.Object, + loggerMock.Object); + + // Verify error file was created and contains the error. + Assert.True(errorFile.Exists); + using var errorFileStream = errorFile.OpenRead(); + using var streamReader = new StreamReader(errorFileStream); + var errorFileContents = await streamReader.ReadToEndAsync(); + var error = JsonSerializer.Deserialize(errorFileContents); + Assert.Equal(exceptionMessage, error.Message); + Assert.Equal(errorCode, error.Code); + } + + private bool EntryPointsAreEqual(EntryPointOperation entryPointA, EntryPointOperation entryPointB) + { + var method = typeof(Extensions) + .GetMethod("ValueEquals", BindingFlags.Static | BindingFlags.NonPublic, null, new[] { typeof(EntryPointOperation), typeof(EntryPointOperation) }, null); + object[] parameters = { entryPointA, entryPointB }; + return (bool)method.Invoke(null, parameters); + } + + private bool BytecodesAreEqual(ArraySegment bytecodeA, ArraySegment bytecodeB) + { + if (bytecodeA.Count != bytecodeB.Count) + { + return false; + } + + for (var i = 0; i < bytecodeA.Count; ++i) + { + if (bytecodeA[i] != bytecodeB[i]) + { + return false; + } + } + + return true; + } + } +} diff --git a/src/Qir/Controller/Tests.QirController/LoggerTests.cs b/src/Qir/Controller/Tests.QirController/LoggerTests.cs new file mode 100644 index 00000000000..ee7adfee70a --- /dev/null +++ b/src/Qir/Controller/Tests.QirController/LoggerTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.Quantum.Qir.Utility; +using Moq; +using Xunit; + +namespace Tests.QirController +{ + public class LoggerTests + { + private readonly Mock clockMock; + private readonly Logger logger; + + public LoggerTests() + { + clockMock = new Mock(); + logger = new Logger(clockMock.Object); + } + + [Fact] + public void TestLogInfo() + { + using var consoleOutput = new StringWriter(); + var message = "some message"; + var time = DateTimeOffset.MinValue; + clockMock.SetupGet(obj => obj.Now).Returns(time); + var expectedLog = $"{time} [INFO]: some message" + Environment.NewLine; + Console.SetOut(consoleOutput); + logger.LogInfo(message); + var actualLog = consoleOutput.ToString(); + Assert.Equal(expectedLog, actualLog); + } + + [Fact] + public void TestLogError() + { + using var consoleOutput = new StringWriter(); + var message = "some message"; + var time = DateTimeOffset.MinValue; + clockMock.SetupGet(obj => obj.Now).Returns(time); + var expectedLog = $"{time} [ERROR]: some message" + Environment.NewLine; + Console.SetOut(consoleOutput); + logger.LogError(message); + var actualLog = consoleOutput.ToString(); + Assert.Equal(expectedLog, actualLog); + } + + [Fact] + public void TestLogExceptionWithoutStackTrace() + { + using var consoleOutput = new StringWriter(); + var time = DateTimeOffset.MinValue; + clockMock.SetupGet(obj => obj.Now).Returns(time); + var exception = new InvalidOperationException(); + var expectedLog = $"{time} [ERROR]: " + + "Exception encountered: System.InvalidOperationException: " + + exception.Message + Environment.NewLine + exception.StackTrace + Environment.NewLine; + Console.SetOut(consoleOutput); + logger.LogException(exception); + var actualLog = consoleOutput.ToString(); + Assert.Equal(expectedLog, actualLog); + } + + [Fact] + public void TestLogExceptionWithStackTrace() + { + using var consoleOutput = new StringWriter(); + var time = DateTimeOffset.MinValue; + clockMock.SetupGet(obj => obj.Now).Returns(time); + Exception exception; + try + { + throw new InvalidOperationException(); + } + // Throw exception to generate stack trace. + catch (Exception thrownException) + { + exception = thrownException; + } + + var expectedLog = $"{time} [ERROR]: " + + "Exception encountered: System.InvalidOperationException: " + + exception.Message + Environment.NewLine + exception.StackTrace + Environment.NewLine; + Console.SetOut(consoleOutput); + logger.LogException(exception); + var actualLog = consoleOutput.ToString(); + Assert.Equal(expectedLog, actualLog); + } + } +} diff --git a/src/Qir/Controller/Tests.QirController/Tests.QirController.csproj b/src/Qir/Controller/Tests.QirController/Tests.QirController.csproj new file mode 100644 index 00000000000..ba5183ca742 --- /dev/null +++ b/src/Qir/Controller/Tests.QirController/Tests.QirController.csproj @@ -0,0 +1,20 @@ + + + + netcoreapp3.1 + false + + + + + + + + + + + + + + + diff --git a/src/Qir/Controller/Utility/Clock.cs b/src/Qir/Controller/Utility/Clock.cs new file mode 100644 index 00000000000..7f1fdc8c78e --- /dev/null +++ b/src/Qir/Controller/Utility/Clock.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Quantum.Qir.Utility +{ + public class Clock : IClock + { + public DateTimeOffset Now => DateTimeOffset.Now; + } +} diff --git a/src/Qir/Controller/Utility/IClock.cs b/src/Qir/Controller/Utility/IClock.cs new file mode 100644 index 00000000000..da69204b74a --- /dev/null +++ b/src/Qir/Controller/Utility/IClock.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Quantum.Qir.Utility +{ + /// + /// Mockable clock interface. + /// + public interface IClock + { + public DateTimeOffset Now { get; } + } +} diff --git a/src/Qir/Controller/Utility/ILogger.cs b/src/Qir/Controller/Utility/ILogger.cs new file mode 100644 index 00000000000..7049bee9582 --- /dev/null +++ b/src/Qir/Controller/Utility/ILogger.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Quantum.Qir.Utility +{ + /// + /// Logger for internal traces and errors. + /// + /// For now, this thinly wraps console logging. In the future, this might be extended to log to files or other systems. + public interface ILogger + { + /// + /// Logs a message at the "information" level. + /// + /// Message to log. + void LogInfo(string message); + + /// + /// Logs a message at the "error" level. + /// + /// Message to log. + void LogError(string message); + + /// + /// Formats an exception into an error log. Logs the exception type, message, and stack trace. + /// + /// Exception to log. + void LogException(Exception e); + } +} diff --git a/src/Qir/Controller/Utility/Logger.cs b/src/Qir/Controller/Utility/Logger.cs new file mode 100644 index 00000000000..4aa2074a760 --- /dev/null +++ b/src/Qir/Controller/Utility/Logger.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.Quantum.Qir.Utility +{ + public class Logger : ILogger + { + private readonly IClock clock; + + public Logger(IClock clock) + { + this.clock = clock; + } + + // {timestamp} [{log level}]: {message}. + private const string LogFormat = "{0} [{1}]: {2}"; + + // ...{exception type}: {exception message}{Environment.NewLine}{stack trace}. + private const string ExceptionMessageFormat = "Exception encountered: {0}: {1}{2}{3}"; + private const string InfoLevel = "INFO"; + private const string ErrorLevel = "ERROR"; + + public void LogInfo(string message) + { + Console.WriteLine(LogFormat, clock.Now, InfoLevel, message); + } + + public void LogError(string message) + { + Console.WriteLine(LogFormat, clock.Now, ErrorLevel, message); + } + + public void LogException(Exception e) + { + var message = string.Format(ExceptionMessageFormat, e.GetType(), e.Message, Environment.NewLine, e.StackTrace); + LogError(message); + } + } +} diff --git a/src/Qir/Controller/test-cases/01.err b/src/Qir/Controller/test-cases/01.err deleted file mode 100644 index 760589cb5d6..00000000000 --- a/src/Qir/Controller/test-cases/01.err +++ /dev/null @@ -1 +0,0 @@ -error \ No newline at end of file diff --git a/src/Qir/Controller/test-cases/01.in b/src/Qir/Controller/test-cases/01.in deleted file mode 100644 index 770eab4480b..00000000000 --- a/src/Qir/Controller/test-cases/01.in +++ /dev/null @@ -1 +0,0 @@ -input \ No newline at end of file diff --git a/src/Qir/Controller/test-cases/01.out b/src/Qir/Controller/test-cases/01.out deleted file mode 100644 index 6caf68aff42..00000000000 --- a/src/Qir/Controller/test-cases/01.out +++ /dev/null @@ -1 +0,0 @@ -output \ No newline at end of file diff --git a/src/Qir/Controller/test-cases/internal-error-test.err b/src/Qir/Controller/test-cases/internal-error-test.err new file mode 100644 index 00000000000..cc5c5a256f3 --- /dev/null +++ b/src/Qir/Controller/test-cases/internal-error-test.err @@ -0,0 +1,4 @@ +{ + "code": "InternalError", + "message": "An internal error occurred." +} \ No newline at end of file diff --git a/src/Qir/Controller/test-cases/internal-error-test.in b/src/Qir/Controller/test-cases/internal-error-test.in new file mode 100644 index 00000000000..37fc5162678 --- /dev/null +++ b/src/Qir/Controller/test-cases/internal-error-test.in @@ -0,0 +1 @@ +any input will do for now, but as errors become more specific, the corresponding test will need to change. \ No newline at end of file diff --git a/src/Qir/Controller/test-qir-controller.ps1 b/src/Qir/Controller/test-qir-controller.ps1 index 876626b703a..ad70d23d1b7 100644 --- a/src/Qir/Controller/test-qir-controller.ps1 +++ b/src/Qir/Controller/test-qir-controller.ps1 @@ -8,6 +8,7 @@ Write-Host "##[info]Test QIR Controller" $controllerProject = (Join-Path $PSScriptRoot QirController.csproj) $testCasesFolder = (Join-Path $PSScriptRoot "test-cases") $testArtifactsFolder = (Join-Path $PSScriptRoot "test-artifacts") + if (!(Test-Path $testArtifactsFolder -PathType Container)) { New-Item -ItemType Directory -Force -Path $testArtifactsFolder } @@ -19,23 +20,29 @@ Foreach-Object { # Get the paths to the output and error files to pass to the QIR controller. $outputFile = (Join-Path $testArtifactsFolder ($_.BaseName + ".out")) $errorFile = (Join-Path $testArtifactsFolder ($_.BaseName + ".err")) - dotnet run --project $controllerProject -- --input $_.FullName --output $outputFile --error $errorFile + dotnet run --project $controllerProject -- --input $_.FullName --output $outputFile --error $errorFile --includeDirectory "placeholder for now" --libraryDirectory "placeholder for now" # Compare the expected content of the output and error files vs the actual content. $expectedOutputFile = (Join-Path $testCasesFolder ($_.BaseName + ".out")) - $expectedOutput = Get-Content -Path $expectedOutputFile -Raw - $actualOutput = Get-Content -Path $outputFile -Raw - if (-not ($expectedOutput -ceq $actualOutput)) { - Write-Host "##vso[task.logissue type=error;]Failed QIR Controller test case: $($_.BaseName)" - Write-Host "##[info]Expected output:" - Write-Host $expectedOutput - Write-Host "##[info]Actual output:" - Write-Host $actualOutput - $script:all_ok = $False - break + $expectedErrorFile = (Join-Path $testCasesFolder ($_.BaseName + ".err")) + + if ((Test-Path $expectedOutputFile)) { + $expectedOutput = Get-Content -Path $expectedOutputFile -Raw + $actualOutput = Get-Content -Path $outputFile -Raw + if (-not ($expectedOutput -ceq $actualOutput)) { + Write-Host "##vso[task.logissue type=error;]Failed QIR Controller test case: $($_.BaseName)" + Write-Host "##[info]Expected output:" + Write-Host $expectedOutput + Write-Host "##[info]Actual output:" + Write-Host $actualOutput + $script:all_ok = $False + } + else { + Write-Host "##[info]Test case '$($_.BaseName)' passed" + } + continue; } - $expectedErrorFile = (Join-Path $testCasesFolder ($_.BaseName + ".err")) $expectedError = Get-Content -Path $expectedErrorFile -Raw $actualError = Get-Content -Path $errorFile -Raw if (-not ($expectedError -ceq $actualError)) { @@ -45,10 +52,11 @@ Foreach-Object { Write-Host "##[info]Actual error:" Write-Host $actualError $script:all_ok = $False - break + continue + } + else { + Write-Host "##[info]Test case '$($_.BaseName)' passed" } - - Write-Host "##[info]Test case '$($_.BaseName)' passed" } if (-not $all_ok) {