|
| 1 | +/* |
| 2 | + * Copyright (C) 2024 Jimmy Aguilar Mena |
| 3 | + * |
| 4 | + * This program is free software: you can redistribute it and/or modify |
| 5 | + * it under the terms of the GNU General Public License as published by |
| 6 | + * the Free Software Foundation, either version 3 of the License, or |
| 7 | + * (at your option) any later version. |
| 8 | + * |
| 9 | + * This program is distributed in the hope that it will be useful, |
| 10 | + * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 11 | + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 12 | + * GNU General Public License for more details. |
| 13 | + * |
| 14 | + * You should have received a copy of the GNU General Public License |
| 15 | + * along with this program. If not, see <http://www.gnu.org/licenses/>. |
| 16 | + */ |
| 17 | + |
| 18 | +#include <cstdint> |
| 19 | +#include <string> |
| 20 | +#include <fstream> |
| 21 | +#include <filesystem> |
| 22 | +#include <functional> |
| 23 | +#include <map> |
| 24 | +#include <vector> |
| 25 | +#include <iostream> |
| 26 | +#include <cassert> |
| 27 | +#include <cstdlib> |
| 28 | +#include <cstring> |
| 29 | + |
| 30 | +// ============================================================ |
| 31 | +// Binary format — must match the structs in Parser.cpp exactly |
| 32 | +// ============================================================ |
| 33 | + |
| 34 | +struct TraceHeader { |
| 35 | + uint32_t _id; |
| 36 | + uint32_t _nentries; |
| 37 | + uint64_t _tid; |
| 38 | + uint64_t _startGTime; |
| 39 | +}; |
| 40 | + |
| 41 | +struct EventEntry { |
| 42 | + uint64_t _time; |
| 43 | + uint16_t _id; |
| 44 | + uint16_t _core; |
| 45 | + uint32_t _value; |
| 46 | +}; |
| 47 | + |
| 48 | +// ============================================================ |
| 49 | +// Helpers |
| 50 | +// ============================================================ |
| 51 | + |
| 52 | +static void writeTraceFile( |
| 53 | + const std::filesystem::path &path, |
| 54 | + uint32_t id, |
| 55 | + uint64_t tid, |
| 56 | + uint64_t startGTime, |
| 57 | + const std::vector<EventEntry> &events) |
| 58 | +{ |
| 59 | + std::ofstream f(path, std::ios::binary); |
| 60 | + if (!f) |
| 61 | + throw std::runtime_error("Cannot open for writing: " + path.string()); |
| 62 | + |
| 63 | + TraceHeader hdr{id, static_cast<uint32_t>(events.size()), tid, startGTime}; |
| 64 | + f.write(reinterpret_cast<const char *>(&hdr), sizeof(hdr)); |
| 65 | + |
| 66 | + if (!events.empty()) |
| 67 | + f.write(reinterpret_cast<const char *>(events.data()), |
| 68 | + static_cast<std::streamsize>(events.size() * sizeof(EventEntry))); |
| 69 | +} |
| 70 | + |
| 71 | +static int runParser(const std::string &parserPath, const std::filesystem::path &dir) |
| 72 | +{ |
| 73 | + const std::string cmd = parserPath + " " + dir.string() + " > /dev/null 2>&1"; |
| 74 | + return std::system(cmd.c_str()); |
| 75 | +} |
| 76 | + |
| 77 | +static std::vector<std::string> readPRV(const std::filesystem::path &dir) |
| 78 | +{ |
| 79 | + std::ifstream f(dir / "Trace.prv"); |
| 80 | + if (!f) |
| 81 | + throw std::runtime_error("Trace.prv not found in " + dir.string()); |
| 82 | + |
| 83 | + std::vector<std::string> lines; |
| 84 | + std::string line; |
| 85 | + while (std::getline(f, line)) { |
| 86 | + if (!line.empty()) |
| 87 | + lines.push_back(line); |
| 88 | + } |
| 89 | + return lines; |
| 90 | +} |
| 91 | + |
| 92 | +/** RAII wrapper for a mkdtemp temporary directory. */ |
| 93 | +struct TempDir { |
| 94 | + std::filesystem::path path; |
| 95 | + |
| 96 | + TempDir() |
| 97 | + { |
| 98 | + char tmpl[] = "/tmp/test_parser_XXXXXX"; |
| 99 | + const char *p = ::mkdtemp(tmpl); |
| 100 | + if (!p) |
| 101 | + throw std::runtime_error( |
| 102 | + std::string("mkdtemp failed: ") + strerror(errno)); |
| 103 | + path = p; |
| 104 | + } |
| 105 | + |
| 106 | + ~TempDir() noexcept |
| 107 | + { |
| 108 | + std::filesystem::remove_all(path); |
| 109 | + } |
| 110 | +}; |
| 111 | + |
| 112 | +// ============================================================ |
| 113 | +// Auto-registration infrastructure |
| 114 | +// ============================================================ |
| 115 | + |
| 116 | +static std::string gParserPath; |
| 117 | + |
| 118 | +inline std::map<std::string, std::function<void()>> &getTestRegistry() |
| 119 | +{ |
| 120 | + static std::map<std::string, std::function<void()>> registry; |
| 121 | + return registry; |
| 122 | +} |
| 123 | + |
| 124 | +struct TestRegistrar { |
| 125 | + TestRegistrar(const std::string &name, std::function<void()> fn) { |
| 126 | + getTestRegistry().emplace(name, fn); |
| 127 | + } |
| 128 | +}; |
| 129 | + |
| 130 | +#define DEFINE_TEST(name) \ |
| 131 | + static void name(); \ |
| 132 | + static TestRegistrar registrar_##name(#name, name); \ |
| 133 | + static void name() |
| 134 | + |
| 135 | +// ============================================================ |
| 136 | +// Tests |
| 137 | +// ============================================================ |
| 138 | + |
| 139 | +/** Single-thread trace: verify event count, header duration, and exact PRV lines. */ |
| 140 | +DEFINE_TEST(test_single_thread) |
| 141 | +{ |
| 142 | + TempDir tmp; |
| 143 | + |
| 144 | + const std::vector<EventEntry> events = { |
| 145 | + {100, 1, 0, 1}, |
| 146 | + {200, 1, 0, 2}, |
| 147 | + {300, 1, 0, 0}, |
| 148 | + }; |
| 149 | + writeTraceFile(tmp.path / "thread_1.bin", 1, 1001, 1000000000ULL, events); |
| 150 | + |
| 151 | + assert(runParser(gParserPath, tmp.path) == 0); |
| 152 | + |
| 153 | + const std::vector<std::string> lines = readPRV(tmp.path); |
| 154 | + |
| 155 | + // 1 header line + 3 event lines |
| 156 | + assert(lines.size() == 4); |
| 157 | + assert(lines[0].substr(0, 8) == "#Paraver"); |
| 158 | + |
| 159 | + // Duration = lastTime - firstTime = 300 - 100 = 200 |
| 160 | + assert(lines[0].find("200_ns") != std::string::npos); |
| 161 | + |
| 162 | + // 1 file → (1:1) at end of header |
| 163 | + assert(lines[0].find("(1:1)") != std::string::npos); |
| 164 | + |
| 165 | + // PRV format: 2:<core>:1:1:<thread>:<time>:<event_id>:<value> |
| 166 | + assert(lines[1] == "2:0:1:1:1:100:1:1"); |
| 167 | + assert(lines[2] == "2:0:1:1:1:200:1:2"); |
| 168 | + assert(lines[3] == "2:0:1:1:1:300:1:0"); |
| 169 | +} |
| 170 | + |
| 171 | +/** Two-thread trace: events must emerge from the heap in strict time order. */ |
| 172 | +DEFINE_TEST(test_two_thread_merge_order) |
| 173 | +{ |
| 174 | + TempDir tmp; |
| 175 | + |
| 176 | + // Thread 1: events at times 100, 300, 500 |
| 177 | + const std::vector<EventEntry> ev1 = { |
| 178 | + {100, 1, 0, 1}, |
| 179 | + {300, 1, 0, 2}, |
| 180 | + {500, 1, 0, 0}, |
| 181 | + }; |
| 182 | + // Thread 2: events at times 200, 400, 600 |
| 183 | + const std::vector<EventEntry> ev2 = { |
| 184 | + {200, 2, 1, 10}, |
| 185 | + {400, 2, 1, 20}, |
| 186 | + {600, 2, 1, 0}, |
| 187 | + }; |
| 188 | + |
| 189 | + writeTraceFile(tmp.path / "thread_1.bin", 1, 1001, 1000000000ULL, ev1); |
| 190 | + writeTraceFile(tmp.path / "thread_2.bin", 2, 1002, 1000000000ULL, ev2); |
| 191 | + |
| 192 | + assert(runParser(gParserPath, tmp.path) == 0); |
| 193 | + |
| 194 | + const std::vector<std::string> lines = readPRV(tmp.path); |
| 195 | + |
| 196 | + // 1 header + 6 event lines |
| 197 | + assert(lines.size() == 7); |
| 198 | + assert(lines[0].substr(0, 8) == "#Paraver"); |
| 199 | + |
| 200 | + // Duration = 600 - 100 = 500 |
| 201 | + assert(lines[0].find("500_ns") != std::string::npos); |
| 202 | + |
| 203 | + // Events must appear strictly ordered by time |
| 204 | + assert(lines[1].find(":100:") != std::string::npos); |
| 205 | + assert(lines[2].find(":200:") != std::string::npos); |
| 206 | + assert(lines[3].find(":300:") != std::string::npos); |
| 207 | + assert(lines[4].find(":400:") != std::string::npos); |
| 208 | + assert(lines[5].find(":500:") != std::string::npos); |
| 209 | + assert(lines[6].find(":600:") != std::string::npos); |
| 210 | + |
| 211 | + // Spot-check thread attribution on two events |
| 212 | + assert(lines[1] == "2:0:1:1:1:100:1:1"); |
| 213 | + assert(lines[2] == "2:1:1:1:2:200:2:10"); |
| 214 | +} |
| 215 | + |
| 216 | +/** Max core value must appear correctly in the Paraver header. */ |
| 217 | +DEFINE_TEST(test_max_core_in_header) |
| 218 | +{ |
| 219 | + TempDir tmp; |
| 220 | + |
| 221 | + // Events on cores 0, 3, 7 — header must report 7 |
| 222 | + const std::vector<EventEntry> events = { |
| 223 | + {100, 1, 0, 1}, |
| 224 | + {200, 1, 3, 2}, |
| 225 | + {300, 1, 7, 0}, |
| 226 | + }; |
| 227 | + writeTraceFile(tmp.path / "thread_1.bin", 1, 1001, 1000000000ULL, events); |
| 228 | + |
| 229 | + assert(runParser(gParserPath, tmp.path) == 0); |
| 230 | + |
| 231 | + const std::vector<std::string> lines = readPRV(tmp.path); |
| 232 | + assert(!lines.empty()); |
| 233 | + |
| 234 | + // Paraver header: ...<duration>_ns:1(<maxCore>):... |
| 235 | + assert(lines[0].find(":1(7):") != std::string::npos); |
| 236 | +} |
| 237 | + |
| 238 | +/** Thread-count field in the Paraver header must equal the number of .bin files. */ |
| 239 | +DEFINE_TEST(test_thread_count_in_header) |
| 240 | +{ |
| 241 | + TempDir tmp; |
| 242 | + |
| 243 | + const std::vector<EventEntry> events = {{100, 1, 0, 1}, {200, 1, 0, 0}}; |
| 244 | + |
| 245 | + writeTraceFile(tmp.path / "t1.bin", 1, 1001, 1000000000ULL, events); |
| 246 | + writeTraceFile(tmp.path / "t2.bin", 2, 1002, 1000000000ULL, events); |
| 247 | + writeTraceFile(tmp.path / "t3.bin", 3, 1003, 1000000000ULL, events); |
| 248 | + |
| 249 | + assert(runParser(gParserPath, tmp.path) == 0); |
| 250 | + |
| 251 | + const std::vector<std::string> lines = readPRV(tmp.path); |
| 252 | + assert(!lines.empty()); |
| 253 | + |
| 254 | + // 3 files → (3:1) near the end of the header line |
| 255 | + assert(lines[0].find("(3:1)") != std::string::npos); |
| 256 | +} |
| 257 | + |
| 258 | +/** Non-monotonic timestamps in a trace file must cause the parser to fail. */ |
| 259 | +DEFINE_TEST(test_non_monotonic_rejected) |
| 260 | +{ |
| 261 | + TempDir tmp; |
| 262 | + |
| 263 | + // Second event timestamp is smaller than the first — invalid |
| 264 | + const std::vector<EventEntry> events = { |
| 265 | + {300, 1, 0, 1}, |
| 266 | + {100, 1, 0, 0}, |
| 267 | + }; |
| 268 | + writeTraceFile(tmp.path / "thread_1.bin", 1, 1001, 1000000000ULL, events); |
| 269 | + |
| 270 | + assert(runParser(gParserPath, tmp.path) != 0); |
| 271 | +} |
| 272 | + |
| 273 | +// ============================================================ |
| 274 | +// Entry point |
| 275 | +// ============================================================ |
| 276 | + |
| 277 | +int main(int argc, char **argv) |
| 278 | +{ |
| 279 | + if (argc < 3) { |
| 280 | + std::cerr << "Usage: " << argv[0] << " <test_name> <parser_path>\n"; |
| 281 | + return 1; |
| 282 | + } |
| 283 | + |
| 284 | + const std::string testName = argv[1]; |
| 285 | + gParserPath = argv[2]; |
| 286 | + |
| 287 | + std::map<std::string, std::function<void()>> ®istry = getTestRegistry(); |
| 288 | + const std::map<std::string, std::function<void()>>::iterator it = registry.find(testName); |
| 289 | + if (it == registry.end()) { |
| 290 | + std::cerr << "Unknown test: " << testName << "\n"; |
| 291 | + return 1; |
| 292 | + } |
| 293 | + |
| 294 | + try { |
| 295 | + it->second(); |
| 296 | + std::cout << "PASS: " << testName << "\n"; |
| 297 | + return 0; |
| 298 | + } catch (const std::exception &e) { |
| 299 | + std::cerr << "FAILED: " << e.what() << "\n"; |
| 300 | + return 1; |
| 301 | + } |
| 302 | +} |
0 commit comments