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.
152 lines
5.4 KiB
C++
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"));
|
|
}
|