diff --git a/build/test.ps1 b/build/test.ps1 index 89b3061ae4..d3144a25ca 100644 --- a/build/test.ps1 +++ b/build/test.ps1 @@ -43,6 +43,10 @@ function Test-One { --no-build ` --logger trx ` --filter $_ ` + <# Enable additional output from the test logger, but + without also turning on lots of additional noise from msbuild. + #> ` + --logger:"console;verbosity=detailed" ` /property:DefineConstants=$Env:ASSEMBLY_CONSTANTS ` /property:InformationalVersion=$Env:SEMVER_VERSION ` /property:Version=$Env:ASSEMBLY_VERSION diff --git a/src/Core/Loggers/NugetLogger.cs b/src/Core/Loggers/NugetLogger.cs index e8b4e3e0b9..20fb810ce6 100644 --- a/src/Core/Loggers/NugetLogger.cs +++ b/src/Core/Loggers/NugetLogger.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#nullable enable using System; using System.Collections.Generic; @@ -14,31 +15,24 @@ namespace Microsoft.Quantum.IQSharp.Common /// public class NuGetLogger : NuGet.Common.LoggerBase { - private ILogger _logger { get; set; } + private ILogger? _logger { get; set; } public List Logs { get; private set; } - public NuGetLogger(ILogger logger) + public NuGetLogger(ILogger? logger) { _logger = logger; this.Logs = new List(); } - public static LogLevel MapLevel(NuGet.Common.LogLevel original) - { - switch (original) + public static LogLevel MapLevel(NuGet.Common.LogLevel original) => + original switch { - case NuGet.Common.LogLevel.Error: - return LogLevel.Error; - case NuGet.Common.LogLevel.Warning: - return LogLevel.Warning; - case NuGet.Common.LogLevel.Information: - return LogLevel.Information; - case NuGet.Common.LogLevel.Debug: - return LogLevel.Debug; - default: - return LogLevel.Trace; - } - } + NuGet.Common.LogLevel.Error => LogLevel.Error, + NuGet.Common.LogLevel.Warning => LogLevel.Warning, + NuGet.Common.LogLevel.Information => LogLevel.Information, + NuGet.Common.LogLevel.Debug => LogLevel.Debug, + _ => LogLevel.Trace + }; public override void Log(NuGet.Common.ILogMessage m) { diff --git a/src/Core/References/NugetPackages.cs b/src/Core/References/NugetPackages.cs index e766df6a1e..5621991c32 100644 --- a/src/Core/References/NugetPackages.cs +++ b/src/Core/References/NugetPackages.cs @@ -111,7 +111,7 @@ public IEnumerable Repositories public NugetPackages( IOptions config, - ILogger logger + ILogger? logger ) { this.Logger = new NuGetLogger(logger); diff --git a/src/Jupyter/Jupyter.csproj b/src/Jupyter/Jupyter.csproj index 8069c89c43..fe4234dc56 100644 --- a/src/Jupyter/Jupyter.csproj +++ b/src/Jupyter/Jupyter.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/Kernel/CustomShell/ClientInfoHandler.cs b/src/Kernel/CustomShell/ClientInfoHandler.cs index 7b5b4355e7..06b2842dbc 100644 --- a/src/Kernel/CustomShell/ClientInfoHandler.cs +++ b/src/Kernel/CustomShell/ClientInfoHandler.cs @@ -17,34 +17,34 @@ namespace Microsoft.Quantum.IQSharp.Kernel internal class ClientInfoContent : MessageContent { [JsonProperty("hosting_environment")] - public string HostingEnvironment { get; set; } + public string? HostingEnvironment { get; set; } [JsonProperty("iqsharp_version")] - public string IQSharpVersion { get; set; } + public string? IQSharpVersion { get; set; } [JsonProperty("user_agent")] - public string UserAgent { get; set; } + public string? UserAgent { get; set; } [JsonProperty("client_id")] - public string ClientId { get; set; } + public string? ClientId { get; set; } [JsonProperty("client_isnew")] public bool? ClientIsNew { get; set; } [JsonProperty("client_host")] - public string ClientHost { get; set; } + public string? ClientHost { get; set; } [JsonProperty("client_origin")] - public string ClientOrigin { get; set; } + public string? ClientOrigin { get; set; } [JsonProperty("client_first_origin")] - public string ClientFirstOrigin { get; set; } + public string? ClientFirstOrigin { get; set; } [JsonProperty("client_language")] - public string ClientLanguage { get; set; } + public string? ClientLanguage { get; set; } [JsonProperty("client_country")] - public string ClientCountry { get; set; } + public string? ClientCountry { get; set; } [JsonProperty("telemetry_opt_out")] public bool? TelemetryOptOut { get; set; } @@ -69,7 +69,7 @@ internal static ClientInfoContent AsClientInfoContent(this IMetadataController m /// internal class ClientInfoHandler : IShellHandler { - public string UserAgent { get; private set; } + public string? UserAgent { get; private set; } private readonly ILogger logger; private readonly IMetadataController metadata; private readonly IShellServer shellServer; diff --git a/src/Kernel/CustomShell/EchoHandler.cs b/src/Kernel/CustomShell/EchoHandler.cs index dc469b4138..3a109eccbb 100644 --- a/src/Kernel/CustomShell/EchoHandler.cs +++ b/src/Kernel/CustomShell/EchoHandler.cs @@ -13,7 +13,7 @@ namespace Microsoft.Quantum.IQSharp.Kernel internal class EchoReplyContent : MessageContent { [JsonProperty("value")] - public string Value { get; set; } + public string Value { get; set; } = ""; } /// @@ -44,7 +44,7 @@ IShellServer shellServer public async Task HandleAsync(Message message) { // Find out the thing we need to echo back. - var value = (message.Content as UnknownContent).Data["value"] as string; + var value = (message.Content as UnknownContent)?.Data?["value"] as string ?? ""; // Send the echo both as an output and as a reply so that clients // can test both kinds of callbacks. await Task.Run(() => diff --git a/src/Kernel/CustomShell/MessageExtensions.cs b/src/Kernel/CustomShell/MessageExtensions.cs index ce349ad0e7..22341f3c67 100644 --- a/src/Kernel/CustomShell/MessageExtensions.cs +++ b/src/Kernel/CustomShell/MessageExtensions.cs @@ -22,18 +22,28 @@ public static class MessageExtensions public static T To(this Message message) where T : MessageContent { - var content = (message.Content as UnknownContent); - var result = Activator.CreateInstance(); - foreach (var property in typeof(T).GetProperties().Where(p => p.CanWrite)) + if (message.Content is UnknownContent content) { - var jsonPropertyAttribute = property.GetCustomAttributes(true).OfType().FirstOrDefault(); - var propertyName = jsonPropertyAttribute?.PropertyName ?? property.Name; - if (content.Data.TryGetValue(propertyName, out var value)) + var result = Activator.CreateInstance(); + foreach (var property in typeof(T).GetProperties().Where(p => p.CanWrite)) { - property.SetValue(result, content.Data[propertyName]); + var jsonPropertyAttribute = property.GetCustomAttributes(true).OfType().FirstOrDefault(); + var propertyName = jsonPropertyAttribute?.PropertyName ?? property.Name; + if (content.Data.TryGetValue(propertyName, out var value)) + { + property.SetValue(result, content.Data[propertyName]); + } } + return result; + } + else if (message.Content is T decoded) + { + return decoded; + } + else + { + throw new Exception($"Attempted to convert a message with content type {message.Content.GetType()} to content type {typeof(T)}; can only convert from unknown content or subclasses of the desired content type."); } - return result; } } } diff --git a/src/Kernel/IQSharpEngine.cs b/src/Kernel/IQSharpEngine.cs index 85b0300578..d229ea53aa 100644 --- a/src/Kernel/IQSharpEngine.cs +++ b/src/Kernel/IQSharpEngine.cs @@ -19,6 +19,7 @@ using System.Threading.Tasks; using System.Collections.Immutable; using Microsoft.Quantum.IQSharp.AzureClient; +using Microsoft.Quantum.QsCompiler.BondSchemas; namespace Microsoft.Quantum.IQSharp.Kernel { @@ -29,6 +30,24 @@ namespace Microsoft.Quantum.IQSharp.Kernel public class IQSharpEngine : BaseEngine { private readonly PerformanceMonitor performanceMonitor; + private readonly IConfigurationSource configurationSource; + private readonly IServiceProvider services; + private readonly ILogger logger; + + // NB: These properties may be null if the engine has not fully started + // up yet. + internal ISnippets? Snippets { get; private set; } = null; + + internal ISymbolResolver? SymbolsResolver { get; private set; } = null; + + internal IMagicSymbolResolver? MagicResolver { get; private set; } = null; + + internal IWorkspace? Workspace { get; private set; } = null; + + private TaskCompletionSource initializedSource = new TaskCompletionSource(); + + /// + public override Task Initialized => initializedSource.Task; /// /// The main constructor. It expects an `ISnippets` instance that takes care @@ -41,20 +60,42 @@ public IQSharpEngine( IServiceProvider services, IConfigurationSource configurationSource, PerformanceMonitor performanceMonitor, - IShellRouter shellRouter, - IEventService eventService, - IMagicSymbolResolver magicSymbolResolver, - IReferences references + IShellRouter shellRouter ) : base(shell, shellRouter, context, logger, services) { this.performanceMonitor = performanceMonitor; performanceMonitor.Start(); + this.configurationSource = configurationSource; + this.services = services; + this.logger = logger; + } + + /// + public override void Start() + { + base.Start(); + // Before anything else, make sure to start the right background + // thread on the Q# compilation loader to initialize serializers + // and deserializers. Since that runs in the background, starting + // the engine should not be blocked, and other services can + // continue to initialize while the Q# compilation loader works. + // + // For more details, see: + // https://github.com/microsoft/qsharp-compiler/pull/727 + // https://github.com/microsoft/qsharp-compiler/pull/810 + logger.LogDebug("Loading serialization and deserialziation protocols."); + Protocols.Initialize(); + + logger.LogDebug("Getting services required to start IQ# engine."); this.Snippets = services.GetService(); this.SymbolsResolver = services.GetService(); - this.MagicResolver = magicSymbolResolver; + this.MagicResolver = services.GetService(); this.Workspace = services.GetService(); + var references = services.GetService(); + var eventService = services.GetService(); + logger.LogDebug("Registering IQ# display and JSON encoders."); RegisterDisplayEncoder(new IQSharpSymbolToHtmlResultEncoder()); RegisterDisplayEncoder(new IQSharpSymbolToTextResultEncoder()); RegisterDisplayEncoder(new TaskStatusToTextEncoder()); @@ -71,13 +112,15 @@ IReferences references .Concat(AzureClient.JsonConverters.AllConverters) .ToArray()); + logger.LogDebug("Registering IQ# symbol resolvers."); RegisterSymbolResolver(this.SymbolsResolver); RegisterSymbolResolver(this.MagicResolver); + logger.LogDebug("Loading known assemblies and registering package loading."); RegisterPackageLoadedEvent(services, logger, references); // Handle new shell messages. - shellRouter.RegisterHandlers(); + ShellRouter.RegisterHandlers(); // Report performance after completing startup. performanceMonitor.Report(); @@ -86,8 +129,12 @@ IReferences references Process.GetCurrentProcess().Id ); - eventService?.TriggerServiceInitialized(this); + + var initializedSuccessfully = initializedSource.TrySetResult(true); + #if DEBUG + Debug.Assert(initializedSuccessfully, "Was unable to complete initialization task."); + #endif } /// @@ -108,14 +155,17 @@ private void RegisterPackageLoadedEvent(IServiceProvider services, ILogger logge // Register new display encoders when packages load. references.PackageLoaded += (sender, args) => { - logger.LogDebug("Scanning for display encoders after loading {Package}.", args.PackageId); + logger.LogDebug("Scanning for display encoders and magic symbols after loading {Package}.", args.PackageId); foreach (var assembly in references.Assemblies .Select(asm => asm.Assembly) .Where(asm => !knownAssemblies.Contains(asm.GetName())) ) { // Look for display encoders in the new assembly. - logger.LogDebug("Found new assembly {Name}, looking for display encoders.", assembly.FullName); + logger.LogDebug("Found new assembly {Name}, looking for display encoders and magic symbols.", assembly.FullName); + // Use the magic resolver to find magic symbols in the new assembly; + // it will cache the results for the next magic resolution. + this.MagicResolver?.FindMagic(new AssemblyInfo(assembly)); // If types from an assembly cannot be loaded, log a warning and continue. var relevantTypes = Enumerable.Empty(); @@ -167,14 +217,6 @@ private void RegisterPackageLoadedEvent(IServiceProvider services, ILogger logge }; } - internal ISnippets Snippets { get; } - - internal ISymbolResolver SymbolsResolver { get; } - - internal ISymbolResolver MagicResolver { get; } - - internal IWorkspace Workspace { get; } - /// /// This is the method used to execute Jupyter "normal" cells. In this case, a normal /// cell is expected to have a Q# snippet, which gets compiled and we return the name of @@ -188,9 +230,15 @@ public override async Task ExecuteMundane(string input, IChanne { try { - await Workspace.Initialization; + await this.Initialized; + // Once the engine is initialized, we know that Workspace + // and Snippets are both not-null. + var workspace = this.Workspace!; + var snippets = this.Snippets!; + + await workspace.Initialization; - var code = Snippets.Compile(input); + var code = snippets.Compile(input); foreach (var m in code.warnings) { channel.Stdout(m); } diff --git a/src/Kernel/Kernel.csproj b/src/Kernel/Kernel.csproj index 78f2e8028e..8c5f7f668e 100644 --- a/src/Kernel/Kernel.csproj +++ b/src/Kernel/Kernel.csproj @@ -6,6 +6,7 @@ Microsoft.Quantum.IQSharp.Kernel Microsoft.Quantum.IQSharp.Kernel true + enable diff --git a/src/Kernel/KernelApp/IQSharpKernelApp.cs b/src/Kernel/KernelApp/IQSharpKernelApp.cs index b601366848..3df1806039 100644 --- a/src/Kernel/KernelApp/IQSharpKernelApp.cs +++ b/src/Kernel/KernelApp/IQSharpKernelApp.cs @@ -63,7 +63,7 @@ public static class KernelEventsExtensions /// /// The event service where the EventSubPub lives /// The typed EventPubSub for the KernelStarted event - public static EventPubSub OnKernelStarted(this IEventService eventService) + public static EventPubSub? OnKernelStarted(this IEventService eventService) { return eventService?.Events(); } @@ -73,7 +73,7 @@ public static EventPubSub OnKernelStarted( /// /// The event service where the EventSubPub lives /// The typed EventPubSub for the KernelStopped event - public static EventPubSub OnKernelStopped(this IEventService eventService) + public static EventPubSub? OnKernelStopped(this IEventService eventService) { return eventService?.Events(); } diff --git a/src/Kernel/Magic/LsMagicMagic.cs b/src/Kernel/Magic/LsMagicMagic.cs index 24004e80ed..04412f93cf 100644 --- a/src/Kernel/Magic/LsMagicMagic.cs +++ b/src/Kernel/Magic/LsMagicMagic.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Linq; using Microsoft.Jupyter.Core; @@ -54,8 +55,15 @@ as well as those defined in any packages that have been loaded in the current }) { this.resolver = resolver; - (engine as IQSharpEngine).RegisterDisplayEncoder(new MagicSymbolSummariesToHtmlEncoder()); - (engine as IQSharpEngine).RegisterDisplayEncoder(new MagicSymbolSummariesToTextEncoder()); + if (engine is IQSharpEngine iQSharpEngine) + { + iQSharpEngine.RegisterDisplayEncoder(new MagicSymbolSummariesToHtmlEncoder()); + iQSharpEngine.RegisterDisplayEncoder(new MagicSymbolSummariesToTextEncoder()); + } + else + { + throw new Exception($"Expected execution engine to be an IQ# engine, but was {engine.GetType()}."); + } } /// diff --git a/src/Kernel/Magic/Resolution/IMagicResolver.cs b/src/Kernel/Magic/Resolution/IMagicResolver.cs index a827328592..57f5d61f36 100644 --- a/src/Kernel/Magic/Resolution/IMagicResolver.cs +++ b/src/Kernel/Magic/Resolution/IMagicResolver.cs @@ -20,7 +20,7 @@ namespace Microsoft.Quantum.IQSharp.Kernel /// public interface IMagicSymbolResolver : ISymbolResolver { - ISymbol ISymbolResolver.Resolve(string symbolName) => + ISymbol? ISymbolResolver.Resolve(string symbolName) => this.Resolve(symbolName); /// @@ -29,11 +29,16 @@ ISymbol ISymbolResolver.Resolve(string symbolName) => /// /// The magic symbol name to resolve. /// The resolved object, or null if none was found. - public new MagicSymbol Resolve(string symbolName); + public new MagicSymbol? Resolve(string symbolName); /// /// Returns the list of all objects defined in loaded assemblies. /// public IEnumerable FindAllMagicSymbols(); + + /// + /// Finds the MagicSymbols inside an assembly, and returns an instance of each. + /// + public IEnumerable FindMagic(AssemblyInfo assm); } } diff --git a/src/Kernel/Magic/Resolution/MagicResolver.cs b/src/Kernel/Magic/Resolution/MagicResolver.cs index dae5b7c112..a09eda8fa7 100644 --- a/src/Kernel/Magic/Resolution/MagicResolver.cs +++ b/src/Kernel/Magic/Resolution/MagicResolver.cs @@ -84,7 +84,7 @@ private IEnumerable RelevantAssemblies() /// Symbol names without a dot are resolved to the first symbol /// whose base name matches the given name. /// - public MagicSymbol Resolve(string symbolName) + public MagicSymbol? Resolve(string symbolName) { if (symbolName == null || !symbolName.TrimStart().StartsWith("%")) return null; @@ -102,59 +102,76 @@ public MagicSymbol Resolve(string symbolName) return null; } - /// - /// Finds the MagicSymbols inside an assembly, and returns an instance of each. - /// + /// public IEnumerable FindMagic(AssemblyInfo assm) { var result = new MagicSymbol[0]; - if (cache.TryGetValue(assm, out result)) + lock (cache) { - return result; - } - - this.logger.LogInformation($"Looking for MagicSymbols in {assm.Assembly.FullName}"); + if (cache.TryGetValue(assm, out result)) + { + return result; + } - // If types from an assembly cannot be loaded, log a warning and continue. - var allMagic = new List(); - try - { - var magicTypes = assm.Assembly - .GetTypes() - .Where(t => - { - if (!t.IsClass && t.IsAbstract) { return false; } - var matched = t.IsSubclassOf(typeof(MagicSymbol)); - this.logger.LogDebug("Class {Class} subclass of MagicSymbol? {Matched}", t.FullName, matched); - return matched; - }); + this.logger.LogInformation($"Looking for MagicSymbols in {assm.Assembly.FullName}"); - foreach (var t in magicTypes) + // If types from an assembly cannot be loaded, log a warning and continue. + var allMagic = new List(); + try { - try + var magicTypes = assm.Assembly + .GetTypes() + .Where(t => + { + if (!t.IsClass && t.IsAbstract) { return false; } + var matched = t.IsSubclassOf(typeof(MagicSymbol)); + + // This logging statement is expensive, so we only run it when we need to for debugging. + #if DEBUG + this.logger.LogDebug("Class {Class} subclass of MagicSymbol? {Matched}", t.FullName, matched); + #endif + + return matched; + }); + + foreach (var t in magicTypes) { - var m = ActivatorUtilities.CreateInstance(services, t) as MagicSymbol; - allMagic.Add(m); - this.logger.LogInformation($"Found MagicSymbols {m.Name} ({t.FullName})"); - } - catch (Exception e) - { - this.logger.LogWarning($"Unable to create instance of MagicSymbol {t.FullName}. Magic will not be enabled.\nMessage:{e.Message}"); + try + { + var symbol = ActivatorUtilities.CreateInstance(services, t); + if (symbol is MagicSymbol magic) + { + allMagic.Add(magic); + this.logger.LogInformation($"Found MagicSymbols {magic.Name} ({t.FullName})"); + } + else if (symbol is {}) + { + throw new Exception($"Unable to create magic symbol of type {t}; service activator returned an object of type {symbol.GetType()}, which is not a subtype of MagicSymbol."); + } + else if (symbol == null) + { + throw new Exception($"Unable to create magic symbol of type {t}; service activator returned null."); + } + } + catch (Exception e) + { + this.logger.LogWarning($"Unable to create instance of MagicSymbol {t.FullName}. Magic will not be enabled.\nMessage:{e.Message}"); + } } } - } - catch (Exception ex) - { - this.logger.LogWarning( - ex, - "Encountered exception loading types from {AssemblyName}.", - assm.Assembly.FullName - ); - } + catch (Exception ex) + { + this.logger.LogWarning( + ex, + "Encountered exception loading types from {AssemblyName}.", + assm.Assembly.FullName + ); + } - result = allMagic.ToArray(); - cache[assm] = result; + result = allMagic.ToArray(); + cache[assm] = result; + } return result; } diff --git a/src/Tests/IQsharpEngineTests.cs b/src/Tests/IQsharpEngineTests.cs index 358b155cda..b7b58f4b1c 100644 --- a/src/Tests/IQsharpEngineTests.cs +++ b/src/Tests/IQsharpEngineTests.cs @@ -27,10 +27,13 @@ namespace Tests.IQSharp [TestClass] public class IQSharpEngineTests { - public IQSharpEngine Init(string workspace = "Workspace") + public async Task Init(string workspace = "Workspace") { var engine = Startup.Create(workspace); - engine.Workspace.Initialization.Wait(); + engine.Start(); + await engine.Initialized; + Assert.IsNotNull(engine.Workspace); + await engine.Workspace!.Initialization; return engine; } @@ -46,7 +49,17 @@ public static void PrintResult(ExecutionResult result, MockChannel channel) foreach (var m in channel.msgs) Console.WriteLine($" {m}"); } - public static async Task AssertCompile(IQSharpEngine engine, string source, params string[] expectedOps) + public static string SessionAsString(IEnumerable session) => + string.Join("\n", + session.Select(message => + $"\tHeader: {JsonConvert.SerializeObject(message.Header)}\n" + + $"\tParent header: {JsonConvert.SerializeObject(message.ParentHeader)}\n" + + $"\tMetadata: {JsonConvert.SerializeObject(message.Metadata)}\n" + + $"\tContent: {JsonConvert.SerializeObject(message.Content)}\n\n" + ) + ); + + public static async Task AssertCompile(IQSharpEngine engine, string source, params string[] expectedOps) { var channel = new MockChannel(); var response = await engine.ExecuteMundane(source, channel); @@ -57,10 +70,12 @@ public static async Task AssertCompile(IQSharpEngine engine, string sour return response.Output?.ToString(); } - public static async Task AssertSimulate(IQSharpEngine engine, string snippetName, params string[] messages) + public static async Task AssertSimulate(IQSharpEngine engine, string snippetName, params string[] messages) { + await engine.Initialized; var configSource = new ConfigurationSource(skipLoading: true); - var simMagic = new SimulateMagic(engine.SymbolsResolver, configSource); + + var simMagic = new SimulateMagic(engine.SymbolsResolver!, configSource); var channel = new MockChannel(); var response = await simMagic.Execute(snippetName, channel); PrintResult(response, channel); @@ -70,17 +85,20 @@ public static async Task AssertSimulate(IQSharpEngine engine, string sni return response.Output?.ToString(); } - public static async Task AssertEstimate(IQSharpEngine engine, string snippetName, params string[] messages) + public static async Task AssertEstimate(IQSharpEngine engine, string snippetName, params string[] messages) { + await engine.Initialized; + Assert.IsNotNull(engine.SymbolsResolver); + var channel = new MockChannel(); - var estimateMagic = new EstimateMagic(engine.SymbolsResolver); + var estimateMagic = new EstimateMagic(engine.SymbolsResolver!); var response = await estimateMagic.Execute(snippetName, channel); var result = response.Output as DataTable; PrintResult(response, channel); Assert.AreEqual(ExecuteStatus.Ok, response.Status); Assert.IsNotNull(result); - Assert.AreEqual(9, result.Rows.Count); - var keys = result.Rows.Cast().Select(row => row.ItemArray[0]).ToList(); + Assert.AreEqual(9, result?.Rows.Count); + var keys = result?.Rows.Cast().Select(row => row.ItemArray[0]).ToList(); CollectionAssert.Contains(keys, "T"); CollectionAssert.Contains(keys, "CNOT"); CollectionAssert.AreEqual(messages.Select(ChannelWithNewLines.Format).ToArray(), channel.msgs.ToArray()); @@ -90,13 +108,15 @@ public static async Task AssertEstimate(IQSharpEngine engine, string sni private async Task AssertTrace(string name, ExecutionPath expectedPath, int expectedDepth) { - var engine = Init("Workspace.ExecutionPathTracer"); + var engine = await Init("Workspace.ExecutionPathTracer"); var snippets = engine.Snippets as Snippets; + Assert.IsNotNull(snippets); + Assert.IsNotNull(engine.SymbolsResolver); var configSource = new ConfigurationSource(skipLoading: true); - var wsMagic = new WorkspaceMagic(snippets.Workspace); + var wsMagic = new WorkspaceMagic(snippets!.Workspace); var pkgMagic = new PackageMagic(snippets.GlobalReferences); - var traceMagic = new TraceMagic(engine.SymbolsResolver, configSource); + var traceMagic = new TraceMagic(engine.SymbolsResolver!, configSource); var channel = new MockChannel(); @@ -121,26 +141,27 @@ private async Task AssertTrace(string name, ExecutionPath expectedPath, int expe var content = message.Content as ExecutionPathVisualizerContent; Assert.IsNotNull(content); - Assert.AreEqual(expectedDepth, content.RenderDepth); + Assert.AreEqual(expectedDepth, content?.RenderDepth); - var path = content.ExecutionPath.ToObject(); + var path = content?.ExecutionPath.ToObject(); Assert.IsNotNull(path); - Assert.AreEqual(expectedPath.ToJson(), path.ToJson()); + Assert.AreEqual(expectedPath.ToJson(), path!.ToJson()); } [TestMethod] public async Task CompileOne() { - var engine = Init(); + var engine = await Init(); await AssertCompile(engine, SNIPPETS.HelloQ, "HelloQ"); } [TestMethod] public async Task CompileAndSimulate() { - var engine = Init(); + var engine = await Init(); + Assert.IsNotNull(engine.SymbolsResolver); var configSource = new ConfigurationSource(skipLoading: true); - var simMagic = new SimulateMagic(engine.SymbolsResolver, configSource); + var simMagic = new SimulateMagic(engine.SymbolsResolver!, configSource); var channel = new MockChannel(); // Try running without compiling it, fails: @@ -161,7 +182,7 @@ public async Task CompileAndSimulate() [TestMethod] public async Task SimulateWithArguments() { - var engine = Init(); + var engine = await Init(); // Compile it: await AssertCompile(engine, SNIPPETS.Reverse, "Reverse"); @@ -174,7 +195,7 @@ public async Task SimulateWithArguments() [TestMethod] public async Task OpenNamespaces() { - var engine = Init(); + var engine = await Init(); // Compile: await AssertCompile(engine, SNIPPETS.OpenNamespaces1); @@ -188,7 +209,7 @@ public async Task OpenNamespaces() [TestMethod] public async Task OpenAliasedNamespaces() { - var engine = Init(); + var engine = await Init(); // Compile: await AssertCompile(engine, SNIPPETS.OpenAliasedNamespace); @@ -201,7 +222,7 @@ public async Task OpenAliasedNamespaces() [TestMethod] public async Task CompileApplyWithin() { - var engine = Init(); + var engine = await Init(); // Compile: await AssertCompile(engine, SNIPPETS.ApplyWithinBlock, "ApplyWithinBlock"); @@ -213,7 +234,7 @@ public async Task CompileApplyWithin() [TestMethod] public async Task Estimate() { - var engine = Init(); + var engine = await Init(); var channel = new MockChannel(); // Compile it: @@ -226,14 +247,15 @@ public async Task Estimate() [TestMethod] public async Task Toffoli() { - var engine = Init(); + var engine = await Init(); var channel = new MockChannel(); + Assert.IsNotNull(engine.SymbolsResolver); // Compile it: await AssertCompile(engine, SNIPPETS.HelloQ, "HelloQ"); // Run with toffoli simulator: - var toffoliMagic = new ToffoliMagic(engine.SymbolsResolver); + var toffoliMagic = new ToffoliMagic(engine.SymbolsResolver!); var response = await toffoliMagic.Execute("HelloQ", channel); var result = response.Output as Dictionary; PrintResult(response, channel); @@ -245,7 +267,7 @@ public async Task Toffoli() [TestMethod] public async Task DependsOnWorkspace() { - var engine = Init(); + var engine = await Init(); // Compile it: await AssertCompile(engine, SNIPPETS.DependsOnWorkspace, "DependsOnWorkspace"); @@ -258,7 +280,7 @@ public async Task DependsOnWorkspace() [TestMethod] public async Task UpdateSnippet() { - var engine = Init(); + var engine = await Init(); // Compile it: await AssertCompile(engine, SNIPPETS.HelloQ, "HelloQ"); @@ -276,7 +298,7 @@ public async Task UpdateSnippet() [TestMethod] public async Task DumpToFile() { - var engine = Init(); + var engine = await Init(); // Compile DumpMachine snippet. await AssertCompile(engine, SNIPPETS.DumpToFile, "DumpToFile"); @@ -304,7 +326,7 @@ public async Task DumpToFile() [TestMethod] public async Task UpdateDependency() { - var engine = Init(); + var engine = await Init(); // Compile HelloQ await AssertCompile(engine, SNIPPETS.HelloQ, "HelloQ"); @@ -322,7 +344,7 @@ public async Task UpdateDependency() [TestMethod] public async Task ReportWarnings() { - var engine = Init(); + var engine = await Init(); { var channel = new MockChannel(); @@ -331,7 +353,9 @@ public async Task ReportWarnings() Assert.AreEqual(ExecuteStatus.Ok, response.Status); Assert.AreEqual(3, channel.msgs.Count); Assert.AreEqual(0, channel.errors.Count); - Assert.AreEqual("ThreeWarnings", new ListToTextResultEncoder().Encode(response.Output).Value.Data); + Assert.AreEqual("ThreeWarnings", + new ListToTextResultEncoder().Encode(response.Output)?.Data + ); } { @@ -341,14 +365,16 @@ public async Task ReportWarnings() Assert.AreEqual(ExecuteStatus.Ok, response.Status); Assert.AreEqual(1, channel.msgs.Count); Assert.AreEqual(0, channel.errors.Count); - Assert.AreEqual("OneWarning", new ListToTextResultEncoder().Encode(response.Output).Value.Data); + Assert.AreEqual("OneWarning", + new ListToTextResultEncoder().Encode(response.Output)?.Data + ); } } [TestMethod] public async Task ReportErrors() { - var engine = Init(); + var engine = await Init(); var channel = new MockChannel(); var response = await engine.ExecuteMundane(SNIPPETS.TwoErrors, channel); @@ -361,10 +387,11 @@ public async Task ReportErrors() [TestMethod] public async Task TestPackages() { - var engine = Init(); + var engine = await Init(); var snippets = engine.Snippets as Snippets; + Assert.IsNotNull(snippets); - var pkgMagic = new PackageMagic(snippets.GlobalReferences); + var pkgMagic = new PackageMagic(snippets!.GlobalReferences); var references = ((References)pkgMagic.References); var packageCount = references.AutoLoadPackages.Count; var channel = new MockChannel(); @@ -373,9 +400,9 @@ public async Task TestPackages() PrintResult(response, channel); Assert.AreEqual(ExecuteStatus.Ok, response.Status); Assert.AreEqual(0, channel.msgs.Count); - Assert.AreEqual(packageCount, result.Length); - Assert.AreEqual("Microsoft.Quantum.Standard::0.0.0", result[0]); - Assert.AreEqual("Microsoft.Quantum.Standard.Visualization::0.0.0", result[1]); + Assert.AreEqual(packageCount, result?.Length); + Assert.AreEqual("Microsoft.Quantum.Standard::0.0.0", result?[0]); + Assert.AreEqual("Microsoft.Quantum.Standard.Visualization::0.0.0", result?[1]); // Try compiling TrotterEstimateEnergy, it should fail due to the lack // of chemistry package. @@ -389,7 +416,7 @@ public async Task TestPackages() Assert.AreEqual(ExecuteStatus.Ok, response.Status); Assert.AreEqual(0, channel.msgs.Count); Assert.IsNotNull(result); - Assert.AreEqual(packageCount + 1, result.Length); + Assert.AreEqual(packageCount + 1, result?.Length); // Now it should compile: await AssertCompile(engine, SNIPPETS.UseJordanWignerEncodingData, "UseJordanWignerEncodingData"); @@ -398,9 +425,10 @@ public async Task TestPackages() [TestMethod] public async Task TestInvalidPackages() { - var engine = Init(); + var engine = await Init(); var snippets = engine.Snippets as Snippets; - var pkgMagic = new PackageMagic(snippets.GlobalReferences); + Assert.IsNotNull(snippets); + var pkgMagic = new PackageMagic(snippets!.GlobalReferences); var channel = new MockChannel(); var response = await pkgMagic.Execute("microsoft.invalid.quantum", channel); @@ -415,15 +443,16 @@ public async Task TestInvalidPackages() [TestMethod] public async Task TestProjectMagic() { - var engine = Init(); + var engine = await Init(); var snippets = engine.Snippets as Snippets; - var projectMagic = new ProjectMagic(snippets.Workspace); + Assert.IsNotNull(snippets); + var projectMagic = new ProjectMagic(snippets!.Workspace); var channel = new MockChannel(); var response = await projectMagic.Execute("../Workspace.ProjectReferences/Workspace.ProjectReferences.csproj", channel); Assert.AreEqual(ExecuteStatus.Ok, response.Status); var loadedProjectFiles = response.Output as string[]; - Assert.AreEqual(3, loadedProjectFiles.Length); + Assert.AreEqual(3, loadedProjectFiles?.Length); } [TestMethod] @@ -441,19 +470,20 @@ public async Task TestWho() var result = response.Output as string[]; PrintResult(response, channel); Assert.AreEqual(ExecuteStatus.Ok, response.Status); - Assert.AreEqual(5, result.Length); - Assert.AreEqual("HelloQ", result[0]); - Assert.AreEqual("Tests.qss.NoOp", result[4]); + Assert.AreEqual(5, result?.Length); + Assert.AreEqual("HelloQ", result?[0]); + Assert.AreEqual("Tests.qss.NoOp", result?[4]); } [TestMethod] public async Task TestWorkspace() { - var engine = Init("Workspace.Chemistry"); + var engine = await Init("Workspace.Chemistry"); var snippets = engine.Snippets as Snippets; + Assert.IsNotNull(snippets); - var wsMagic = new WorkspaceMagic(snippets.Workspace); - var pkgMagic = new PackageMagic(snippets.GlobalReferences); + var wsMagic = new WorkspaceMagic(snippets!.Workspace); + var pkgMagic = new PackageMagic(snippets!.GlobalReferences); var channel = new MockChannel(); var result = new string[0]; @@ -490,7 +520,7 @@ public async Task TestWorkspace() result = response.Output as string[]; PrintResult(response, channel); Assert.AreEqual(ExecuteStatus.Ok, response.Status); - Assert.AreEqual(2, result.Length); + Assert.AreEqual(2, result?.Length); // Compilation must work: await AssertCompile(engine, SNIPPETS.DependsOnChemistryWorkspace, "DependsOnChemistryWorkspace"); @@ -518,26 +548,26 @@ public async Task TestResolver() // Intrinsics: var symbol = resolver.Resolve("X"); Assert.IsNotNull(symbol); - Assert.AreEqual("Microsoft.Quantum.Intrinsic.X", symbol.Name); + Assert.AreEqual("Microsoft.Quantum.Intrinsic.X", symbol!.Name); // FQN Intrinsics: symbol = resolver.Resolve("Microsoft.Quantum.Intrinsic.X"); Assert.IsNotNull(symbol); - Assert.AreEqual("Microsoft.Quantum.Intrinsic.X", symbol.Name); + Assert.AreEqual("Microsoft.Quantum.Intrinsic.X", symbol!.Name); // From namespace: symbol = resolver.Resolve("Tests.qss.CCNOTDriver"); Assert.IsNotNull(symbol); - Assert.AreEqual("Tests.qss.CCNOTDriver", symbol.Name); + Assert.AreEqual("Tests.qss.CCNOTDriver", symbol!.Name); symbol = resolver.Resolve("CCNOTDriver"); Assert.IsNotNull(symbol); - Assert.AreEqual("Tests.qss.CCNOTDriver", symbol.Name); + Assert.AreEqual("Tests.qss.CCNOTDriver", symbol!.Name); // Snippets: symbol = resolver.Resolve("HelloQ"); Assert.IsNotNull(symbol); - Assert.AreEqual("HelloQ", symbol.Name); + Assert.AreEqual("HelloQ", symbol!.Name); // resolver is case sensitive: symbol = resolver.Resolve("helloq"); @@ -553,13 +583,17 @@ public void TestResolveMagic() { var resolver = Startup.Create("Workspace.Broken"); + // We use the null-forgiving operator on symbol below, as the C# 8 + // nullable reference feature does not incorporate the result of + // Assert.IsNotNull. + var symbol = resolver.Resolve("%workspace"); Assert.IsNotNull(symbol); - Assert.AreEqual("%workspace", symbol.Name); + Assert.AreEqual("%workspace", symbol!.Name); symbol = resolver.Resolve("%package") as MagicSymbol; Assert.IsNotNull(symbol); - Assert.AreEqual("%package", symbol.Name); + Assert.AreEqual("%package", symbol!.Name); Assert.IsNotNull(resolver.Resolve("%who")); Assert.IsNotNull(resolver.Resolve("%estimate")); @@ -581,11 +615,12 @@ public void TestResolveMagic() [TestMethod] public async Task TestDebugMagic() { - var engine = Init(); + var engine = await Init(); + Assert.IsNotNull(engine.SymbolsResolver); await AssertCompile(engine, SNIPPETS.SimpleDebugOperation, "SimpleDebugOperation"); var configSource = new ConfigurationSource(skipLoading: true); - var debugMagic = new DebugMagic(engine.SymbolsResolver, configSource, engine.ShellRouter, engine.ShellServer, null); + var debugMagic = new DebugMagic(engine.SymbolsResolver!, configSource, engine.ShellRouter, engine.ShellServer, null); // Start a debug session var channel = new MockChannel(); @@ -599,7 +634,7 @@ public async Task TestDebugMagic() var content = message.Content as DebugSessionContent; Assert.IsNotNull(content); - var debugSessionId = content.DebugSession; + var debugSessionId = content!.DebugSession; // Send several iqsharp_debug_advance messages var debugAdvanceMessage = new Message @@ -631,26 +666,42 @@ public async Task TestDebugMagic() Assert.AreEqual(System.Threading.Tasks.TaskStatus.RanToCompletion, debugTask.Status); // Ensure that expected messages were sent - Assert.AreEqual("iqsharp_debug_sessionstart", channel.iopubMessages[0].Header.MessageType); - Assert.AreEqual("iqsharp_debug_opstart", channel.iopubMessages[1].Header.MessageType); - Assert.AreEqual("iqsharp_debug_sessionend", channel.iopubMessages.Last().Header.MessageType); + try + { + Assert.AreEqual("iqsharp_debug_sessionstart", channel.iopubMessages[0].Header.MessageType); + Assert.AreEqual("iqsharp_debug_opstart", channel.iopubMessages[1].Header.MessageType); + Assert.AreEqual("iqsharp_debug_sessionend", channel.iopubMessages.Last().Header.MessageType); + } + catch (AssertFailedException ex) + { + await Console.Error.WriteLineAsync( + "IOPub messages sent by %debug were incorrect.\nReceived messages:\n" + + SessionAsString(channel.iopubMessages) + ); + throw ex; + } Assert.IsTrue(channel.msgs[0].Contains("Starting debug session")); Assert.IsTrue(channel.msgs[1].Contains("Finished debug session")); // Verify debug status content var debugStatusContent = channel.iopubMessages[1].Content as DebugStatusContent; - Assert.IsNotNull(debugStatusContent.State); - Assert.AreEqual(debugSessionId, debugStatusContent.DebugSession); + Assert.IsNotNull(debugStatusContent?.State); + Assert.AreEqual(debugSessionId, debugStatusContent?.DebugSession); } [TestMethod] public async Task TestDebugMagicCancel() { - var engine = Init(); + var engine = await Init(); + // Since Init guarantees that engine services have started, we + // assert non-nullness here. + Assert.IsNotNull(engine.SymbolsResolver); await AssertCompile(engine, SNIPPETS.SimpleDebugOperation, "SimpleDebugOperation"); var configSource = new ConfigurationSource(skipLoading: true); - var debugMagic = new DebugMagic(engine.SymbolsResolver, configSource, engine.ShellRouter, engine.ShellServer, null); + // We asserted that SymbolsResolver is not null above, and can use + // the null-forgiving operator here as a result. + var debugMagic = new DebugMagic(engine.SymbolsResolver!, configSource, engine.ShellRouter, engine.ShellServer, null); // Start a debug session var channel = new MockChannel(); @@ -664,8 +715,19 @@ public async Task TestDebugMagicCancel() Assert.ThrowsException(() => debugTask.Wait()); // Ensure that expected messages were sent - Assert.AreEqual("iqsharp_debug_sessionstart", channel.iopubMessages[0].Header.MessageType); - Assert.AreEqual("iqsharp_debug_sessionend", channel.iopubMessages[1].Header.MessageType); + try + { + Assert.AreEqual("iqsharp_debug_sessionstart", channel.iopubMessages[0].Header.MessageType); + Assert.AreEqual("iqsharp_debug_sessionend", channel.iopubMessages[1].Header.MessageType); + } + catch (AssertFailedException ex) + { + await Console.Error.WriteLineAsync( + "IOPub messages sent by %debug were incorrect.\nReceived messages:\n" + + SessionAsString(channel.iopubMessages) + ); + throw ex; + } Assert.IsTrue(channel.msgs[0].Contains("Starting debug session")); Assert.IsTrue(channel.msgs[1].Contains("Finished debug session")); } diff --git a/src/Tests/NugetPackagesTests.cs b/src/Tests/NugetPackagesTests.cs index e4adae231b..d1f708c864 100644 --- a/src/Tests/NugetPackagesTests.cs +++ b/src/Tests/NugetPackagesTests.cs @@ -36,7 +36,7 @@ public async Task GetLatestVersion() { var mgr = Init(); - async Task TestOne(string pkg, string version) + async Task TestOne(string pkg, string? version) { var actual = await mgr.GetLatestVersion(pkg); diff --git a/src/Tests/SerializationTests.cs b/src/Tests/SerializationTests.cs index 0a714710a9..03a37e36a0 100644 --- a/src/Tests/SerializationTests.cs +++ b/src/Tests/SerializationTests.cs @@ -36,9 +36,9 @@ public async Task SerializeFlatUdtInstance() { var complex = new Complex((12.0, 1.4)); var token = JToken.Parse(JsonConvert.SerializeObject(complex, JsonConverters.TupleConverters)); - Assert.AreEqual(typeof(Complex).FullName, token["@type"].Value()); - Assert.AreEqual(12, token["Item1"].Value()); - Assert.AreEqual(1.4, token["Item2"].Value()); + Assert.AreEqual(typeof(Complex).FullName, token?["@type"]?.Value()); + Assert.AreEqual(12, token?["Item1"]?.Value()); + Assert.AreEqual(1.4, token?["Item2"]?.Value()); } [TestMethod] @@ -62,13 +62,13 @@ public async Task SerializeNestedUdtInstance() var jsonData = JsonConvert.SerializeObject(testValue, JsonConverters.TupleConverters); System.Console.WriteLine(jsonData); var token = JToken.Parse(jsonData); - Assert.AreEqual(typeof(QubitState).FullName, token["@type"].Value()); - Assert.AreEqual(typeof(Complex).FullName, token["Item1"]["@type"].Value()); - Assert.AreEqual(0.1, token["Item1"]["Item1"].Value()); - Assert.AreEqual(0.2, token["Item1"]["Item2"].Value()); - Assert.AreEqual(typeof(Complex).FullName, token["Item2"]["@type"].Value()); - Assert.AreEqual(0.3, token["Item2"]["Item1"].Value()); - Assert.AreEqual(0.4, token["Item2"]["Item2"].Value()); + Assert.AreEqual(typeof(QubitState).FullName, token?["@type"]?.Value()); + Assert.AreEqual(typeof(Complex).FullName, token?["Item1"]?["@type"]?.Value()); + Assert.AreEqual(0.1, token?["Item1"]?["Item1"]?.Value()); + Assert.AreEqual(0.2, token?["Item1"]?["Item2"]?.Value()); + Assert.AreEqual(typeof(Complex).FullName, token?["Item2"]?["@type"]?.Value()); + Assert.AreEqual(0.3, token?["Item2"]?["Item1"]?.Value()); + Assert.AreEqual(0.4, token?["Item2"]?["Item2"]?.Value()); } [TestMethod] diff --git a/src/Tests/SnippetsControllerTests.cs b/src/Tests/SnippetsControllerTests.cs index cf4c88bc29..2126ff3f31 100644 --- a/src/Tests/SnippetsControllerTests.cs +++ b/src/Tests/SnippetsControllerTests.cs @@ -174,7 +174,7 @@ public async Task DependsOnWorkspace() // Run: var results = await AssertSimulate(controller, "DependsOnWorkspace", "Hello Foo again!") as QArray; Assert.IsNotNull(results); - Assert.AreEqual(5, results.Length); + Assert.AreEqual(5, results!.Length); } [TestMethod] diff --git a/src/Tests/TelemetryTests.cs b/src/Tests/TelemetryTests.cs index 770364fb3f..2c7c438823 100644 --- a/src/Tests/TelemetryTests.cs +++ b/src/Tests/TelemetryTests.cs @@ -39,6 +39,16 @@ public class TelemetryTests { public static readonly Type TelemetryServiceType = typeof(MockTelemetryService); + public IQSharpEngine Init(IServiceProvider services) + { + var engine = services.GetService() as IQSharpEngine; + engine.Start(); + engine.Initialized.Wait(); + Assert.IsNotNull(engine.Workspace); + engine.Workspace!.Initialization.Wait(); + return engine; + } + [TestMethod] public void MockTelemetryService() { @@ -249,7 +259,7 @@ public void JupyterActions() var services = Startup.CreateServiceProvider(workspace); var logger = GetAppLogger(services); - var engine = services.GetService() as IQSharpEngine; + var engine = Init(services); var channel = new MockChannel(); logger.Events.Clear(); diff --git a/src/Tests/Tests.IQsharp.csproj b/src/Tests/Tests.IQsharp.csproj index 4dbee92f77..507ec365cd 100644 --- a/src/Tests/Tests.IQsharp.csproj +++ b/src/Tests/Tests.IQsharp.csproj @@ -5,6 +5,7 @@ x64 false 1701 + enable diff --git a/src/Tests/WorkspaceTests.cs b/src/Tests/WorkspaceTests.cs index 9b4fe32996..43e956ff0c 100644 --- a/src/Tests/WorkspaceTests.cs +++ b/src/Tests/WorkspaceTests.cs @@ -59,10 +59,10 @@ public async Task ReloadWorkspace() var fileName = Path.Combine(Path.GetFullPath("Workspace"), "BasicOps.qs"); File.SetLastWriteTimeUtc(fileName, DateTime.UtcNow); ws.Reload(); - op = ws.AssemblyInfo.Operations.FirstOrDefault(o => o.FullName == "Tests.qss.NoOp"); + op = ws.AssemblyInfo?.Operations.FirstOrDefault(o => o.FullName == "Tests.qss.NoOp"); + Assert.IsNotNull(op); Assert.IsFalse(ws.HasErrors); Assert.AreNotSame(originalAssembly, ws.AssemblyInfo); - Assert.IsNotNull(op); } [TestMethod]