rewrite all to PIP
This commit is contained in:
@@ -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
|
||||
@@ -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"));
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
Reference in New Issue
Block a user