mirror of
https://github.com/beefytech/Beef.git
synced 2025-06-08 11:38:21 +02:00
665 lines
16 KiB
Beef
665 lines
16 KiB
Beef
using System;
|
|
using System.Collections;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.IO;
|
|
|
|
namespace IDE
|
|
{
|
|
class TestManager
|
|
{
|
|
public class ProjectInfo
|
|
{
|
|
public Project mProject;
|
|
public String mTestExePath ~ delete _;
|
|
public int32 mTestCount;
|
|
public int32 mExecutedCount;
|
|
public int32 mSkipCount;
|
|
public int32 mFailedCount;
|
|
public String mWorkingDir ~ 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 bool mFailed;
|
|
public bool mExecuted;
|
|
}
|
|
|
|
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 int32 mCurTestIdx = -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;
|
|
|
|
public bool HasProjects
|
|
{
|
|
get
|
|
{
|
|
return !mProjectInfos.IsEmpty;
|
|
}
|
|
}
|
|
|
|
public ~this()
|
|
{
|
|
if (mTestInstance != null)
|
|
{
|
|
mTestInstance.mThread.Join();
|
|
}
|
|
}
|
|
|
|
public bool IsRunning()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
public void AddProject(Project project, StringView workingDir)
|
|
{
|
|
var projectInfo = new ProjectInfo();
|
|
projectInfo.mProject = project;
|
|
projectInfo.mWorkingDir = new String(workingDir);
|
|
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(curProjectInfo.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 curTestRunCount = 0;
|
|
bool testsFinished = false;
|
|
bool failed = false;
|
|
int testFailCount = 0;
|
|
bool testHadOutput = false;
|
|
|
|
int exitCode = 0;
|
|
|
|
String queuedOutText = scope .();
|
|
|
|
Stopwatch testTimer = scope .();
|
|
|
|
void FlushTestStart()
|
|
{
|
|
if (testHadOutput)
|
|
return;
|
|
|
|
if ((testInstance.mCurTestIdx >= 0) && (testInstance.mCurTestIdx < testInstance.mTestEntries.Count))
|
|
{
|
|
testHadOutput = true;
|
|
String outputLine = scope String();
|
|
let testEntry = testInstance.mTestEntries[testInstance.mCurTestIdx];
|
|
outputLine.AppendF($"Testing '{testEntry.mName}'");
|
|
QueueOutputLine(outputLine);
|
|
}
|
|
}
|
|
|
|
void FlushOutText(bool force)
|
|
{
|
|
if (queuedOutText.IsEmpty)
|
|
return;
|
|
if ((!queuedOutText.EndsWith('\n')) && (force))
|
|
queuedOutText.Append('\n');
|
|
int lastCrPos = -1;
|
|
while (true)
|
|
{
|
|
int crPos = queuedOutText.IndexOf('\n', lastCrPos + 1);
|
|
if (crPos == -1)
|
|
break;
|
|
|
|
FlushTestStart();
|
|
|
|
String outputLine = scope String();
|
|
outputLine.Append(" >");
|
|
if (crPos - lastCrPos - 2 > 0)
|
|
outputLine.Append(StringView(queuedOutText, lastCrPos + 1, crPos - lastCrPos - 1));
|
|
|
|
QueueOutputLine(outputLine);
|
|
lastCrPos = crPos;
|
|
}
|
|
|
|
if (lastCrPos != -1)
|
|
queuedOutText.Remove(0, lastCrPos + 1);
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
if ((mDebug) && (gApp.mDebugger.IsPaused()))
|
|
{
|
|
FlushTestStart();
|
|
}
|
|
|
|
if (testTimer.ElapsedMilliseconds > 1000)
|
|
{
|
|
FlushTestStart();
|
|
}
|
|
|
|
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);
|
|
|
|
if (cmd.IsWhiteSpace)
|
|
continue;
|
|
|
|
/*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":
|
|
testTimer.Stop();
|
|
testTimer.Start();
|
|
FlushOutText(true);
|
|
testHadOutput = false;
|
|
if ((testInstance.mCurTestIdx == -1) || (curTestRunCount > 0))
|
|
{
|
|
testInstance.mCurTestIdx++;
|
|
curTestRunCount = 0;
|
|
}
|
|
|
|
curProjectInfo.mTestCount = (.)testInstance.mTestEntries.Count;
|
|
|
|
while (true)
|
|
{
|
|
if (testInstance.mCurTestIdx < testInstance.mTestEntries.Count)
|
|
{
|
|
curTestRunCount++;
|
|
bool skipEntry = false;
|
|
|
|
let testEntry = testInstance.mTestEntries[testInstance.mCurTestIdx];
|
|
|
|
if ((!skipEntry) && (testEntry.mIgnore) && (!mIncludeIgnored))
|
|
{
|
|
QueueOutputLine("Test Ignored: {0}", testEntry.mName);
|
|
skipEntry = true;
|
|
}
|
|
|
|
if (skipEntry)
|
|
{
|
|
curProjectInfo.mSkipCount++;
|
|
testInstance.mCurTestIdx++;
|
|
curTestRunCount = 0;
|
|
continue;
|
|
}
|
|
|
|
curProjectInfo.mExecutedCount++;
|
|
testEntry.mExecuted = true;
|
|
|
|
String clientCmd = scope $":TestRun\t{testInstance.mCurTestIdx}";
|
|
if ((gApp.mTestBreakOnFailure) && (mDebug) && (!testEntry.mShouldFail))
|
|
clientCmd.Append("\tFailBreak");
|
|
clientCmd.Append("\n");
|
|
|
|
if (testInstance.mPipeServer.Write(clientCmd) case .Err)
|
|
failed = true;
|
|
}
|
|
else
|
|
{
|
|
if (testInstance.mPipeServer.Write(":TestFinish\n") case .Err)
|
|
failed = true;
|
|
}
|
|
break;
|
|
}
|
|
case ":TestResult":
|
|
testTimer.Stop();
|
|
int timeMS = int32.Parse(cmdParts[1]).Get();
|
|
var testEntry = testInstance.mTestEntries[testInstance.mCurTestIdx];
|
|
|
|
if (testEntry.mShouldFail)
|
|
{
|
|
if (testEntry.mFailed)
|
|
{
|
|
QueueOutputLine("Test expectedly failed: {0} Time: {1}ms", testEntry.mName, timeMS);
|
|
}
|
|
else
|
|
{
|
|
curProjectInfo.mFailedCount++;
|
|
QueueOutputLine("ERROR: Test should have failed but didn't: '{0}' defined at line {2}:{3} in {1}", testEntry.mName, testEntry.mFilePath, testEntry.mLine + 1, testEntry.mColumn + 1);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (testHadOutput)
|
|
QueueOutputLine(" Test Time: {1}ms", testEntry.mName, timeMS);
|
|
else
|
|
QueueOutputLine("Test '{0}' Time: {1}ms", testEntry.mName, timeMS);
|
|
}
|
|
case ":TestFail",
|
|
":TestFatal":
|
|
var testEntry = testInstance.mTestEntries[testInstance.mCurTestIdx];
|
|
if (!testEntry.mFailed)
|
|
{
|
|
testFailCount++;
|
|
testEntry.mFailed = true;
|
|
|
|
if (!testEntry.mShouldFail)
|
|
{
|
|
curProjectInfo.mFailedCount++;
|
|
FlushTestStart();
|
|
QueueOutputLine("ERROR: Test failed. {}", cmd.Substring(cmdParts[0].Length + 1));
|
|
}
|
|
}
|
|
if (cmdParts[0] == ":TestFatal")
|
|
{
|
|
if (testInstance.mPipeServer.Write(":TestContinue\n") case .Err)
|
|
failed = true;
|
|
}
|
|
break;
|
|
case ":TestWrite":
|
|
String str = scope String()..Append(cmd.Substring(cmdParts[0].Length + 1));
|
|
str.Replace('\r', '\n');
|
|
queuedOutText.Append(str);
|
|
FlushOutText(false);
|
|
case ":TestFinish":
|
|
testsFinished = true;
|
|
default:
|
|
if ((cmdParts.Count < 5) || (cmdParts[0].StartsWith(":")))
|
|
{
|
|
QueueOutputLine("ERROR: Failed communicate with test target '{0}'", curProjectInfo.mTestExePath);
|
|
TestFailed();
|
|
return;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
FlushOutText(true);
|
|
|
|
/*if ((testFailCount > 0) && (!failed))
|
|
{
|
|
failed = true;
|
|
TestFailed();
|
|
}*/
|
|
|
|
if (mWantsStop)
|
|
{
|
|
QueueOutputLine("Tests aborted");
|
|
}
|
|
else if (!testsFinished)
|
|
{
|
|
var str = scope String();
|
|
if (testInstance.mCurTestIdx == -1)
|
|
{
|
|
str.AppendF("Failed to start tests");
|
|
}
|
|
else if (testInstance.mCurTestIdx < testInstance.mTestEntries.Count)
|
|
{
|
|
var testEntry = testInstance.mTestEntries[testInstance.mCurTestIdx];
|
|
if (testEntry.mShouldFail)
|
|
{
|
|
// Success
|
|
testEntry.mFailed = true;
|
|
QueueOutputLine("Test expectedly failed: {0}", testEntry.mName);
|
|
}
|
|
else
|
|
{
|
|
if (!testEntry.mFailed)
|
|
{
|
|
curProjectInfo.mFailedCount++;
|
|
testEntry.mFailed = true;
|
|
testFailCount++;
|
|
QueueOutputLine("Failed test '{0}' defined at line {2}:{3} in {1}", testEntry.mName, testEntry.mFilePath, testEntry.mLine + 1, testEntry.mColumn + 1);
|
|
}
|
|
}
|
|
/*if (testInstance.mShouldFailIdx == testInstance.mCurTestIdx)
|
|
{
|
|
// Success
|
|
QueueOutputLine("Test expectedly failed: {0}", testEntry.mName);
|
|
}
|
|
else
|
|
{
|
|
str.AppendF("Failed test '{0}' defined at line {2}:{3} in {1}", testEntry.mName, testEntry.mFilePath, testEntry.mLine + 1, testEntry.mColumn + 1);
|
|
}*/
|
|
}
|
|
else
|
|
{
|
|
str.AppendF("Failed to finish tests");
|
|
}
|
|
|
|
if (str.Length > 0)
|
|
{
|
|
var errStr = scope String();
|
|
errStr.AppendF("ERROR: {0}", str);
|
|
QueueOutputLine(errStr);
|
|
TestFailed();
|
|
}
|
|
}
|
|
else if (exitCode != 0)
|
|
{
|
|
QueueOutputLine("ERROR: Test process exited with error code: {0}", exitCode);
|
|
TestFailed();
|
|
}
|
|
else if (testInstance.mTestEntries.IsEmpty)
|
|
{
|
|
QueueOutputLine(
|
|
$"""
|
|
WARNING: No test methods defined in project '{mProjectInfos[testInstance.mProjectIdx].mProject.mProjectName}'.
|
|
Consider adding a [Test] attribute to a static method in a project whose build type is set to 'Test'.
|
|
If you do have test methods defined, make sure the Workspace properties has that project's 'Test' configuration selected.
|
|
""");
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
int32 nextTestIdx = -1;
|
|
|
|
bool doNext = true;
|
|
var curProjectInfo = GetCurProjectInfo();
|
|
if (curProjectInfo != null)
|
|
{
|
|
if (mTestInstance != null)
|
|
{
|
|
if (mTestInstance.mThread.Join(0))
|
|
{
|
|
if (mTestInstance.mCurTestIdx < mTestInstance.mTestEntries.Count - 1)
|
|
{
|
|
nextTestIdx = mTestInstance.mCurTestIdx + 1;
|
|
}
|
|
|
|
DeleteAndNullify!(mTestInstance);
|
|
}
|
|
else
|
|
doNext = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.Assert(mTestInstance == null);
|
|
}
|
|
|
|
if (doNext)
|
|
{
|
|
if (mWantsStop)
|
|
{
|
|
mIsDone = true;
|
|
return;
|
|
}
|
|
|
|
Debug.Assert(mTestInstance == null);
|
|
|
|
if (nextTestIdx == -1)
|
|
{
|
|
PrintProjectSummary();
|
|
mProjectInfoIdx++;
|
|
if (mProjectInfoIdx >= mProjectInfos.Count)
|
|
{
|
|
mIsDone = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
mTestInstance = new TestInstance();
|
|
mTestInstance.mProjectIdx = mProjectInfoIdx;
|
|
mTestInstance.mCurTestIdx = nextTestIdx;
|
|
|
|
curProjectInfo = GetCurProjectInfo();
|
|
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(curProjectInfo.mWorkingDir);
|
|
|
|
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);
|
|
if (!gApp.mDebugger.OpenFile(curProjectInfo.mTestExePath, curProjectInfo.mTestExePath, mTestInstance.mArgs, mTestInstance.mWorkingDir, envBlock, true, false))
|
|
{
|
|
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;
|
|
gApp.mDebugger.mDebugIdx++;
|
|
}
|
|
|
|
mTestInstance.mThread.Start(false);
|
|
}
|
|
}
|
|
|
|
public void PrintProjectSummary()
|
|
{
|
|
var curProjectInfo = GetCurProjectInfo();
|
|
if (curProjectInfo == null)
|
|
return;
|
|
|
|
String completeStr = scope $"Completed {curProjectInfo.mExecutedCount} of {curProjectInfo.mTestCount} tests for '{curProjectInfo.mProject.mProjectName}'";
|
|
QueueOutputLine(completeStr);
|
|
if (curProjectInfo.mFailedCount > 0)
|
|
{
|
|
String failStr = scope $"ERROR: Failed {curProjectInfo.mFailedCount} test{((curProjectInfo.mFailedCount != 1) ? "s" : "")}";
|
|
QueueOutputLine(failStr);
|
|
}
|
|
QueueOutputLine("");
|
|
}
|
|
|
|
public void TestFailed()
|
|
{
|
|
gApp.TestFailed();
|
|
mIsDone = true;
|
|
mFailed = true;
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
mWantsStop = true;
|
|
}
|
|
}
|
|
}
|