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.
141 lines
5.0 KiB
C++
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());
|
|
}
|