Files
SEQUA-Engine/studio/host/supervisor/tests/test_process.cpp
T
Ange-Marie MAURIN - A2M Systèmes 832bd52fc9 feat(studio/host): brique supervisor, cycle de vie du Core
En-têtes repris d'un subagent interrompu, implémentation et tests écrits ensuite : CoreProcess (CreateProcessW avec héritage limité au journal, citation compatible CommandLineToArgvW, journal en ajout ; posix_spawn sous POSIX, non testé), is_process_alive, fichier d'exécution atomique, copies fantômes et nettoyage, read_last_lines, FileWatcher. 15 cas (Debug, ASan) avec un processus enfant de test.
2026-11-06 23:06:34 +01:00

152 lines
5.4 KiB
C++

// SPDX-License-Identifier: MPL-2.0
#include "sequa/studio/host/files.hpp"
#include "sequa/studio/host/paths.hpp"
#include "sequa/studio/host/process.hpp"
#include "sequa/studio/host/shadow.hpp"
#include "temp_dir.hpp"
#include <doctest/doctest.h>
#include <algorithm>
#include <chrono>
#include <string>
#include <vector>
using namespace sequa::studio::host;
using namespace std::chrono_literals;
namespace {
const std::filesystem::path child = SEQUA_TEST_CHILD;
bool contains(const std::vector<std::string>& lines, const std::string& expected)
{
return std::find(lines.begin(), lines.end(), expected) != lines.end();
}
} // namespace
TEST_CASE("Processus : code de sortie et journal en ajout (stdout et stderr)")
{
testing::TempDir dir;
const auto log = log_file_path(dir.path(), 9100);
LaunchOptions options;
options.log_path = log;
auto first = CoreProcess::launch(child, {"--out", "premier", "--err", "erreur", "--exit", "7"}, options);
REQUIRE(first.has_value());
CHECK(first->pid() != 0);
CHECK(first->wait_for(5s) == 7);
CHECK_FALSE(first->running());
auto second = CoreProcess::launch(child, {"--out", "second"}, options);
REQUIRE(second.has_value());
CHECK(second->wait_for(5s) == 0);
const auto lines = read_last_lines(log, 100);
CHECK(contains(lines, "premier"));
CHECK(contains(lines, "erreur"));
CHECK(contains(lines, "second")); // le premier lancement n'a pas été écrasé
const auto headers = std::count_if(lines.begin(), lines.end(),
[](const std::string& line) { return line.starts_with("===== "); });
CHECK(headers == 2);
}
TEST_CASE("Processus : arguments avec espaces, guillemets et antislashs")
{
testing::TempDir dir;
LaunchOptions options;
options.log_path = dir.path() / "echo.log";
const std::vector<std::string> tricky = {"simple", "avec espace", "guillemet \" milieu",
"fin antislash\\", "C:\\Program Files\\", "\\\\serveur\\\"partage",
"", "accentué é"};
std::vector<std::string> args = {"--echo-rest"};
args.insert(args.end(), tricky.begin(), tricky.end());
auto process = CoreProcess::launch(child, args, options);
REQUIRE(process.has_value());
REQUIRE(process->wait_for(5s) == 0);
const auto lines = read_last_lines(options.log_path, 100);
for (const std::string& argument : tricky) {
CAPTURE(argument);
CHECK(contains(lines, "[" + argument + "]"));
}
}
TEST_CASE("Processus : wait_for avec delai, terminate, is_process_alive")
{
auto process = CoreProcess::launch(child, {"--sleep", "10000"});
REQUIRE(process.has_value());
CHECK(process->running());
CHECK(is_process_alive(process->pid()));
CHECK_FALSE(process->wait_for(100ms).has_value());
process->terminate();
CHECK_FALSE(process->running());
CHECK(process->wait_for(0ms).has_value());
CHECK_FALSE(is_process_alive(process->pid()));
CHECK_FALSE(is_process_alive(0));
}
TEST_CASE("Processus : executable absent -> nullopt ; deplacement")
{
CHECK_FALSE(CoreProcess::launch("introuvable-sequa.exe", {}).has_value());
auto process = CoreProcess::launch(child, {"--exit", "3"});
REQUIRE(process.has_value());
CoreProcess moved = std::move(*process);
CHECK(moved.wait_for(5s) == 3);
}
TEST_CASE("Processus : le destructeur ne tue pas le processus")
{
std::uint32_t pid = 0;
{
auto process = CoreProcess::launch(child, {"--sleep", "1500"});
REQUIRE(process.has_value());
pid = process->pid();
}
CHECK(is_process_alive(pid));
}
TEST_CASE("Copie fantome : nom, nettoyage, copie en cours d'execution conservee")
{
testing::TempDir dir;
const auto bin = bin_dir(dir.path());
const auto first = make_shadow_copy(child, bin);
const auto second = make_shadow_copy(child, bin); // même seconde : suffixe -1
REQUIRE(first.has_value());
REQUIRE(second.has_value());
CHECK(*first != *second);
CHECK(is_shadow_copy_name(first->filename().string()));
CHECK(is_shadow_copy_name(second->filename().string()));
CHECK_FALSE(make_shadow_copy(dir.path() / "absent.exe", bin).has_value());
// Un fichier étranger dans bin/ n'est jamais touché.
const auto foreign = bin / "autre-outil.exe";
std::filesystem::copy_file(child, foreign);
auto running = CoreProcess::launch(*first, {"--sleep", "3000"});
REQUIRE(running.has_value());
const std::size_t removed = cleanup_shadow_copies(bin);
#if defined(_WIN32)
CHECK(removed == 1); // la copie en cours d'exécution est verrouillée
CHECK(std::filesystem::exists(*first));
#else
CHECK(removed == 2);
#endif
CHECK_FALSE(std::filesystem::exists(*second));
CHECK(std::filesystem::exists(foreign));
running->terminate();
CHECK(cleanup_shadow_copies(bin) <= 1);
}
TEST_CASE("Copie fantome : reconnaissance du motif")
{
CHECK(is_shadow_copy_name("sequa-core-20261106T210000Z-1234.exe"));
CHECK(is_shadow_copy_name("sequa-core-20261106T210000Z-1234-2"));
CHECK_FALSE(is_shadow_copy_name("sequa-core.exe"));
CHECK_FALSE(is_shadow_copy_name("sequa-core-20261106T210000Z.exe"));
CHECK_FALSE(is_shadow_copy_name("sequa-core-2026110XT210000Z-1.exe"));
CHECK_FALSE(is_shadow_copy_name("sequa-core-20261106T210000Z-12a.exe"));
CHECK_FALSE(is_shadow_copy_name("autre-20261106T210000Z-1.exe"));
}