Skip to content

Commit 6a2ea01

Browse files
committed
Improve testing infrastructure
1 parent ccc4926 commit 6a2ea01

3 files changed

Lines changed: 330 additions & 6 deletions

File tree

CMakeLists.txt

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,43 @@ else() # additional warnings
1919
add_compile_options(-Wall -Wextra -Wpedantic)
2020
endif()
2121

22-
add_executable(main.x main.cpp)
23-
target_compile_definitions(main.x PRIVATE PROFILER_ENABLED=2)
22+
add_executable(tests_profiler.x EXCLUDE_FROM_ALL tests/tests_profiler.cpp)
23+
target_compile_definitions(tests_profiler.x PRIVATE PROFILER_ENABLED=2)
24+
target_include_directories(tests_profiler.x PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
2425

2526
if (NOT CMAKE_CXX_COMPILER_ID MATCHES "MSVC" AND TBB_FOUND)
26-
target_link_libraries(main.x TBB::tbb)
27+
target_link_libraries(tests_profiler.x TBB::tbb)
2728
endif()
2829

2930

3031
add_executable(Parser.x Parser.cpp)
3132

33+
add_executable(test_parser.x EXCLUDE_FROM_ALL tests/test_parser.cpp)
34+
3235
enable_testing()
3336

34-
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/main.cpp" main_cpp)
35-
string(REGEX MATCHALL "\nDEFINE_TEST\\([^)]+\\)" matches "${main_cpp}")
37+
# Build test binaries on demand — runs as the first CTest step via a fixture,
38+
# so a plain "cmake --build ." never compiles the test executables.
39+
add_test(NAME build_test_binaries
40+
COMMAND ${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR}
41+
--target tests_profiler.x test_parser.x)
42+
set_tests_properties(build_test_binaries PROPERTIES
43+
FIXTURES_SETUP test_binaries_built
44+
TIMEOUT 300)
45+
46+
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/tests/tests_profiler.cpp" tests_profiler_cpp)
47+
string(REGEX MATCHALL "\nDEFINE_TEST\\([^)]+\\)" matches "${tests_profiler_cpp}")
48+
foreach(match ${matches})
49+
string(REGEX MATCH "\nDEFINE_TEST\\(([^)]+)\\)" _ "${match}")
50+
add_test(NAME "${CMAKE_MATCH_1}" COMMAND $<TARGET_FILE:tests_profiler.x> "${CMAKE_MATCH_1}")
51+
set_tests_properties("${CMAKE_MATCH_1}" PROPERTIES FIXTURES_REQUIRED test_binaries_built)
52+
endforeach()
53+
54+
file(READ "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_parser.cpp" test_parser_cpp)
55+
string(REGEX MATCHALL "\nDEFINE_TEST\\([^)]+\\)" matches "${test_parser_cpp}")
3656
foreach(match ${matches})
3757
string(REGEX MATCH "\nDEFINE_TEST\\(([^)]+)\\)" _ "${match}")
38-
add_test(NAME "${CMAKE_MATCH_1}" COMMAND $<TARGET_FILE:main.x> "${CMAKE_MATCH_1}")
58+
add_test(NAME "${CMAKE_MATCH_1}"
59+
COMMAND $<TARGET_FILE:test_parser.x> "${CMAKE_MATCH_1}" $<TARGET_FILE:Parser.x>)
60+
set_tests_properties("${CMAKE_MATCH_1}" PROPERTIES FIXTURES_REQUIRED test_binaries_built)
3961
endforeach()

tests/test_parser.cpp

Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
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()>> &registry = 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+
}
File renamed without changes.

0 commit comments

Comments
 (0)