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;
47 std::unique_ptr<FILE,
decltype(&pclose)> pipe(popen(cmd.c_str(),
"r"),
52 while (fgets(buffer.data(), buffer.size(), pipe.get()) !=
nullptr) {
53 result += buffer.data();
60 std::string cmd =
"test -d " + dir +
" && echo yes";
62 return result.find(
"yes") != std::string::npos;
67 const char* val = std::getenv(name);
68 return val ? val : default_val;
76 auto filter_label = parser.
GetString(
"label");
77 bool is_json = formatter.
IsJson();
81 for (
const auto& suite : kTestSuites) {
82 if (filter_label.has_value() && suite.label != filter_label.value()) {
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");
95 std::string entry = absl::StrFormat(
96 "%s: %s [%s]", suite.label, suite.description, suite.requirements);
103 std::string build_dir =
"build";
104 if (!BuildDirExists(build_dir)) {
105 build_dir =
"build_fast";
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);
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);
121 formatter.
AddField(
"build_directory", build_dir);
123 formatter.
AddField(
"build_directory",
"not_found");
125 "Run 'cmake --preset mac-test && cmake --build "
126 "--preset mac-test' to build tests");
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()) {
136 std::cout << absl::StrFormat(
" %-15s %s\n", suite.label,
138 std::cout << absl::StrFormat(
" Requirements: %s\n",
140 if (suite.requires_rom) {
141 std::cout <<
" ⚠ Requires ROM file\n";
143 if (suite.requires_ai) {
144 std::cout <<
" ⚠ Requires AI runtime\n";
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";
156 return absl::OkStatus();
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();
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";
177 if (!BuildDirExists(build_dir)) {
178 return absl::NotFoundError(absl::StrFormat(
179 "Build directory '%s' not found. Run cmake to configure first.",
183 formatter.
AddField(
"build_directory", build_dir);
185 formatter.
AddField(
"preset", preset.empty() ?
"default" : preset);
188 std::string ctest_cmd =
"ctest --test-dir " + build_dir +
" -L " + label;
190 ctest_cmd +=
" --output-on-failure";
192 ctest_cmd +=
" 2>&1";
195 std::cout <<
"\n=== Running Tests ===\n\n";
196 std::cout <<
"Command: " << ctest_cmd <<
"\n\n";
200 std::string output = ExecuteCommand(ctest_cmd);
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) {
213 if (line.find(
"100%") != std::string::npos) {
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());
223 sscanf(line.c_str(),
"%d tests passed, %d tests failed out of %d",
224 &passed, &failed, &total);
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);
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);
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";
246 std::cout <<
"\n⚠ No tests found for label '" << label <<
"'\n";
250 return absl::OkStatus();
256 bool is_json = formatter.
IsJson();
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",
"");
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());
268 std::vector<std::string> build_dirs = {
"build",
"build_fast",
"build_ai",
269 "build_test",
"build_agent"};
272 for (
const auto& dir : build_dirs) {
273 if (BuildDirExists(dir)) {
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)";
288 formatter.
AddField(
"active_preset", active_preset);
292 for (
const auto& suite : kTestSuites) {
293 bool available =
true;
294 if (suite.requires_rom && rom_path.empty()) {
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);
324 std::cout <<
"╠═══════════════════════════════════════════════════════════════╣\n";
325 std::cout <<
"║ Available Test Suites: ║\n";
326 for (
const auto& suite : kTestSuites) {
327 bool available =
true;
329 if (suite.requires_rom && rom_path.empty()) {
331 reason =
" (needs ROM)";
333 if (suite.requires_ai) {
334 reason =
" (needs AI)";
336 std::cout << absl::StrFormat(
"║ %s %-15s%-40s ║\n",
337 available ?
"✓" :
"✗", suite.label, reason);
339 std::cout <<
"╚═══════════════════════════════════════════════════════════════╝\n";
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";
348 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