rewrite all to PIP

This commit is contained in:
2026-07-16 08:40:05 +03:00
parent 5a29468b7e
commit 46e12ef5fe
23 changed files with 1431 additions and 990 deletions
-189
View File
@@ -1,189 +0,0 @@
import json
import shutil
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from app import app, PIPELINES_DIR, LOGS_DIR
BASE_DIR = Path(__file__).parent.parent
@pytest.fixture(autouse=True)
def clean_storage():
"""Clean storage before and after each test."""
if PIPELINES_DIR.exists():
shutil.rmtree(PIPELINES_DIR)
if LOGS_DIR.exists():
shutil.rmtree(LOGS_DIR)
PIPELINES_DIR.mkdir(parents=True, exist_ok=True)
LOGS_DIR.mkdir(parents=True, exist_ok=True)
yield
if PIPELINES_DIR.exists():
shutil.rmtree(PIPELINES_DIR)
if LOGS_DIR.exists():
shutil.rmtree(LOGS_DIR)
def make_pipeline(name="test", prompt_count=1):
return {
"id": "test-pipeline-id",
"name": name,
"prompts": [
{
"id": f"prompt-{i}",
"text": f"test prompt {i}",
"title": f"Test Prompt {i}",
"order": i
}
for i in range(prompt_count)
],
"created_at": "2024-01-01T00:00:00+00:00",
"updated_at": "2024-01-01T00:00:00+00:00"
}
@pytest.fixture
def client():
return TestClient(app)
class TestListPipelines:
def test_empty(self, client):
resp = client.get("/api/pipelines")
assert resp.status_code == 200
assert resp.json() == []
def test_with_pipelines(self, client):
p = make_pipeline()
client.post("/api/pipelines", json=p)
resp = client.get("/api/pipelines")
assert resp.status_code == 200
data = resp.json()
assert len(data) == 1
assert data[0]["name"] == "test"
class TestCreatePipeline:
def test_create(self, client):
p = make_pipeline()
resp = client.post("/api/pipelines", json=p)
assert resp.status_code == 200
data = resp.json()
assert data["name"] == "test"
assert data["id"] == "test-pipeline-id"
def test_duplicate_id(self, client):
p = make_pipeline()
client.post("/api/pipelines", json=p)
resp = client.post("/api/pipelines", json=p)
assert resp.status_code == 409
class TestDeletePipeline:
def test_delete(self, client):
p = make_pipeline()
client.post("/api/pipelines", json=p)
resp = client.delete("/api/pipelines/test-pipeline-id")
assert resp.status_code == 200
assert resp.json()["deleted"] == "test-pipeline-id"
def test_delete_not_found(self, client):
resp = client.delete("/api/pipelines/nonexistent")
assert resp.status_code == 404
class TestExecutePipeline:
def test_not_found(self, client):
resp = client.post("/api/pipelines/nonexistent/execute")
assert resp.status_code == 404
def test_execute(self, client, monkeypatch):
"""Test pipeline execution by patching opencode with echo."""
import asyncio
class FakeStream:
async def readline(self):
return b""
class FakeProcess:
returncode = 0
async def wait(self):
return 0
async def fake_create_subprocess_exec(*args, **kwargs):
proc = FakeProcess()
proc.stdout = FakeStream()
proc.stderr = FakeStream()
return proc
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
p = make_pipeline()
client.post("/api/pipelines", json=p)
resp = client.post("/api/pipelines/test-pipeline-id/execute")
assert resp.status_code == 200
data = resp.json()
assert "run_id" in data
assert "log_file" in data
def test_log_file_created(self, client, monkeypatch):
"""Test that log file is created after execution."""
import asyncio
class FakeStream:
async def readline(self):
return b""
class FakeProcess:
returncode = 0
async def wait(self):
return 0
async def fake_create_subprocess_exec(*args, **kwargs):
proc = FakeProcess()
proc.stdout = FakeStream()
proc.stderr = FakeStream()
return proc
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
p = make_pipeline()
client.post("/api/pipelines", json=p)
resp = client.post("/api/pipelines/test-pipeline-id/execute")
data = resp.json()
log_path = Path(data["log_file"])
assert log_path.exists()
content = log_path.read_text()
assert "Starting pipeline" in content
assert "Pipeline finished" in content
def test_error_step_stops_pipeline(self, client, monkeypatch):
"""Test that pipeline stops on first error."""
import asyncio
class FakeStream:
async def readline(self):
return b""
class FakeProcess:
returncode = 1
async def wait(self):
return 1
async def fake_create_subprocess_exec(*args, **kwargs):
proc = FakeProcess()
proc.stdout = FakeStream()
proc.stderr = FakeStream()
return proc
monkeypatch.setattr(asyncio, "create_subprocess_exec", fake_create_subprocess_exec)
p = make_pipeline(prompt_count=3)
client.post("/api/pipelines", json=p)
resp = client.post("/api/pipelines/test-pipeline-id/execute")
data = resp.json()
log_path = Path(data["log_file"])
content = log_path.read_text()
assert "Pipeline failed at step 1" in content
assert "Step 2" not in content
+166
View File
@@ -0,0 +1,166 @@
#include "pipeline.h"
#include <gtest/gtest.h>
#include <pidir.h>
#include <pifile.h>
static const PIString testDir = "/tmp/pipeline-runner-test/";
static const PIString testPipelinesDir = testDir + "pipelines/";
static const PIString testLogsDir = testDir + "logs/";
static void setupDirs() {
PIDir::make(testDir);
PIDir::make(testPipelinesDir);
PIDir::make(testLogsDir);
}
static void cleanupDirs() {
// Remove all files first using their full path
PIDir pdir(testPipelinesDir);
for (const auto & e: pdir.entries()) {
if (e.isFile()) PIFile::remove(e.path);
}
PIDir ldir(testLogsDir);
for (const auto & e: ldir.entries()) {
if (e.isFile()) PIFile::remove(e.path);
}
PIDir::remove(testPipelinesDir);
PIDir::remove(testLogsDir);
PIDir::remove(testDir);
}
static Pipeline makeTestPipeline(const PIString & id = "test-id") {
Pipeline pl;
pl.id = id;
pl.name = "Test Pipeline";
pl.working_dir = "/tmp/workdir";
pl.created_at = nowISO();
pl.updated_at = pl.created_at;
Prompt p1;
p1.id = "prompt-1";
p1.text = "Do something";
p1.title = "Step 1";
p1.order = 0;
pl.prompts << p1;
Prompt p2;
p2.id = "prompt-2";
p2.text = "Do something else";
p2.title = "Step 2";
p2.order = 1;
pl.prompts << p2;
return pl;
}
TEST(PipelineTest, SaveAndLoad) {
setupDirs();
Pipeline pl = makeTestPipeline();
EXPECT_TRUE(savePipeline(testPipelinesDir, pl));
PIVector<Pipeline> loaded = loadPipelines(testPipelinesDir);
EXPECT_EQ(loaded.size(), 1);
EXPECT_EQ(loaded[0].id, pl.id);
EXPECT_EQ(loaded[0].name, pl.name);
EXPECT_EQ(loaded[0].working_dir, pl.working_dir);
EXPECT_EQ(loaded[0].prompts.size(), 2);
cleanupDirs();
}
TEST(PipelineTest, FindPipeline) {
setupDirs();
Pipeline pl = makeTestPipeline("find-me");
savePipeline(testPipelinesDir, pl);
Pipeline found = findPipeline(testPipelinesDir, "find-me");
EXPECT_FALSE(found.id.isEmpty());
EXPECT_EQ(found.id, "find-me");
EXPECT_EQ(found.name, "Test Pipeline");
Pipeline notFound = findPipeline(testPipelinesDir, "nonexistent");
EXPECT_TRUE(notFound.id.isEmpty());
cleanupDirs();
}
TEST(PipelineTest, DeletePipeline) {
setupDirs();
Pipeline pl = makeTestPipeline("del-me");
savePipeline(testPipelinesDir, pl);
EXPECT_TRUE(::deletePipeline(testPipelinesDir, "del-me"));
EXPECT_TRUE(findPipeline(testPipelinesDir, "del-me").id.isEmpty());
EXPECT_FALSE(::deletePipeline(testPipelinesDir, "nonexistent"));
cleanupDirs();
}
TEST(PipelineTest, MultiplePipelines) {
setupDirs();
Pipeline pl1 = makeTestPipeline("pl-1");
pl1.name = "Pipeline 1";
savePipeline(testPipelinesDir, pl1);
Pipeline pl2 = makeTestPipeline("pl-2");
pl2.name = "Pipeline 2";
savePipeline(testPipelinesDir, pl2);
PIVector<Pipeline> loaded = loadPipelines(testPipelinesDir);
EXPECT_EQ(loaded.size(), 2);
cleanupDirs();
}
TEST(PipelineTest, EmptyDir) {
setupDirs();
PIVector<Pipeline> loaded = loadPipelines(testPipelinesDir);
EXPECT_TRUE(loaded.isEmpty());
cleanupDirs();
}
TEST(PipelineTest, LogAppendAndRead) {
setupDirs();
appendLog(testLogsDir, "run-1", "First log line");
appendLog(testLogsDir, "run-1", "Second log line");
appendLog(testLogsDir, "run-2", "Different run");
PIString log1 = readLog(testLogsDir, "run-1");
EXPECT_TRUE(log1.contains("First log line"));
EXPECT_TRUE(log1.contains("Second log line"));
EXPECT_FALSE(log1.contains("Different run"));
PIString log2 = readLog(testLogsDir, "run-2");
EXPECT_TRUE(log2.contains("Different run"));
EXPECT_FALSE(log2.contains("First log line"));
PIString emptyLog = readLog(testLogsDir, "nonexistent");
EXPECT_TRUE(emptyLog.isEmpty());
cleanupDirs();
}
TEST(UtilTest, GenerateUUID) {
PIString uuid1 = generateUUID();
PIString uuid2 = generateUUID();
EXPECT_FALSE(uuid1.isEmpty());
EXPECT_NE(uuid1, uuid2);
// UUID format: 8-4-4-4-12
EXPECT_EQ(uuid1.find("-"), 8);
}
TEST(UtilTest, NowISO) {
PIString ts = nowISO();
EXPECT_FALSE(ts.isEmpty());
// ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ
EXPECT_EQ(ts.find("-"), 4);
EXPECT_TRUE(ts.contains("T"));
EXPECT_TRUE(ts.endsWith("Z"));
}
+137
View File
@@ -0,0 +1,137 @@
#include "pipeline.h"
#include "runner.h"
#include <gtest/gtest.h>
#include <pidir.h>
#include <pifile.h>
#include <piprocess.h>
#include <pisystemtime.h>
static const PIString testDir = "/tmp/pipeline-runner-test/";
static const PIString testPipelinesDir = testDir + "pipelines/";
static const PIString testLogsDir = testDir + "logs/";
static void setupDirs() {
PIDir::make(testDir);
PIDir::make(testPipelinesDir);
PIDir::make(testLogsDir);
}
static void cleanupDirs() {
PIDir pdir(testPipelinesDir);
for (const auto & e: pdir.entries()) {
if (e.isFile()) PIFile::remove(e.path);
}
PIDir ldir(testLogsDir);
for (const auto & e: ldir.entries()) {
if (e.isFile()) PIFile::remove(e.path);
}
PIDir::remove(testPipelinesDir);
PIDir::remove(testLogsDir);
PIDir::remove(testDir);
}
static Pipeline makeEchoPipeline() {
Pipeline pl;
pl.id = "echo-test";
pl.name = "Echo Test";
pl.working_dir = "/tmp";
pl.created_at = nowISO();
pl.updated_at = pl.created_at;
Prompt p;
p.id = "prompt-1";
p.text = "echo hello";
p.title = "Echo Step";
p.order = 0;
pl.prompts << p;
return pl;
}
TEST(RunnerTest, StartRunNotFound) {
setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir);
PIString runId = runner.startRun("nonexistent");
EXPECT_TRUE(runId.isEmpty());
cleanupDirs();
}
TEST(RunnerTest, StartRunAndGetState) {
setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir);
// Create a pipeline that runs 'echo hello' (uses shell command, not opencode)
Pipeline pl;
pl.id = "state-test";
pl.name = "State Test";
pl.working_dir = "/tmp";
pl.created_at = nowISO();
pl.updated_at = pl.created_at;
Prompt p;
p.id = "p1";
p.text = "test";
p.title = "Step 1";
p.order = 0;
pl.prompts << p;
savePipeline(testPipelinesDir, pl);
PIString runId = runner.startRun("state-test");
EXPECT_FALSE(runId.isEmpty());
RunState state = runner.getRunState(runId);
EXPECT_EQ(state.run_id, runId);
EXPECT_EQ(state.pipeline_id, "state-test");
EXPECT_EQ(state.status, RunStatus::Running);
EXPECT_EQ(state.steps.size(), 1);
EXPECT_EQ(state.steps[0].title, "Step 1");
cleanupDirs();
}
TEST(RunnerTest, GetStateNotFound) {
setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir);
RunState state = runner.getRunState("nonexistent");
EXPECT_TRUE(state.run_id.isEmpty());
cleanupDirs();
}
TEST(RunnerTest, IsRunActive) {
setupDirs();
PipelineRunner runner(testPipelinesDir, testLogsDir);
Pipeline pl;
pl.id = "active-test";
pl.name = "Active Test";
pl.working_dir = "/tmp";
pl.created_at = nowISO();
pl.updated_at = pl.created_at;
Prompt p;
p.id = "p1";
p.text = "test";
p.title = "Step 1";
p.order = 0;
pl.prompts << p;
savePipeline(testPipelinesDir, pl);
PIString runId = runner.startRun("active-test");
EXPECT_TRUE(runner.isRunActive(runId));
cleanupDirs();
}
TEST(RunnerTest, RunStatusToString) {
EXPECT_EQ(runStatusToString(RunStatus::Pending), "pending");
EXPECT_EQ(runStatusToString(RunStatus::Running), "running");
EXPECT_EQ(runStatusToString(RunStatus::Completed), "completed");
EXPECT_EQ(runStatusToString(RunStatus::Error), "error");
}