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