Skip to content

Commit f459cf1

Browse files
[CAP-9243] add workflows unit tests (#317)
Add better unit test coverage to workflows logic, targeting the loader and service layers (not cmd). GitOrigin-RevId: 38d71a277745e07756a911baf41727d51f357184
1 parent 7dc7c55 commit f459cf1

7 files changed

Lines changed: 1110 additions & 20 deletions

File tree

pkg/taskrun/taskrun_test.go

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package taskrun
2+
3+
import (
4+
"strings"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
11+
wfclient "github.com/render-oss/cli/pkg/client/workflows"
12+
"github.com/render-oss/cli/pkg/pointers"
13+
)
14+
15+
func TestTaskRunDetailsFormat(t *testing.T) {
16+
t.Run("nil input returns nil", func(t *testing.T) {
17+
result := TaskRunDetailsFormat(nil)
18+
assert.Nil(t, result)
19+
})
20+
21+
t.Run("all fields populated", func(t *testing.T) {
22+
startedAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
23+
completedAt := time.Date(2025, 1, 15, 10, 35, 0, 0, time.UTC)
24+
25+
var input wfclient.TaskData
26+
require.NoError(t, input.FromTaskData1(map[string]any{"key": "value"}))
27+
28+
taskRun := &wfclient.TaskRunDetails{
29+
Id: "tr-1",
30+
Status: wfclient.Completed,
31+
StartedAt: &startedAt,
32+
CompletedAt: &completedAt,
33+
Input: input,
34+
Error: pointers.From("something went wrong"),
35+
Results: wfclient.TaskRunResult{42},
36+
}
37+
38+
kvs := TaskRunDetailsFormat(taskRun)
39+
require.NotNil(t, kvs)
40+
41+
kvMap := make(map[string]string, len(kvs))
42+
for _, kv := range kvs {
43+
kvMap[kv.Key] = kv.Value
44+
}
45+
46+
assert.Equal(t, "tr-1", kvMap["ID"])
47+
assert.Contains(t, kvMap["Status"], string(wfclient.Completed))
48+
assert.Equal(t, startedAt.Format(time.RFC3339), kvMap["Started At"])
49+
assert.Equal(t, completedAt.Format(time.RFC3339), kvMap["Completed At"])
50+
assert.Contains(t, kvMap["Input"], "key")
51+
assert.Equal(t, "something went wrong", kvMap["Error"])
52+
assert.Contains(t, kvMap["Results"], "42")
53+
})
54+
55+
t.Run("nil StartedAt and CompletedAt produce empty string values", func(t *testing.T) {
56+
var input wfclient.TaskData
57+
require.NoError(t, input.FromTaskData0([]any{}))
58+
59+
taskRun := &wfclient.TaskRunDetails{
60+
Id: "tr-2",
61+
Status: wfclient.Pending,
62+
StartedAt: nil,
63+
CompletedAt: nil,
64+
Input: input,
65+
}
66+
67+
kvs := TaskRunDetailsFormat(taskRun)
68+
kvMap := make(map[string]string, len(kvs))
69+
for _, kv := range kvs {
70+
kvMap[kv.Key] = kv.Value
71+
}
72+
73+
assert.Equal(t, "", kvMap["Started At"])
74+
assert.Equal(t, "", kvMap["Completed At"])
75+
})
76+
77+
t.Run("error field appended when non-nil", func(t *testing.T) {
78+
var input wfclient.TaskData
79+
require.NoError(t, input.FromTaskData0([]any{}))
80+
81+
taskRun := &wfclient.TaskRunDetails{
82+
Id: "tr-3",
83+
Status: wfclient.Failed,
84+
Input: input,
85+
Error: pointers.From("task timed out"),
86+
}
87+
88+
kvs := TaskRunDetailsFormat(taskRun)
89+
keys := make([]string, 0, len(kvs))
90+
for _, kv := range kvs {
91+
keys = append(keys, kv.Key)
92+
}
93+
94+
assert.Contains(t, keys, "Error")
95+
})
96+
97+
t.Run("error field omitted when nil", func(t *testing.T) {
98+
var input wfclient.TaskData
99+
require.NoError(t, input.FromTaskData0([]any{}))
100+
101+
taskRun := &wfclient.TaskRunDetails{
102+
Id: "tr-4",
103+
Status: wfclient.Running,
104+
Input: input,
105+
Error: nil,
106+
}
107+
108+
kvs := TaskRunDetailsFormat(taskRun)
109+
keys := make([]string, 0, len(kvs))
110+
for _, kv := range kvs {
111+
keys = append(keys, kv.Key)
112+
}
113+
114+
assert.NotContains(t, keys, "Error")
115+
})
116+
117+
t.Run("results field appended when non-nil", func(t *testing.T) {
118+
var input wfclient.TaskData
119+
require.NoError(t, input.FromTaskData0([]any{}))
120+
121+
taskRun := &wfclient.TaskRunDetails{
122+
Id: "tr-5",
123+
Status: wfclient.Completed,
124+
Input: input,
125+
Results: wfclient.TaskRunResult{"result-data"},
126+
}
127+
128+
kvs := TaskRunDetailsFormat(taskRun)
129+
keys := make([]string, 0, len(kvs))
130+
for _, kv := range kvs {
131+
keys = append(keys, kv.Key)
132+
}
133+
134+
assert.Contains(t, keys, "Results")
135+
})
136+
137+
t.Run("results field omitted when nil", func(t *testing.T) {
138+
var input wfclient.TaskData
139+
require.NoError(t, input.FromTaskData0([]any{}))
140+
141+
taskRun := &wfclient.TaskRunDetails{
142+
Id: "tr-6",
143+
Status: wfclient.Running,
144+
Input: input,
145+
Results: nil,
146+
}
147+
148+
kvs := TaskRunDetailsFormat(taskRun)
149+
keys := make([]string, 0, len(kvs))
150+
for _, kv := range kvs {
151+
keys = append(keys, kv.Key)
152+
}
153+
154+
assert.NotContains(t, keys, "Results")
155+
})
156+
}
157+
158+
func TestRow(t *testing.T) {
159+
t.Run("pending run with no dates", func(t *testing.T) {
160+
taskRun := &wfclient.TaskRun{
161+
Id: "tr-1",
162+
Status: wfclient.Pending,
163+
StartedAt: nil,
164+
CompletedAt: nil,
165+
}
166+
167+
row := Row(taskRun)
168+
require.Len(t, row, 5)
169+
assert.Equal(t, "tr-1", row[0])
170+
assert.Equal(t, "pending", row[1])
171+
assert.Equal(t, "", row[2]) // started
172+
assert.Equal(t, "", row[3]) // completed
173+
assert.Equal(t, "", row[4]) // duration
174+
})
175+
176+
t.Run("running with only StartedAt", func(t *testing.T) {
177+
startedAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
178+
179+
taskRun := &wfclient.TaskRun{
180+
Id: "tr-2",
181+
Status: wfclient.Running,
182+
StartedAt: &startedAt,
183+
CompletedAt: nil,
184+
}
185+
186+
row := Row(taskRun)
187+
require.Len(t, row, 5)
188+
assert.Equal(t, "tr-2", row[0])
189+
assert.Equal(t, "running", row[1])
190+
assert.Equal(t, startedAt.Format(time.RFC3339), row[2])
191+
assert.Equal(t, "", row[3]) // completed
192+
assert.Equal(t, "", row[4]) // duration
193+
})
194+
195+
t.Run("completed with both dates", func(t *testing.T) {
196+
startedAt := time.Date(2025, 1, 15, 10, 30, 0, 0, time.UTC)
197+
completedAt := time.Date(2025, 1, 15, 10, 35, 30, 0, time.UTC)
198+
199+
taskRun := &wfclient.TaskRun{
200+
Id: "tr-3",
201+
Status: wfclient.Completed,
202+
StartedAt: &startedAt,
203+
CompletedAt: &completedAt,
204+
}
205+
206+
row := Row(taskRun)
207+
require.Len(t, row, 5)
208+
assert.Equal(t, "tr-3", row[0])
209+
assert.Equal(t, "completed", row[1])
210+
assert.Equal(t, startedAt.Format(time.RFC3339), row[2])
211+
assert.Equal(t, completedAt.Format(time.RFC3339), row[3])
212+
213+
// Duration should be 5m30s
214+
duration := completedAt.Sub(startedAt)
215+
assert.Equal(t, duration.String(), row[4])
216+
assert.True(t, strings.Contains(row[4], "5m30s"), "expected duration to contain 5m30s, got %s", row[4])
217+
})
218+
}

pkg/tui/views/workflows/loader.go

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,40 @@ import (
1616
"github.com/render-oss/cli/pkg/workflowversion"
1717
)
1818

19-
type WorkflowLoaderDeps interface {
20-
TaskRepo() *tasks.Repo
21-
WorkflowService() *workflow.Service
22-
WorkflowVersionRepo() *version.Repo
23-
WorkflowRepo() *workflow.Repo
19+
// taskRepo defines the task operations the loader needs (unexported; used for testability).
20+
type taskRepo interface {
21+
RunTask(ctx context.Context, taskID string, input *workflows.TaskData) (*workflows.TaskRun, error)
22+
GetTask(ctx context.Context, id string) (*workflows.Task, error)
23+
ListTasks(ctx context.Context, params *client.ListTasksParams) (client.Cursor, []*workflows.Task, error)
24+
ListTaskRuns(ctx context.Context, params *client.ListTaskRunsParams) (client.Cursor, []*workflows.TaskRun, error)
25+
GetTaskRunDetails(ctx context.Context, taskRunID string) (*workflows.TaskRunDetails, error)
2426
}
2527

28+
// versionRepo defines the version operations the loader needs (unexported; used for testability).
29+
type versionRepo interface {
30+
ListVersions(ctx context.Context, workflowID string, params *client.ListWorkflowVersionsParams) (client.Cursor, []*wfclient.WorkflowVersion, error)
31+
GetVersion(ctx context.Context, workflowVersionID string) (*wfclient.WorkflowVersion, error)
32+
TriggerRelease(ctx context.Context, workflowID string, input version.TriggerReleaseInput) error
33+
}
34+
35+
// workflowRepo defines the workflow operations the loader needs (unexported; used for testability).
36+
type workflowRepoIface interface {
37+
GetWorkflow(ctx context.Context, id string) (*wfclient.Workflow, error)
38+
}
39+
40+
const (
41+
defaultVersionPollInterval = 5 * time.Second
42+
defaultVersionReleasePollInterval = time.Second
43+
)
44+
2645
type WorkflowLoader struct {
27-
taskRepo *tasks.Repo
46+
taskRepo taskRepo
2847
workflowService *workflow.Service
29-
workflowVersionRepo *version.Repo
30-
workflowRepo *workflow.Repo
48+
workflowVersionRepo versionRepo
49+
workflowRepo workflowRepoIface
50+
51+
versionPollInterval *time.Duration
52+
versionReleasePollInterval *time.Duration
3153
}
3254

3355
func NewWorkflowLoader(taskRepo *tasks.Repo, workflowService *workflow.Service, workflowVersionRepo *version.Repo, workflowRepo *workflow.Repo) *WorkflowLoader {
@@ -117,7 +139,11 @@ func (w *WorkflowLoader) ReleaseVersion(ctx context.Context, input VersionReleas
117139

118140
func (w *WorkflowLoader) WaitForVersion(ctx context.Context, workflowID, workflowVersionID string) (*wfclient.WorkflowVersion, error) {
119141
timeoutTimer := time.NewTimer(versionTimeout)
120-
ticker := time.NewTicker(5 * time.Second)
142+
pollInterval := defaultVersionPollInterval
143+
if w.versionPollInterval != nil {
144+
pollInterval = *w.versionPollInterval
145+
}
146+
ticker := time.NewTicker(pollInterval)
121147
defer ticker.Stop()
122148

123149
// Check immediately before waiting for the first tick
@@ -148,7 +174,11 @@ func (w *WorkflowLoader) WaitForVersion(ctx context.Context, workflowID, workflo
148174

149175
func (w *WorkflowLoader) WaitForVersionRelease(ctx context.Context, workflowID string) (*wfclient.WorkflowVersion, error) {
150176
timeoutTimer := time.NewTimer(versionReleaseTimeout)
151-
ticker := time.NewTicker(time.Second)
177+
releasePollInterval := defaultVersionReleasePollInterval
178+
if w.versionReleasePollInterval != nil {
179+
releasePollInterval = *w.versionReleasePollInterval
180+
}
181+
ticker := time.NewTicker(releasePollInterval)
152182
defer ticker.Stop()
153183

154184
// Check immediately before waiting for the first tick

0 commit comments

Comments
 (0)