diff --git a/packaging/debian/debian_config.json b/packaging/debian/debian_config.json index 7d9fc1e3d..3f41586ff 100644 --- a/packaging/debian/debian_config.json +++ b/packaging/debian/debian_config.json @@ -41,7 +41,7 @@ compilers, package managers and other utilities that developers need.", "bin/dotnet-compile" : "usr/bin/dotnet-compile", "bin/dotnet-compile-csc" : "usr/bin/dotnet-compile-csc", "bin/dotnet-compile-native" : "/usr/bin/dotnet-compile-native", - "bin/dotnet-init":"usr/bin/dotnet-init", + "bin/dotnet-init":"usr/bin/dotnet-init", "bin/dotnet-publish" : "usr/bin/dotnet-publish", "bin/dotnet-repl" : "usr/bin/dotnet-repl", "bin/dotnet-repl-csi" : "usr/bin/dotnet-repl-csi", diff --git a/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs b/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs index 83993dd48..8166c9b92 100644 --- a/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs +++ b/src/Microsoft.DotNet.ProjectModel/ProjectContext.cs @@ -99,5 +99,28 @@ namespace Microsoft.DotNet.ProjectModel .Build(); } } + public string GetAssemblyPath(string buildConfiguration) + { + return Path.Combine( + GetOutputDirectoryPath(buildConfiguration), + ProjectFile.Name + FileNameSuffixes.DotNet.DynamicLib); + } + + public string GetPdbPath(string buildConfiguration) + { + return Path.Combine( + GetOutputDirectoryPath(buildConfiguration), + ProjectFile.Name + FileNameSuffixes.DotNet.ProgramDatabase); + } + + private string GetOutputDirectoryPath(string buildConfiguration) + { + return Path.Combine( + ProjectDirectory, + DirectoryNames.Bin, + buildConfiguration, + TargetFramework.GetShortFolderName(), + ProjectModel.RuntimeIdentifier.Current); + } } } \ No newline at end of file diff --git a/src/Microsoft.DotNet.Tools.Test/Program.cs b/src/Microsoft.DotNet.Tools.Test/Program.cs new file mode 100644 index 000000000..0a253c47d --- /dev/null +++ b/src/Microsoft.DotNet.Tools.Test/Program.cs @@ -0,0 +1,305 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.Dnx.Runtime.Common.CommandLine; +using Microsoft.DotNet.ProjectModel; +using Microsoft.Extensions.Testing.Abstractions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NuGet.Frameworks; + +namespace Microsoft.DotNet.Tools.Test +{ + public class Program + { + public static int Main(string[] args) + { + DebugHelper.HandleDebugSwitch(ref args); + + var app = new CommandLineApplication(false) + { + Name = "dotnet test", + FullName = ".NET Test Driver", + Description = "Test Driver for the .NET Platform" + }; + + app.HelpOption("-?|-h|--help"); + + var parentProcessIdOption = app.Option("--parentProcessId", "Used by IDEs to specify their process ID. Test will exit if the parent process does.", CommandOptionType.SingleValue); + var portOption = app.Option("--port", "Used by IDEs to specify a port number to listen for a connection.", CommandOptionType.SingleValue); + var projectPath = app.Argument("", "The project to test, defaults to the current directory. Can be a path to a project.json or a project directory."); + + app.OnExecute(() => + { + try + { + // Register for parent process's exit event + if (parentProcessIdOption.HasValue()) + { + int processId; + + if (!Int32.TryParse(parentProcessIdOption.Value(), out processId)) + { + throw new InvalidOperationException($"Invalid process id '{parentProcessIdOption.Value()}'. Process id must be an integer."); + } + + RegisterForParentProcessExit(processId); + } + + var projectContexts = CreateProjectContexts(projectPath.Value); + + var projectContext = projectContexts.First(); + + var testRunner = projectContext.ProjectFile.TestRunner; + + if (portOption.HasValue()) + { + int port; + + if (!Int32.TryParse(portOption.Value(), out port)) + { + throw new InvalidOperationException($"{portOption.Value()} is not a valid port number."); + } + + return RunDesignTime(port, projectContext, testRunner); + } + else + { + return RunConsole(projectContext, app, testRunner); + } + } + catch (InvalidOperationException ex) + { + TestHostTracing.Source.TraceEvent(TraceEventType.Error, 0, ex.ToString()); + return -1; + } + catch (Exception ex) + { + TestHostTracing.Source.TraceEvent(TraceEventType.Error, 0, ex.ToString()); + return -2; + } + + }); + + return app.Execute(args); + } + + private static int RunConsole(ProjectContext projectContext, CommandLineApplication app, string testRunner) + { + var commandArgs = new List {projectContext.GetAssemblyPath(Constants.DefaultConfiguration)}; + commandArgs.AddRange(app.RemainingArguments); + + return Command.Create($"{GetCommandName(testRunner)}", commandArgs, projectContext.TargetFramework) + .ForwardStdErr() + .ForwardStdOut() + .Execute() + .ExitCode; + } + + private static int RunDesignTime(int port, ProjectContext projectContext, string testRunner) + { + Console.WriteLine("Listening on port {0}", port); + using (var channel = ReportingChannel.ListenOn(port)) + { + Console.WriteLine("Client accepted {0}", channel.Socket.LocalEndPoint); + + HandleDesignTimeMessages(projectContext, testRunner, channel); + + return 0; + } + } + + private static void HandleDesignTimeMessages(ProjectContext projectContext, string testRunner, ReportingChannel channel) + { + try + { + var message = channel.ReadQueue.Take(); + + if (message.MessageType == "ProtocolVersion") + { + HandleProtocolVersionMessage(message, channel); + + // Take the next message, which should be the command to execute. + message = channel.ReadQueue.Take(); + } + + if (message.MessageType == "TestDiscovery.Start") + { + HandleTestDiscoveryStartMessage(testRunner, channel, projectContext); + } + else if (message.MessageType == "TestExecution.Start") + { + HandleTestExecutionStartMessage(testRunner, message, channel, projectContext); + } + else + { + HandleUnknownMessage(message, channel); + } + } + catch (Exception ex) + { + channel.SendError(ex); + } + } + + private static void HandleProtocolVersionMessage(Message message, ReportingChannel channel) + { + var version = message.Payload?.ToObject().Version; + var supportedVersion = 1; + TestHostTracing.Source.TraceInformation( + "[ReportingChannel]: Requested Version: {0} - Using Version: {1}", + version, + supportedVersion); + + channel.Send(new Message() + { + MessageType = "ProtocolVersion", + Payload = JToken.FromObject(new ProtocolVersionMessage() + { + Version = supportedVersion, + }), + }); + } + + private static void HandleTestDiscoveryStartMessage(string testRunner, ReportingChannel channel, ProjectContext projectContext) + { + TestHostTracing.Source.TraceInformation("Starting Discovery"); + + var commandArgs = new List { projectContext.GetAssemblyPath(Constants.DefaultConfiguration) }; + + commandArgs.AddRange(new[] + { + "--list", + "--designtime" + }); + + ExecuteRunnerCommand(testRunner, channel, commandArgs); + + channel.Send(new Message() + { + MessageType = "TestDiscovery.Response", + }); + + TestHostTracing.Source.TraceInformation("Completed Discovery"); + } + + private static void HandleTestExecutionStartMessage(string testRunner, Message message, ReportingChannel channel, ProjectContext projectContext) + { + TestHostTracing.Source.TraceInformation("Starting Execution"); + + var commandArgs = new List { projectContext.GetAssemblyPath(Constants.DefaultConfiguration) }; + + commandArgs.AddRange(new[] + { + "--designtime" + }); + + var tests = message.Payload?.ToObject().Tests; + if (tests != null) + { + foreach (var test in tests) + { + commandArgs.Add("--test"); + commandArgs.Add(test); + } + } + + ExecuteRunnerCommand(testRunner, channel, commandArgs); + + channel.Send(new Message() + { + MessageType = "TestExecution.Response", + }); + + TestHostTracing.Source.TraceInformation("Completed Execution"); + } + + private static void HandleUnknownMessage(Message message, ReportingChannel channel) + { + var error = string.Format("Unexpected message type: '{0}'.", message.MessageType); + + TestHostTracing.Source.TraceEvent(TraceEventType.Error, 0, error); + + channel.SendError(error); + + throw new InvalidOperationException(error); + } + + private static void ExecuteRunnerCommand(string testRunner, ReportingChannel channel, List commandArgs) + { + var result = Command.Create(GetCommandName(testRunner), commandArgs, new NuGetFramework("DNXCore", Version.Parse("5.0"))) + .OnOutputLine(line => + { + try + { + channel.Send(JsonConvert.DeserializeObject(line)); + } + catch + { + TestHostTracing.Source.TraceInformation(line); + } + }) + .Execute(); + + if (result.ExitCode != 0) + { + channel.SendError($"{GetCommandName(testRunner)} returned '{result.ExitCode}'."); + } + } + + private static string GetCommandName(string testRunner) + { + return $"dotnet-test-{testRunner}"; + } + + private static void RegisterForParentProcessExit(int id) + { + var parentProcess = Process.GetProcesses().FirstOrDefault(p => p.Id == id); + + if (parentProcess != null) + { + parentProcess.EnableRaisingEvents = true; + parentProcess.Exited += (sender, eventArgs) => + { + TestHostTracing.Source.TraceEvent( + TraceEventType.Information, + 0, + "Killing the current process as parent process has exited."); + + Process.GetCurrentProcess().Kill(); + }; + } + else + { + TestHostTracing.Source.TraceEvent( + TraceEventType.Information, + 0, + "Failed to register for parent process's exit event. " + + $"Parent process with id '{id}' was not found."); + } + } + + private static IEnumerable CreateProjectContexts(string projectPath) + { + projectPath = projectPath ?? Directory.GetCurrentDirectory(); + + if (!projectPath.EndsWith(Project.FileName)) + { + projectPath = Path.Combine(projectPath, Project.FileName); + } + + if (!File.Exists(projectPath)) + { + throw new InvalidOperationException($"{projectPath} does not exist."); + } + + return ProjectContext.CreateContextForEachFramework(projectPath); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Testing.Abstractions/DIA/DataKind.cs b/src/Microsoft.Extensions.Testing.Abstractions/DIA/DataKind.cs new file mode 100644 index 000000000..13b8d7c39 --- /dev/null +++ b/src/Microsoft.Extensions.Testing.Abstractions/DIA/DataKind.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace dia2 +{ + public enum DataKind + { + DataIsUnknown, + DataIsLocal, + DataIsStaticLocal, + DataIsParam, + DataIsObjectPtr, + DataIsFileStatic, + DataIsGlobal, + DataIsMember, + DataIsStaticMember, + DataIsConstant + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Testing.Abstractions/ITestExecutionSink.cs b/src/Microsoft.Extensions.Testing.Abstractions/ITestExecutionSink.cs new file mode 100644 index 000000000..0557a2287 --- /dev/null +++ b/src/Microsoft.Extensions.Testing.Abstractions/ITestExecutionSink.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace Microsoft.Extensions.Testing.Abstractions +{ + public interface ITestExecutionSink + { + void SendTestStarted(Test test); + + void SendTestResult(TestResult testResult); + } +} diff --git a/src/Microsoft.Extensions.Testing.Abstractions/LineDelimitedJsonStream.cs b/src/Microsoft.Extensions.Testing.Abstractions/LineDelimitedJsonStream.cs new file mode 100644 index 000000000..c7ef529d8 --- /dev/null +++ b/src/Microsoft.Extensions.Testing.Abstractions/LineDelimitedJsonStream.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation and contributors. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.IO; +using Newtonsoft.Json; + +namespace Microsoft.Extensions.Testing.Abstractions +{ + class LineDelimitedJsonStream + { + private readonly StreamWriter _stream; + + public LineDelimitedJsonStream(Stream stream) + { + _stream = new StreamWriter(stream); + } + + public void Send(object @object) + { + _stream.WriteLine(JsonConvert.SerializeObject(@object)); + + _stream.Flush(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Testing.Abstractions/ProjectContextExtensions.cs b/src/Microsoft.Extensions.Testing.Abstractions/ProjectContextExtensions.cs deleted file mode 100644 index 503a013d6..000000000 --- a/src/Microsoft.Extensions.Testing.Abstractions/ProjectContextExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.IO; -using Microsoft.DotNet.ProjectModel; - -namespace Microsoft.Extensions.Testing.Abstractions -{ - public static class ProjectContextExtensions - { - public static string AssemblyPath(this ProjectContext projectContext, string buildConfiguration) - { - return Path.Combine( - projectContext.OutputDirectoryPath(buildConfiguration), - projectContext.ProjectFile.Name + FileNameSuffixes.DotNet.DynamicLib); - } - public static string PdbPath(this ProjectContext projectContext, string buildConfiguration) - { - return Path.Combine( - projectContext.OutputDirectoryPath(buildConfiguration), - projectContext.ProjectFile.Name + FileNameSuffixes.DotNet.ProgramDatabase); - } - - private static string OutputDirectoryPath(this ProjectContext projectContext, string buildConfiguration) - { - return Path.Combine( - projectContext.ProjectDirectory, - DirectoryNames.Bin, - buildConfiguration, - projectContext.TargetFramework.GetShortFolderName(), - RuntimeIdentifier.Current); - } - } -} diff --git a/src/Microsoft.Extensions.Testing.Abstractions/StreamingTestDiscoverySink.cs b/src/Microsoft.Extensions.Testing.Abstractions/StreamingTestDiscoverySink.cs new file mode 100644 index 000000000..713ce446b --- /dev/null +++ b/src/Microsoft.Extensions.Testing.Abstractions/StreamingTestDiscoverySink.cs @@ -0,0 +1,30 @@ +using System; +using System.IO; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Extensions.Testing.Abstractions +{ + public class StreamingTestDiscoverySink : ITestDiscoverySink + { + private readonly LineDelimitedJsonStream _stream; + + public StreamingTestDiscoverySink(Stream stream) + { + _stream = new LineDelimitedJsonStream(stream); + } + + public void SendTestFound(Test test) + { + if (test == null) + { + throw new ArgumentNullException(nameof(test)); + } + + _stream.Send(new Message + { + MessageType = "TestDiscovery.TestFound", + Payload = JToken.FromObject(test), + }); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.Testing.Abstractions/StreamingTestExecutionSink.cs b/src/Microsoft.Extensions.Testing.Abstractions/StreamingTestExecutionSink.cs new file mode 100644 index 000000000..240ca788d --- /dev/null +++ b/src/Microsoft.Extensions.Testing.Abstractions/StreamingTestExecutionSink.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Extensions.Testing.Abstractions +{ + public class StreamingTestExecutionSink : ITestExecutionSink + { + private readonly LineDelimitedJsonStream _stream; + private readonly ConcurrentDictionary _runningTests; + + + public StreamingTestExecutionSink(Stream stream) + { + _stream = new LineDelimitedJsonStream(stream); + _runningTests = new ConcurrentDictionary(); + } + + public void SendTestStarted(Test test) + { + if (test == null) + { + throw new ArgumentNullException(nameof(test)); + } + + if (test.FullyQualifiedName != null) + { + var state = new TestState() { StartTime = DateTimeOffset.Now, }; + _runningTests.TryAdd(test.FullyQualifiedName, state); + } + + _stream.Send(new Message + { + MessageType = "TestExecution.TestStarted", + Payload = JToken.FromObject(test), + }); + } + + public void SendTestResult(TestResult testResult) + { + if (testResult == null) + { + throw new ArgumentNullException(nameof(testResult)); + } + + if (testResult.StartTime == default(DateTimeOffset) && testResult.Test.FullyQualifiedName != null) + { + TestState state; + _runningTests.TryRemove(testResult.Test.FullyQualifiedName, out state); + + testResult.StartTime = state.StartTime; + } + + if (testResult.EndTime == default(DateTimeOffset)) + { + testResult.EndTime = DateTimeOffset.Now; + } + + _stream.Send(new Message + { + MessageType = "TestExecution.TestResult", + Payload = JToken.FromObject(testResult), + }); + } + + private class TestState + { + public DateTimeOffset StartTime { get; set; } + } + } +} \ No newline at end of file