2015-11-30 16:24:03 -08:00
// 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
{
2016-01-30 21:47:50 -08:00
public class TestCommand
2015-11-30 16:24:03 -08:00
{
2016-01-30 21:47:50 -08:00
public static int Run ( string [ ] args )
2015-11-30 16:24:03 -08:00
{
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 ) ;
2016-02-05 13:26:12 -08:00
var configurationOption = app . Option ( "-c|--configuration <CONFIGURATION>" , "Configuration under which to build" , CommandOptionType . SingleValue ) ;
2015-11-30 16:24:03 -08:00
var projectPath = app . Argument ( "<PROJECT>" , "The project to test, defaults to the current directory. Can be a path to a project.json or a project directory." ) ;
2016-01-26 06:39:13 -08:00
2015-11-30 16:24:03 -08:00
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 ) ;
}
2016-01-26 06:39:13 -08:00
2015-11-30 16:24:03 -08:00
var projectContexts = CreateProjectContexts ( projectPath . Value ) ;
var projectContext = projectContexts . First ( ) ;
var testRunner = projectContext . ProjectFile . TestRunner ;
2016-02-05 13:26:12 -08:00
var configuration = configurationOption . Value ( ) ? ? Constants . DefaultConfiguration ;
2016-01-26 06:39:13 -08:00
2015-11-30 16:24:03 -08:00
if ( portOption . HasValue ( ) )
{
int port ;
2016-01-26 06:39:13 -08:00
2015-11-30 16:24:03 -08:00
if ( ! Int32 . TryParse ( portOption . Value ( ) , out port ) )
{
throw new InvalidOperationException ( $"{portOption.Value()} is not a valid port number." ) ;
}
2016-02-05 13:26:12 -08:00
return RunDesignTime ( port , projectContext , testRunner , configuration ) ;
2015-11-30 16:24:03 -08:00
}
else
{
2016-02-05 13:26:12 -08:00
return RunConsole ( projectContext , app , testRunner , configuration ) ;
2015-11-30 16:24:03 -08:00
}
}
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 ) ;
}
2016-02-05 13:26:12 -08:00
private static int RunConsole ( ProjectContext projectContext , CommandLineApplication app , string testRunner , string configuration )
2015-11-30 16:24:03 -08:00
{
2016-02-03 10:57:25 -08:00
var commandArgs = new List < string > { projectContext . GetOutputPaths ( configuration ) . CompilationFiles . Assembly } ;
2015-11-30 16:24:03 -08:00
commandArgs . AddRange ( app . RemainingArguments ) ;
2016-01-30 21:47:50 -08:00
return Command . CreateDotNet ( $"{GetCommandName(testRunner)}" , commandArgs , projectContext . TargetFramework )
2015-11-30 16:24:03 -08:00
. ForwardStdErr ( )
. ForwardStdOut ( )
. Execute ( )
. ExitCode ;
}
2016-02-05 13:26:12 -08:00
private static int RunDesignTime ( int port , ProjectContext projectContext , string testRunner , string configuration )
2015-11-30 16:24:03 -08:00
{
Console . WriteLine ( "Listening on port {0}" , port ) ;
using ( var channel = ReportingChannel . ListenOn ( port ) )
{
Console . WriteLine ( "Client accepted {0}" , channel . Socket . LocalEndPoint ) ;
2016-02-05 13:26:12 -08:00
HandleDesignTimeMessages ( projectContext , testRunner , channel , configuration ) ;
2015-11-30 16:24:03 -08:00
return 0 ;
}
}
2016-02-05 13:26:12 -08:00
private static void HandleDesignTimeMessages ( ProjectContext projectContext , string testRunner , ReportingChannel channel , string configuration )
2015-11-30 16:24:03 -08:00
{
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" )
{
2016-02-05 13:26:12 -08:00
HandleTestDiscoveryStartMessage ( testRunner , channel , projectContext , configuration ) ;
2015-11-30 16:24:03 -08:00
}
else if ( message . MessageType = = "TestExecution.Start" )
{
2016-02-05 13:26:12 -08:00
HandleTestExecutionStartMessage ( testRunner , message , channel , projectContext , configuration ) ;
2015-11-30 16:24:03 -08:00
}
else
{
HandleUnknownMessage ( message , channel ) ;
}
}
catch ( Exception ex )
{
channel . SendError ( ex ) ;
}
}
private static void HandleProtocolVersionMessage ( Message message , ReportingChannel channel )
{
var version = message . Payload ? . ToObject < ProtocolVersionMessage > ( ) . 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 ,
} ) ,
} ) ;
}
2016-02-05 13:26:12 -08:00
private static void HandleTestDiscoveryStartMessage ( string testRunner , ReportingChannel channel , ProjectContext projectContext , string configuration )
2015-11-30 16:24:03 -08:00
{
TestHostTracing . Source . TraceInformation ( "Starting Discovery" ) ;
2016-02-03 10:57:25 -08:00
var commandArgs = new List < string > { projectContext . GetOutputPaths ( configuration ) . CompilationFiles . Assembly } ;
2015-11-30 16:24:03 -08:00
commandArgs . AddRange ( new [ ]
{
"--list" ,
"--designtime"
} ) ;
ExecuteRunnerCommand ( testRunner , channel , commandArgs ) ;
channel . Send ( new Message ( )
{
MessageType = "TestDiscovery.Response" ,
} ) ;
TestHostTracing . Source . TraceInformation ( "Completed Discovery" ) ;
}
2016-02-05 13:26:12 -08:00
private static void HandleTestExecutionStartMessage ( string testRunner , Message message , ReportingChannel channel , ProjectContext projectContext , string configuration )
2015-11-30 16:24:03 -08:00
{
TestHostTracing . Source . TraceInformation ( "Starting Execution" ) ;
2016-02-03 10:57:25 -08:00
var commandArgs = new List < string > { projectContext . GetOutputPaths ( configuration ) . CompilationFiles . Assembly } ;
2015-11-30 16:24:03 -08:00
commandArgs . AddRange ( new [ ]
{
"--designtime"
} ) ;
var tests = message . Payload ? . ToObject < RunTestsMessage > ( ) . 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 ) ;
}
2016-01-26 06:39:13 -08:00
2015-11-30 16:24:03 -08:00
private static void ExecuteRunnerCommand ( string testRunner , ReportingChannel channel , List < string > commandArgs )
{
2016-01-30 21:47:50 -08:00
var result = Command . CreateDotNet ( GetCommandName ( testRunner ) , commandArgs , new NuGetFramework ( "DNXCore" , Version . Parse ( "5.0" ) ) )
2015-11-30 16:24:03 -08:00
. OnOutputLine ( line = >
{
try
{
channel . Send ( JsonConvert . DeserializeObject < Message > ( line ) ) ;
}
catch
{
TestHostTracing . Source . TraceInformation ( line ) ;
}
} )
. Execute ( ) ;
if ( result . ExitCode ! = 0 )
{
channel . SendError ( $"{GetCommandName(testRunner)} returned '{result.ExitCode}'." ) ;
}
}
private static string GetCommandName ( string testRunner )
{
2016-01-30 21:47:50 -08:00
return $"test-{testRunner}" ;
2015-11-30 16:24:03 -08:00
}
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 < ProjectContext > 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 ) ;
}
}
}