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