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