Skip to content
Merged
Prev Previous commit
Next Next commit
show time until deadline
  • Loading branch information
johnstcn committed Jun 16, 2022
commit dad6695c4f4251b94baf8e23cf00dc09971d171b
10 changes: 6 additions & 4 deletions cli/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func list() *cobra.Command {
}})
tableWriter.SetColumnConfigs(cliui.FilterTableColumns(header, columns))

now := time.Now()
for _, workspace := range workspaces {
status := ""
inProgress := false
Expand Down Expand Up @@ -85,7 +86,7 @@ func list() *cobra.Command {
status = "Failed"
}

duration := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
lastBuilt := time.Now().UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
autostartDisplay := "-"
if !ptr.NilOrEmpty(workspace.AutostartSchedule) {
if sched, err := schedule.Weekly(*workspace.AutostartSchedule); err == nil {
Expand All @@ -97,8 +98,9 @@ func list() *cobra.Command {
if !ptr.NilOrZero(workspace.TTLMillis) {
dur := time.Duration(*workspace.TTLMillis) * time.Millisecond
autostopDisplay = durationDisplay(dur)
if has, ext := hasExtension(workspace); has {
autostopDisplay += fmt.Sprintf(" (%s%s)", sign(ext), durationDisplay(ext))
if !workspace.LatestBuild.Deadline.IsZero() && workspace.LatestBuild.Deadline.After(now) && status == "Running" {
remaining := time.Until(workspace.LatestBuild.Deadline)
autostopDisplay = fmt.Sprintf("%s (%s)", autostopDisplay, relative(remaining))
}
}

Expand All @@ -107,7 +109,7 @@ func list() *cobra.Command {
user.Username + "/" + workspace.Name,
workspace.TemplateName,
status,
durationDisplay(duration),
durationDisplay(lastBuilt),
workspace.Outdated,
autostartDisplay,
autostopDisplay,
Expand Down
4 changes: 1 addition & 3 deletions cli/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,7 @@ func displaySchedule(workspace codersdk.Workspace, out io.Writer) error {
schedNextStop = "-"
} else {
schedNextStop = workspace.LatestBuild.Deadline.In(loc).Format(timeFormat + " on " + dateFormat)
if found, dur := hasExtension(workspace); found {
schedNextStop += fmt.Sprintf(" (%s%s from schedule)", sign(dur), durationDisplay(dur))
}
schedNextStop = fmt.Sprintf("%s (in %s)", schedNextStop, durationDisplay(time.Until(workspace.LatestBuild.Deadline)))
}
}

Expand Down
2 changes: 1 addition & 1 deletion cli/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ func TestScheduleShow(t *testing.T) {
assert.Contains(t, lines[0], "Starts at : 7:30AM Mon-Fri (Europe/Dublin)")
assert.Contains(t, lines[1], "Starts next : 7:30AM IST on ")
assert.Contains(t, lines[2], "Stops at : 8h after start")
assert.Contains(t, lines[3], "Stops next : ")
assert.NotContains(t, lines[3], "Stops next : -")
}
})

Expand Down
44 changes: 7 additions & 37 deletions cli/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,10 @@ import (
"strings"
"time"

"golang.org/x/exp/constraints"
"golang.org/x/xerrors"

"github.com/coder/coder/coderd/autobuild/schedule"
"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/coderd/util/tz"

"github.com/coder/coder/codersdk"
)

var errInvalidScheduleFormat = xerrors.New("Schedule must be in the format Mon-Fri 09:00AM America/Chicago")
Expand Down Expand Up @@ -63,41 +59,15 @@ func durationDisplay(d time.Duration) string {
return sign + durationDisplay
}

// Sign returns the sign of n. 0 is considered positive.
func sign[T constraints.Integer | constraints.Float | time.Duration](n T) string {
if n < 0 {
return "-"
}
return "+"
}

// hasExtension returns the deadline extension of ws, if it is present.
// This is calculated from the time the provisioner job was completed.
// Note that the extension may be negative.
func hasExtension(ws codersdk.Workspace) (bool, time.Duration) {
if ws.LatestBuild.Transition != codersdk.WorkspaceTransitionStart {
return false, 0
}
if ws.LatestBuild.Job.CompletedAt == nil || ws.LatestBuild.Job.CompletedAt.IsZero() {
return false, 0
// relative relativizes a duration with the prefix "ago" or "in"
func relative(d time.Duration) string {
if d > 0 {
return "in " + durationDisplay(d)
}
if ws.LatestBuild.Deadline.IsZero() {
return false, 0
if d < 0 {
return durationDisplay(d) + " ago"
}
if ptr.NilOrZero(ws.TTLMillis) {
return false, 0
}
ttl := time.Duration(*ws.TTLMillis) * time.Millisecond
delta := ws.LatestBuild.Deadline.Add(-ttl).Sub(*ws.LatestBuild.Job.CompletedAt)
cutoff := time.Minute
if delta < -cutoff {
return true, delta
}
if delta > cutoff {
return true, delta
}

return false, 0
return "now"
}

// parseCLISchedule parses a schedule in the format HH:MM{AM|PM} [DOW] [LOCATION]
Expand Down
128 changes: 4 additions & 124 deletions cli/util_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/util/ptr"
"github.com/coder/coder/codersdk"
)

func TestDurationDisplay(t *testing.T) {
Expand Down Expand Up @@ -44,126 +41,9 @@ func TestDurationDisplay(t *testing.T) {
}
}

func TestSign(t *testing.T) {
t.Parallel()
assert.Equal(t, sign(0), "+")
assert.Equal(t, sign(1), "+")
assert.Equal(t, sign(-1), "-")
}

func TestHasExtension(t *testing.T) {
func TestRelative(t *testing.T) {
t.Parallel()
for _, testCase := range []struct {
Name string
Workspace codersdk.Workspace
ExpectedFound bool
ExpectedDuration time.Duration
}{
{
Name: "Stopped",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Transition: codersdk.WorkspaceTransitionStop,
},
},
ExpectedFound: false,
ExpectedDuration: 0,
},
{
Name: "Building",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Job: codersdk.ProvisionerJob{
CompletedAt: nil,
},
Transition: codersdk.WorkspaceTransitionStart,
},
},
ExpectedFound: false,
ExpectedDuration: 0,
},
{
Name: "BuiltNoDeadline",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Time{},
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
},
ExpectedFound: false,
ExpectedDuration: 0,
},
{
Name: "BuiltNoTTL",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Now().Add(8 * time.Hour),
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
TTLMillis: nil, // explicit
},
ExpectedFound: false,
ExpectedDuration: 0,
},
{
Name: "PositiveDelta",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Now().Add(9*time.Hour + 30*time.Second),
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
TTLMillis: ptr.Ref(8 * time.Hour.Milliseconds()),
},
ExpectedFound: true,
ExpectedDuration: time.Hour,
},
{
Name: "NegativeDelta",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Now().Add(7 * time.Hour),
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
TTLMillis: ptr.Ref(8 * time.Hour.Milliseconds()),
},
ExpectedFound: true,
ExpectedDuration: -time.Hour,
},
{
Name: "Epsilon",
Workspace: codersdk.Workspace{
LatestBuild: codersdk.WorkspaceBuild{
Deadline: time.Now().Add(8 * time.Hour),
Job: codersdk.ProvisionerJob{
CompletedAt: ptr.Ref(time.Now()),
},
Transition: codersdk.WorkspaceTransitionStart,
},
TTLMillis: ptr.Ref(8 * time.Hour.Milliseconds()),
},
ExpectedFound: false,
ExpectedDuration: 0,
},
} {
testCase := testCase
t.Run(testCase.Name, func(t *testing.T) {
t.Parallel()
actualFound, actualDuration := hasExtension(testCase.Workspace)
if assert.Equal(t, testCase.ExpectedFound, actualFound) {
assert.InDelta(t, testCase.ExpectedDuration, actualDuration, float64(time.Minute))
}
})
}
assert.Equal(t, relative(time.Minute), "in 1m")
assert.Equal(t, relative(-time.Minute), "1m ago")
assert.Equal(t, relative(0), "now")
}