mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix: use quartz clock in task status test (#22969)
Replace time.Since() usage with a quartz.Clock injected via RootCmd to
ensure relative time strings ("Xs ago") are deterministic.
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/provisioner/echo"
|
"github.com/coder/coder/v2/provisioner/echo"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
"github.com/coder/quartz"
|
||||||
"github.com/coder/serpent"
|
"github.com/coder/serpent"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,6 +41,18 @@ func New(t testing.TB, args ...string) (*serpent.Invocation, config.Root) {
|
|||||||
return NewWithCommand(t, cmd, args...)
|
return NewWithCommand(t, cmd, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewWithClock is like New, but injects the given clock for
|
||||||
|
// tests that are time-dependent.
|
||||||
|
func NewWithClock(t testing.TB, clk quartz.Clock, args ...string) (*serpent.Invocation, config.Root) {
|
||||||
|
var root cli.RootCmd
|
||||||
|
root.SetClock(clk)
|
||||||
|
|
||||||
|
cmd, err := root.Command(root.AGPL())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
return NewWithCommand(t, cmd, args...)
|
||||||
|
}
|
||||||
|
|
||||||
type logWriter struct {
|
type logWriter struct {
|
||||||
prefix string
|
prefix string
|
||||||
log slog.Logger
|
log slog.Logger
|
||||||
|
|||||||
+15
@@ -39,6 +39,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||||
"github.com/coder/pretty"
|
"github.com/coder/pretty"
|
||||||
|
"github.com/coder/quartz"
|
||||||
"github.com/coder/serpent"
|
"github.com/coder/serpent"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -230,6 +231,10 @@ func (r *RootCmd) RunWithSubcommands(subcommands []*serpent.Command) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, error) {
|
func (r *RootCmd) Command(subcommands []*serpent.Command) (*serpent.Command, error) {
|
||||||
|
if r.clock == nil {
|
||||||
|
r.clock = quartz.NewReal()
|
||||||
|
}
|
||||||
|
|
||||||
fmtLong := `Coder %s — A tool for provisioning self-hosted development environments with Terraform.
|
fmtLong := `Coder %s — A tool for provisioning self-hosted development environments with Terraform.
|
||||||
`
|
`
|
||||||
hiddenAgentAuth := &AgentAuth{}
|
hiddenAgentAuth := &AgentAuth{}
|
||||||
@@ -548,6 +553,16 @@ type RootCmd struct {
|
|||||||
useKeyring bool
|
useKeyring bool
|
||||||
keyringServiceName string
|
keyringServiceName string
|
||||||
useKeyringWithGlobalConfig bool
|
useKeyringWithGlobalConfig bool
|
||||||
|
|
||||||
|
// clock is used for time-dependent operations. Initialized to
|
||||||
|
// quartz.NewReal() in Command() if not set via SetClock.
|
||||||
|
clock quartz.Clock
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetClock sets the clock used for time-dependent operations.
|
||||||
|
// Must be called before Command() to take effect.
|
||||||
|
func (r *RootCmd) SetClock(clk quartz.Clock) {
|
||||||
|
r.clock = clk
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureClientURL loads the client URL from the config file if it
|
// ensureClientURL loads the client URL from the config file if it
|
||||||
|
|||||||
+5
-5
@@ -90,7 +90,7 @@ func (r *RootCmd) taskStatus() *serpent.Command {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tsr := toStatusRow(task)
|
tsr := toStatusRow(task, r.clock.Now())
|
||||||
out, err := formatter.Format(ctx, []taskStatusRow{tsr})
|
out, err := formatter.Format(ctx, []taskStatusRow{tsr})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return xerrors.Errorf("format task status: %w", err)
|
return xerrors.Errorf("format task status: %w", err)
|
||||||
@@ -112,7 +112,7 @@ func (r *RootCmd) taskStatus() *serpent.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Only print if something changed
|
// Only print if something changed
|
||||||
newStatusRow := toStatusRow(task)
|
newStatusRow := toStatusRow(task, r.clock.Now())
|
||||||
if !taskStatusRowEqual(lastStatusRow, newStatusRow) {
|
if !taskStatusRowEqual(lastStatusRow, newStatusRow) {
|
||||||
out, err := formatter.Format(ctx, []taskStatusRow{newStatusRow})
|
out, err := formatter.Format(ctx, []taskStatusRow{newStatusRow})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -166,10 +166,10 @@ func taskStatusRowEqual(r1, r2 taskStatusRow) bool {
|
|||||||
taskStateEqual(r1.CurrentState, r2.CurrentState)
|
taskStateEqual(r1.CurrentState, r2.CurrentState)
|
||||||
}
|
}
|
||||||
|
|
||||||
func toStatusRow(task codersdk.Task) taskStatusRow {
|
func toStatusRow(task codersdk.Task, now time.Time) taskStatusRow {
|
||||||
tsr := taskStatusRow{
|
tsr := taskStatusRow{
|
||||||
Task: task,
|
Task: task,
|
||||||
ChangedAgo: time.Since(task.UpdatedAt).Truncate(time.Second).String() + " ago",
|
ChangedAgo: now.Sub(task.UpdatedAt).Truncate(time.Second).String() + " ago",
|
||||||
}
|
}
|
||||||
tsr.Healthy = task.WorkspaceAgentHealth != nil &&
|
tsr.Healthy = task.WorkspaceAgentHealth != nil &&
|
||||||
task.WorkspaceAgentHealth.Healthy &&
|
task.WorkspaceAgentHealth.Healthy &&
|
||||||
@@ -178,7 +178,7 @@ func toStatusRow(task codersdk.Task) taskStatusRow {
|
|||||||
!task.WorkspaceAgentLifecycle.ShuttingDown()
|
!task.WorkspaceAgentLifecycle.ShuttingDown()
|
||||||
|
|
||||||
if task.CurrentState != nil {
|
if task.CurrentState != nil {
|
||||||
tsr.ChangedAgo = time.Since(task.CurrentState.Timestamp).Truncate(time.Second).String() + " ago"
|
tsr.ChangedAgo = now.Sub(task.CurrentState.Timestamp).Truncate(time.Second).String() + " ago"
|
||||||
}
|
}
|
||||||
return tsr
|
return tsr
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-9
@@ -19,6 +19,7 @@ import (
|
|||||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||||
"github.com/coder/coder/v2/codersdk"
|
"github.com/coder/coder/v2/codersdk"
|
||||||
"github.com/coder/coder/v2/testutil"
|
"github.com/coder/coder/v2/testutil"
|
||||||
|
"github.com/coder/quartz"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_TaskStatus(t *testing.T) {
|
func Test_TaskStatus(t *testing.T) {
|
||||||
@@ -28,12 +29,12 @@ func Test_TaskStatus(t *testing.T) {
|
|||||||
args []string
|
args []string
|
||||||
expectOutput string
|
expectOutput string
|
||||||
expectError string
|
expectError string
|
||||||
hf func(context.Context, time.Time) func(http.ResponseWriter, *http.Request)
|
hf func(context.Context, quartz.Clock) func(http.ResponseWriter, *http.Request)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
args: []string{"doesnotexist"},
|
args: []string{"doesnotexist"},
|
||||||
expectError: httpapi.ResourceNotFoundResponse.Message,
|
expectError: httpapi.ResourceNotFoundResponse.Message,
|
||||||
hf: func(ctx context.Context, _ time.Time) func(w http.ResponseWriter, r *http.Request) {
|
hf: func(ctx context.Context, _ quartz.Clock) func(w http.ResponseWriter, r *http.Request) {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/api/v2/tasks/me/doesnotexist":
|
case "/api/v2/tasks/me/doesnotexist":
|
||||||
@@ -49,7 +50,8 @@ func Test_TaskStatus(t *testing.T) {
|
|||||||
args: []string{"exists"},
|
args: []string{"exists"},
|
||||||
expectOutput: `STATE CHANGED STATUS HEALTHY STATE MESSAGE
|
expectOutput: `STATE CHANGED STATUS HEALTHY STATE MESSAGE
|
||||||
0s ago active true working Thinking furiously...`,
|
0s ago active true working Thinking furiously...`,
|
||||||
hf: func(ctx context.Context, now time.Time) func(w http.ResponseWriter, r *http.Request) {
|
hf: func(ctx context.Context, clk quartz.Clock) func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
now := clk.Now()
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
case "/api/v2/tasks/me/exists":
|
case "/api/v2/tasks/me/exists":
|
||||||
@@ -84,7 +86,8 @@ func Test_TaskStatus(t *testing.T) {
|
|||||||
4s ago active true
|
4s ago active true
|
||||||
3s ago active true working Reticulating splines...
|
3s ago active true working Reticulating splines...
|
||||||
2s ago active true complete Splines reticulated successfully!`,
|
2s ago active true complete Splines reticulated successfully!`,
|
||||||
hf: func(ctx context.Context, now time.Time) func(http.ResponseWriter, *http.Request) {
|
hf: func(ctx context.Context, clk quartz.Clock) func(http.ResponseWriter, *http.Request) {
|
||||||
|
now := clk.Now()
|
||||||
var calls atomic.Int64
|
var calls atomic.Int64
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
@@ -215,7 +218,7 @@ func Test_TaskStatus(t *testing.T) {
|
|||||||
"created_at": "2025-08-26T12:34:56Z",
|
"created_at": "2025-08-26T12:34:56Z",
|
||||||
"updated_at": "2025-08-26T12:34:56Z"
|
"updated_at": "2025-08-26T12:34:56Z"
|
||||||
}`,
|
}`,
|
||||||
hf: func(ctx context.Context, now time.Time) func(http.ResponseWriter, *http.Request) {
|
hf: func(ctx context.Context, _ quartz.Clock) func(http.ResponseWriter, *http.Request) {
|
||||||
ts := time.Date(2025, 8, 26, 12, 34, 56, 0, time.UTC)
|
ts := time.Date(2025, 8, 26, 12, 34, 56, 0, time.UTC)
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
@@ -252,8 +255,8 @@ func Test_TaskStatus(t *testing.T) {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = testutil.Context(t, testutil.WaitShort)
|
ctx = testutil.Context(t, testutil.WaitShort)
|
||||||
now = time.Now().UTC() // TODO: replace with quartz
|
mClock = quartz.NewMock(t)
|
||||||
srv = httptest.NewServer(http.HandlerFunc(tc.hf(ctx, now)))
|
srv = httptest.NewServer(http.HandlerFunc(tc.hf(ctx, mClock)))
|
||||||
client = codersdk.New(testutil.MustURL(t, srv.URL))
|
client = codersdk.New(testutil.MustURL(t, srv.URL))
|
||||||
sb = strings.Builder{}
|
sb = strings.Builder{}
|
||||||
args = []string{"task", "status", "--watch-interval", testutil.IntervalFast.String()}
|
args = []string{"task", "status", "--watch-interval", testutil.IntervalFast.String()}
|
||||||
@@ -261,10 +264,10 @@ func Test_TaskStatus(t *testing.T) {
|
|||||||
|
|
||||||
t.Cleanup(srv.Close)
|
t.Cleanup(srv.Close)
|
||||||
args = append(args, tc.args...)
|
args = append(args, tc.args...)
|
||||||
inv, root := clitest.New(t, args...)
|
inv, cfgDir := clitest.NewWithClock(t, mClock, args...)
|
||||||
inv.Stdout = &sb
|
inv.Stdout = &sb
|
||||||
inv.Stderr = &sb
|
inv.Stderr = &sb
|
||||||
clitest.SetupConfig(t, client, root)
|
clitest.SetupConfig(t, client, cfgDir)
|
||||||
err := inv.WithContext(ctx).Run()
|
err := inv.WithContext(ctx).Run()
|
||||||
if tc.expectError == "" {
|
if tc.expectError == "" {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user