10#include "absl/status/status.h"
11#include "absl/strings/str_format.h"
12#include "absl/strings/str_split.h"
28 {
"stable",
"Core unit and integration tests (fast, reliable)",
"None",
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,
34 {
"headless_gui",
"GUI tests in headless mode (CI-safe)",
"None",
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},
45 std::array<char, 256> buffer;
48 std::unique_ptr<FILE,
decltype(&_pclose)> pipe(_popen(cmd.c_str(),
"r"),
51 std::unique_ptr<FILE,
decltype(&pclose)> pipe(popen(cmd.c_str(),
"r"),
57 while (fgets(buffer.data(), buffer.size(), pipe.get()) !=
nullptr) {
58 result += buffer.data();
65 std::string cmd =
"test -d " + dir +
" && echo yes";
67 return result.find(
"yes") != std::string::npos;
72 const char* val = std::getenv(name);
73 return val ? val : default_val;
81 auto filter_label = parser.
GetString(
"label");
82 bool is_json = formatter.
IsJson();
86 for (
const auto& suite : kTestSuites) {
87 if (filter_label.has_value() && suite.label != filter_label.value()) {
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");
100 std::string entry = absl::StrFormat(
101 "%s: %s [%s]", suite.label, suite.description, suite.requirements);
108 std::string build_dir =
"build";
109 if (!BuildDirExists(build_dir)) {
110 build_dir =
"build_fast";
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);
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);
126 formatter.
AddField(
"build_directory", build_dir);
128 formatter.
AddField(
"build_directory",
"not_found");
130 "Run 'cmake --preset mac-test && cmake --build "
131 "--preset mac-test' to build tests");
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()) {
141 std::cout << absl::StrFormat(
" %-15s %s\n", suite.label,
143 std::cout << absl::StrFormat(
" Requirements: %s\n",
145 if (suite.requires_rom) {
146 std::cout <<
" ⚠ Requires ROM file\n";
148 if (suite.requires_ai) {
149 std::cout <<
" ⚠ Requires AI runtime\n";
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";
161 return absl::OkStatus();
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();
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";
182 if (!BuildDirExists(build_dir)) {
183 return absl::NotFoundError(absl::StrFormat(
184 "Build directory '%s' not found. Run cmake to configure first.",
188 formatter.
AddField(
"build_directory", build_dir);
190 formatter.
AddField(
"preset", preset.empty() ?
"default" : preset);
193 std::string ctest_cmd =
"ctest --test-dir " + build_dir +
" -L " + label;
195 ctest_cmd +=
" --output-on-failure";
197 ctest_cmd +=
" 2>&1";
200 std::cout <<
"\n=== Running Tests ===\n\n";
201 std::cout <<
"Command: " << ctest_cmd <<
"\n\n";
205 std::string output = ExecuteCommand(ctest_cmd);
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) {
218 if (line.find(
"100%") != std::string::npos) {
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());
228 sscanf(line.c_str(),
"%d tests passed, %d tests failed out of %d",
229 &passed, &failed, &total);
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);
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);
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";
251 std::cout <<
"\n⚠ No tests found for label '" << label <<
"'\n";
255 return absl::OkStatus();
261 bool is_json = formatter.
IsJson();
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;
270 std::string skip_rom = GetEnvOrDefault(
"YAZE_SKIP_ROM_TESTS",
"");
271 std::string enable_ui = GetEnvOrDefault(
"YAZE_ENABLE_UI_TESTS",
"");
274 rom_vanilla.empty() ?
"not set" : rom_vanilla);
276 rom_expanded.empty() ?
"not set" : rom_expanded);
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());
283 std::vector<std::string> build_dirs = {
"build",
"build_fast",
"build_ai",
284 "build_test",
"build_agent"};
287 for (
const auto& dir : build_dirs) {
288 if (BuildDirExists(dir)) {
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)";
303 formatter.
AddField(
"active_preset", active_preset);
307 for (
const auto& suite : kTestSuites) {
308 bool available =
true;
309 if (suite.requires_rom && rom_vanilla.empty()) {
321 std::cout <<
"╔════════════════════════════════════════════════════════════"
323 std::cout <<
"║ TEST CONFIGURATION "
325 std::cout <<
"╠════════════════════════════════════════════════════════════"
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 <<
"╠════════════════════════════════════════════════════════════"
340 std::cout <<
"║ Available Build Directories: "
342 for (
const auto& dir : build_dirs) {
343 if (BuildDirExists(dir)) {
344 std::cout << absl::StrFormat(
"║ ✓ %-55s ║\n", dir);
347 std::cout <<
"╠════════════════════════════════════════════════════════════"
349 std::cout <<
"║ Available Test Suites: "
351 for (
const auto& suite : kTestSuites) {
352 bool available =
true;
354 if (suite.requires_rom && rom_vanilla.empty()) {
356 reason =
" (needs ROM)";
358 if (suite.requires_ai) {
359 reason =
" (needs AI)";
361 std::cout << absl::StrFormat(
"║ %s %-15s%-40s ║\n",
362 available ?
"✓" :
"✗", suite.label, reason);
364 std::cout <<
"╚════════════════════════════════════════════════════════════"
367 if (rom_vanilla.empty()) {
368 std::cout <<
"\nTo enable ROM-dependent tests:\n";
370 <<
" export YAZE_TEST_ROM_VANILLA=/path/to/alttp_vanilla.sfc\n";
371 std::cout <<
" cmake ... -DYAZE_ENABLE_ROM_TESTS=ON\n";
375 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
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.
std::string GetEnvOrDefault(const char *name, const std::string &default_val)
bool BuildDirExists(const std::string &dir)
std::string ExecuteCommand(const std::string &cmd)
const TestSuite kTestSuites[]
Namespace for the command line interface.
const char * requirements