2019-08-23 11:56:54 -07:00
using System ;
using System.Collections.Generic ;
using System.Diagnostics ;
using System.Threading ;
using System.IO ;
namespace IDE
{
class TestManager
{
public class ProjectInfo
{
public Project mProject ;
public String mTestExePath ~ delete _ ;
}
public class TestEntry
{
public String mName ~ delete _ ;
public String mFilePath ~ delete _ ;
public int mLine ;
public int mColumn ;
public bool mShouldFail ;
public bool mProfile ;
public bool mIgnore ;
}
public class TestInstance
{
public SpawnedProcess mProcess ~ delete _ ;
public Thread mThread ~ delete _ ;
public int mProjectIdx ;
public List < TestEntry > mTestEntries = new . ( ) ~ DeleteContainerAndItems ! ( _ ) ;
public String mPipeName ~ delete _ ;
public String mArgs ~ delete _ ;
public String mWorkingDir ~ delete _ ;
public NamedPipe mPipeServer ~ delete _ ;
public int mShouldFailIdx = - 1 ;
}
public bool mIsDone ;
public bool mIsRunning ;
public bool mWantsStop ;
public int mProjectInfoIdx = - 1 ;
public TestInstance mTestInstance ~ delete _ ;
public List < ProjectInfo > mProjectInfos = new . ( ) ~ DeleteContainerAndItems ! ( _ ) ;
public List < String > mQueuedOutput = new . ( ) ~ DeleteContainerAndItems ! ( _ ) ;
public Monitor mMonitor = new Monitor ( ) ~ delete _ ;
public String mPrevConfigName ~ delete _ ;
public bool mDebug ;
public bool mIncludeIgnored ;
public bool mFailed ;
2019-12-13 14:25:15 -08:00
public bool HasProjects
{
get
{
return ! mProjectInfos . IsEmpty ;
}
}
2019-08-23 11:56:54 -07:00
public ~ this ( )
{
if ( mTestInstance ! = null )
{
mTestInstance . mThread . Join ( ) ;
}
}
public bool IsRunning ( )
{
return true ;
}
public void AddProject ( Project project )
{
var projectInfo = new ProjectInfo ( ) ;
projectInfo . mProject = project ;
mProjectInfos . Add ( projectInfo ) ;
}
public bool IsTesting ( Project project )
{
return GetProjectInfo ( project ) ! = null ;
}
public ProjectInfo GetProjectInfo ( Project project )
{
int projectIdx = mProjectInfos . FindIndex ( scope ( info ) = > info . mProject = = project ) ;
if ( projectIdx = = - 1 )
return null ;
return mProjectInfos [ projectIdx ] ;
}
public void Start ( )
{
mIsRunning = true ;
}
public void BuildFailed ( )
{
mIsDone = true ;
}
ProjectInfo GetCurProjectInfo ( )
{
if ( ( mProjectInfoIdx > = 0 ) & & ( mProjectInfoIdx < mProjectInfos . Count ) )
return mProjectInfos [ mProjectInfoIdx ] ;
return null ;
}
void QueueOutputLine ( StringView str )
{
using ( mMonitor . Enter ( ) )
mQueuedOutput . Add ( new String ( str ) ) ;
}
void QueueOutputLine ( StringView str , params Object [ ] args )
{
using ( mMonitor . Enter ( ) )
{
var formattedStr = new String ( ) ;
formattedStr . AppendF ( str , params args ) ;
mQueuedOutput . Add ( formattedStr ) ;
}
}
public void TestProc ( TestInstance testInstance )
{
var curProjectInfo = GetCurProjectInfo ( ) ;
if ( ! mDebug )
{
var startInfo = scope ProcessStartInfo ( ) ;
startInfo . CreateNoWindow = ! gApp . mTestEnableConsole ;
startInfo . SetFileName ( curProjectInfo . mTestExePath ) ;
startInfo . SetArguments ( testInstance . mArgs ) ;
startInfo . SetWorkingDirectory ( testInstance . mWorkingDir ) ;
mTestInstance . mProcess = new SpawnedProcess ( ) ;
if ( testInstance . mProcess . Start ( startInfo ) case . Err )
{
TestFailed ( ) ;
QueueOutputLine ( "ERROR: Failed execute '{0}'" , curProjectInfo . mTestExePath ) ;
return ;
}
}
String clientStr = scope String ( ) ;
int curTestIdx = - 1 ;
int curTestRunCount = 0 ;
bool testsFinished = false ;
bool failed = false ;
int exitCode = 0 ;
while ( true )
{
int doneCount = 0 ;
for ( int itr < 2 )
{
bool hadData = false ;
uint8 [ 1024 ] data ;
switch ( testInstance . mPipeServer . TryRead ( . ( & data , 1024 ) , 20 ) )
{
case . Ok ( let size ) :
{
clientStr . Append ( ( char8 * ) & data , size ) ;
hadData = true ;
}
default :
}
while ( true )
{
int crPos = clientStr . IndexOf ( '\n' ) ;
if ( crPos = = - 1 )
break ;
String cmd = scope String ( ) ;
cmd . Append ( clientStr , 0 , crPos ) ;
clientStr . Remove ( 0 , crPos + 1 ) ;
2020-01-06 13:49:35 -08:00
if ( cmd . IsWhiteSpace )
continue ;
2019-08-23 11:56:54 -07:00
/ * String outStr = scope String ( ) ;
outStr . AppendF ( "CMD: {0}" , cmd ) ;
QueueOutput ( outStr ) ; * /
List < StringView > cmdParts = scope . ( cmd . Split ( '\t' ) ) ;
switch ( cmdParts [ 0 ] )
{
case ":TestInit" :
case ":TestBegin" :
case ":TestQuery" :
if ( ( curTestIdx = = - 1 ) | | ( curTestRunCount > 0 ) )
{
curTestIdx + + ;
curTestRunCount = 0 ;
}
while ( true )
{
if ( curTestIdx < testInstance . mTestEntries . Count )
{
curTestRunCount + + ;
bool skipEntry = false ;
let testEntry = testInstance . mTestEntries [ curTestIdx ] ;
if ( testEntry . mShouldFail )
{
skipEntry = testInstance . mShouldFailIdx ! = curTestIdx ;
}
else if ( testInstance . mShouldFailIdx ! = - 1 )
{
skipEntry = true ;
}
if ( ( ! skipEntry ) & & ( testEntry . mIgnore ) & & ( ! mIncludeIgnored ) )
{
QueueOutputLine ( "Test Ignored: {0}" , testEntry . mName ) ;
skipEntry = true ;
}
if ( skipEntry )
{
curTestIdx + + ;
curTestRunCount = 0 ;
continue ;
}
var clientCmd = scope String ( ) ;
clientCmd . AppendF ( ":TestRun\t{0}\n" , curTestIdx ) ;
if ( testInstance . mPipeServer . Write ( clientCmd ) case . Err )
failed = true ;
}
else
{
if ( testInstance . mPipeServer . Write ( ":TestFinish\n" ) case . Err )
failed = true ;
}
break ;
}
case ":TestResult" :
int timeMS = int32 . Parse ( cmdParts [ 1 ] ) . Get ( ) ;
var testEntry = testInstance . mTestEntries [ curTestIdx ] ;
if ( testEntry . mShouldFail )
{
QueueOutputLine ( "ERROR: Test should have failed but didn't: {0} Time: {1}ms" , testEntry . mName , timeMS ) ;
failed = true ;
}
else
QueueOutputLine ( "Test completed: {0} Time: {1}ms" , testEntry . mName , timeMS ) ;
case ":TestFinish" :
testsFinished = true ;
default :
2020-01-06 13:49:35 -08:00
if ( ( cmdParts . Count < 5 ) | | ( cmdParts [ 0 ] . StartsWith ( ":" ) ) )
{
QueueOutputLine ( "ERROR: Failed communicate with test target '{0}'" , curProjectInfo . mTestExePath ) ;
TestFailed ( ) ;
return ;
}
2019-08-23 11:56:54 -07:00
Debug . Assert ( cmdParts [ 0 ] [ 0 ] ! = ':' ) ;
let attribs = cmdParts [ 1 ] ;
TestEntry testEntry = new TestEntry ( ) ;
testEntry . mName = new String ( cmdParts [ 0 ] ) ;
testEntry . mFilePath = new String ( cmdParts [ 2 ] ) ;
testEntry . mLine = int32 . Parse ( cmdParts [ 3 ] ) . Get ( ) ;
testEntry . mColumn = int32 . Parse ( cmdParts [ 4 ] ) . Get ( ) ;
testEntry . mShouldFail = attribs . Contains ( "Sf" ) ;
testEntry . mProfile = attribs . Contains ( "Pr" ) ;
testEntry . mIgnore = attribs . Contains ( "Ig" ) ;
testInstance . mTestEntries . Add ( testEntry ) ;
}
}
if ( mWantsStop )
{
if ( testInstance . mProcess ! = null )
testInstance . mProcess . Kill ( ) ;
}
if ( ! hadData )
{
bool processDone ;
if ( testInstance . mProcess ! = null )
processDone = testInstance . mProcess . WaitFor ( 0 ) ;
else
processDone = ! gApp . mDebugger . mIsRunning ;
if ( processDone )
{
if ( testInstance . mProcess ! = null )
{
exitCode = testInstance . mProcess . ExitCode ;
}
doneCount + + ;
}
}
}
if ( doneCount = = 2 )
break ;
if ( failed )
{
TestFailed ( ) ;
break ;
}
}
if ( mWantsStop )
{
QueueOutputLine ( "Tests aborted" ) ;
}
else if ( ! testsFinished )
{
var str = scope String ( ) ;
if ( curTestIdx = = - 1 )
{
str . AppendF ( "Failed to start tests" ) ;
}
else if ( curTestIdx < testInstance . mTestEntries . Count )
{
var testEntry = testInstance . mTestEntries [ curTestIdx ] ;
if ( testInstance . mShouldFailIdx = = curTestIdx )
{
// Success
QueueOutputLine ( "Test expectedly failed: {0}" , testEntry . mName ) ;
}
else
{
str . AppendF ( "ERROR: Failed test '{0}' at line {2}:{3} in {1}" , testEntry . mName , testEntry . mFilePath , testEntry . mLine + 1 , testEntry . mColumn + 1 ) ;
}
}
else
{
str . AppendF ( "ERROR: Failed to finish tests" ) ;
}
if ( str . Length > 0 )
{
var errStr = scope String ( ) ;
errStr . AppendF ( "ERROR: {0}" , str ) ;
QueueOutputLine ( errStr ) ;
TestFailed ( ) ;
}
}
else if ( exitCode ! = 0 )
{
2020-01-06 13:49:35 -08:00
QueueOutputLine ( "ERROR: Test process exited with error code: {0}" , exitCode ) ;
TestFailed ( ) ;
}
else if ( testInstance . mTestEntries . IsEmpty )
{
QueueOutputLine ( "WARNING: No test methods defined. Consider adding a [Test] attribute to a static method in a project whose build type is set to 'Test'." ) ;
2019-08-23 11:56:54 -07:00
}
}
public void Update ( )
{
using ( mMonitor . Enter ( ) )
{
while ( mQueuedOutput . Count > 0 )
{
var str = mQueuedOutput . PopFront ( ) ;
gApp . OutputLineSmart ( str ) ;
delete str ;
}
}
if ( ( ! mIsRunning ) | | ( mIsDone ) )
return ;
if ( mWantsStop )
{
if ( gApp . mDebugger . mIsRunning )
gApp . mDebugger . Terminate ( ) ;
}
int nextShouldFailIdx = - 1 ;
bool doNext = true ;
var curProjectInfo = GetCurProjectInfo ( ) ;
if ( curProjectInfo ! = null )
{
if ( mTestInstance ! = null )
{
if ( mTestInstance . mThread . Join ( 0 ) )
{
for ( int entryIdx = mTestInstance . mShouldFailIdx + 1 ; entryIdx < mTestInstance . mTestEntries . Count ; entryIdx + + )
{
let testEntry = mTestInstance . mTestEntries [ entryIdx ] ;
if ( testEntry . mShouldFail )
{
nextShouldFailIdx = entryIdx ;
break ;
}
}
DeleteAndNullify ! ( mTestInstance ) ;
}
else
doNext = false ;
}
}
else
{
Debug . Assert ( mTestInstance = = null ) ;
}
if ( doNext )
{
if ( mWantsStop )
{
mIsDone = true ;
return ;
}
Debug . Assert ( mTestInstance = = null ) ;
if ( nextShouldFailIdx = = - 1 )
{
mProjectInfoIdx + + ;
if ( mProjectInfoIdx > = mProjectInfos . Count )
{
mIsDone = true ;
return ;
}
}
mTestInstance = new TestInstance ( ) ;
mTestInstance . mProjectIdx = mProjectInfoIdx ;
mTestInstance . mShouldFailIdx = nextShouldFailIdx ;
curProjectInfo = GetCurProjectInfo ( ) ;
if ( mTestInstance . mShouldFailIdx ! = - 1 )
gApp . OutputLineSmart ( "Starting should-fail testing on {0}..." , curProjectInfo . mProject . mProjectName ) ;
else
gApp . OutputLineSmart ( "Starting testing on {0}..." , curProjectInfo . mProject . mProjectName ) ;
mTestInstance . mThread = new Thread ( new ( ) = > { TestProc ( mTestInstance ) ; } ) ;
mTestInstance . mPipeName = new String ( ) ;
mTestInstance . mPipeName . AppendF ( "__bfTestPipe{0}_{1}" , Process . CurrentId , mTestInstance . mProjectIdx ) ;
mTestInstance . mArgs = new String ( ) ;
mTestInstance . mArgs . Append ( mTestInstance . mPipeName ) ;
//mTestInstance.mWorkingDir = new String();
//Path.GetDirectoryName(curProjectInfo.mTestExePath, mTestInstance.mWorkingDir);
mTestInstance . mWorkingDir = new String ( gApp . mInstallDir ) ;
mTestInstance . mPipeServer = new NamedPipe ( ) ;
if ( mTestInstance . mPipeServer . Create ( "." , mTestInstance . mPipeName , . AllowTimeouts ) case . Err )
{
QueueOutputLine ( "ERROR: Failed to create named pipe for test" ) ;
TestFailed ( ) ;
return ;
}
if ( mDebug )
{
gApp . [ Friend ] CheckDebugVisualizers ( ) ;
var envVars = scope Dictionary < String , String > ( ) ;
defer { for ( var kv in envVars ) { delete kv . key ; delete kv . value ; } }
Environment . GetEnvironmentVariables ( envVars ) ;
var envBlock = scope List < char8 > ( ) ;
Environment . EncodeEnvironmentVariables ( envVars , envBlock ) ;
2019-09-02 17:39:47 -07:00
if ( ! gApp . mDebugger . OpenFile ( curProjectInfo . mTestExePath , curProjectInfo . mTestExePath , mTestInstance . mArgs , mTestInstance . mWorkingDir , envBlock , true ) )
2019-08-23 11:56:54 -07:00
{
QueueOutputLine ( "ERROR: Failed debug '{0}'" , curProjectInfo . mTestExePath ) ;
TestFailed ( ) ;
return ;
}
gApp . mDebugger . ClearInvalidBreakpoints ( ) ;
gApp . mTargetDidInitBreak = false ;
gApp . mTargetHadFirstBreak = false ;
gApp . mDebugger . RehupBreakpoints ( true ) ;
gApp . mDebugger . Run ( ) ;
gApp . mDebugger . mIsRunning = true ;
}
mTestInstance . mThread . Start ( false ) ;
}
}
public void TestFailed ( )
{
gApp . TestFailed ( ) ;
mIsDone = true ;
mFailed = true ;
}
public void Stop ( )
{
mWantsStop = true ;
}
}
}