// IACore-OSS; The Core Library for All IA Open Source Projects // Copyright (C) 2026 IAS (ias@iasoft.dev) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. #include #include using namespace IACore; // ----------------------------------------------------------------------------- // Platform Abstraction for Test Commands // ----------------------------------------------------------------------------- #if IA_PLATFORM_WINDOWS #define CMD_ECHO_EXE "cmd.exe" #define CMD_ARG_PREFIX "/c echo" #define NULL_DEVICE "NUL" #else #define CMD_ECHO_EXE "/bin/echo" #define CMD_ARG_PREFIX "" #define NULL_DEVICE "/dev/null" #endif IAT_BEGIN_BLOCK(Core, ProcessOps) // ------------------------------------------------------------------------- // 1. Basic Execution (Exit Code 0) // ------------------------------------------------------------------------- auto test_basic_run() -> bool { // Simple "echo hello" String captured; const auto result = ProcessOps::spawn_process_sync(CMD_ECHO_EXE, CMD_ARG_PREFIX " HelloIA", [&](StringView line) { captured = line; }); IAT_CHECK(result.has_value()); IAT_CHECK_EQ(*result, 0); // Exit code 0 // We check if "HelloIA" is contained or equal. IAT_CHECK(captured.find("HelloIA") != String::npos); return true; } // ------------------------------------------------------------------------- // 2. Argument Parsing // ------------------------------------------------------------------------- auto test_arguments() -> bool { Vec lines; // Echo two distinct words. // Windows: cmd.exe /c echo one two // Linux: /bin/echo one two String args = String(CMD_ARG_PREFIX) + " one two"; if (!args.empty() && args[0] == ' ') { args.erase(0, 1); // cleanup space if prefix empty } const auto result = ProcessOps::spawn_process_sync(CMD_ECHO_EXE, args, [&](StringView line) { lines.push_back(String(line)); }); IAT_CHECK_EQ(*result, 0); IAT_CHECK(lines.size() > 0); // Output should contain "one two" IAT_CHECK(lines[0].find("one two") != String::npos); return true; } // ------------------------------------------------------------------------- // 3. Error / Non-Zero Exit Codes // ------------------------------------------------------------------------- auto test_exit_codes() -> bool { // We need a command that returns non-zero. // Windows: cmd /c exit 1 // Linux: /bin/sh -c "exit 1" String cmd; String arg; #if IA_PLATFORM_WINDOWS cmd = "cmd.exe"; arg = "/c exit 42"; #else cmd = "/bin/sh"; arg = "-c \"exit 42\""; // quotes needed for sh -c #endif const auto result = ProcessOps::spawn_process_sync(cmd, arg, [](StringView) {}); IAT_CHECK(result.has_value()); IAT_CHECK_EQ(*result, 42); return true; } // ------------------------------------------------------------------------- // 4. Missing Executable Handling // ------------------------------------------------------------------------- auto test_missing_exe() -> bool { // Try to run a random string const auto result = ProcessOps::spawn_process_sync("sdflkjghsdflkjg", "", [](StringView) {}); // Windows: CreateProcess usually fails -> returns unexpected // Linux: execvp fails inside child, returns 127 via waitpid #if IA_PLATFORM_WINDOWS IAT_CHECK_NOT(result.has_value()); // Should be an error string #else // Linux fork succeeds, but execvp fails, returning 127 IAT_CHECK(result.has_value()); IAT_CHECK_EQ(*result, 127); #endif return true; } // ------------------------------------------------------------------------- // 5. Line Buffer Logic (The 4096 split test) // ------------------------------------------------------------------------- auto test_large_output() -> bool { // Need to generate output larger than the internal 4096 buffer // to ensure the "partial line" logic works when a line crosses a buffer // boundary. String massive_string; massive_string.reserve(5000); for (i32 i = 0; i < 500; ++i) { massive_string += "1234567890"; // 5000 chars } String cmd; String arg; #if IA_PLATFORM_WINDOWS cmd = "cmd.exe"; // Windows has command line length limits (~8k), 5k is safe. arg = "/c echo " + massive_string; #else cmd = "/bin/echo"; arg = massive_string; #endif String captured; const auto result = ProcessOps::spawn_process_sync( cmd, arg, [&](StringView line) { captured += line; }); IAT_CHECK(result.has_value()); IAT_CHECK_EQ(*result, 0); // If the LineBuffer failed to stitch chunks, the length wouldn't match // or we would get multiple callbacks if we expected 1 line. IAT_CHECK_EQ(captured.length(), massive_string.length()); return true; } // ------------------------------------------------------------------------- // 6. Multi-Line Handling // ------------------------------------------------------------------------- auto test_multi_line() -> bool { // Windows: cmd /c "echo A && echo B" // Linux: /bin/sh -c "echo A; echo B" String cmd; String arg; #if IA_PLATFORM_WINDOWS cmd = "cmd.exe"; arg = "/c \"echo LineA && echo LineB\""; #else cmd = "/bin/sh"; arg = "-c \"echo LineA; echo LineB\""; #endif i32 line_count = 0; bool found_a = false; bool found_b = false; IA_UNUSED const auto res = ProcessOps::spawn_process_sync(cmd, arg, [&](StringView line) { line_count++; if (line.find("LineA") != String::npos) { found_a = true; } if (line.find("LineB") != String::npos) { found_b = true; } }); IAT_CHECK(found_a); IAT_CHECK(found_b); // We expect at least 2 lines. // (Windows sometimes echoes the command itself depending on echo settings, // but we check contents) IAT_CHECK(line_count >= 2); return true; } // ------------------------------------------------------------------------- // 6. Complex Command Line Arguments Handling // ------------------------------------------------------------------------- auto test_complex_arguments() -> bool { // Should parse as 3 arguments: // 1. -DDEFINED_MSG="Hello World" // 2. -v // 3. path/to/file const String complex_args = "-DDEFINED_MSG=\\\"Hello World\\\" -v path/to/file"; const String cmd = CMD_ECHO_EXE; String final_args; #if IA_PLATFORM_WINDOWS final_args = "/c echo " + complex_args; #else final_args = complex_args; #endif String captured; const auto result = ProcessOps::spawn_process_sync( cmd, final_args, [&](StringView line) { captured += line; }); IAT_CHECK(result.has_value()); IAT_CHECK_EQ(*result, 0); // Verify the quotes were preserved in the output IAT_CHECK(captured.find("Hello World") != String::npos); return true; } // ------------------------------------------------------------------------- // Registration // ------------------------------------------------------------------------- IAT_BEGIN_TEST_LIST() IAT_ADD_TEST(test_basic_run); IAT_ADD_TEST(test_arguments); IAT_ADD_TEST(test_exit_codes); IAT_ADD_TEST(test_missing_exe); IAT_ADD_TEST(test_large_output); IAT_ADD_TEST(test_multi_line); IAT_ADD_TEST(test_complex_arguments); IAT_END_TEST_LIST() IAT_END_BLOCK() IAT_REGISTER_ENTRY(Core, ProcessOps)