2019-12-26 23:01:54 -05:00
// Copyright Epic Games, Inc. All Rights Reserved.
2018-09-25 10:11:35 -04:00
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using System.Threading ;
using System.Text.RegularExpressions ;
using System.Drawing ;
2022-11-12 18:42:11 -05:00
using EpicGames.Core ;
2018-09-25 10:11:35 -04:00
namespace Gauntlet
{
public class TestExecutorOptions
{
2021-06-23 17:51:32 -04:00
[AutoParamWithNames(1, "iterations", "repeat")]
public int TestIterations ;
2018-09-25 10:11:35 -04:00
[AutoParam(false)]
public bool StopOnError ;
[AutoParam(false)]
public bool NoTimeout ;
[AutoParam(300)]
public int Wait ;
[AutoParam(1)]
public int Parallel ;
2018-11-14 19:05:13 -05:00
[AutoParam(true)]
public bool DeferReports ;
2018-09-25 10:11:35 -04:00
}
/// <summary>
/// Class that is manages the creation and execution of one or more tests
/// </summary>
2022-10-11 02:54:50 -04:00
public class TestExecutor
2018-09-25 10:11:35 -04:00
{
class TestExecutionInfo
{
public enum ExecutionResult
{
NotStarted ,
TimedOut ,
Passed ,
2023-05-04 16:48:26 -04:00
Failed ,
Skipped
2018-09-25 10:11:35 -04:00
}
public ITestNode TestNode ;
public DateTime FirstReadyCheckTime ;
2021-09-08 16:42:26 -04:00
public DateTime TimeSetupBegan ;
public DateTime TimeSetupEnded ;
public DateTime TimeTestEnded ;
2018-09-25 10:11:35 -04:00
public ExecutionResult Result ;
2018-11-14 19:05:13 -05:00
public TestResult FinalResult ;
public string CancellationReason ;
2022-10-11 02:54:50 -04:00
2021-09-08 16:42:26 -04:00
/// <summary>
/// Time the test had to wait before running
/// </summary>
public TimeSpan WaitDuration { get { return ( TimeSetupBegan - FirstReadyCheckTime ) ; } }
/// <summary>
/// Time the test had to wait before running
/// </summary>
public TimeSpan SetupDuration { get { return ( TimeSetupEnded - TimeSetupBegan ) ; } }
/// <summary>
/// Time the test took to run
/// </summary>
public TimeSpan TestDuration { get { return ( TimeTestEnded - TimeSetupEnded ) ; } }
2023-01-31 14:46:39 -05:00
/// <summary>
/// Creates TestExecutionInfo instance from an ITestNode. Sets FirstReadyCheckTime, copies the node
/// and sets CancellationReason to empty string.
/// </summary>
/// <param name="InNode"></param>
2018-09-25 10:11:35 -04:00
public TestExecutionInfo ( ITestNode InNode )
{
2021-09-08 16:42:26 -04:00
FirstReadyCheckTime = TimeSetupBegan = TimeSetupEnded = TimeTestEnded = DateTime . MinValue ;
2018-09-25 10:11:35 -04:00
TestNode = InNode ;
2023-01-31 14:46:39 -05:00
CancellationReason = string . Empty ;
2018-09-25 10:11:35 -04:00
}
public override string ToString ( )
{
return TestNode . ToString ( ) ;
}
} ;
private List < TestExecutionInfo > RunningTests ;
private DateTime StartTime ;
private int CurrentTestPass ;
TestExecutorOptions Options ;
2022-10-11 02:54:50 -04:00
protected string BuildCommandThatLaunchedExecutor ;
2018-09-25 10:11:35 -04:00
public bool IsRunning { get ; private set ; }
public bool IsCancelled { get ; private set ; }
protected bool HaveReceivedPostAbort { get ; private set ; }
/// <summary>
/// Constructor that fills in some member variables
2022-10-11 02:54:50 -04:00
/// @Param the BuildCommand that was used to start this Test Executor.
/// Useful to log out or know where this test executor came from.
2018-09-25 10:11:35 -04:00
/// </summary>
2022-10-11 02:54:50 -04:00
public TestExecutor ( string InBuildCommand )
2018-09-25 10:11:35 -04:00
{
2022-11-16 14:47:44 -05:00
BuildCommandThatLaunchedExecutor = InBuildCommand . Split ( "." ) . Last ( ) ;
2018-09-25 10:11:35 -04:00
RunningTests = new List < TestExecutionInfo > ( ) ;
}
public void Dispose ( )
{
}
/// <summary>
/// Executes the provided tests. Currently tests are executed synchronously
/// </summary>
/// <param name="Context"></param>
public bool ExecuteTests ( TestExecutorOptions InOptions , IEnumerable < ITestNode > RequiredTests )
{
Options = InOptions ;
2022-11-12 18:42:11 -05:00
Log . Info ( "Preparing to start {Number} automation test(s)" , RequiredTests . Count ( ) ) ;
2018-09-25 10:11:35 -04:00
// install a cancel handler so we can stop parallel-for gracefully
Action CancelHandler = delegate ( )
{
Log . Info ( "Cancelling Tests" ) ;
IsCancelled = true ;
} ;
Action PostCancelHandler = delegate ( )
{
HaveReceivedPostAbort = true ;
} ;
Globals . AbortHandlers . Add ( CancelHandler ) ;
Globals . PostAbortHandlers . Add ( PostCancelHandler ) ;
StartTime = DateTime . Now ;
CurrentTestPass = 0 ;
IsRunning = true ;
int MaxParallelTasks = 0 ;
int MaxStartingTasks = 0 ;
// sort by priority
if ( Options . Parallel > 1 )
{
RequiredTests = RequiredTests . OrderBy ( Node = > Node . Priority ) ;
}
2021-06-23 17:51:32 -04:00
Dictionary < string , int > TestIterationsPasssed = new Dictionary < string , int > ( ) ;
Dictionary < string , int > TestIterationsFailed = new Dictionary < string , int > ( ) ;
Dictionary < string , int > TestIterationsPassedWithWarnings = new Dictionary < string , int > ( ) ;
for ( CurrentTestPass = 0 ; CurrentTestPass < Options . TestIterations ; CurrentTestPass + + )
2018-09-25 10:11:35 -04:00
{
2021-06-23 17:51:32 -04:00
// do not start a pass if canceled
2018-09-25 10:11:35 -04:00
if ( IsCancelled )
{
break ;
}
if ( CurrentTestPass > 0 )
{
// if repeating tests wait a little bit. If there was a crash CR might still be
// processing things.
Thread . Sleep ( 10000 ) ;
}
DateTime StartPassTime = DateTime . Now ;
2022-11-12 18:42:11 -05:00
Log . Info ( "Starting test iteration {Index} of {Total}" , CurrentTestPass + 1 , Options . TestIterations ) ;
2018-09-25 10:11:35 -04:00
// Tests that we want to run
List < TestExecutionInfo > PendingTests = RequiredTests . Select ( N = > new TestExecutionInfo ( N ) ) . ToList ( ) ;
// Tests that are in the process of starting
List < TestExecutionInfo > StartingTests = new List < TestExecutionInfo > ( ) ;
List < Thread > StartingTestThreads = new List < Thread > ( ) ;
// Tests that have been started and we're ticking/checking
List < TestExecutionInfo > RunningTests = new List < TestExecutionInfo > ( ) ;
// Completed tests
List < TestExecutionInfo > CompletedTests = new List < TestExecutionInfo > ( ) ;
DateTime LastUpdateMsg = DateTime . MinValue ;
DateTime LastReadyCheck = DateTime . MinValue ;
DateTime LastStatusUpdateTime = DateTime . MinValue ;
const double ReadyCheckPeriod = 30.0 ;
const double StatusUpdatePeriod = 60.0 ;
while ( CompletedTests . Count ( ) < RequiredTests . Count ( ) & & IsCancelled = = false )
{
Monitor . Enter ( Globals . MainLock ) ;
int SecondsRunning = ( int ) ( DateTime . Now - StartPassTime ) . TotalSeconds ;
int InProgressCount = RunningTests . Count ( ) + StartingTests . Count ( ) ;
double TimeSinceLastReadyCheck = ( DateTime . Now - LastReadyCheck ) . TotalSeconds ;
// Are any tests ready to run?
2021-06-23 17:51:32 -04:00
if ( InProgressCount < Options . Parallel
& & PendingTests . Count ( ) > 0
2018-09-25 10:11:35 -04:00
& & TimeSinceLastReadyCheck > = ReadyCheckPeriod )
{
TestExecutionInfo TestToStart = null ;
List < ITestNode > TestsFailingToStart = new List < ITestNode > ( ) ;
// find a node that can run, and
// find the first test that can run....
for ( int i = 0 ; i < PendingTests . Count ( ) ; i + + )
{
TestExecutionInfo NodeInfo = PendingTests [ i ] ;
ITestNode Node = NodeInfo . TestNode ;
bool IsTestReady = false ;
try
{
IsTestReady = Node . IsReadyToStart ( ) ;
}
catch ( System . Exception ex )
{
2022-11-12 18:42:11 -05:00
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , "Test {Name} threw an exception during ready check. Ex: {Exception}" , Node , ex ) ;
2022-03-18 18:53:33 -04:00
Node . AddTestEvent ( new UnrealTestEvent ( EventSeverity . Error , "Test Failed to Start" , new List < string > { ex . Message } ) ) ;
2018-09-25 10:11:35 -04:00
PendingTests [ i ] = null ;
2021-09-08 16:42:26 -04:00
NodeInfo . TimeSetupBegan = NodeInfo . TimeSetupEnded = NodeInfo . TimeTestEnded = DateTime . Now ;
2018-09-25 10:11:35 -04:00
CompletedTests . Add ( NodeInfo ) ;
}
if ( IsTestReady )
{
// if ready then take it and stop looking
TestToStart = NodeInfo ;
if ( NodeInfo . FirstReadyCheckTime = = DateTime . MinValue )
{
NodeInfo . FirstReadyCheckTime = DateTime . Now ;
}
break ;
}
else
{
// track the time that this test should have been able to run due to no other tests
// consuming resources (at least locally...)
// TODO - what about the situation where no tests can run so all FirstCheck times are set, but
// then a test starts and consumes all resources?
if ( RunningTests . Count ( ) = = 0 & & StartingTests . Count ( ) = = 0 )
{
if ( NodeInfo . FirstReadyCheckTime = = DateTime . MinValue )
{
NodeInfo . FirstReadyCheckTime = DateTime . Now ;
}
double TimeWaiting = ( DateTime . Now - NodeInfo . FirstReadyCheckTime ) . TotalSeconds ;
if ( TimeWaiting > = Options . Wait )
{
2023-10-11 12:22:40 -04:00
Log . Error ( KnownLogEvents . Gauntlet_DeviceEvent , "Test {TestName} has been waiting to run resource-free for {Time:00} seconds. Removing from wait list" , Node , TimeWaiting ) ;
2023-06-05 14:32:35 -04:00
DevicePool . Instance . ReportDeviceReservationState ( ) ;
2023-10-11 12:22:40 -04:00
Node . AddTestEvent ( new UnrealTestEvent ( EventSeverity . Error , "Insufficient devices found" , new List < string > { string . Format ( "Test {0} was unable to find enough devices after trying for {1:00} seconds." , Node , TimeWaiting ) , "This is not a test-related failure." } ) ) ;
2018-09-25 10:11:35 -04:00
PendingTests [ i ] = null ;
2021-09-08 16:42:26 -04:00
NodeInfo . TimeSetupBegan = NodeInfo . TimeSetupEnded = NodeInfo . TimeTestEnded = DateTime . Now ;
2023-10-11 12:22:40 -04:00
NodeInfo . Result = TestExecutionInfo . ExecutionResult . Failed ;
2018-09-25 10:11:35 -04:00
CompletedTests . Add ( NodeInfo ) ;
}
}
}
}
// remove anything we nulled
PendingTests = PendingTests . Where ( T = > T ! = null ) . ToList ( ) ;
if ( TestToStart ! = null )
{
2022-11-12 18:42:11 -05:00
Log . Info ( "Test {Name} is ready to run" , TestToStart ) ;
2018-09-25 10:11:35 -04:00
PendingTests . Remove ( TestToStart ) ;
StartingTests . Add ( TestToStart ) ;
// StartTest is the only thing we do on a thread because it's likely to be the most time consuming
// as build are copied so will get the most benefit from happening in parallel
Thread StartThread = new Thread ( ( ) = >
{
Thread . CurrentThread . IsBackground = true ;
// start the test, this also fills in the pre/post start times
2021-06-23 17:51:32 -04:00
bool Started = StartTest ( TestToStart , CurrentTestPass , Options . TestIterations ) ;
2018-09-25 10:11:35 -04:00
lock ( Globals . MainLock )
{
if ( Started = = false )
{
2021-09-08 16:42:26 -04:00
TestToStart . TimeSetupEnded = TestToStart . TimeTestEnded = DateTime . Now ;
2018-09-25 10:11:35 -04:00
CompletedTests . Add ( TestToStart ) ;
2022-11-12 18:42:11 -05:00
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , "Test {Name} failed to start" , TestToStart ) ;
2018-09-25 10:11:35 -04:00
}
else
{
RunningTests . Add ( TestToStart ) ;
}
StartingTests . Remove ( TestToStart ) ;
StartingTestThreads . Remove ( Thread . CurrentThread ) ;
}
} ) ;
if ( StartingTests . Count > MaxStartingTasks )
{
MaxStartingTasks = StartingTests . Count ;
}
// track the thread and start it
StartingTestThreads . Add ( StartThread ) ;
2021-06-23 17:51:32 -04:00
StartThread . Start ( ) ;
2018-09-25 10:11:35 -04:00
}
else
{
// don't check for a while as querying kits for availability can be expensive
LastReadyCheck = DateTime . Now ;
}
}
// Tick all running tests
foreach ( TestExecutionInfo TestInfo in RunningTests )
{
2023-01-31 14:46:39 -05:00
// TickTest contains logic for determining run time, timeouts, cancellations, and many other
// parts of the test process. If overriding TickTest in your Test class, be sure to call base.TickTest.
2018-09-25 10:11:35 -04:00
TestResult Result = TickTest ( TestInfo ) ;
// invalid = no result yet
if ( Result = = TestResult . Invalid )
{
2021-09-08 16:42:26 -04:00
TimeSpan RunningTime = DateTime . Now - TestInfo . TimeSetupEnded ;
2018-09-25 10:11:35 -04:00
if ( ( SecondsRunning % 60 ) = = 0 )
{
2022-11-12 18:42:11 -05:00
Log . Verbose ( "Test {Name} is still running. {Elapsed:00} seconds elapsed, will timeout in {Max:00} seconds" ,
2018-09-25 10:11:35 -04:00
TestInfo ,
RunningTime . TotalSeconds ,
TestInfo . TestNode . MaxDuration - RunningTime . TotalSeconds ) ;
LastUpdateMsg = DateTime . Now ;
}
}
else
{
2021-09-08 16:42:26 -04:00
TestInfo . TimeTestEnded = DateTime . Now ;
2018-09-25 10:11:35 -04:00
TestInfo . Result = Result = = TestResult . Passed ? TestExecutionInfo . ExecutionResult . Passed : TestExecutionInfo . ExecutionResult . Failed ;
CompletedTests . Add ( TestInfo ) ;
}
}
// remove any tests that were completed
2021-06-23 17:51:32 -04:00
RunningTests = RunningTests . Where ( R = > CompletedTests . Contains ( R ) = = false ) . ToList ( ) ;
2018-09-25 10:11:35 -04:00
if ( ( DateTime . Now - LastStatusUpdateTime ) . TotalSeconds > = StatusUpdatePeriod )
{
LastStatusUpdateTime = DateTime . Now ;
2022-11-12 18:42:11 -05:00
Log . Info ( "Status: Completed:{Completed}, Running:{Running}, Starting: {Starting}, Waiting:{Waiting}" ,
2018-09-25 10:11:35 -04:00
CompletedTests . Count ( ) , RunningTests . Count ( ) , StartingTests . Count ( ) , PendingTests . Count ( ) ) ;
}
if ( InProgressCount > MaxParallelTasks )
{
MaxParallelTasks = RunningTests . Count ( ) ;
}
// Release our global lock before we loop
Monitor . Exit ( Globals . MainLock ) ;
// sleep a while before we tick our running tasks again
Thread . Sleep ( 500 ) ;
}
if ( IsCancelled )
{
DateTime StartTime = DateTime . Now ;
Log . Info ( "Cleaning up pending and running tests." ) ;
while ( HaveReceivedPostAbort = = false )
{
Thread . Sleep ( 500 ) ;
double Elapsed = ( DateTime . Now - StartTime ) . TotalSeconds ;
if ( Elapsed > = 5 )
{
2022-11-12 18:42:11 -05:00
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , "Giving up waiting for tests after {Elapsed:00} seconds" , Elapsed ) ;
2018-09-25 10:11:35 -04:00
break ;
}
}
if ( StartingTestThreads . Count > 0 )
{
foreach ( Thread T in StartingTestThreads )
{
2022-03-28 19:48:01 -04:00
// SYSLIB0006: 'Thread.Abort()' is obsolete: 'Thread.Abord it not supported and throws PlatformNotSupportedException'
//Log.Info("Aborting startup thread");
//T.Abort();
Log . Info ( $"Thread is still running: {T.Name} {T.ManagedThreadId}" ) ;
2018-09-25 10:11:35 -04:00
}
Thread . Sleep ( 1000 ) ;
}
foreach ( TestExecutionInfo TestInfo in StartingTests )
{
2022-11-12 18:42:11 -05:00
Log . Info ( "Forcing pending test {Name} to run CleanupTest" , TestInfo . TestNode . Name ) ;
2018-09-25 10:11:35 -04:00
TestInfo . TestNode . CleanupTest ( ) ;
CompletedTests . Add ( TestInfo ) ;
}
foreach ( TestExecutionInfo TestInfo in RunningTests )
{
2022-11-12 18:42:11 -05:00
Log . Info ( "Ticking test {Name} to cancel" , TestInfo . TestNode . Name ) ;
2018-09-25 10:11:35 -04:00
TestResult Res = TickTest ( TestInfo ) ;
CompletedTests . Add ( TestInfo ) ;
if ( Res ! = TestResult . Failed )
{
2022-11-12 18:42:11 -05:00
Log . Warning ( KnownLogEvents . Gauntlet_TestEvent , "Ticking of cancelled test {Name} returned {Result}" , TestInfo . TestNode . Name , Res ) ;
2018-09-25 10:11:35 -04:00
}
}
}
else
{
TimeSpan PassDuration = DateTime . Now - StartPassTime ;
int FailedCount = 0 ;
int TestCount = CompletedTests . Count ;
CompletedTests . ForEach ( T = >
{
2021-09-08 16:42:26 -04:00
TimeSpan TimeWaiting = T . FirstReadyCheckTime - T . TimeSetupBegan ;
TimeSpan SetupTime = T . TimeSetupEnded - T . TimeSetupBegan ;
TimeSpan TestDuration = T . TimeTestEnded - T . TimeSetupEnded ;
2018-09-25 10:11:35 -04:00
// status msg, kept uniform to avoid spam on notifiers (ie. don't include timestamps, etc)
string Msg = string . Format ( "Test {0} {1}" , T . TestNode , T . Result ) ;
2023-05-04 16:48:26 -04:00
bool TestHadErrors = T . Result ! = TestExecutionInfo . ExecutionResult . Passed & & T . Result ! = TestExecutionInfo . ExecutionResult . Skipped ;
2021-06-23 17:51:32 -04:00
bool TestHadWarnings = T . TestNode . GetWarnings ( ) . Any ( ) ;
if ( TestHadErrors )
2018-09-25 10:11:35 -04:00
{
FailedCount + + ;
}
2021-06-23 17:51:32 -04:00
// increment counts for each test
if ( ! TestIterationsPasssed . ContainsKey ( T . TestNode . Name ) )
{
TestIterationsPasssed [ T . TestNode . Name ] = 0 ;
TestIterationsFailed [ T . TestNode . Name ] = 0 ;
TestIterationsPassedWithWarnings [ T . TestNode . Name ] = 0 ;
}
if ( TestHadErrors )
{
TestIterationsFailed [ T . TestNode . Name ] + + ;
}
else if ( TestHadWarnings )
{
TestIterationsPassedWithWarnings [ T . TestNode . Name ] + + ;
}
else
{
TestIterationsPasssed [ T . TestNode . Name ] + + ;
}
2018-09-25 10:11:35 -04:00
Log . Info ( Msg ) ;
// log test timing to info
2022-11-12 18:42:11 -05:00
Log . Info ( "Test Time: {Duration:mm\\:ss} (Waited:{Waited:mm\\:ss}, Setup:{Setup:mm\\:ss})" , TestDuration , TimeWaiting , SetupTime ) ;
2018-09-25 10:11:35 -04:00
} ) ;
if ( Options . Parallel > 1 )
{
2022-11-12 18:42:11 -05:00
Log . Info ( "MaxParallelTasks: {Count}" , MaxParallelTasks ) ;
Log . Info ( "MaxStartingTasks: {Count}" , MaxStartingTasks ) ;
2018-09-25 10:11:35 -04:00
}
2018-11-14 19:05:13 -05:00
// report all tests
2022-07-08 13:54:37 -04:00
ReportMainSummary ( CurrentTestPass + 1 , Options . TestIterations , PassDuration , CompletedTests ) ;
2018-09-25 10:11:35 -04:00
2021-06-23 17:51:32 -04:00
if ( FailedCount > 0 & & Options . StopOnError )
2018-09-25 10:11:35 -04:00
{
2021-06-23 17:51:32 -04:00
break ;
2018-09-25 10:11:35 -04:00
}
}
2021-06-23 17:51:32 -04:00
} // foreach pass
2018-09-25 10:11:35 -04:00
2021-06-23 17:51:32 -04:00
int TotalTests = RequiredTests . Count ( ) * Options . TestIterations ;
int FailedTestCount = TestIterationsFailed . Values . Sum ( ) ;
// show details for multi passes
if ( Options . TestIterations > 1 )
{
MarkdownBuilder MB = new MarkdownBuilder ( ) ;
MB . HorizontalLine ( ) ;
MB . Paragraph ( string . Format ( "All iterations completed. {0} of {1} executed tests completed without error" , TotalTests - FailedTestCount , TotalTests ) ) ;
List < string > Lines = new List < string > ( ) ;
foreach ( ITestNode Test in RequiredTests )
2018-09-25 10:11:35 -04:00
{
2021-06-23 17:51:32 -04:00
int IterationsWithErrors = TestIterationsFailed [ Test . Name ] ;
int IterationsWithWarnings = TestIterationsPassedWithWarnings [ Test . Name ] ;
int IterationsWithPasses = TestIterationsPasssed [ Test . Name ] ;
2018-09-25 10:11:35 -04:00
2021-06-23 17:51:32 -04:00
Lines . Add ( string . Format ( "{0}: {1} Iterations, {2} Passed, {3} Passed with Warnings, {4} Failed" ,
Test . Name , Options . TestIterations , IterationsWithPasses , IterationsWithWarnings , IterationsWithErrors ) ) ;
2018-09-25 10:11:35 -04:00
}
2021-06-23 17:51:32 -04:00
MB . UnorderedList ( Lines ) ;
if ( FailedTestCount > 0 )
{
MB . Paragraph ( string . Format ( "Error: {0} of {1} executed tests failed." , FailedTestCount , TotalTests ) ) ;
}
else
{
MB . Paragraph ( string . Format ( "{0} total tests passed." , TotalTests ) ) ;
}
MB . HorizontalLine ( ) ;
2022-11-12 18:42:11 -05:00
Log . Info ( MB . ToString ( ) ) ;
2018-09-25 10:11:35 -04:00
}
IsRunning = false ;
Globals . AbortHandlers . Remove ( CancelHandler ) ;
Globals . PostAbortHandlers . Remove ( PostCancelHandler ) ;
2021-06-23 17:51:32 -04:00
return FailedTestCount = = 0 & & ! IsCancelled ;
2018-09-25 10:11:35 -04:00
}
/// <summary>
/// Executes a single test
/// </summary>
/// <param name="Test">Test to execute</param>
/// <param name="Context">The context to execute this test under</param>
/// <returns></returns>
private bool StartTest ( TestExecutionInfo TestInfo , int Pass , int NumPasses )
{
string Name = TestInfo . TestNode . Name ;
2022-11-12 18:42:11 -05:00
Log . Info ( "Starting Test {Name}" , TestInfo ) ;
2018-09-25 10:11:35 -04:00
try
{
2021-09-08 16:42:26 -04:00
TestInfo . TimeSetupBegan = DateTime . Now ;
2018-09-25 10:11:35 -04:00
if ( TestInfo . TestNode . StartTest ( Pass , NumPasses ) )
{
2021-09-08 16:42:26 -04:00
TestInfo . TimeSetupEnded = DateTime . Now ;
2022-11-12 18:42:11 -05:00
Log . Info ( "Launched test {Name} at {Time}" , Name , TestInfo . TimeSetupEnded . ToString ( "h:mm:ss" ) ) ;
2021-05-12 18:10:03 -04:00
return true ;
2018-09-25 10:11:35 -04:00
}
}
catch ( Exception Ex )
{
2022-11-12 18:42:11 -05:00
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , "Test {Name} threw an exception during launch. Skipping test. Ex: {Exception}\n{Callstack}" , Name , Ex . Message , Ex . StackTrace ) ;
2018-09-25 10:11:35 -04:00
}
2021-05-12 18:10:03 -04:00
return false ;
2018-09-25 10:11:35 -04:00
}
2018-11-14 19:05:13 -05:00
/// <summary>
/// Report the summary for a single test
/// </summary>
/// <param name="TestInfo"></param>
/// <returns></returns>
void ReportTestSummary ( TestExecutionInfo TestInfo )
{
2021-02-03 14:57:28 -04:00
Log . SuspendSanitization ( ) ;
2022-11-12 18:42:11 -05:00
string Summary = TestInfo . TestNode . GetTestSummary ( ) ;
2021-02-03 14:57:28 -04:00
// Show summary
2022-11-12 18:42:11 -05:00
Summary . Split ( '\n' ) . ToList ( ) . ForEach ( L = > {
if ( L . Contains ( "Error: " ) )
{
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , " " + L ) ;
}
else if ( L . Contains ( "Warning: " ) )
{
Log . Warning ( KnownLogEvents . Gauntlet_TestEvent , " " + L ) ;
}
else
{
Log . Info ( " " + L ) ;
}
} ) ;
2021-02-03 14:57:28 -04:00
Log . ResumeSanitization ( ) ;
// list warnings/errors if the test wants that
if ( TestInfo . TestNode . LogWarningsAndErrorsAfterSummary )
{
2022-11-12 18:42:11 -05:00
TestInfo . TestNode . GetErrors ( ) . ToList ( ) . ForEach ( E = > Log . Error ( KnownLogEvents . Gauntlet_TestEvent , E ) ) ;
2021-02-03 14:57:28 -04:00
2022-11-12 18:42:11 -05:00
TestInfo . TestNode . GetWarnings ( ) . ToList ( ) . ForEach ( E = > Log . Warning ( KnownLogEvents . Gauntlet_TestEvent , E ) ) ;
2021-02-03 14:57:28 -04:00
}
// display the final result
2018-11-14 19:05:13 -05:00
if ( TestInfo . FinalResult ! = TestResult . Passed )
{
2022-11-12 18:42:11 -05:00
Log . Info ( "{TestInfo} result={Result}" , TestInfo , TestInfo . FinalResult ) ;
2018-11-14 19:05:13 -05:00
if ( string . IsNullOrEmpty ( TestInfo . CancellationReason ) = = false )
{
2022-11-12 18:42:11 -05:00
Log . Info ( "\tReason: {Reason}" , TestInfo . CancellationReason ) ;
2021-05-27 13:40:37 -04:00
}
2018-11-14 19:05:13 -05:00
}
else
{
2019-05-02 00:10:12 -04:00
if ( TestInfo . TestNode . GetWarnings ( ) . Any ( ) )
2018-11-14 19:05:13 -05:00
{
2022-11-12 18:42:11 -05:00
Log . Info ( "{TestInfo} result={Result} with warnings" , TestInfo , TestInfo . FinalResult ) ;
2018-11-14 19:05:13 -05:00
}
else
{
2022-11-12 18:42:11 -05:00
Log . Info ( "{TestInfo} result={Result}" , TestInfo , TestInfo . FinalResult ) ;
2018-11-14 19:05:13 -05:00
}
}
2019-05-02 00:10:12 -04:00
2022-10-11 02:54:50 -04:00
// display how the test could be ran locally
string RunLocalString = TestInfo . TestNode . GetRunLocalCommand ( BuildCommandThatLaunchedExecutor ) ;
Log . Info ( "How to run locally: ({0})" , RunLocalString ) ;
2018-11-14 19:05:13 -05:00
}
/// <summary>
/// Reports the summary of this pass, including a sumamry for each test
/// </summary>
/// <param name="CurrentPass"></param>
/// <param name="NumPasses"></param>
/// <param name="Duration"></param>
/// <param name="AllInfo"></param>
/// <returns></returns>
2022-07-08 13:54:37 -04:00
void ReportMainSummary ( int CurrentPass , int NumPasses , TimeSpan Duration , IEnumerable < TestExecutionInfo > AllInfo )
2018-11-14 19:05:13 -05:00
{
MarkdownBuilder MB = new MarkdownBuilder ( ) ;
int TestCount = AllInfo . Count ( ) ;
int FailedCount = AllInfo . Where ( T = > T . FinalResult ! = TestResult . Passed ) . Count ( ) ;
2019-05-02 00:10:12 -04:00
int WarningCount = AllInfo . Where ( T = > T . TestNode . GetWarnings ( ) . Any ( ) ) . Count ( ) ;
2018-11-14 19:05:13 -05:00
var SortedInfo = AllInfo ;
// sort our tests by failed/warning/ok
if ( FailedCount > 0 | | WarningCount > 0 )
{
SortedInfo = AllInfo . OrderByDescending ( T = >
{
if ( T . FinalResult ! = TestResult . Passed )
{
return 10 ;
}
2019-05-02 00:10:12 -04:00
if ( T . TestNode . GetWarnings ( ) . Any ( ) )
2018-11-14 19:05:13 -05:00
{
return 5 ;
}
return 0 ;
} ) ;
}
2021-02-03 14:57:28 -04:00
// write all test results at once
if ( Options . DeferReports )
{
// write each tests full summary
foreach ( TestExecutionInfo Info in SortedInfo )
{
ReportTestSummary ( Info ) ;
}
}
2018-11-14 19:05:13 -05:00
2021-02-03 14:57:28 -04:00
// only show pass info for multiple passes
if ( NumPasses > 1 )
{
2022-11-12 18:42:11 -05:00
Log . Info ( "Completed test pass {Index} of {Total}." , CurrentPass , NumPasses ) ;
2021-02-03 14:57:28 -04:00
}
// only show count of passed/failed etc for multiple test
if ( TestCount > 1 )
{
MB . H2 ( string . Format ( "{0} of {1} Tests Passed in {2:mm\\:ss}. ({3} Failed, {4} Passed with Warnings)" ,
TestCount - FailedCount , TestCount , Duration , FailedCount , WarningCount ) ) ;
2021-06-10 13:13:24 -04:00
List < string > TestResults = new List < string > ( ) ;
foreach ( TestExecutionInfo Info in SortedInfo )
{
string WarningString = Info . TestNode . GetWarnings ( ) . Any ( ) ? " With Warnings" : "" ;
2021-09-08 16:42:26 -04:00
TestResults . Add ( string . Format ( "\t{0} result={1}{2} (Waited={3:mm\\:ss}, Duration={4:mm\\:ss})" ,
Info , Info . FinalResult , WarningString ,
Info . WaitDuration , Info . TestDuration ) ) ;
2021-06-10 13:13:24 -04:00
}
2018-11-14 19:05:13 -05:00
2021-06-10 13:13:24 -04:00
MB . UnorderedList ( TestResults ) ;
2018-11-14 19:05:13 -05:00
}
// write the markdown out with each line indented
MB . ToString ( ) . Split ( '\n' ) . ToList ( ) . ForEach ( L = > Log . Info ( " " + L ) ) ;
}
2018-09-25 10:11:35 -04:00
TestResult TickTest ( TestExecutionInfo TestInfo )
{
// Give the test a chance to update itself
try
{
TestInfo . TestNode . TickTest ( ) ;
}
catch ( Exception Ex )
{
2018-11-14 19:05:13 -05:00
TestInfo . CancellationReason = string . Format ( "Test {0} threw an exception. Cancelling. Ex: {1}\n{2}" , TestInfo . TestNode . Name , Ex . Message , Ex . StackTrace ) ;
TestInfo . FinalResult = TestResult . Cancelled ;
2018-09-25 10:11:35 -04:00
}
// Does the test still say it's running?
bool TestIsRunning = TestInfo . TestNode . GetTestStatus ( ) = = TestStatus . InProgress ;
2021-09-08 16:42:26 -04:00
TimeSpan RunningTime = DateTime . Now - TestInfo . TimeSetupEnded ;
2020-09-01 14:07:48 -04:00
2018-09-25 10:11:35 -04:00
if ( TestIsRunning & & RunningTime . TotalSeconds > TestInfo . TestNode . MaxDuration & & ! Options . NoTimeout )
{
2020-09-01 14:07:48 -04:00
if ( TestInfo . TestNode . MaxDurationReachedResult = = EMaxDurationReachedResult . Failure )
{
TestInfo . CancellationReason = string . Format ( "Terminating Test {0} due to maximum duration of {1} seconds. " , TestInfo . TestNode , TestInfo . TestNode . MaxDuration ) ;
TestInfo . FinalResult = TestResult . TimedOut ;
2022-11-12 18:42:11 -05:00
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , TestInfo . CancellationReason ) ;
2020-09-01 14:07:48 -04:00
}
else if ( TestInfo . TestNode . MaxDurationReachedResult = = EMaxDurationReachedResult . Success )
{
TestInfo . FinalResult = TestResult . Passed ;
TestIsRunning = false ;
2022-11-12 18:42:11 -05:00
Log . Info ( "Test {Name} successfully reached maximum duration of {Time} seconds. " , TestInfo . TestNode , TestInfo . TestNode . MaxDuration ) ;
2020-09-01 14:07:48 -04:00
}
2018-09-25 10:11:35 -04:00
}
if ( IsCancelled )
{
2018-11-14 19:05:13 -05:00
TestInfo . CancellationReason = string . Format ( "Cancelling Test {0} on request" , TestInfo . TestNode ) ;
TestInfo . FinalResult = TestResult . Cancelled ;
2022-11-12 18:42:11 -05:00
Log . Info ( TestInfo . CancellationReason ) ;
2018-09-25 10:11:35 -04:00
}
2020-12-04 17:30:52 -04:00
if ( ! string . IsNullOrEmpty ( TestInfo . CancellationReason ) )
{
TestInfo . TestNode . SetCancellationReason ( TestInfo . CancellationReason ) ;
}
2018-11-14 19:05:13 -05:00
// if the test is not running. or we've determined a result for it..
if ( TestIsRunning = = false | | TestInfo . FinalResult ! = TestResult . Invalid )
2018-09-25 10:11:35 -04:00
{
// Request the test stop
try
{
// Note - we log in this order to try and make it easy to grep the log and find the
// artifcat links
Log . Info ( "*" ) ;
Log . Info ( "****************************************************************" ) ;
2022-11-12 18:42:11 -05:00
Log . Info ( "Finished Test: {Name} in {Time:mm\\:ss}" , TestInfo , DateTime . Now - TestInfo . TimeSetupEnded ) ;
2018-09-25 10:11:35 -04:00
2018-11-14 19:05:13 -05:00
// Tell the test it's done. If it still thinks its running it was cancelled
2021-06-10 13:13:24 -04:00
TestInfo . TestNode . StopTest ( TestIsRunning ? StopReason . MaxDuration : StopReason . Completed ) ;
2021-09-08 16:42:26 -04:00
TestInfo . TimeTestEnded = DateTime . Now ;
2018-09-25 10:11:35 -04:00
TestResult NodeResult = TestInfo . TestNode . GetTestResult ( ) ;
2018-11-14 19:05:13 -05:00
TestInfo . FinalResult = ( TestInfo . FinalResult ! = TestResult . Invalid ) ? TestInfo . FinalResult : NodeResult ;
2018-09-25 10:11:35 -04:00
2019-06-05 08:42:16 -04:00
bool bCanFinalizeTest = true ;
2018-11-14 19:05:13 -05:00
if ( TestInfo . FinalResult = = TestResult . WantRetry )
2018-09-25 10:11:35 -04:00
{
2022-11-12 18:42:11 -05:00
Log . Info ( "{Name} requested retry. Cleaning up old test and relaunching" , TestInfo ) ;
2018-09-25 10:11:35 -04:00
2021-09-08 16:42:26 -04:00
DateTime OriginalStartTime = TestInfo . TimeSetupEnded ;
2018-09-25 10:11:35 -04:00
2019-06-05 08:42:16 -04:00
bool bIsRestarted = TestInfo . TestNode . RestartTest ( ) ;
if ( bIsRestarted )
{
// Mark us as still running
TestInfo . CancellationReason = "" ;
TestInfo . FinalResult = TestResult . Invalid ;
bCanFinalizeTest = false ;
}
2021-08-12 18:53:24 -04:00
else
{
TestInfo . CancellationReason = "Failed to restart during retry." ;
2022-11-12 18:42:11 -05:00
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , TestInfo . CancellationReason ) ;
2021-08-12 18:53:24 -04:00
TestInfo . FinalResult = TestResult . Failed ;
}
2018-09-25 10:11:35 -04:00
}
2019-06-05 08:42:16 -04:00
if ( bCanFinalizeTest )
2018-09-25 10:11:35 -04:00
{
2022-11-12 18:42:11 -05:00
Log . Info ( "{Name} result={Result}" , TestInfo , TestInfo . FinalResult ) ;
2018-09-25 10:11:35 -04:00
2018-11-14 19:05:13 -05:00
if ( ! Options . DeferReports )
2018-09-25 10:11:35 -04:00
{
2018-11-14 19:05:13 -05:00
ReportTestSummary ( TestInfo ) ;
2018-09-25 10:11:35 -04:00
}
2021-06-23 17:51:32 -04:00
TestInfo . TestNode . SetTestResult ( TestInfo . FinalResult ) ;
2019-06-05 08:42:16 -04:00
// now cleanup
try
{
TestInfo . TestNode . CleanupTest ( ) ;
}
catch ( System . Exception ex )
{
2022-11-12 18:42:11 -05:00
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , "Test {Name} threw an exception while cleaning up. Ex: {Exception}" , TestInfo . TestNode . Name , ex . Message ) ;
2019-06-05 08:42:16 -04:00
}
2018-09-25 10:11:35 -04:00
}
Log . Info ( "****************************************************************" ) ;
Log . Info ( "*" ) ;
}
catch ( System . Exception ex )
{
2018-11-14 19:05:13 -05:00
if ( TestIsRunning )
2018-09-25 10:11:35 -04:00
{
2022-11-12 18:42:11 -05:00
Log . Warning ( KnownLogEvents . Gauntlet_TestEvent , "Cancelled Test {Name} threw an exception while stopping. Ex: {Eception}\n{Callstack}" ,
2018-09-25 10:11:35 -04:00
TestInfo . TestNode . Name , ex . Message , ex . StackTrace ) ;
}
else
{
2022-11-12 18:42:11 -05:00
Log . Error ( KnownLogEvents . Gauntlet_TestEvent , "Test {Name} threw an exception while stopping. Ex: {Exception}\n{Callstack}" ,
2018-09-25 10:11:35 -04:00
TestInfo . TestNode . Name , ex . Message , ex . StackTrace ) ;
}
2018-11-14 19:05:13 -05:00
TestInfo . FinalResult = TestResult . Failed ;
2018-09-25 10:11:35 -04:00
}
}
2018-11-14 19:05:13 -05:00
return TestInfo . FinalResult ;
2018-09-25 10:11:35 -04:00
}
/// <summary>
/// Waits for all pending tests to complete. Returns true/false based on whether
/// all tests completed successfully.
/// </summary>
bool WaitForTests ( )
{
2022-11-12 18:42:11 -05:00
Log . Info ( "Waiting for {Count} tests to complete" , RunningTests . Count ) ;
2018-09-25 10:11:35 -04:00
DateTime LastUpdateMsg = DateTime . Now ;
bool AllTestsPassed = true ;
while ( RunningTests . Count > 0 )
{
List < TestExecutionInfo > RemainingTests = new List < TestExecutionInfo > ( ) ;
foreach ( TestExecutionInfo Process in RunningTests )
{
TestResult Result = TickTest ( Process ) ;
// invalid = no
if ( Result = = TestResult . Invalid )
{
RemainingTests . Add ( Process ) ;
2021-09-08 16:42:26 -04:00
TimeSpan RunningTime = DateTime . Now - Process . TimeSetupEnded ;
2018-09-25 10:11:35 -04:00
if ( ( DateTime . Now - LastUpdateMsg ) . TotalSeconds > 60.0f )
{
2022-11-12 18:42:11 -05:00
Log . Verbose ( "Test {Name} is still running. {Elapsed:00} seconds elapsed, will timeout in {Max:00} seconds" ,
2018-09-25 10:11:35 -04:00
Process . TestNode . Name ,
RunningTime . TotalSeconds ,
Process . TestNode . MaxDuration - RunningTime . TotalSeconds ) ;
LastUpdateMsg = DateTime . Now ;
}
}
else
{
if ( Result ! = TestResult . Passed )
{
AllTestsPassed = false ;
}
2022-11-12 18:42:11 -05:00
Log . Info ( "Test {Name} Result: {Result}" , Process . TestNode . Name , Result ) ;
2018-09-25 10:11:35 -04:00
}
}
RunningTests = RemainingTests ;
Thread . Sleep ( 1000 ) ;
}
return AllTestsPassed ;
}
}
}