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/provisioner/echo"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
@@ -40,6 +41,18 @@ func New(t testing.TB, args ...string) (*serpent.Invocation, config.Root) {
|
||||
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 {
|
||||
prefix string
|
||||
log slog.Logger
|
||||
|
||||
+15
@@ -39,6 +39,7 @@ import (
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/codersdk/agentsdk"
|
||||
"github.com/coder/pretty"
|
||||
"github.com/coder/quartz"
|
||||
"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) {
|
||||
if r.clock == nil {
|
||||
r.clock = quartz.NewReal()
|
||||
}
|
||||
|
||||
fmtLong := `Coder %s — A tool for provisioning self-hosted development environments with Terraform.
|
||||
`
|
||||
hiddenAgentAuth := &AgentAuth{}
|
||||
@@ -548,6 +553,16 @@ type RootCmd struct {
|
||||
useKeyring bool
|
||||
keyringServiceName string
|
||||
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
|
||||
|
||||
+5
-5
@@ -90,7 +90,7 @@ func (r *RootCmd) taskStatus() *serpent.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
tsr := toStatusRow(task)
|
||||
tsr := toStatusRow(task, r.clock.Now())
|
||||
out, err := formatter.Format(ctx, []taskStatusRow{tsr})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("format task status: %w", err)
|
||||
@@ -112,7 +112,7 @@ func (r *RootCmd) taskStatus() *serpent.Command {
|
||||
}
|
||||
|
||||
// Only print if something changed
|
||||
newStatusRow := toStatusRow(task)
|
||||
newStatusRow := toStatusRow(task, r.clock.Now())
|
||||
if !taskStatusRowEqual(lastStatusRow, newStatusRow) {
|
||||
out, err := formatter.Format(ctx, []taskStatusRow{newStatusRow})
|
||||
if err != nil {
|
||||
@@ -166,10 +166,10 @@ func taskStatusRowEqual(r1, r2 taskStatusRow) bool {
|
||||
taskStateEqual(r1.CurrentState, r2.CurrentState)
|
||||
}
|
||||
|
||||
func toStatusRow(task codersdk.Task) taskStatusRow {
|
||||
func toStatusRow(task codersdk.Task, now time.Time) taskStatusRow {
|
||||
tsr := taskStatusRow{
|
||||
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 &&
|
||||
task.WorkspaceAgentHealth.Healthy &&
|
||||
@@ -178,7 +178,7 @@ func toStatusRow(task codersdk.Task) taskStatusRow {
|
||||
!task.WorkspaceAgentLifecycle.ShuttingDown()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
+12
-9
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/quartz"
|
||||
)
|
||||
|
||||
func Test_TaskStatus(t *testing.T) {
|
||||
@@ -28,12 +29,12 @@ func Test_TaskStatus(t *testing.T) {
|
||||
args []string
|
||||
expectOutput 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"},
|
||||
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) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v2/tasks/me/doesnotexist":
|
||||
@@ -49,7 +50,8 @@ func Test_TaskStatus(t *testing.T) {
|
||||
args: []string{"exists"},
|
||||
expectOutput: `STATE CHANGED STATUS HEALTHY STATE MESSAGE
|
||||
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) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v2/tasks/me/exists":
|
||||
@@ -84,7 +86,8 @@ func Test_TaskStatus(t *testing.T) {
|
||||
4s ago active true
|
||||
3s ago active true working Reticulating splines...
|
||||
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
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
@@ -215,7 +218,7 @@ func Test_TaskStatus(t *testing.T) {
|
||||
"created_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)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
@@ -252,8 +255,8 @@ func Test_TaskStatus(t *testing.T) {
|
||||
|
||||
var (
|
||||
ctx = testutil.Context(t, testutil.WaitShort)
|
||||
now = time.Now().UTC() // TODO: replace with quartz
|
||||
srv = httptest.NewServer(http.HandlerFunc(tc.hf(ctx, now)))
|
||||
mClock = quartz.NewMock(t)
|
||||
srv = httptest.NewServer(http.HandlerFunc(tc.hf(ctx, mClock)))
|
||||
client = codersdk.New(testutil.MustURL(t, srv.URL))
|
||||
sb = strings.Builder{}
|
||||
args = []string{"task", "status", "--watch-interval", testutil.IntervalFast.String()}
|
||||
@@ -261,10 +264,10 @@ func Test_TaskStatus(t *testing.T) {
|
||||
|
||||
t.Cleanup(srv.Close)
|
||||
args = append(args, tc.args...)
|
||||
inv, root := clitest.New(t, args...)
|
||||
inv, cfgDir := clitest.NewWithClock(t, mClock, args...)
|
||||
inv.Stdout = &sb
|
||||
inv.Stderr = &sb
|
||||
clitest.SetupConfig(t, client, root)
|
||||
clitest.SetupConfig(t, client, cfgDir)
|
||||
err := inv.WithContext(ctx).Run()
|
||||
if tc.expectError == "" {
|
||||
assert.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user