12#include "absl/strings/match.h"
13#include "absl/strings/str_cat.h"
14#include "absl/strings/str_format.h"
15#include "absl/strings/str_join.h"
16#include "absl/strings/str_split.h"
33namespace fs = std::filesystem;
48 if (build_path.is_relative()) {
70 const std::string& preset) {
72 return absl::InvalidArgumentError(
"Preset name cannot be empty");
78 return absl::InvalidArgumentError(
79 absl::StrFormat(
"Invalid preset '%s'. Available presets: %s",
80 preset, absl::StrJoin(available,
", ")));
87 return absl::InternalError(
88 absl::StrFormat(
"Failed to create build directory: %s", ec.message()));
92 std::string command = absl::StrFormat(
93 "cmake --preset %s -B \"%s\"",
97 command +=
" --debug-output";
101 absl::StrFormat(
"Configuring with preset '%s'", preset));
105 const std::string& target,
const std::string& config) {
109 return absl::FailedPreconditionError(
110 absl::StrFormat(
"Build directory '%s' not configured. Run Configure first.",
115 std::string command = absl::StrFormat(
118 if (!config.empty()) {
119 command += absl::StrFormat(
" --config %s", config);
122 if (!target.empty()) {
123 command += absl::StrFormat(
" --target %s", target);
127 command +=
" --parallel";
130 command +=
" --verbose";
134 absl::StrFormat(
"Building %s",
135 target.empty() ?
"all targets" : target));
139 const std::string& filter,
const std::string& rom_path) {
143 return absl::FailedPreconditionError(
144 absl::StrFormat(
"Build directory '%s' not configured. Run Configure first.",
149 std::string command = absl::StrFormat(
150 "ctest --test-dir \"%s\" --output-on-failure",
154 if (!filter.empty()) {
156 if (filter ==
"unit" || filter ==
"integration" || filter ==
"e2e" ||
157 filter ==
"stable" || filter ==
"experimental" ||
158 filter ==
"rom_dependent") {
159 command += absl::StrFormat(
" -L %s", filter);
162 command += absl::StrFormat(
" -R \"%s\"", filter);
167 std::string env_setup;
168 if (!rom_path.empty()) {
169 if (!fs::exists(rom_path)) {
170 return absl::NotFoundError(
171 absl::StrFormat(
"ROM file not found: %s", rom_path));
174 env_setup = absl::StrFormat(
"set YAZE_TEST_ROM_PATH=\"%s\" && ", rom_path);
176 env_setup = absl::StrFormat(
"YAZE_TEST_ROM_PATH=\"%s\" ", rom_path);
181 command +=
" --parallel";
184 command +=
" --verbose";
187 std::string full_command = env_setup + command;
190 absl::StrFormat(
"Running tests%s",
191 filter.empty() ?
"" : absl::StrFormat(
" (filter: %s)", filter)));
205 "Last operation: %s (exit code: %d, duration: %lds)",
221 result.
output =
"Build directory does not exist, nothing to clean";
223 result.
duration = std::chrono::seconds(0);
228 std::string command = absl::StrFormat(
229 "cmake --build \"%s\" --target clean",
239 return fs::exists(build_path) &&
240 fs::exists(build_path /
"CMakeCache.txt");
259 return absl::OkStatus();
267 const std::string& command,
const std::string& operation_name) {
271 return absl::UnavailableError(
"Another build operation is in progress");
276 auto start_time = std::chrono::steady_clock::now();
282 auto end_time = std::chrono::steady_clock::now();
283 auto duration = std::chrono::duration_cast<std::chrono::seconds>(
284 end_time - start_time);
287 auto& build_result = *result;
288 build_result.duration = duration;
289 build_result.command_executed = command;
306 const std::string& command,
const std::chrono::seconds& timeout) {
314 fs::path original_dir = fs::current_path();
318 fs::current_path(project_root, ec);
320 return absl::InternalError(
321 absl::StrFormat(
"Failed to change directory: %s", ec.message()));
327 std::string full_command = absl::StrFormat(
"cmd /c %s 2>&1", command);
328 FILE* pipe = _popen(full_command.c_str(),
"r");
331 std::string full_command = command +
" 2>&1";
332 FILE* pipe = popen(full_command.c_str(),
"r");
336 fs::current_path(original_dir, ec);
337 return absl::InternalError(
"Failed to execute command");
341 std::stringstream output_stream;
342 std::stringstream error_stream;
343 std::array<char, 4096> buffer;
344 size_t total_output = 0;
345 auto start_time = std::chrono::steady_clock::now();
349 int pipe_fd = fileno(pipe);
350 int flags = fcntl(pipe_fd, F_GETFL, 0);
351 fcntl(pipe_fd, F_SETFL, flags | O_NONBLOCK);
356 auto current_time = std::chrono::steady_clock::now();
357 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
358 current_time - start_time);
360 if (elapsed >= timeout) {
367 fs::current_path(original_dir, ec);
368 return absl::DeadlineExceededError(
369 absl::StrFormat(
"Command timed out after %ld seconds",
374 if (fgets(buffer.data(), buffer.size(), pipe) !=
nullptr) {
375 size_t len = strlen(buffer.data());
381 output_stream << buffer.data();
384 std::string line(buffer.data());
385 std::transform(line.begin(), line.end(), line.begin(), ::tolower);
386 if (line.find(
"error") != std::string::npos ||
387 line.find(
"failed") != std::string::npos ||
388 line.find(
"fatal") != std::string::npos) {
389 error_stream << buffer.data();
399 if (errno == EAGAIN || errno == EWOULDBLOCK) {
400 std::this_thread::sleep_for(std::chrono::milliseconds(100));
413 int status = pclose(pipe);
414 if (WIFEXITED(status)) {
416 }
else if (WIFSIGNALED(status)) {
417 result.
exit_code = 128 + WTERMSIG(status);
424 fs::current_path(original_dir, ec);
428 result.
output = output_stream.str();
433 return absl::CancelledError(
"Operation was cancelled");
441 fs::path current = fs::current_path();
442 fs::path root = current;
445 while (!root.empty() && root != root.root_path()) {
447 if (fs::exists(root /
"CMakeLists.txt") &&
448 fs::exists(root /
"src" /
"yaze.cc") &&
449 fs::exists(root /
"CMakePresets.json")) {
450 return root.string();
453 if (fs::exists(root /
".git")) {
455 if (fs::exists(root /
"src" /
"cli") &&
456 fs::exists(root /
"src" /
"app")) {
457 return root.string();
460 root = root.parent_path();
464 return current.string();
470#elif defined(__APPLE__)
472#elif defined(__linux__)
480 std::vector<std::string> presets;
482 fs::path presets_file = fs::path(
GetProjectRoot()) /
"CMakePresets.json";
483 if (!fs::exists(presets_file)) {
488 std::ifstream file(presets_file);
493 std::stringstream buffer;
494 buffer << file.rdbuf();
495 std::string content = buffer.str();
502 bool in_configure_presets =
false;
503 bool in_preset_object =
false;
504 bool is_hidden =
false;
505 std::string current_preset_name;
508 std::regex configure_presets_regex(
"\"configurePresets\"\\s*:\\s*\\[");
509 std::regex preset_name_regex(
"\"name\"\\s*:\\s*\"([^\"]+)\"");
510 std::regex hidden_regex(
"\"hidden\"\\s*:\\s*true");
511 std::regex condition_regex(
"\"condition\"\\s*:");
513 std::istringstream stream(content);
517 while (std::getline(stream, line)) {
519 if (std::regex_search(line, configure_presets_regex)) {
520 in_configure_presets =
true;
524 if (!in_configure_presets)
continue;
527 for (
char c : line) {
530 if (brace_count == 1) {
531 in_preset_object =
true;
533 current_preset_name.clear();
535 }
else if (c ==
'}') {
537 if (brace_count == 0 && in_preset_object) {
539 if (!current_preset_name.empty() && !is_hidden) {
541 bool include =
false;
543 if (platform ==
"Windows") {
545 if (absl::StartsWith(current_preset_name,
"win-") ||
546 absl::StartsWith(current_preset_name,
"ci-windows") ||
547 (!absl::StartsWith(current_preset_name,
"mac-") &&
548 !absl::StartsWith(current_preset_name,
"lin-") &&
549 !absl::StartsWith(current_preset_name,
"ci-"))) {
552 }
else if (platform ==
"Darwin") {
554 if (absl::StartsWith(current_preset_name,
"mac-") ||
555 absl::StartsWith(current_preset_name,
"ci-macos") ||
556 (!absl::StartsWith(current_preset_name,
"win-") &&
557 !absl::StartsWith(current_preset_name,
"lin-") &&
558 !absl::StartsWith(current_preset_name,
"ci-"))) {
561 }
else if (platform ==
"Linux") {
563 if (absl::StartsWith(current_preset_name,
"lin-") ||
564 absl::StartsWith(current_preset_name,
"ci-linux") ||
565 (!absl::StartsWith(current_preset_name,
"win-") &&
566 !absl::StartsWith(current_preset_name,
"mac-") &&
567 !absl::StartsWith(current_preset_name,
"ci-"))) {
573 presets.push_back(current_preset_name);
576 in_preset_object =
false;
578 }
else if (c ==
']' && brace_count == -1) {
580 in_configure_presets =
false;
585 if (in_preset_object) {
588 if (std::regex_search(line, match, preset_name_regex)) {
589 current_preset_name = match[1].str();
593 if (std::regex_search(line, hidden_regex)) {
600 std::sort(presets.begin(), presets.end());
607 return std::find(available.begin(), available.end(), preset) != available.end();
625 if (!parser.
GetString(
"preset").has_value()) {
626 return absl::InvalidArgumentError(
"--preset is required");
628 return absl::OkStatus();
636 std::string preset = parser.
GetString(
"preset").value();
637 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
638 bool verbose = parser.
HasFlag(
"verbose");
651 return result.status();
656 formatter.
AddField(
"preset", preset);
658 formatter.
AddField(
"success", result->success ?
"true" :
"false");
659 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
661 absl::StrFormat(
"%ld seconds", result->duration.count()));
663 if (!result->output.empty()) {
665 const size_t max_lines = 100;
666 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
667 if (lines.size() > max_lines) {
668 std::vector<std::string> truncated(
669 lines.end() - max_lines, lines.end());
671 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
672 lines.size() - max_lines,
673 absl::StrJoin(truncated,
"\n")));
675 formatter.
AddField(
"output", result->output);
679 if (!result->error_output.empty()) {
680 formatter.
AddField(
"errors", result->error_output);
685 if (!result->success) {
686 return absl::InternalError(
"Configuration failed");
689 return absl::OkStatus();
695 return absl::OkStatus();
703 std::string target = parser.
GetString(
"target").value_or(
"");
704 std::string config = parser.
GetString(
"config").value_or(
"");
705 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
706 bool verbose = parser.
HasFlag(
"verbose");
714 build_tool_ = std::make_unique<BuildTool>(tool_config);
719 return result.status();
724 formatter.
AddField(
"target", target.empty() ?
"all" : target);
725 if (!config.empty()) {
726 formatter.
AddField(
"configuration", config);
728 formatter.
AddField(
"build_directory", build_dir);
729 formatter.
AddField(
"success", result->success ?
"true" :
"false");
730 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
732 absl::StrFormat(
"%ld seconds", result->duration.count()));
735 if (!result->output.empty()) {
736 const size_t max_lines = 100;
737 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
738 if (lines.size() > max_lines) {
739 std::vector<std::string> truncated(
740 lines.end() - max_lines, lines.end());
742 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
743 lines.size() - max_lines,
744 absl::StrJoin(truncated,
"\n")));
745 formatter.
AddField(
"output_truncated",
"true");
747 formatter.
AddField(
"output", result->output);
751 if (!result->error_output.empty()) {
752 formatter.
AddField(
"errors", result->error_output);
757 if (!result->success) {
758 return absl::InternalError(
"Build failed");
761 return absl::OkStatus();
767 return absl::OkStatus();
775 std::string filter = parser.
GetString(
"filter").value_or(
"");
776 std::string rom_path = parser.
GetString(
"rom-path").value_or(
"");
777 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
778 bool verbose = parser.
HasFlag(
"verbose");
785 config.
timeout = std::chrono::seconds(300);
790 auto result =
build_tool_->RunTests(filter, rom_path);
792 return result.status();
797 if (!filter.empty()) {
798 formatter.
AddField(
"filter", filter);
800 if (!rom_path.empty()) {
801 formatter.
AddField(
"rom_path", rom_path);
803 formatter.
AddField(
"build_directory", build_dir);
804 formatter.
AddField(
"success", result->success ?
"true" :
"false");
805 formatter.
AddField(
"exit_code", std::to_string(result->exit_code));
807 absl::StrFormat(
"%ld seconds", result->duration.count()));
810 if (!result->output.empty()) {
812 std::regex summary_regex(
813 R
"((\d+)% tests passed, (\d+) tests failed out of (\d+))");
815 if (std::regex_search(result->output, match, summary_regex)) {
816 formatter.
AddField(
"tests_total", match[3].str());
817 formatter.
AddField(
"tests_failed", match[2].str());
818 formatter.
AddField(
"pass_rate", match[1].str() +
"%");
822 const size_t max_lines = 50;
823 std::vector<std::string> lines = absl::StrSplit(result->output,
'\n');
824 if (lines.size() > max_lines) {
825 std::vector<std::string> truncated(
826 lines.end() - max_lines, lines.end());
828 absl::StrFormat(
"[...truncated %zu lines...]\n%s",
829 lines.size() - max_lines,
830 absl::StrJoin(truncated,
"\n")));
832 formatter.
AddField(
"output", result->output);
836 if (!result->error_output.empty()) {
837 formatter.
AddField(
"errors", result->error_output);
842 if (!result->success) {
843 return absl::InternalError(
"Tests failed");
846 return absl::OkStatus();
852 return absl::OkStatus();
859 std::string build_dir = parser.
GetString(
"build-dir").value_or(
"build_ai");
871 formatter.
AddField(
"build_directory", build_dir);
872 formatter.
AddField(
"directory_ready",
873 build_tool_->IsBuildDirectoryReady() ?
"true" :
"false");
874 formatter.
AddField(
"operation_running",
875 status.is_running ?
"true" :
"false");
877 if (status.is_running) {
878 formatter.
AddField(
"current_operation", status.current_operation);
881 auto now = std::chrono::system_clock::now();
882 auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
883 now - status.start_time);
885 absl::StrFormat(
"%ld seconds", elapsed.count()));
888 if (!status.last_result_summary.empty()) {
889 formatter.
AddField(
"last_result", status.last_result_summary);
893 auto presets =
build_tool_->ListAvailablePresets();
894 if (!presets.empty()) {
896 for (
const auto& preset : presets) {
904 if (last_result.has_value()) {
906 formatter.
AddField(
"command", last_result->command_executed);
907 formatter.
AddField(
"success", last_result->success ?
"true" :
"false");
908 formatter.
AddField(
"exit_code", std::to_string(last_result->exit_code));
910 absl::StrFormat(
"%ld seconds", last_result->duration.count()));
916 return absl::OkStatus();
The Rom class is used to load, save, and modify Rom data. This is a generic SNES ROM container and do...
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.