GCC Code Coverage Report


Directory: ../
File: test/util/TestUtil.cpp
Date: 2025-11-03 22:22:45
Coverage Exec Excl Total
Lines: 100.0% 0 115 115
Functions: -% 0 13 13
Branches: -% 0 284 284

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