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 {
"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},
47 std::array<char, 256> buffer;
50 std::unique_ptr<FILE,
decltype(&_pclose)> pipe(_popen(cmd.c_str(),
"r"),
53 std::unique_ptr<FILE,
decltype(&pclose)> pipe(popen(cmd.c_str(),
"r"),
59 while (fgets(buffer.data(), buffer.size(), pipe.get()) !=
nullptr) {
60 result += buffer.data();
67 std::string cmd =
"test -d " + dir +
" && echo yes";
69 return result.find(
"yes") != std::string::npos;
74 const char* val = std::getenv(name);
75 return val ? val : default_val;
83 auto filter_label = parser.
GetString(
"label");
84 bool is_json = formatter.
IsJson();
88 for (
const auto& suite : kTestSuites) {
89 if (filter_label.has_value() && suite.label != filter_label.value()) {
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");
102 std::string entry = absl::StrFormat(
103 "%s: %s [%s]", suite.label, suite.description, suite.requirements);
110 std::string build_dir =
"build";
111 if (!BuildDirExists(build_dir)) {
112 build_dir =
"build_fast";
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);
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);
128 formatter.
AddField(
"build_directory", build_dir);
130 formatter.
AddField(
"build_directory",
"not_found");
132 "Run 'cmake --preset mac-test && cmake --build "
133 "--preset mac-test' to build tests");
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()) {
143 std::cout << absl::StrFormat(
" %-15s %s\n", suite.label,
145 std::cout << absl::StrFormat(
" Requirements: %s\n",
147 if (suite.requires_rom) {
148 std::cout <<
" ⚠ Requires ROM file\n";
150 if (suite.requires_ai) {
151 std::cout <<
" ⚠ Requires AI runtime\n";
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";
163 return absl::OkStatus();
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();
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";
185 if (!BuildDirExists(build_dir)) {
186 return absl::NotFoundError(absl::StrFormat(
187 "Build directory '%s' not found. Run cmake to configure first.",
191 formatter.
AddField(
"build_directory", build_dir);
193 formatter.
AddField(
"preset", preset.empty() ?
"default" : preset);
194 if (!filter.empty()) {
195 formatter.
AddField(
"filter", filter);
199 std::string ctest_cmd =
"ctest --test-dir " + build_dir +
" -L " + label;
200 if (!filter.empty()) {
201 ctest_cmd +=
" -R \"" + filter +
"\"";
204 ctest_cmd +=
" --output-on-failure";
206 ctest_cmd +=
" 2>&1";
209 std::cout <<
"\n=== Running Tests ===\n\n";
210 std::cout <<
"Command: " << ctest_cmd <<
"\n\n";
214 std::string output = ExecuteCommand(ctest_cmd);
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) {
227 if (line.find(
"100%") != std::string::npos) {
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());
237 sscanf(line.c_str(),
"%d tests passed, %d tests failed out of %d",
238 &passed, &failed, &total);
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);
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);
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";
260 std::cout <<
"\n⚠ No tests found for label '" << label <<
"'\n";
264 return absl::OkStatus();
270 bool is_json = formatter.
IsJson();
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;
279 std::string skip_rom = GetEnvOrDefault(
"YAZE_SKIP_ROM_TESTS",
"");
280 std::string enable_ui = GetEnvOrDefault(
"YAZE_ENABLE_UI_TESTS",
"");
283 rom_vanilla.empty() ?
"not set" : rom_vanilla);
285 rom_expanded.empty() ?
"not set" : rom_expanded);
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());
292 std::vector<std::string> build_dirs = {
"build",
"build_fast",
"build_ai",
293 "build_test",
"build_agent"};
296 for (
const auto& dir : build_dirs) {
297 if (BuildDirExists(dir)) {
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)";
312 formatter.
AddField(
"active_preset", active_preset);
316 for (
const auto& suite : kTestSuites) {
317 bool available =
true;
318 if (suite.requires_rom && rom_vanilla.empty()) {
330 std::cout <<
"╔════════════════════════════════════════════════════════════"
332 std::cout <<
"║ TEST CONFIGURATION "
334 std::cout <<
"╠════════════════════════════════════════════════════════════"
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 <<
"╠════════════════════════════════════════════════════════════"
349 std::cout <<
"║ Available Build Directories: "
351 for (
const auto& dir : build_dirs) {
352 if (BuildDirExists(dir)) {
353 std::cout << absl::StrFormat(
"║ ✓ %-55s ║\n", dir);
356 std::cout <<
"╠════════════════════════════════════════════════════════════"
358 std::cout <<
"║ Available Test Suites: "
360 for (
const auto& suite : kTestSuites) {
361 bool available =
true;
363 if (suite.requires_rom && rom_vanilla.empty()) {
365 reason =
" (needs ROM)";
367 if (suite.requires_ai) {
368 reason =
" (needs AI)";
370 std::cout << absl::StrFormat(
"║ %s %-15s%-40s ║\n",
371 available ?
"✓" :
"✗", suite.label, reason);
373 std::cout <<
"╚════════════════════════════════════════════════════════════"
376 if (rom_vanilla.empty()) {
377 std::cout <<
"\nTo enable ROM-dependent tests:\n";
379 <<
" export YAZE_TEST_ROM_VANILLA=/path/to/alttp_vanilla.sfc\n";
380 std::cout <<
" cmake ... -DYAZE_ENABLE_ROM_TESTS=ON\n";
384 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