Files
Ange-Marie MAURIN - A2M Systèmes 464ff6ce88 refactor(platform): lanceur de processus commun, enfant lié au parent
Le lancement de processus de Studio host (CoreProcess) devient sequa::platform::ChildProcess dans platform/process, pour être partagé par Studio host et le Core (qui lancera le rendu, ADR 0009). Nouvelle option kill_with_parent : l'enfant meurt avec le parent, même brutalement (job object KILL_ON_JOB_CLOSE sous Windows, fork + PR_SET_PDEATHSIG sous Linux, ignorée sous macOS). Les tests de copie fantôme restent dans studio/host/supervisor (test_shadow.cpp) avec l'enfant de test de platform/process.
2026-11-07 14:41:56 +01:00

141 lines
5.0 KiB
C++

// SPDX-License-Identifier: MPL-2.0
#include "sequa/platform/process.hpp"
#include "temp_dir.hpp"
#include <doctest/doctest.h>
#include <algorithm>
#include <chrono>
#include <fstream>
#include <string>
#include <vector>
using namespace sequa::platform;
using namespace std::chrono_literals;
namespace {
const std::filesystem::path child = SEQUA_TEST_CHILD;
// Les journaux de test restent petits : lecture entière, lignes séparées
// (équivalent local à read_last_lines du supervisor, qui reste du côté Studio).
std::vector<std::string> read_lines(const std::filesystem::path& path)
{
std::ifstream file(path, std::ios::binary);
std::vector<std::string> lines;
for (std::string line; std::getline(file, line);) {
if (!line.empty() && line.back() == '\r') {
line.pop_back();
}
lines.push_back(std::move(line));
}
return lines;
}
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 = dir.path() / "journal-9100.log";
LaunchOptions options;
options.log_path = log;
auto first = ChildProcess::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 = ChildProcess::launch(child, {"--out", "second"}, options);
REQUIRE(second.has_value());
CHECK(second->wait_for(5s) == 0);
const auto lines = read_lines(log);
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 = ChildProcess::launch(child, args, options);
REQUIRE(process.has_value());
REQUIRE(process->wait_for(5s) == 0);
const auto lines = read_lines(options.log_path);
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 = ChildProcess::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(ChildProcess::launch("introuvable-sequa.exe", {}).has_value());
auto process = ChildProcess::launch(child, {"--exit", "3"});
REQUIRE(process.has_value());
ChildProcess 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 = ChildProcess::launch(child, {"--sleep", "1500"});
REQUIRE(process.has_value());
pid = process->pid();
}
CHECK(is_process_alive(pid));
}
TEST_CASE("Processus : kill_with_parent (job object sous Windows, fork + PR_SET_PDEATHSIG sous Linux)")
{
// Dans le même processus de test, on ne peut pas observer la mort du parent
// (c'est ce processus qui porte le test). On vérifie donc ici que le
// lancement avec l'option réussit, que l'enfant tourne et reste supervisable
// normalement, et que terminate() rend la sortie comme sans l'option ; la
// mort du parent (job object refermé par la disparition de ses handles,
// SIGKILL de PR_SET_PDEATHSIG) est le mécanisme système lui-même, à
// confirmer en dehors du test unitaire (ADR 0009).
LaunchOptions options;
options.kill_with_parent = true;
auto process = ChildProcess::launch(child, {"--sleep", "10000"}, options);
REQUIRE(process.has_value());
CHECK(process->running());
process->terminate();
CHECK(process->wait_for(2s).has_value());
CHECK_FALSE(process->running());
}