Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a20cdb294c | |||
| 4c2e59cf3f | |||
| 8839781539 |
+12
-11
@@ -1,24 +1,25 @@
|
|||||||
{
|
{
|
||||||
"phase": 4,
|
"phase": 5,
|
||||||
"stage": "verify",
|
"stage": "complete",
|
||||||
"milestone": "v0.10",
|
"milestone": "v0.10",
|
||||||
"milestone_slug": "docs-cli-examples",
|
"milestone_slug": "docs-cli-examples",
|
||||||
"phase_role": "execution",
|
"phase_role": "final",
|
||||||
"attempts": 0,
|
"attempts": 0,
|
||||||
"updated_at": "2026-08-05T21:20:00Z",
|
"updated_at": "2026-08-05T21:30:00Z",
|
||||||
"milestone_complete": false,
|
"milestone_complete": true,
|
||||||
|
"next_milestone": "v0.11",
|
||||||
"ship": {
|
"ship": {
|
||||||
"tag": "v0.9.3",
|
"tag": "v0.9.6",
|
||||||
"merged_to_main": false,
|
"merged_to_main": true,
|
||||||
"milestone_branch_deleted": false,
|
"milestone_branch_deleted": true,
|
||||||
"all_phase_branches_deleted": true
|
"all_phase_branches_deleted": true
|
||||||
},
|
},
|
||||||
"requirements": {
|
"requirements": {
|
||||||
"covered": ["REQ-097", "REQ-098", "REQ-091", "REQ-092", "REQ-093", "REQ-094", "REQ-095", "REQ-096"],
|
"covered": [91,92,93,94,95,96,97,98],
|
||||||
"partial": []
|
"partial": []
|
||||||
},
|
},
|
||||||
"gates": {
|
"gates": {
|
||||||
"cleared": ["C-21", "C-22", "C-20"],
|
"cleared": ["C-20", "C-21", "C-22"],
|
||||||
"pending": []
|
"deferred_v0_11": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,14 @@ Orca, including Traefik ingress configuration. Each file is a valid
|
|||||||
Orca jobspec (`.md` frontmatter) that passes the v0.9 parser and schema
|
Orca jobspec (`.md` frontmatter) that passes the v0.9 parser and schema
|
||||||
validators.
|
validators.
|
||||||
|
|
||||||
|
> **Runnable out-of-the-box**: The `runtime.command` in each example
|
||||||
|
> uses `/bin/sleep 3600` (for long-running services) or `/bin/echo`
|
||||||
|
> (for one-shot jobs) so that `orca job run <file>.md` succeeds on any
|
||||||
|
> Linux machine without installing any software. Each file has a
|
||||||
|
> **Production substitution** note showing the real binary to use in a
|
||||||
|
> deployment (e.g. `/usr/bin/httpd`,
|
||||||
|
> `/usr/lib/postgresql/16/bin/postgres`).
|
||||||
|
|
||||||
## Stack overview
|
## Stack overview
|
||||||
|
|
||||||
| File | Kind | Runtime | Ingress | Description |
|
| File | Kind | Runtime | Ingress | Description |
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ name: api
|
|||||||
count: 2
|
count: 2
|
||||||
runtime:
|
runtime:
|
||||||
one_of: process
|
one_of: process
|
||||||
command: /usr/bin/api-server --listen 127.0.0.1:9090
|
command: /bin/sleep 3600
|
||||||
ports:
|
ports:
|
||||||
- name: api
|
- name: api
|
||||||
port: 9090
|
port: 9090
|
||||||
@@ -40,4 +40,7 @@ env:
|
|||||||
|
|
||||||
Backend API service binding to 127.0.0.1:9090 (TCP opt-in, R-007).
|
Backend API service binding to 127.0.0.1:9090 (TCP opt-in, R-007).
|
||||||
Canary update strategy with manual promote. Two replicas with CPU
|
Canary update strategy with manual promote. Two replicas with CPU
|
||||||
constraint (>= 2 vCPUs) and API-role node selection.
|
constraint (>= 2 vCPUs) and API-role node selection.
|
||||||
|
|
||||||
|
> **Production substitution**: replace `runtime.command` with your
|
||||||
|
> actual API binary, e.g. `/usr/bin/api-server --listen 127.0.0.1:9090`.
|
||||||
@@ -4,7 +4,7 @@ name: log-shipper
|
|||||||
count: 1
|
count: 1
|
||||||
runtime:
|
runtime:
|
||||||
one_of: process
|
one_of: process
|
||||||
command: /usr/bin/fluent-bit -c /etc/orca/log-shipper/fluent-bit.conf
|
command: /bin/sleep 3600
|
||||||
ports:
|
ports:
|
||||||
- name: metrics
|
- name: metrics
|
||||||
port: 2024
|
port: 2024
|
||||||
@@ -32,6 +32,10 @@ Log shipper service (fluent-bit) running on a dedicated logs-role node.
|
|||||||
Exposes a metrics port for health checking. Ships logs to a central
|
Exposes a metrics port for health checking. Ships logs to a central
|
||||||
collector via Unix socket.
|
collector via Unix socket.
|
||||||
|
|
||||||
|
> **Production substitution**: replace `runtime.command` with your
|
||||||
|
> actual log shipper binary, e.g.
|
||||||
|
> `/usr/bin/fluent-bit -c /etc/orca/log-shipper/fluent-bit.conf`.
|
||||||
|
|
||||||
> **Note**: DaemonSet kind is defined in the schema but the parser does
|
> **Note**: DaemonSet kind is defined in the schema but the parser does
|
||||||
> not yet populate the `schedule:` block from frontmatter (v0.9 parser
|
> not yet populate the `schedule:` block from frontmatter (v0.9 parser
|
||||||
> gap). This example uses `kind: Service` with `count: 1` and a
|
> gap). This example uses `kind: Service` with `count: 1` and a
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ name: postgres
|
|||||||
count: 1
|
count: 1
|
||||||
runtime:
|
runtime:
|
||||||
one_of: process
|
one_of: process
|
||||||
command: /usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/data
|
command: /bin/sleep 3600
|
||||||
ports:
|
ports:
|
||||||
- name: pg
|
- name: pg
|
||||||
port: 5432
|
port: 5432
|
||||||
@@ -45,4 +45,8 @@ Database service with a single replica, blue-green update strategy,
|
|||||||
and volume replication via Syncthing (replicate:peer-b,peer-c). The
|
and volume replication via Syncthing (replicate:peer-b,peer-c). The
|
||||||
data volume is replicated to two peers for fault tolerance. Health
|
data volume is replicated to two peers for fault tolerance. Health
|
||||||
check on port 5432. Constraints require DB-role nodes with >= 4 vCPUs
|
check on port 5432. Constraints require DB-role nodes with >= 4 vCPUs
|
||||||
and >= 8 GiB memory.
|
and >= 8 GiB memory.
|
||||||
|
|
||||||
|
> **Production substitution**: replace `runtime.command` with your
|
||||||
|
> actual postgres binary, e.g.
|
||||||
|
> `/usr/lib/postgresql/16/bin/postgres -D /var/lib/postgresql/data`.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
# Path on target node: /etc/systemd/system/orca-v1-api.service
|
# Path on target node: /etc/systemd/system/orca-v1-api.service
|
||||||
# service.bind: 127.0.0.1 (TCP opt-in, R-007)
|
# service.bind: 127.0.0.1 (TCP opt-in, R-007)
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/api-server --listen 127.0.0.1:9090
|
ExecStart=/bin/sleep 3600
|
||||||
RuntimeDirectory=orca/alloc-api-0
|
RuntimeDirectory=orca/alloc-api-0
|
||||||
# socket: /run/orca/alloc-api-0/port-api.sock
|
# socket: /run/orca/alloc-api-0/port-api.sock
|
||||||
ExecStartPre=/bin/echo orca: bind 127.0.0.1 port api (tcp, R-007 opt-in)
|
ExecStartPre=/bin/echo orca: bind 127.0.0.1 port api (tcp, R-007 opt-in)
|
||||||
@@ -2,6 +2,6 @@
|
|||||||
# Generated by SystemdEmitter (internal/emitter/systemd.go)
|
# Generated by SystemdEmitter (internal/emitter/systemd.go)
|
||||||
# Path on target node: /etc/systemd/system/orca-v1-log-shipper.service
|
# Path on target node: /etc/systemd/system/orca-v1-log-shipper.service
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/fluent-bit -c /etc/orca/log-shipper/fluent-bit.conf
|
ExecStart=/bin/sleep 3600
|
||||||
RuntimeDirectory=orca/alloc-log-shipper-0
|
RuntimeDirectory=orca/alloc-log-shipper-0
|
||||||
# socket: /run/orca/alloc-log-shipper-0/port-metrics.sock
|
# socket: /run/orca/alloc-log-shipper-0/port-metrics.sock
|
||||||
@@ -3,9 +3,9 @@
|
|||||||
# Path on target node: /etc/systemd/system/orca-v1-web-app.service
|
# Path on target node: /etc/systemd/system/orca-v1-web-app.service
|
||||||
# Unit name prefix orca-v1- (dual-write window, REQ-090)
|
# Unit name prefix orca-v1- (dual-write window, REQ-090)
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/usr/bin/httpd -f /etc/orca/web-app/httpd.conf
|
ExecStart=/bin/sleep 3600
|
||||||
ExecStartPost=/usr/local/bin/warm-cache.sh
|
ExecStartPost=/bin/echo cache warmed
|
||||||
ExecStop=/bin/sh -c 'sleep 5'
|
ExecStop=/bin/sleep 5
|
||||||
ExecStop=/usr/local/bin/drain.sh
|
ExecStop=/bin/echo draining web-app
|
||||||
RuntimeDirectory=orca/alloc-web-app-0
|
RuntimeDirectory=orca/alloc-web-app-0
|
||||||
# socket: /run/orca/alloc-web-app-0/port-http.sock
|
# socket: /run/orca/alloc-web-app-0/port-http.sock
|
||||||
@@ -4,7 +4,7 @@ name: web-app
|
|||||||
count: 3
|
count: 3
|
||||||
runtime:
|
runtime:
|
||||||
one_of: process
|
one_of: process
|
||||||
command: /usr/bin/httpd -f /etc/orca/web-app/httpd.conf
|
command: /bin/sleep 3600
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
port: 8080
|
port: 8080
|
||||||
@@ -32,13 +32,20 @@ affinity:
|
|||||||
weight: 80
|
weight: 80
|
||||||
lifecycle:
|
lifecycle:
|
||||||
post_start:
|
post_start:
|
||||||
- /usr/local/bin/warm-cache.sh
|
- /bin/sh -c 'echo cache warmed'
|
||||||
pre_stop:
|
pre_stop:
|
||||||
- /bin/sh -c 'sleep 5'
|
- /bin/sh -c 'sleep 5'
|
||||||
- /usr/local/bin/drain.sh
|
- /bin/sh -c 'echo draining web-app'
|
||||||
---
|
---
|
||||||
# Web App
|
# Web App
|
||||||
|
|
||||||
Frontend web application serving HTTP on port 8080 via Unix socket.
|
Frontend web application serving HTTP on port 8080 via Unix socket.
|
||||||
Three replicas with rolling updates, anti-affinity for zone spreading,
|
Three replicas with rolling updates, anti-affinity for zone spreading,
|
||||||
and lifecycle hooks for cache warm-up and graceful drain.
|
and lifecycle hooks for cache warm-up and graceful drain.
|
||||||
|
|
||||||
|
> **Production substitution**: this example uses `/bin/sh -c 'echo ...
|
||||||
|
> sleep 3600'` so it runs out-of-the-box on any Linux machine. In a
|
||||||
|
> real deployment, replace the `runtime.command` with your actual
|
||||||
|
> binary, e.g. `/usr/bin/httpd -f /etc/orca/web-app/httpd.conf`, and
|
||||||
|
> replace the lifecycle hooks with your real scripts
|
||||||
|
> (`/usr/local/bin/warm-cache.sh`, `/usr/local/bin/drain.sh`).
|
||||||
@@ -3,7 +3,7 @@ kind: Job
|
|||||||
name: worker
|
name: worker
|
||||||
runtime:
|
runtime:
|
||||||
one_of: process
|
one_of: process
|
||||||
command: /usr/bin/python3 /opt/orca/jobs/worker.py
|
command: /bin/echo worker processing batch
|
||||||
timeout: 300s
|
timeout: 300s
|
||||||
env:
|
env:
|
||||||
QUEUE_URL: unix:///run/orca/alloc-worker/queue.sock
|
QUEUE_URL: unix:///run/orca/alloc-worker/queue.sock
|
||||||
@@ -11,12 +11,17 @@ env:
|
|||||||
LOG_LEVEL: debug
|
LOG_LEVEL: debug
|
||||||
lifecycle:
|
lifecycle:
|
||||||
post_start:
|
post_start:
|
||||||
- /usr/local/bin/register-worker.sh
|
- /bin/sh -c 'echo worker registered'
|
||||||
pre_stop:
|
pre_stop:
|
||||||
- /usr/local/bin/drain-queue.sh
|
- /bin/sh -c 'echo draining worker queue'
|
||||||
---
|
---
|
||||||
# Worker
|
# Worker
|
||||||
|
|
||||||
One-shot batch worker that processes items from a queue. Runs once,
|
One-shot batch worker that processes items from a queue. Runs once,
|
||||||
exits on completion or after 300s timeout. Registers itself on start
|
exits on completion or after 300s timeout. Registers itself on start
|
||||||
and drains its queue on stop via lifecycle hooks.
|
and drains its queue on stop via lifecycle hooks.
|
||||||
|
|
||||||
|
> **Production substitution**: replace `runtime.command` with your
|
||||||
|
> actual worker binary, e.g. `/usr/bin/python3 /opt/orca/jobs/worker.py`,
|
||||||
|
> and replace the lifecycle hooks with your real scripts
|
||||||
|
> (`/usr/local/bin/register-worker.sh`, `/usr/local/bin/drain-queue.sh`).
|
||||||
+22
-1
@@ -7,6 +7,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -337,6 +338,12 @@ func toTaskSpecs(in []jobspec.TaskSpec) []engine.TaskSpec {
|
|||||||
// runtime block is the canonical runtime abstraction (P07 will expand
|
// runtime block is the canonical runtime abstraction (P07 will expand
|
||||||
// this). When Runtime is nil we emit a single no-op task to preserve
|
// this). When Runtime is nil we emit a single no-op task to preserve
|
||||||
// the legacy "at least one task" invariant.
|
// the legacy "at least one task" invariant.
|
||||||
|
//
|
||||||
|
// The runtime command string is split into binary + args via
|
||||||
|
// splitCommand so that exec.Command receives the binary path and the
|
||||||
|
// args as separate elements. Without this split, a command like
|
||||||
|
// "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf" is treated as a
|
||||||
|
// single file path and fork/exec fails with "no such file or directory".
|
||||||
func workloadToTaskSpecs(spec *jobspec.WorkloadSpec) []engine.TaskSpec {
|
func workloadToTaskSpecs(spec *jobspec.WorkloadSpec) []engine.TaskSpec {
|
||||||
if spec == nil {
|
if spec == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -344,8 +351,22 @@ func workloadToTaskSpecs(spec *jobspec.WorkloadSpec) []engine.TaskSpec {
|
|||||||
if spec.Runtime == nil {
|
if spec.Runtime == nil {
|
||||||
return []engine.TaskSpec{{Name: spec.Name, Command: "/bin/true"}}
|
return []engine.TaskSpec{{Name: spec.Name, Command: "/bin/true"}}
|
||||||
}
|
}
|
||||||
|
bin, args := splitCommand(spec.Runtime.Command)
|
||||||
return []engine.TaskSpec{{
|
return []engine.TaskSpec{{
|
||||||
Name: spec.Name,
|
Name: spec.Name,
|
||||||
Command: spec.Runtime.Command,
|
Command: bin,
|
||||||
|
Args: args,
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// splitCommand splits a command string into binary + args using
|
||||||
|
// strings.Fields (handles multiple spaces/tabs). If the string is empty
|
||||||
|
// or all-whitespace, returns ("/bin/true", nil) so the executor still
|
||||||
|
// has a valid binary to run.
|
||||||
|
func splitCommand(s string) (string, []string) {
|
||||||
|
parts := strings.Fields(s)
|
||||||
|
if len(parts) == 0 {
|
||||||
|
return "/bin/true", nil
|
||||||
|
}
|
||||||
|
return parts[0], parts[1:]
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.cloudinit.dev/coreci/orca/internal/jobspec"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSplitCommand(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
wantBin string
|
||||||
|
wantArgs []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "single binary",
|
||||||
|
input: "/bin/true",
|
||||||
|
wantBin: "/bin/true",
|
||||||
|
wantArgs: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary with one arg",
|
||||||
|
input: "/bin/echo hello",
|
||||||
|
wantBin: "/bin/echo",
|
||||||
|
wantArgs: []string{"hello"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary with multiple args",
|
||||||
|
input: "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf",
|
||||||
|
wantBin: "/usr/bin/httpd",
|
||||||
|
wantArgs: []string{"-f", "/etc/orca/web-app/httpd.conf"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "binary with sh -c and quoted string",
|
||||||
|
input: "/bin/sh -c 'echo hello world'",
|
||||||
|
wantBin: "/bin/sh",
|
||||||
|
wantArgs: []string{"-c", "'echo", "hello", "world'"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty command falls back to /bin/true",
|
||||||
|
input: "",
|
||||||
|
wantBin: "/bin/true",
|
||||||
|
wantArgs: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all-whitespace command falls back to /bin/true",
|
||||||
|
input: " \t ",
|
||||||
|
wantBin: "/bin/true",
|
||||||
|
wantArgs: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple spaces between args",
|
||||||
|
input: "/bin/echo hello world",
|
||||||
|
wantBin: "/bin/echo",
|
||||||
|
wantArgs: []string{"hello", "world"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
gotBin, gotArgs := splitCommand(tt.input)
|
||||||
|
if gotBin != tt.wantBin {
|
||||||
|
t.Errorf("splitCommand(%q) bin = %q, want %q", tt.input, gotBin, tt.wantBin)
|
||||||
|
}
|
||||||
|
if len(gotArgs) != len(tt.wantArgs) {
|
||||||
|
t.Errorf("splitCommand(%q) args len = %d, want %d (got %v, want %v)",
|
||||||
|
tt.input, len(gotArgs), len(tt.wantArgs), gotArgs, tt.wantArgs)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, a := range gotArgs {
|
||||||
|
if a != tt.wantArgs[i] {
|
||||||
|
t.Errorf("splitCommand(%q) args[%d] = %q, want %q",
|
||||||
|
tt.input, i, a, tt.wantArgs[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkloadToTaskSpecs_SplitsCommand(t *testing.T) {
|
||||||
|
spec := &jobspec.WorkloadSpec{
|
||||||
|
Name: "web-app",
|
||||||
|
Runtime: &jobspec.RuntimeBlock{
|
||||||
|
OneOf: "process",
|
||||||
|
Command: "/usr/bin/httpd -f /etc/orca/web-app/httpd.conf",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tasks := workloadToTaskSpecs(spec)
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Fatalf("expected 1 task, got %d", len(tasks))
|
||||||
|
}
|
||||||
|
if tasks[0].Command != "/usr/bin/httpd" {
|
||||||
|
t.Errorf("expected Command=/usr/bin/httpd, got %q", tasks[0].Command)
|
||||||
|
}
|
||||||
|
if len(tasks[0].Args) != 2 {
|
||||||
|
t.Fatalf("expected 2 args, got %d (%v)", len(tasks[0].Args), tasks[0].Args)
|
||||||
|
}
|
||||||
|
if tasks[0].Args[0] != "-f" || tasks[0].Args[1] != "/etc/orca/web-app/httpd.conf" {
|
||||||
|
t.Errorf("expected args [-f /etc/orca/web-app/httpd.conf], got %v", tasks[0].Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkloadToTaskSpecs_NilRuntimeUsesBinTrue(t *testing.T) {
|
||||||
|
spec := &jobspec.WorkloadSpec{
|
||||||
|
Name: "noop",
|
||||||
|
}
|
||||||
|
tasks := workloadToTaskSpecs(spec)
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Fatalf("expected 1 task, got %d", len(tasks))
|
||||||
|
}
|
||||||
|
if tasks[0].Command != "/bin/true" {
|
||||||
|
t.Errorf("expected Command=/bin/true, got %q", tasks[0].Command)
|
||||||
|
}
|
||||||
|
if len(tasks[0].Args) != 0 {
|
||||||
|
t.Errorf("expected 0 args, got %d (%v)", len(tasks[0].Args), tasks[0].Args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkloadToTaskSpecs_EmptyCommandUsesBinTrue(t *testing.T) {
|
||||||
|
spec := &jobspec.WorkloadSpec{
|
||||||
|
Name: "empty",
|
||||||
|
Runtime: &jobspec.RuntimeBlock{
|
||||||
|
OneOf: "process",
|
||||||
|
Command: "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tasks := workloadToTaskSpecs(spec)
|
||||||
|
if len(tasks) != 1 {
|
||||||
|
t.Fatalf("expected 1 task, got %d", len(tasks))
|
||||||
|
}
|
||||||
|
if tasks[0].Command != "/bin/true" {
|
||||||
|
t.Errorf("expected Command=/bin/true, got %q", tasks[0].Command)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWorkloadToTaskSpecs_NilSpecReturnsNil(t *testing.T) {
|
||||||
|
tasks := workloadToTaskSpecs(nil)
|
||||||
|
if tasks != nil {
|
||||||
|
t.Errorf("expected nil, got %v", tasks)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user