feat: add ProcessExecutor, FAKE mode, fix thread lifecycle crash
- Add ProcessExecutor class wrapping PIProcess with proper cleanup - Add CMake FAKE option (-DFAKE=ON) for testing without opencode - Add 3 FAKE-mode tests for full pipeline execution, status transitions, output - Fix PIThread lifecycle: use startOnce() instead of start(), track threads, wait + delete in destructor to prevent use-after-free of PIDeque<PIChar> - Update build.sh with --fake flag and automatic cache invalidation - Upgrade CMake to C++17 standard - Update AGENTS.md with build commands and FAKE mode
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
#include <pifile.h>
|
||||
#include <piprocess.h>
|
||||
#include <pisystemtime.h>
|
||||
#include <pitime.h>
|
||||
|
||||
static const PIString testDir = "/tmp/pipeline-runner-test/";
|
||||
static const PIString testPipelinesDir = testDir + "pipelines/";
|
||||
@@ -31,6 +32,16 @@ static void cleanupDirs() {
|
||||
PIDir::remove(testDir);
|
||||
}
|
||||
|
||||
static void waitForRun(PipelineRunner & runner, const PIString & runId, int timeoutMs = 5000) {
|
||||
for (int i = 0; i < timeoutMs / 50; ++i) {
|
||||
RunState state = runner.getRunState(runId);
|
||||
if (state.status == RunStatus::Completed || state.status == RunStatus::Error) {
|
||||
return;
|
||||
}
|
||||
piMSleep(50);
|
||||
}
|
||||
}
|
||||
|
||||
static Pipeline makeEchoPipeline() {
|
||||
Pipeline pl;
|
||||
pl.id = "echo-test";
|
||||
@@ -90,6 +101,8 @@ TEST(RunnerTest, StartRunAndGetState) {
|
||||
EXPECT_EQ(state.steps.size(), 1);
|
||||
EXPECT_EQ(state.steps[0].title, "Step 1");
|
||||
|
||||
// Wait for background thread to finish before cleanup
|
||||
waitForRun(runner, runId);
|
||||
cleanupDirs();
|
||||
}
|
||||
|
||||
@@ -126,6 +139,8 @@ TEST(RunnerTest, IsRunActive) {
|
||||
PIString runId = runner.startRun("active-test");
|
||||
EXPECT_TRUE(runner.isRunActive(runId));
|
||||
|
||||
// Wait for background thread to finish before cleanup
|
||||
waitForRun(runner, runId);
|
||||
cleanupDirs();
|
||||
}
|
||||
|
||||
@@ -135,3 +150,148 @@ TEST(RunnerTest, RunStatusToString) {
|
||||
EXPECT_EQ(runStatusToString(RunStatus::Completed), "completed");
|
||||
EXPECT_EQ(runStatusToString(RunStatus::Error), "error");
|
||||
}
|
||||
|
||||
#ifdef FAKE
|
||||
|
||||
static Pipeline makeFakePipeline(const PIString & id, const PIString & name, int numSteps) {
|
||||
Pipeline pl;
|
||||
pl.id = id;
|
||||
pl.name = name;
|
||||
pl.working_dir = "/tmp";
|
||||
pl.created_at = nowISO();
|
||||
pl.updated_at = pl.created_at;
|
||||
|
||||
for (int i = 0; i < numSteps; ++i) {
|
||||
Prompt p;
|
||||
p.id = "p" + PIString::fromNumber(i);
|
||||
p.text = "fake text " + PIString::fromNumber(i);
|
||||
p.title = "Fake Step " + PIString::fromNumber(i);
|
||||
p.order = i;
|
||||
pl.prompts << p;
|
||||
}
|
||||
|
||||
return pl;
|
||||
}
|
||||
|
||||
TEST(FakeRunnerTest, FullPipelineCompletes) {
|
||||
setupDirs();
|
||||
PipelineRunner runner(testPipelinesDir, testLogsDir);
|
||||
|
||||
Pipeline pl = makeFakePipeline("fake-full", "Fake Full Pipeline", 3);
|
||||
savePipeline(testPipelinesDir, pl);
|
||||
|
||||
PIString runId = runner.startRun("fake-full");
|
||||
EXPECT_FALSE(runId.isEmpty());
|
||||
|
||||
// Wait for pipeline to finish (3 steps * 100ms = 300ms, allow extra time)
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
piMSleep(50);
|
||||
RunState state = runner.getRunState(runId);
|
||||
if (state.status == RunStatus::Completed) {
|
||||
bool allDone = true;
|
||||
for (int j = 0; j < state.steps.size(); ++j) {
|
||||
if (state.steps[j].status != RunStatus::Completed) {
|
||||
allDone = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allDone) break;
|
||||
}
|
||||
if (state.status == RunStatus::Error) break;
|
||||
}
|
||||
|
||||
RunState state = runner.getRunState(runId);
|
||||
EXPECT_EQ(state.status, RunStatus::Completed);
|
||||
EXPECT_EQ(state.steps.size(), 3);
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
EXPECT_EQ(state.steps[i].status, RunStatus::Completed);
|
||||
EXPECT_EQ(state.steps[i].returncode, 0);
|
||||
}
|
||||
|
||||
// Wait for background thread to finish before cleanup
|
||||
waitForRun(runner, runId, 2000);
|
||||
cleanupDirs();
|
||||
}
|
||||
|
||||
TEST(FakeRunnerTest, RunStatusTransitions) {
|
||||
setupDirs();
|
||||
PipelineRunner runner(testPipelinesDir, testLogsDir);
|
||||
|
||||
Pipeline pl = makeFakePipeline("fake-transition", "Fake Transition Pipeline", 2);
|
||||
savePipeline(testPipelinesDir, pl);
|
||||
|
||||
PIString runId = runner.startRun("fake-transition");
|
||||
EXPECT_FALSE(runId.isEmpty());
|
||||
|
||||
// Initially running
|
||||
RunState state = runner.getRunState(runId);
|
||||
EXPECT_EQ(state.status, RunStatus::Running);
|
||||
|
||||
// Wait for completion (2 steps * 100ms = 200ms, allow extra time)
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
piMSleep(50);
|
||||
state = runner.getRunState(runId);
|
||||
if (state.status == RunStatus::Completed) {
|
||||
bool allDone = true;
|
||||
for (int j = 0; j < state.steps.size(); ++j) {
|
||||
if (state.steps[j].status != RunStatus::Completed) {
|
||||
allDone = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allDone) break;
|
||||
}
|
||||
if (state.status == RunStatus::Error) break;
|
||||
}
|
||||
|
||||
state = runner.getRunState(runId);
|
||||
EXPECT_EQ(state.status, RunStatus::Completed);
|
||||
EXPECT_FALSE(runner.isRunActive(runId));
|
||||
|
||||
// Wait for background thread to finish before cleanup
|
||||
waitForRun(runner, runId, 2000);
|
||||
cleanupDirs();
|
||||
}
|
||||
|
||||
TEST(FakeRunnerTest, StepOutputContainsFakeMarker) {
|
||||
setupDirs();
|
||||
PipelineRunner runner(testPipelinesDir, testLogsDir);
|
||||
|
||||
Pipeline pl = makeFakePipeline("fake-output", "Fake Output Pipeline", 2);
|
||||
savePipeline(testPipelinesDir, pl);
|
||||
|
||||
PIString runId = runner.startRun("fake-output");
|
||||
EXPECT_FALSE(runId.isEmpty());
|
||||
|
||||
// Wait for completion (2 steps * 100ms = 200ms, allow extra time)
|
||||
for (int i = 0; i < 100; ++i) {
|
||||
piMSleep(50);
|
||||
RunState state = runner.getRunState(runId);
|
||||
if (state.status == RunStatus::Completed) {
|
||||
bool allDone = true;
|
||||
for (int j = 0; j < state.steps.size(); ++j) {
|
||||
if (state.steps[j].status != RunStatus::Completed) {
|
||||
allDone = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (allDone) break;
|
||||
}
|
||||
if (state.status == RunStatus::Error) break;
|
||||
}
|
||||
|
||||
RunState state = runner.getRunState(runId);
|
||||
EXPECT_EQ(state.status, RunStatus::Completed);
|
||||
|
||||
for (int i = 0; i < state.steps.size(); ++i) {
|
||||
EXPECT_TRUE(state.steps[i].output.contains("FAKE"));
|
||||
EXPECT_TRUE(state.steps[i].output.contains("Fake Step " + PIString::fromNumber(i)));
|
||||
EXPECT_TRUE(state.steps[i].error.isEmpty());
|
||||
}
|
||||
|
||||
// Wait for background thread to finish before cleanup
|
||||
waitForRun(runner, runId, 2000);
|
||||
cleanupDirs();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user