yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
test_cli_commands.cc
Go to the documentation of this file.
2
3#include <array>
4#include <cstdio>
5#include <cstdlib>
6#include <iostream>
7#include <memory>
8#include <string>
9
10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_split.h"
13
14namespace yaze::cli {
15
16namespace {
17
18// Test suite definitions
19struct TestSuite {
20 const char* label;
21 const char* description;
22 const char* requirements;
25};
26
28 {"stable", "Core unit and integration tests (fast, reliable)", "None",
29 false, false},
30 {"gui", "GUI smoke tests (ImGui framework validation)",
31 "SDL display or headless", false, false},
32 {"z3ed", "z3ed CLI self-test and smoke tests", "z3ed target built", false,
33 false},
34 {"headless_gui", "GUI tests in headless mode (CI-safe)", "None", false,
35 false},
36 {"rom_dependent", "Tests requiring actual Zelda3 ROM",
37 "YAZE_ENABLE_ROM_TESTS=ON + ROM path", true, false},
38 {"experimental", "AI runtime features and experiments",
39 "YAZE_ENABLE_AI_RUNTIME=ON", false, true},
40 {"benchmark", "Performance and optimization tests", "None", false, false},
41};
42
43// Execute command and capture output
44std::string ExecuteCommand(const std::string& cmd) {
45 std::array<char, 256> buffer;
46 std::string result;
47#ifdef _WIN32
48 std::unique_ptr<FILE, decltype(&_pclose)> pipe(_popen(cmd.c_str(), "r"),
49 _pclose);
50#else
51 std::unique_ptr<FILE, decltype(&pclose)> pipe(popen(cmd.c_str(), "r"),
52 pclose);
53#endif
54 if (!pipe) {
55 return "";
56 }
57 while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) {
58 result += buffer.data();
59 }
60 return result;
61}
62
63// Check if a build directory exists
64bool BuildDirExists(const std::string& dir) {
65 std::string cmd = "test -d " + dir + " && echo yes";
66 std::string result = ExecuteCommand(cmd);
67 return result.find("yes") != std::string::npos;
68}
69
70// Get environment variable or default
71std::string GetEnvOrDefault(const char* name, const std::string& default_val) {
72 const char* val = std::getenv(name);
73 return val ? val : default_val;
74}
75
76} // namespace
77
79 Rom* /*rom*/, const resources::ArgumentParser& parser,
80 resources::OutputFormatter& formatter) {
81 auto filter_label = parser.GetString("label");
82 bool is_json = formatter.IsJson();
83
84 // Output available test suites
85 formatter.BeginArray("suites");
86 for (const auto& suite : kTestSuites) {
87 if (filter_label.has_value() && suite.label != filter_label.value()) {
88 continue;
89 }
90
91 if (is_json) {
92 std::string json = absl::StrFormat(
93 R"({"label":"%s","description":"%s","requirements":"%s",)"
94 R"("requires_rom":%s,"requires_ai":%s})",
95 suite.label, suite.description, suite.requirements,
96 suite.requires_rom ? "true" : "false",
97 suite.requires_ai ? "true" : "false");
98 formatter.AddArrayItem(json);
99 } else {
100 std::string entry = absl::StrFormat(
101 "%s: %s [%s]", suite.label, suite.description, suite.requirements);
102 formatter.AddArrayItem(entry);
103 }
104 }
105 formatter.EndArray();
106
107 // Try to get test count from ctest
108 std::string build_dir = "build";
109 if (!BuildDirExists(build_dir)) {
110 build_dir = "build_fast";
111 }
112
113 if (BuildDirExists(build_dir)) {
114 std::string ctest_cmd =
115 "ctest --test-dir " + build_dir + " -N 2>/dev/null | tail -1";
116 std::string ctest_output = ExecuteCommand(ctest_cmd);
117
118 // Parse "Total Tests: N"
119 if (ctest_output.find("Total Tests:") != std::string::npos) {
120 size_t pos = ctest_output.find("Total Tests:");
121 std::string count_str = ctest_output.substr(pos + 13);
122 int total_tests = std::atoi(count_str.c_str());
123 formatter.AddField("total_tests_discovered", total_tests);
124 }
125
126 formatter.AddField("build_directory", build_dir);
127 } else {
128 formatter.AddField("build_directory", "not_found");
129 formatter.AddField("note",
130 "Run 'cmake --preset mac-test && cmake --build "
131 "--preset mac-test' to build tests");
132 }
133
134 // Text output
135 if (!is_json) {
136 std::cout << "\n=== Available Test Suites ===\n\n";
137 for (const auto& suite : kTestSuites) {
138 if (filter_label.has_value() && suite.label != filter_label.value()) {
139 continue;
140 }
141 std::cout << absl::StrFormat(" %-15s %s\n", suite.label,
142 suite.description);
143 std::cout << absl::StrFormat(" Requirements: %s\n",
144 suite.requirements);
145 if (suite.requires_rom) {
146 std::cout << " ⚠ Requires ROM file\n";
147 }
148 if (suite.requires_ai) {
149 std::cout << " ⚠ Requires AI runtime\n";
150 }
151 std::cout << "\n";
152 }
153
154 std::cout << "=== Quick Commands ===\n\n";
155 std::cout << " ctest --test-dir build -L stable # Run stable tests\n";
156 std::cout << " ctest --test-dir build -L gui # Run GUI tests\n";
157 std::cout << " ctest --test-dir build # Run all tests\n";
158 std::cout << "\n";
159 }
160
161 return absl::OkStatus();
162}
163
165 Rom* /*rom*/, const resources::ArgumentParser& parser,
166 resources::OutputFormatter& formatter) {
167 auto label = parser.GetString("label").value_or("stable");
168 auto preset = parser.GetString("preset").value_or("");
169 bool verbose = parser.HasFlag("verbose");
170 bool is_json = formatter.IsJson();
171
172 // Determine build directory
173 std::string build_dir = "build";
174 if (!preset.empty()) {
175 if (preset.find("test") != std::string::npos) {
176 build_dir = "build_fast";
177 } else if (preset.find("ai") != std::string::npos) {
178 build_dir = "build_ai";
179 }
180 }
181
182 if (!BuildDirExists(build_dir)) {
183 return absl::NotFoundError(absl::StrFormat(
184 "Build directory '%s' not found. Run cmake to configure first.",
185 build_dir));
186 }
187
188 formatter.AddField("build_directory", build_dir);
189 formatter.AddField("label", label);
190 formatter.AddField("preset", preset.empty() ? "default" : preset);
191
192 // Build ctest command
193 std::string ctest_cmd = "ctest --test-dir " + build_dir + " -L " + label;
194 if (verbose) {
195 ctest_cmd += " --output-on-failure";
196 }
197 ctest_cmd += " 2>&1";
198
199 if (!is_json) {
200 std::cout << "\n=== Running Tests ===\n\n";
201 std::cout << "Command: " << ctest_cmd << "\n\n";
202 }
203
204 // Execute ctest
205 std::string output = ExecuteCommand(ctest_cmd);
206
207 // Parse results
208 int passed = 0;
209 int failed = 0;
210 int total = 0;
211
212 // Look for "X tests passed, Y tests failed out of Z"
213 // or "100% tests passed, 0 tests failed out of N"
214 std::vector<std::string> lines = absl::StrSplit(output, '\n');
215 for (const auto& line : lines) {
216 if (line.find("tests passed") != std::string::npos) {
217 // Parse "X tests passed, Y tests failed out of Z"
218 if (line.find("100%") != std::string::npos) {
219 // "100% tests passed, 0 tests failed out of N"
220 size_t out_of_pos = line.find("out of");
221 if (out_of_pos != std::string::npos) {
222 total = std::atoi(line.substr(out_of_pos + 7).c_str());
223 passed = total;
224 failed = 0;
225 }
226 } else {
227 // "X tests passed, Y tests failed out of Z"
228 sscanf(line.c_str(), "%d tests passed, %d tests failed out of %d",
229 &passed, &failed, &total);
230 }
231 }
232 }
233
234 formatter.AddField("tests_passed", passed);
235 formatter.AddField("tests_failed", failed);
236 formatter.AddField("tests_total", total);
237 formatter.AddField("success", failed == 0 && total > 0);
238
239 if (!is_json) {
240 std::cout << output << "\n";
241 std::cout << "=== Summary ===\n";
242 std::cout << absl::StrFormat(" Passed: %d\n", passed);
243 std::cout << absl::StrFormat(" Failed: %d\n", failed);
244 std::cout << absl::StrFormat(" Total: %d\n", total);
245
246 if (failed > 0) {
247 std::cout << "\n⚠ Some tests failed. Run with --verbose for details.\n";
248 } else if (total > 0) {
249 std::cout << "\n✓ All tests passed!\n";
250 } else {
251 std::cout << "\n⚠ No tests found for label '" << label << "'\n";
252 }
253 }
254
255 return absl::OkStatus();
256}
257
259 Rom* /*rom*/, const resources::ArgumentParser& parser,
260 resources::OutputFormatter& formatter) {
261 bool is_json = formatter.IsJson();
262
263 // Check environment variables
264 std::string rom_vanilla = GetEnvOrDefault("YAZE_TEST_ROM_VANILLA", "");
265 std::string rom_expanded = GetEnvOrDefault("YAZE_TEST_ROM_EXPANDED", "");
266 std::string rom_path_legacy = GetEnvOrDefault("YAZE_TEST_ROM_PATH", "");
267 if (rom_vanilla.empty()) {
268 rom_vanilla = rom_path_legacy;
269 }
270 std::string skip_rom = GetEnvOrDefault("YAZE_SKIP_ROM_TESTS", "");
271 std::string enable_ui = GetEnvOrDefault("YAZE_ENABLE_UI_TESTS", "");
272
273 formatter.AddField("rom_vanilla",
274 rom_vanilla.empty() ? "not set" : rom_vanilla);
275 formatter.AddField("rom_expanded",
276 rom_expanded.empty() ? "not set" : rom_expanded);
277 formatter.AddField("rom_path",
278 rom_path_legacy.empty() ? "not set" : rom_path_legacy);
279 formatter.AddField("skip_rom_tests", !skip_rom.empty());
280 formatter.AddField("ui_tests_enabled", !enable_ui.empty());
281
282 // Check available build directories
283 std::vector<std::string> build_dirs = {"build", "build_fast", "build_ai",
284 "build_test", "build_agent"};
285
286 formatter.BeginArray("build_directories");
287 for (const auto& dir : build_dirs) {
288 if (BuildDirExists(dir)) {
289 formatter.AddArrayItem(dir);
290 }
291 }
292 formatter.EndArray();
293
294 // Determine active preset (heuristic based on build dirs)
295 std::string active_preset = "unknown";
296 if (BuildDirExists("build_fast")) {
297 active_preset = "mac-test (fast)";
298 } else if (BuildDirExists("build_ai")) {
299 active_preset = "mac-ai";
300 } else if (BuildDirExists("build")) {
301 active_preset = "mac-dbg (default)";
302 }
303 formatter.AddField("active_preset", active_preset);
304
305 // Check which test suites are available
306 formatter.BeginArray("available_suites");
307 for (const auto& suite : kTestSuites) {
308 bool available = true;
309 if (suite.requires_rom && rom_vanilla.empty()) {
310 available = false;
311 }
312 if (available) {
313 formatter.AddArrayItem(suite.label);
314 }
315 }
316 formatter.EndArray();
317
318 // Text output
319 if (!is_json) {
320 std::cout << "\n";
321 std::cout << "╔════════════════════════════════════════════════════════════"
322 "═══╗\n";
323 std::cout << "║ TEST CONFIGURATION "
324 " ║\n";
325 std::cout << "╠════════════════════════════════════════════════════════════"
326 "═══╣\n";
327 std::cout << absl::StrFormat(
328 "║ ROM Vanilla: %-47s ║\n",
329 rom_vanilla.empty() ? "(not set)" : rom_vanilla.substr(0, 47));
330 std::cout << absl::StrFormat(
331 "║ ROM Expanded: %-46s ║\n",
332 rom_expanded.empty() ? "(not set)" : rom_expanded.substr(0, 46));
333 std::cout << absl::StrFormat("║ Skip ROM Tests: %-43s ║\n",
334 skip_rom.empty() ? "NO" : "YES");
335 std::cout << absl::StrFormat("║ UI Tests Enabled: %-41s ║\n",
336 enable_ui.empty() ? "NO" : "YES");
337 std::cout << absl::StrFormat("║ Active Preset: %-44s ║\n", active_preset);
338 std::cout << "╠════════════════════════════════════════════════════════════"
339 "═══╣\n";
340 std::cout << "║ Available Build Directories: "
341 " ║\n";
342 for (const auto& dir : build_dirs) {
343 if (BuildDirExists(dir)) {
344 std::cout << absl::StrFormat("║ ✓ %-55s ║\n", dir);
345 }
346 }
347 std::cout << "╠════════════════════════════════════════════════════════════"
348 "═══╣\n";
349 std::cout << "║ Available Test Suites: "
350 " ║\n";
351 for (const auto& suite : kTestSuites) {
352 bool available = true;
353 std::string reason;
354 if (suite.requires_rom && rom_vanilla.empty()) {
355 available = false;
356 reason = " (needs ROM)";
357 }
358 if (suite.requires_ai) {
359 reason = " (needs AI)";
360 }
361 std::cout << absl::StrFormat("║ %s %-15s%-40s ║\n",
362 available ? "✓" : "✗", suite.label, reason);
363 }
364 std::cout << "╚════════════════════════════════════════════════════════════"
365 "═══╝\n";
366
367 if (rom_vanilla.empty()) {
368 std::cout << "\nTo enable ROM-dependent tests:\n";
369 std::cout
370 << " export YAZE_TEST_ROM_VANILLA=/path/to/alttp_vanilla.sfc\n";
371 std::cout << " cmake ... -DYAZE_ENABLE_ROM_TESTS=ON\n";
372 }
373 }
374
375 return absl::OkStatus();
376}
377
378} // namespace yaze::cli
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
Definition rom.h:24
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
absl::Status Execute(Rom *rom, const resources::ArgumentParser &parser, resources::OutputFormatter &formatter) override
Execute the command business logic.
Utility for parsing common CLI argument patterns.
std::optional< std::string > GetString(const std::string &name) const
Parse a named argument (e.g., –format=json or –format json)
bool HasFlag(const std::string &name) const
Check if a flag is present.
Utility for consistent output formatting across commands.
void BeginArray(const std::string &key)
Begin an array.
void AddArrayItem(const std::string &item)
Add an item to current array.
void AddField(const std::string &key, const std::string &value)
Add a key-value pair.
bool IsJson() const
Check if using JSON format.
std::string GetEnvOrDefault(const char *name, const std::string &default_val)
std::string ExecuteCommand(const std::string &cmd)
Namespace for the command line interface.