TestExecWindow
SourceFileParser.cs
//------------------------------------------------------------------------------
// Copyright(C) 2022 Gerald Fahrnholz
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// Contact: http://www.gerald-fahrnholz.eu/impressum.php
//------------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TestExecWin_VS2022
{
public class TokenInfo
{
public string token { get; set; } // description read as last from source file
public bool exists { get; set; }
public int startIdx { get; set; }
public int endIdx { get; set; }
public bool fatalError { get; set; }
public TokenInfo()
{
token = "";
exists = false;
startIdx = -1;
endIdx = -1;
fatalError = false;
}
public void NotFound(int in_startIdx)
{
token = "";
exists = false;
startIdx = in_startIdx;
endIdx = -1;
}
}
public class MacroInfo
{
public string param1 { get; set; }
public string param2 { get; set; }
public MacroInfo(string in_param1, string in_param2)
{
param1 = in_param1;
param2 = in_param2;
}
}
public class ParseInfo
{
public string description { get; set; } // description read as last from source file
public string fileFullPath { get; set; }
public int lineIdx { get; set; }
public System.Collections.Generic.List<String> testGroups; // BOOST: hierarchical nested set of test suites, TTB: source file name
public ParseInfo(string in_fileFullPath)
{
description = "";
fileFullPath = in_fileFullPath;
lineIdx = 0;
testGroups = new System.Collections.Generic.List<string>(); // hierarchy of test groups
}
public int GetLineNum()
{
return lineIdx + 1;
}
public void AddTestGroup(string in_testGroup)
{
testGroups.Add(in_testGroup);
}
public void RemoveLastTestGroup()
{
testGroups.RemoveAt(testGroups.Count-1);
}
public void RemoveAllTestGroups()
{
testGroups.Clear();
}
public string GetTestGroupHierarchyString()
{
string allTestGroups = "";
foreach (string grp in testGroups)
{
if (allTestGroups != "")
{
allTestGroups += "/";
}
allTestGroups += grp;
}
return allTestGroups;
}
}
class SourceFileParser
{
private readonly IEventReceiver m_eventReceiver;
public SourceFileParser(IEventReceiver in_eventReceiver)
{
m_eventReceiver = in_eventReceiver;
}
/// [parser_scan_dir]
public void ScanProjectDir(ProjectInfo projectInfo)
{
WriteLine(3, "ScanProjectDir-Begin");
// Recursively scan for .cpp files in project dir and all sub directories
System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(projectInfo.sourceDirPath);
var files = dir.GetFiles("*.cpp", System.IO.SearchOption.AllDirectories);
foreach (var file in files)
{
// Search for test macros and store them within "projectInfo"
ParseFile(projectInfo, file);
}
WriteLine(3, "ScanProjectDir-End");
}
/// [parser_scan_dir]
private bool LineContainsMacro(string in_macro, string line)
{
int posStartMacro = line.IndexOf(in_macro);
if (posStartMacro >= 0) // macro found
{
int posStartComment = line.IndexOf("//");
if ((posStartComment >= 0) && (posStartComment < posStartMacro))
{
WriteLine(3, "LineContainsMacro: macro " + in_macro + " within comment is ignored");
return false;
}
return true;
}
return false;
}
/// [parser_read_file]
private void ParseFile(ProjectInfo projectInfo, System.IO.FileInfo in_file)
{
WriteLine(3, "ParseFile-Begin " + in_file.FullName);
WriteLine(2, "File " + in_file.FullName);
// Read whole source file into array of strings
string[] lines = System.IO.File.ReadAllLines(in_file.FullName);
int numLines = lines.Length;
// During parsing we store information about found description,
// test suite and current line index within ParseInfo
ParseInfo parseInfo = new ParseInfo(in_file.FullName);
while (parseInfo.lineIdx < numLines)
{
string line = lines[parseInfo.lineIdx];
if (LineContainsMacro("TTB_TEST_FUNC_DESC",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_TEST_FUNC_DESC", line, lines);
if (macro != null)
{
parseInfo.description = macro.param2;
projectInfo.appType = AppType.TTB;
parseInfo.RemoveAllTestGroups();
parseInfo.AddTestGroup(in_file.Name);
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("TTB_TEST_FUNC",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_TEST_FUNC", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.TTB;
parseInfo.RemoveAllTestGroups();
parseInfo.AddTestGroup(in_file.Name);
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_AUTO_TEST_CASE_TEMPLATE", line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_AUTO_TEST_CASE_TEMPLATE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
// Add "*" for correct call of test case
parseInfo.description += "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_AUTO_TEST_CASE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_AUTO_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if(LineContainsMacro("TTB_BOOST_TEST_CASE",line))
/// ...
/// [parser_read_file]
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_BOOST_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_FIXTURE_TEST_CASE_TEMPLATE", line)) // also matches TTB_BOOST_FIXTURE_TEST_CASE_TEMPLATE
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_FIXTURE_TEST_CASE_TEMPLATE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1 + "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_DATA_TEST_CASE_F", line)) // also matches TTB_BOOST_DATA_TEST_CASE_F
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_DATA_TEST_CASE_F", line, lines);
if (macro != null)
{
parseInfo.description = macro.param2 + "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_DATA_TEST_CASE", line)) // also matches TTB_BOOST_DATA_TEST_CASE
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_DATA_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1 + "*";
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("BOOST_FIXTURE_TEST_CASE", line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_FIXTURE_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("TTB_BOOST_FIXTURE_TEST_CASE", line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_BOOST_FIXTURE_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if (LineContainsMacro("RUN_TEST_CASE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"TTB_BOOST_TEST_CASE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
projectInfo.appType = AppType.BOOST;
CreateTestFuncInfo(projectInfo, parseInfo);
}
}
else if(LineContainsMacro("BOOST_AUTO_TEST_SUITE_END",line))
{
if (parseInfo.testGroups.Count > 0)
{
parseInfo.RemoveLastTestGroup();
}
else
{
WriteLine(1, "WARNING: Ignoring unbalanced BOOST_AUTO_TEST_SUITE_END in line "
+ parseInfo.GetLineNum() + " of file " + parseInfo.fileFullPath);
}
}
else if (LineContainsMacro("BOOST_AUTO_TEST_SUITE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_AUTO_TEST_SUITE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
parseInfo.AddTestGroup(parseInfo.description);
}
}
else if (LineContainsMacro("BOOST_FIXTURE_TEST_SUITE",line))
{
MacroInfo macro = ReadMacro(projectInfo, parseInfo,
"BOOST_FIXTURE_TEST_SUITE", line, lines);
if (macro != null)
{
parseInfo.description = macro.param1;
parseInfo.AddTestGroup(parseInfo.description);
}
}
++parseInfo.lineIdx;
}
WriteLine(3, "ParseFile-End projectInfo.appType=" + projectInfo.appType);
}
private bool ReadNextToken(TokenInfo tokenInfo, string line)
{
tokenInfo.token = "";
// Skip leading white space
int posStart = tokenInfo.startIdx;
while (posStart < line.Length)
{
if ((line[posStart] == ' ') || (line[posStart] =='\t'))
{
++posStart;
}
else
{
break;
}
}
if (posStart >= line.Length)
{
tokenInfo.NotFound(posStart);
return false;
}
// Start reading
bool readString = (line[posStart] == '"');
bool endOfStringReached = false;
tokenInfo.token += line[posStart];
tokenInfo.startIdx = posStart;
int idx = posStart + 1;
if ((line[posStart]==',') || (line[posStart] == ')'))
{
// token is already comolete
tokenInfo.endIdx = posStart;
tokenInfo.exists = true;
return true;
}
else if (readString) // at least another '"' must be found
{
while ((idx < line.Length) && (!endOfStringReached))
{
tokenInfo.token += line[idx];
endOfStringReached = line[idx] == '"';
++idx;
}
if (endOfStringReached)
{
tokenInfo.endIdx = idx-1;
tokenInfo.exists = true;
return true;
}
else // (idx >= line.Length)
{
WriteLine(1, "ERROR: unsupported syntax: string not closed");
tokenInfo.NotFound(posStart);
tokenInfo.fatalError = true;
return false;
}
}
else // read until " ", ",", ")" or end of line is found
{
string endChars = " ,)";
while ((idx < line.Length) && (endChars.IndexOf(line[idx], 0) < 0))
{
tokenInfo.token += line[idx];
++idx;
}
tokenInfo.endIdx = idx - 1;
tokenInfo.exists = true;
return true;
}
}
private string GetContextInfo(ParseInfo parseInfo)
{
return " [" + parseInfo.fileFullPath + " line " + (parseInfo.lineIdx + 1) + "]";
}
// Reads all macros with format: MACRO(), MACRO(param), MACRO(param,param), MACRO( "witin string, you can usse, ()", param)
// White space is automatically skipped, strings containing arbitrary chars are parsed as a single parameter,
// macro is terminated with closing brace ")", macro may have line breaks (up to 3 lines).
private MacroInfo ReadMacro(ProjectInfo projectInfo, ParseInfo parseInfo, string in_macro, string in_line, string[] in_lines)
{
WriteLine(3, "ReadMacro line " + parseInfo.lineIdx + ": " + in_line);
string line = in_line;
int posStart = line.IndexOf(in_macro) + 1;
posStart = line.IndexOf("(", posStart);
if (posStart < 0) // give up
{
WriteLine(1, "WARNING: unsupported syntax: '( 'not found" + GetContextInfo(parseInfo));
return null;
}
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.startIdx = posStart + 1;
// now start reading after the opening brace "("
int maxNumLinesAllowedForMacro = 3;
bool endReached = false;
int paramIdx = 0;
string[] param = new string[] { "", "" };
while (!endReached && !tokenInfo.fatalError && (maxNumLinesAllowedForMacro) > 0)
{
if (ReadNextToken(tokenInfo, line))
{
WriteLine(3, "ReadMacro token= " + tokenInfo.token);
if ((tokenInfo.token != ",") && (tokenInfo.token != ")"))
{
if (paramIdx <= 1)
{
param[paramIdx] = tokenInfo.token;
++paramIdx;
}
else
{
WriteLine(3, "ReadMacro unexpected param is ignored: " + tokenInfo.token + GetContextInfo(parseInfo));
}
}
else
{
endReached = tokenInfo.token == ")";
}
tokenInfo.startIdx = tokenInfo.endIdx + 1;
}
else
{
if (!tokenInfo.fatalError) // check also next line
{
--maxNumLinesAllowedForMacro;
if (maxNumLinesAllowedForMacro > 0)
{
++parseInfo.lineIdx;
if (parseInfo.lineIdx >= in_lines.Length)
{
WriteLine(1, "WARNING: unsupported syntax: end of file reached" + GetContextInfo(parseInfo));
return null;
}
string nextLine = in_lines[parseInfo.lineIdx];
line = line + nextLine;
}
}
}
}
if (!endReached)
{
WriteLine(1, "WARNING: unsupported syntax: end of macro not found" + GetContextInfo(parseInfo));
return null;
}
return new MacroInfo(param[0],param[1]);
}
/// [parser_func_info]
private void CreateTestFuncInfo(ProjectInfo projectInfo, ParseInfo parseInfo)
{
// Store the found test group if not already existing
string[] groupNames = parseInfo.GetTestGroupHierarchyString().Split(new Char[] { '/'}, StringSplitOptions.RemoveEmptyEntries);
string groupHierarchyString = "";
List<string> groups = new System.Collections.Generic.List<string>();
if (groupNames.Length == 0)
{
if (!projectInfo.testFuncWithoutTestGroupFound)
{
projectInfo.testFuncWithoutTestGroupFound = true;
groups.Add(ProjectInfo.NO_TESTSUITE_STR);
TestGroupEntry tg = new TestGroupEntry(projectInfo.appType == AppType.BOOST);
tg.fileFullPath = parseInfo.fileFullPath;
tg.lineNum = parseInfo.GetLineNum();
tg.AddTestGroupHierarchy(groups);
projectInfo.testGroups.Add(tg);
}
}
else
{
foreach (string grp in groupNames)
{
groups.Add(grp);
groupHierarchyString += (groupHierarchyString.Length > 0 ? "/" : "");
groupHierarchyString += grp;
if (projectInfo.testGroups.Find(x => (x.GetTestGroupHierarchyString() == groupHierarchyString)) == null)
{
TestGroupEntry tg = new TestGroupEntry(projectInfo.appType == AppType.BOOST);
tg.fileFullPath = parseInfo.fileFullPath;
tg.lineNum = parseInfo.GetLineNum();
tg.AddTestGroupHierarchy(groups);
projectInfo.testGroups.Add(tg);
}
}
}
// Store the found test function description
TestFuncEntry tf = new TestFuncEntry(projectInfo.appType == AppType.BOOST);
tf.testFunc = parseInfo.description;
tf.fileFullPath = parseInfo.fileFullPath;
tf.lineNum = parseInfo.GetLineNum();
tf.AddTestGroupHierarchy(parseInfo.testGroups);
projectInfo.testFuncs.Add(tf);
WriteLine(3, "CreateTestFuncInfo: " + tf.testFunc + " within group: " + tf.GetTestGroupHierarchyString()
+ " file: " + tf.fileFullPath + " line: " + tf.lineNum);
}
/// [parser_func_info]
private void WriteLine(int in_outputLevel, String in_info)
{
m_eventReceiver.WriteLine(in_outputLevel, in_info);
}
}
}