6408342a7f
Add table-driven rootCmd.Execute() tests for the node, job, cert, doctor, audit, status, version, and node-capacity subcommand families. Each test runs against a temp ORCA_HOME and asserts stdout/stderr/exit via the existing initTestEnv/resetRootFlags/discardWriter helpers (RESEARCH §1.2). extend resetRootFlags to also reset the per-command flag-bound globals so tests don't leak state between runs. daemon.go is excluded from the ≥70% target (documented in node_test.go): the daemon command starts a long-running mTLS server whose lifecycle is covered by internal/daemon/server_test.go; only its --pprof flag registration is verified here (daemon_test.go). Coverage: go test -cover ./internal/cli → 76.2% overall (78.7% by -func), which includes daemon.go's untested RunE; the non-daemon files exceed 70% comfortably. go test -race PASS. ---ci--- project: orca phase: 1 milestone: v0.8 status: execute ---/ci---
313 lines
8.0 KiB
Go
313 lines
8.0 KiB
Go
package cli
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.cloudinit.dev/coreci/orca/internal/certpaths"
|
|
"git.cloudinit.dev/coreci/orca/internal/model"
|
|
"git.cloudinit.dev/coreci/orca/internal/store"
|
|
)
|
|
|
|
func writeJobSpec(t *testing.T, content string) string {
|
|
t.Helper()
|
|
dir := t.TempDir()
|
|
p := filepath.Join(dir, "spec.hcl")
|
|
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
t.Fatalf("write spec: %v", err)
|
|
}
|
|
return p
|
|
}
|
|
|
|
const trueJobSpec = `job "true" {}
|
|
task "t" {
|
|
command = "/bin/true"
|
|
}
|
|
`
|
|
|
|
const falseJobSpec = `job "false" {}
|
|
task "t" {
|
|
command = "/bin/false"
|
|
}
|
|
`
|
|
|
|
func TestJobRunComplete(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
spec := writeJobSpec(t, trueJobSpec)
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "run", spec})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job run: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "Job complete") {
|
|
t.Errorf("job run output unexpected: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestJobRunCompleteJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
spec := writeJobSpec(t, trueJobSpec)
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "run", spec, "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job run --json: %v", err)
|
|
}
|
|
var result map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
|
|
t.Fatalf("unmarshal job run json: %v\n%s", err, buf.String())
|
|
}
|
|
if result["status"] != "complete" {
|
|
t.Errorf("job run --json status = %v, want complete", result["status"])
|
|
}
|
|
}
|
|
|
|
func TestJobRunFailed(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
spec := writeJobSpec(t, falseJobSpec)
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "run", spec})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for failing job, got nil")
|
|
}
|
|
}
|
|
|
|
func TestJobRunFailedJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
spec := writeJobSpec(t, falseJobSpec)
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "run", spec, "--json"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for failing job --json, got nil")
|
|
}
|
|
if !strings.Contains(buf.String(), "failed") {
|
|
t.Errorf("job run --json failed output unexpected: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestJobRunMissingSpecFile(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "run", "/nonexistent/spec.hcl"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for missing spec file, got nil")
|
|
}
|
|
}
|
|
|
|
func TestJobListEmpty(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "list"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job list: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "No jobs") {
|
|
t.Errorf("job list empty output unexpected: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestJobListJSONEmpty(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "list", "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job list --json: %v", err)
|
|
}
|
|
var jobs []map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &jobs); err != nil {
|
|
t.Fatalf("unmarshal job list json: %v\n%s", err, buf.String())
|
|
}
|
|
if len(jobs) != 0 {
|
|
t.Errorf("job list --json empty = %d jobs, want 0", len(jobs))
|
|
}
|
|
}
|
|
|
|
func TestJobListAfterRun(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
spec := writeJobSpec(t, trueJobSpec)
|
|
resetRootFlags(t)
|
|
rootCmd.SetArgs([]string{"job", "run", spec})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job run: %v", err)
|
|
}
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "list"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job list: %v", err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, "true") {
|
|
t.Errorf("job list missing job name: %s", out)
|
|
}
|
|
}
|
|
|
|
func TestJobStop(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
jobID := seedJob(t, "stopper", model.JobStatusRunning)
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "stop", jobID})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job stop: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "Job stopped") {
|
|
t.Errorf("job stop output unexpected: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestJobStopJSON(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
jobID := seedJob(t, "jsonstopper", model.JobStatusRunning)
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "stop", jobID, "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job stop --json: %v", err)
|
|
}
|
|
var result map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &result); err != nil {
|
|
t.Fatalf("unmarshal job stop json: %v\n%s", err, buf.String())
|
|
}
|
|
if result["status"] != "stopped" {
|
|
t.Errorf("job stop --json status = %v, want stopped", result["status"])
|
|
}
|
|
}
|
|
|
|
func TestJobStopNotFound(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "stop", "nonexistent-id"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for job stop not found, got nil")
|
|
}
|
|
}
|
|
|
|
func TestJobStopMissingID(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "stop"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for job stop without id, got nil")
|
|
}
|
|
}
|
|
|
|
func TestJobLogsEmpty(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
jobID := seedJob(t, "logger", model.JobStatusComplete)
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "logs", jobID})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job logs: %v", err)
|
|
}
|
|
if !strings.Contains(buf.String(), "No tasks") {
|
|
t.Errorf("job logs empty output unexpected: %s", buf.String())
|
|
}
|
|
}
|
|
|
|
func TestJobLogsJSONEmpty(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
jobID := seedJob(t, "jsonlogger", model.JobStatusComplete)
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "logs", jobID, "--json"})
|
|
if err := rootCmd.Execute(); err != nil {
|
|
t.Fatalf("job logs --json: %v", err)
|
|
}
|
|
var tasks []map[string]any
|
|
if err := json.Unmarshal(bytes.TrimSpace(buf.Bytes()), &tasks); err != nil {
|
|
t.Fatalf("unmarshal job logs json: %v\n%s", err, buf.String())
|
|
}
|
|
if len(tasks) != 0 {
|
|
t.Errorf("job logs --json empty = %d tasks, want 0", len(tasks))
|
|
}
|
|
}
|
|
|
|
func TestJobLogsMissingID(t *testing.T) {
|
|
_, cleanup := initTestEnv(t)
|
|
defer cleanup()
|
|
resetRootFlags(t)
|
|
var buf bytes.Buffer
|
|
rootCmd.SetOut(&buf)
|
|
rootCmd.SetErr(&buf)
|
|
rootCmd.SetArgs([]string{"job", "logs"})
|
|
if err := rootCmd.Execute(); err == nil {
|
|
t.Fatal("expected error for job logs without id, got nil")
|
|
}
|
|
}
|
|
|
|
func seedJob(t *testing.T, name string, status model.JobStatus) string {
|
|
t.Helper()
|
|
db, err := store.Open(certpaths.DBPath())
|
|
if err != nil {
|
|
t.Fatalf("open db: %v", err)
|
|
}
|
|
defer db.Close()
|
|
repo := store.NewJobRepo(db)
|
|
j := &model.Job{
|
|
ID: "job-" + name,
|
|
Name: name,
|
|
Spec: "spec.hcl",
|
|
Status: status,
|
|
}
|
|
if err := repo.Insert(t.Context(), j); err != nil {
|
|
t.Fatalf("insert job: %v", err)
|
|
}
|
|
return j.ID
|
|
}
|