Line |
Branch |
Exec |
Source |
1 |
|
|
// Copyright (c) 2021-2024 ChilliBits. All rights reserved. |
2 |
|
|
|
3 |
|
|
// GCOV_EXCL_START |
4 |
|
|
|
5 |
|
|
#include "TestUtil.h" |
6 |
|
|
|
7 |
|
|
#include <dirent.h> |
8 |
|
|
#ifdef OS_UNIX |
9 |
|
|
#include <cstring> // Required by builds on Linux |
10 |
|
|
#endif |
11 |
|
|
|
12 |
|
|
#include <gtest/gtest.h> |
13 |
|
|
|
14 |
|
|
#include "util/CommonUtil.h" |
15 |
|
|
#include "util/FileUtil.h" |
16 |
|
|
|
17 |
|
|
namespace spice::testing { |
18 |
|
|
|
19 |
|
|
using namespace spice::compiler; |
20 |
|
|
|
21 |
|
|
/** |
22 |
|
|
* Collect the test cases in a particular test suite |
23 |
|
|
* |
24 |
|
|
* @param suiteName Name of the test suite |
25 |
|
|
* @param useSubDirs Use subdirectories as test cases |
26 |
|
|
* @return Vector of tests cases |
27 |
|
|
*/ |
28 |
|
− |
std::vector<TestCase> TestUtil::collectTestCases(const char *suiteName, bool useSubDirs) { |
29 |
|
− |
const std::filesystem::path suitePath = std::filesystem::path(PATH_TEST_FILES) / suiteName; |
30 |
|
|
|
31 |
|
− |
std::vector<TestCase> testCases; |
32 |
|
− |
testCases.reserve(EXPECTED_NUMBER_OF_TESTS); |
33 |
|
|
|
34 |
|
− |
if (useSubDirs) { |
35 |
|
|
// Collect subdirectories of the given suite |
36 |
|
− |
const std::vector<std::string> testGroupDirs = getSubdirs(suitePath); |
37 |
|
|
|
38 |
|
|
// Convert them to test cases |
39 |
|
− |
for (const std::string &groupDirName : testGroupDirs) { |
40 |
|
− |
const std::filesystem::path groupPath = suitePath / groupDirName; |
41 |
|
− |
for (const std::string &caseDirName : getSubdirs(groupPath)) { |
42 |
|
− |
const std::filesystem::path testPath = groupPath / caseDirName; |
43 |
|
− |
const TestCase tc = {toCamelCase(groupDirName), toCamelCase(caseDirName), testPath}; |
44 |
|
− |
testCases.push_back(tc); |
45 |
|
− |
} |
46 |
|
− |
} |
47 |
|
− |
} else { |
48 |
|
|
// Collect test cases |
49 |
|
− |
for (const std::string &caseDirName : getSubdirs(suitePath)) { |
50 |
|
− |
const std::filesystem::path testPath = suitePath / caseDirName; |
51 |
|
− |
const TestCase tc = {toCamelCase(suiteName), toCamelCase(caseDirName), testPath}; |
52 |
|
− |
testCases.push_back(tc); |
53 |
|
− |
} |
54 |
|
|
} |
55 |
|
|
|
56 |
|
− |
return testCases; |
57 |
|
− |
} |
58 |
|
|
|
59 |
|
|
/** |
60 |
|
|
* Check if the expected output matches the actual output |
61 |
|
|
* |
62 |
|
|
* @param refPath Path to the reference file |
63 |
|
|
* @param getActualOutput Callback to execute the required steps to get the actual test output |
64 |
|
|
* @param modifyOutputFct Callback to modify the output before comparing it with the reference |
65 |
|
|
* |
66 |
|
|
* @return True, if the ref file was found |
67 |
|
|
*/ |
68 |
|
− |
bool TestUtil::checkRefMatch(const std::filesystem::path &refPath, GetOutputFct getActualOutput, |
69 |
|
|
ModifyOutputFct modifyOutputFct) { |
70 |
|
|
// Cancel if the ref file does not exist |
71 |
|
− |
if (!std::filesystem::exists(refPath)) |
72 |
|
− |
return false; |
73 |
|
|
|
74 |
|
|
// Get actual output |
75 |
|
− |
std::string actualOutput = getActualOutput(); |
76 |
|
− |
if (updateRefs) { // Update refs |
77 |
|
− |
FileUtil::writeToFile(refPath, actualOutput); |
78 |
|
|
} else { // Check refs |
79 |
|
− |
std::string expectedOutput = FileUtil::getFileContent(refPath); |
80 |
|
− |
modifyOutputFct(expectedOutput, actualOutput); |
81 |
|
− |
EXPECT_EQ(expectedOutput, actualOutput) << "Output does not match the reference file: " << refPath; |
82 |
|
− |
} |
83 |
|
|
|
84 |
|
− |
return true; |
85 |
|
− |
} |
86 |
|
|
|
87 |
|
|
/** |
88 |
|
|
* Handle an test error |
89 |
|
|
* |
90 |
|
|
* @param testCase Testcase which has produced the error |
91 |
|
|
* @param error Exception with error message |
92 |
|
|
*/ |
93 |
|
− |
void TestUtil::handleError(const TestCase &testCase, const std::exception &error) { |
94 |
|
− |
std::string errorWhat = error.what(); |
95 |
|
− |
CommonUtil::replaceAll(errorWhat, "\\", "/"); |
96 |
|
|
|
97 |
|
|
// Fail if no ref file exists |
98 |
|
− |
const std::filesystem::path refPath = testCase.testPath / REF_NAME_ERROR_OUTPUT; |
99 |
|
− |
if (!exists(refPath)) |
100 |
|
− |
FAIL() << "Expected no error, but got: " + errorWhat; |
101 |
|
|
|
102 |
|
|
// Check if the exception message matches the expected output |
103 |
|
− |
TestUtil::checkRefMatch(testCase.testPath / REF_NAME_ERROR_OUTPUT, [&] { return errorWhat; }); |
104 |
|
− |
} |
105 |
|
|
|
106 |
|
|
/** |
107 |
|
|
* Get subdirectories of the given path |
108 |
|
|
* |
109 |
|
|
* @param basePath Path to a directory |
110 |
|
|
* @return Vector of subdirs |
111 |
|
|
*/ |
112 |
|
− |
std::vector<std::string> TestUtil::getSubdirs(const std::filesystem::path &basePath) { |
113 |
|
− |
std::vector<std::string> subdirs; |
114 |
|
− |
if (DIR *dir = opendir(basePath.string().c_str()); dir != nullptr) { |
115 |
|
|
dirent *ent; |
116 |
|
− |
while ((ent = readdir(dir)) != nullptr) { |
117 |
|
− |
if (strcmp(ent->d_name, ".") != 0 && strcmp(ent->d_name, "..") != 0) |
118 |
|
− |
subdirs.emplace_back(ent->d_name); |
119 |
|
|
} |
120 |
|
− |
closedir(dir); |
121 |
|
|
} |
122 |
|
− |
return subdirs; |
123 |
|
− |
} |
124 |
|
|
|
125 |
|
|
/** |
126 |
|
|
* Retrieve the contents of a file as a vector of line strings. Empty lines are omitted |
127 |
|
|
* |
128 |
|
|
* @param filePath File path |
129 |
|
|
* @return Vector of strings which are the lines of the file |
130 |
|
|
*/ |
131 |
|
− |
std::vector<std::string> TestUtil::getFileContentLinesVector(const std::filesystem::path &filePath) { |
132 |
|
− |
std::vector<std::string> lines; |
133 |
|
− |
std::ifstream inputFileStream; |
134 |
|
− |
inputFileStream.open(filePath); |
135 |
|
− |
for (std::string line; std::getline(inputFileStream, line);) { |
136 |
|
− |
if (!line.empty()) |
137 |
|
− |
lines.push_back(line); |
138 |
|
− |
} |
139 |
|
− |
return lines; |
140 |
|
− |
} |
141 |
|
|
|
142 |
|
|
/** |
143 |
|
|
* Convert a string to camel case |
144 |
|
|
* |
145 |
|
|
* @param input Input string |
146 |
|
|
* @return Camel-cased string |
147 |
|
|
*/ |
148 |
|
− |
std::string TestUtil::toCamelCase(std::string input) { |
149 |
|
− |
for (auto it = input.begin(); it != input.end(); ++it) { |
150 |
|
− |
if (*it == '-' || *it == '_') { |
151 |
|
− |
it = input.erase(it); |
152 |
|
− |
*it = static_cast<char>(toupper(*it)); |
153 |
|
|
} |
154 |
|
|
} |
155 |
|
− |
return input; |
156 |
|
|
} |
157 |
|
|
|
158 |
|
|
/** |
159 |
|
|
* Check if the provided test case is disabled |
160 |
|
|
* |
161 |
|
|
* @param testCase Test case to check |
162 |
|
|
* @param isGHActions Running tests with GitHub Actions |
163 |
|
|
* @return Disabled or not |
164 |
|
|
*/ |
165 |
|
− |
bool TestUtil::isDisabled(const TestCase &testCase, bool isGHActions) { |
166 |
|
− |
if (exists(testCase.testPath / CTL_SKIP_DISABLED)) |
167 |
|
− |
return true; |
168 |
|
− |
if (isGHActions && exists(testCase.testPath / CTL_SKIP_GH)) |
169 |
|
− |
return true; |
170 |
|
− |
return false; |
171 |
|
|
} |
172 |
|
|
|
173 |
|
|
/** |
174 |
|
|
* Removes the first n lines of the GDB output to not compare target dependent code |
175 |
|
|
* |
176 |
|
|
* @param gdbOutput GDB output to modify |
177 |
|
|
*/ |
178 |
|
− |
void TestUtil::eraseGDBHeader(std::string &gdbOutput) { |
179 |
|
|
// Remove header |
180 |
|
− |
size_t pos = gdbOutput.find(GDB_READING_SYMBOLS_MESSAGE); |
181 |
|
− |
if (pos != std::string::npos) { |
182 |
|
− |
const size_t lineStart = gdbOutput.rfind('\n', pos); |
183 |
|
− |
if (lineStart != std::string::npos) |
184 |
|
− |
gdbOutput.erase(0, lineStart + 1); |
185 |
|
|
} |
186 |
|
|
|
187 |
|
|
// Remove inferior message |
188 |
|
− |
pos = gdbOutput.find(GDB_INFERIOR_MESSAGE); |
189 |
|
− |
if (pos != std::string::npos) |
190 |
|
− |
gdbOutput.erase(pos); |
191 |
|
− |
} |
192 |
|
|
|
193 |
|
|
/** |
194 |
|
|
* Remove lines, containing a certain substring to make the IR string comparable |
195 |
|
|
* |
196 |
|
|
* @param irCode IR code to modify |
197 |
|
|
* @param needle Substring to search for |
198 |
|
|
*/ |
199 |
|
− |
void TestUtil::eraseLinesBySubstring(std::string &irCode, const char *const needle) { |
200 |
|
− |
std::string::size_type pos = 0; |
201 |
|
− |
while ((pos = irCode.find(needle, pos)) != std::string::npos) { |
202 |
|
|
// Find the start of the line that contains the substring |
203 |
|
− |
std::string::size_type lineStart = irCode.rfind('\n', pos); |
204 |
|
− |
if (lineStart == std::string::npos) |
205 |
|
− |
lineStart = 0; |
206 |
|
|
else |
207 |
|
− |
lineStart++; // move past the '\n' |
208 |
|
|
|
209 |
|
|
// Find the end of the line that contains the substring |
210 |
|
− |
std::string::size_type lineEnd = irCode.find('\n', pos); |
211 |
|
− |
if (lineEnd == std::string::npos) |
212 |
|
− |
lineEnd = irCode.length(); |
213 |
|
|
|
214 |
|
|
// Erase the line |
215 |
|
− |
irCode.erase(lineStart, lineEnd - lineStart); |
216 |
|
|
} |
217 |
|
− |
} |
218 |
|
|
|
219 |
|
|
} // namespace spice::testing |
220 |
|
|
|
221 |
|
|
// GCOV_EXCL_STOP |
222 |
|
|
|