@@ -2,7 +2,6 @@ package coderd
22
33import (
44 "database/sql"
5- "runtime"
65 "testing"
76 "time"
87
@@ -22,6 +21,16 @@ import (
2221func Test_ActivityBumpWorkspace (t * testing.T ) {
2322 t .Parallel ()
2423
24+ // We test the below in multiple timezones specifically
25+ // chosen to trigger timezone-related bugs.
26+ timezones := []string {
27+ "Asia/Kolkata" , // No DST, positive fractional offset
28+ "Canada/Newfoundland" , // DST, negative fractional offset
29+ "Europe/Paris" , // DST, positive offset
30+ "US/Arizona" , // No DST, negative offset
31+ "UTC" , // Baseline
32+ }
33+
2534 for _ , tt := range []struct {
2635 name string
2736 transition database.WorkspaceTransition
@@ -92,117 +101,124 @@ func Test_ActivityBumpWorkspace(t *testing.T) {
92101 },
93102 } {
94103 tt := tt
95- t .Run (tt .name , func (t * testing.T ) {
96- t .Parallel ()
97-
98- var (
99- now = dbtime .Now ()
100- ctx = testutil .Context (t , testutil .WaitShort )
101- log = slogtest .Make (t , nil )
102- db , _ = dbtestutil .NewDB (t )
103- org = dbgen .Organization (t , db , database.Organization {})
104- user = dbgen .User (t , db , database.User {
105- Status : database .UserStatusActive ,
106- })
107- _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {
108- UserID : user .ID ,
109- OrganizationID : org .ID ,
110- })
111- templateVersion = dbgen .TemplateVersion (t , db , database.TemplateVersion {
112- OrganizationID : org .ID ,
113- CreatedBy : user .ID ,
114- })
115- template = dbgen .Template (t , db , database.Template {
116- OrganizationID : org .ID ,
117- ActiveVersionID : templateVersion .ID ,
118- CreatedBy : user .ID ,
119- })
120- ws = dbgen .Workspace (t , db , database.Workspace {
121- OwnerID : user .ID ,
122- OrganizationID : org .ID ,
123- TemplateID : template .ID ,
124- Ttl : sql.NullInt64 {Valid : true , Int64 : int64 (tt .workspaceTTL )},
125- })
126- job = dbgen .ProvisionerJob (t , db , database.ProvisionerJob {
127- OrganizationID : org .ID ,
128- CompletedAt : tt .jobCompletedAt ,
104+ for _ , tz := range timezones {
105+ tz := tz
106+ t .Run (tt .name + "/" + tz , func (t * testing.T ) {
107+ t .Parallel ()
108+
109+ var (
110+ now = dbtime .Now ()
111+ ctx = testutil .Context (t , testutil .WaitShort )
112+ log = slogtest .Make (t , nil )
113+ db , _ = dbtestutil .NewDB (t , dbtestutil .WithTimezone (tz ))
114+ org = dbgen .Organization (t , db , database.Organization {})
115+ user = dbgen .User (t , db , database.User {
116+ Status : database .UserStatusActive ,
117+ })
118+ _ = dbgen .OrganizationMember (t , db , database.OrganizationMember {
119+ UserID : user .ID ,
120+ OrganizationID : org .ID ,
121+ })
122+ templateVersion = dbgen .TemplateVersion (t , db , database.TemplateVersion {
123+ OrganizationID : org .ID ,
124+ CreatedBy : user .ID ,
125+ })
126+ template = dbgen .Template (t , db , database.Template {
127+ OrganizationID : org .ID ,
128+ ActiveVersionID : templateVersion .ID ,
129+ CreatedBy : user .ID ,
130+ })
131+ ws = dbgen .Workspace (t , db , database.Workspace {
132+ OwnerID : user .ID ,
133+ OrganizationID : org .ID ,
134+ TemplateID : template .ID ,
135+ Ttl : sql.NullInt64 {Valid : true , Int64 : int64 (tt .workspaceTTL )},
136+ })
137+ job = dbgen .ProvisionerJob (t , db , database.ProvisionerJob {
138+ OrganizationID : org .ID ,
139+ CompletedAt : tt .jobCompletedAt ,
140+ })
141+ _ = dbgen .WorkspaceResource (t , db , database.WorkspaceResource {
142+ JobID : job .ID ,
143+ })
144+ buildID = uuid .New ()
145+ )
146+
147+ var buildNumber int32 = 1
148+ // Insert a number of previous workspace builds.
149+ for i := 0 ; i < 5 ; i ++ {
150+ insertPrevWorkspaceBuild (t , db , org .ID , templateVersion .ID , ws .ID , database .WorkspaceTransitionStart , buildNumber )
151+ buildNumber ++
152+ insertPrevWorkspaceBuild (t , db , org .ID , templateVersion .ID , ws .ID , database .WorkspaceTransitionStop , buildNumber )
153+ buildNumber ++
154+ }
155+
156+ // dbgen.WorkspaceBuild automatically sets deadline to now+1 hour if not set
157+ var buildDeadline time.Time
158+ if tt .buildDeadlineOffset != nil {
159+ buildDeadline = now .Add (* tt .buildDeadlineOffset )
160+ }
161+ var maxDeadline time.Time
162+ if tt .maxDeadlineOffset != nil {
163+ maxDeadline = now .Add (* tt .maxDeadlineOffset )
164+ }
165+ err := db .InsertWorkspaceBuild (ctx , database.InsertWorkspaceBuildParams {
166+ ID : buildID ,
167+ CreatedAt : dbtime .Now (),
168+ UpdatedAt : dbtime .Now (),
169+ BuildNumber : buildNumber ,
170+ InitiatorID : user .ID ,
171+ Reason : database .BuildReasonInitiator ,
172+ WorkspaceID : ws .ID ,
173+ JobID : job .ID ,
174+ TemplateVersionID : templateVersion .ID ,
175+ Transition : tt .transition ,
176+ Deadline : buildDeadline ,
177+ MaxDeadline : maxDeadline ,
129178 })
130- _ = dbgen .WorkspaceResource (t , db , database.WorkspaceResource {
131- JobID : job .ID ,
132- })
133- buildID = uuid .New ()
134- )
135-
136- var buildNumber int32 = 1
137- // Insert a number of previous workspace builds.
138- for i := 0 ; i < 5 ; i ++ {
139- insertPrevWorkspaceBuild (t , db , org .ID , templateVersion .ID , ws .ID , database .WorkspaceTransitionStart , buildNumber )
140- buildNumber ++
141- insertPrevWorkspaceBuild (t , db , org .ID , templateVersion .ID , ws .ID , database .WorkspaceTransitionStop , buildNumber )
142- buildNumber ++
143- }
144-
145- // dbgen.WorkspaceBuild automatically sets deadline to now+1 hour if not set
146- var buildDeadline time.Time
147- if tt .buildDeadlineOffset != nil {
148- buildDeadline = now .Add (* tt .buildDeadlineOffset )
149- }
150- var maxDeadline time.Time
151- if tt .maxDeadlineOffset != nil {
152- maxDeadline = now .Add (* tt .maxDeadlineOffset )
153- }
154- err := db .InsertWorkspaceBuild (ctx , database.InsertWorkspaceBuildParams {
155- ID : buildID ,
156- CreatedAt : dbtime .Now (),
157- UpdatedAt : dbtime .Now (),
158- BuildNumber : buildNumber ,
159- InitiatorID : user .ID ,
160- Reason : database .BuildReasonInitiator ,
161- WorkspaceID : ws .ID ,
162- JobID : job .ID ,
163- TemplateVersionID : templateVersion .ID ,
164- Transition : tt .transition ,
165- Deadline : buildDeadline ,
166- MaxDeadline : maxDeadline ,
167- })
168- require .NoError (t , err , "unexpected error inserting workspace build" )
169- bld , err := db .GetWorkspaceBuildByID (ctx , buildID )
170- require .NoError (t , err , "unexpected error fetching inserted workspace build" )
171-
172- // Validate our initial state before bump
173- require .Equal (t , tt .transition , bld .Transition , "unexpected transition before bump" )
174- require .Equal (t , tt .jobCompletedAt .Time .UTC (), job .CompletedAt .Time .UTC (), "unexpected job completed at before bump" )
175- require .Equal (t , buildDeadline .UTC (), bld .Deadline .UTC (), "unexpected build deadline before bump" )
176- require .Equal (t , maxDeadline .UTC (), bld .MaxDeadline .UTC (), "unexpected max deadline before bump" )
177- require .Equal (t , tt .workspaceTTL , time .Duration (ws .Ttl .Int64 ), "unexpected workspace TTL before bump" )
178-
179- workaroundWindowsTimeResolution (t )
180-
181- // Bump duration is measured from the time of the bump, so we measure from here.
182- start := dbtime .Now ()
183- activityBumpWorkspace (ctx , log , db , bld .WorkspaceID )
184- elapsed := time .Since (start )
185- if elapsed > 15 * time .Second {
186- t .Logf ("warning: activityBumpWorkspace took longer than 15 seconds: %s" , elapsed )
187- }
188- // The actual bump could have happened anywhere in the elapsed time, so we
189- // guess at the approximate time of the bump.
190- approxBumpTime := start .Add (elapsed / 2 )
191-
192- // Validate our state after bump
193- updatedBuild , err := db .GetLatestWorkspaceBuildByWorkspaceID (ctx , bld .WorkspaceID )
194- require .NoError (t , err , "unexpected error getting latest workspace build" )
195- if tt .expectedBump == 0 {
196- require .Equal (t , bld .UpdatedAt .UTC (), updatedBuild .UpdatedAt .UTC (), "should not have bumped updated_at" )
197- require .Equal (t , bld .Deadline .UTC (), updatedBuild .Deadline .UTC (), "should not have bumped deadline" )
198- } else {
179+ require .NoError (t , err , "unexpected error inserting workspace build" )
180+ bld , err := db .GetWorkspaceBuildByID (ctx , buildID )
181+ require .NoError (t , err , "unexpected error fetching inserted workspace build" )
182+
183+ // Validate our initial state before bump
184+ require .Equal (t , tt .transition , bld .Transition , "unexpected transition before bump" )
185+ require .Equal (t , tt .jobCompletedAt .Time .UTC (), job .CompletedAt .Time .UTC (), "unexpected job completed at before bump" )
186+ require .Equal (t , buildDeadline .UTC (), bld .Deadline .UTC (), "unexpected build deadline before bump" )
187+ require .Equal (t , maxDeadline .UTC (), bld .MaxDeadline .UTC (), "unexpected max deadline before bump" )
188+ require .Equal (t , tt .workspaceTTL , time .Duration (ws .Ttl .Int64 ), "unexpected workspace TTL before bump" )
189+
190+ // Wait a bit before bumping as dbtime is rounded to the nearest millisecond.
191+ // This should also hopefully be enough for Windows time resolution to register
192+ // a tick (win32 max timer resolution is apparently between 0.5 and 15.6ms)
193+ <- time .After (testutil .IntervalFast )
194+
195+ // Bump duration is measured from the time of the bump, so we measure from here.
196+ start := dbtime .Now ()
197+ activityBumpWorkspace (ctx , log , db , bld .WorkspaceID )
198+ end := dbtime .Now ()
199+
200+ // Validate our state after bump
201+ updatedBuild , err := db .GetLatestWorkspaceBuildByWorkspaceID (ctx , bld .WorkspaceID )
202+ require .NoError (t , err , "unexpected error getting latest workspace build" )
203+ require .Equal (t , bld .MaxDeadline .UTC (), updatedBuild .MaxDeadline .UTC (), "max_deadline should not have changed" )
204+ if tt .expectedBump == 0 {
205+ require .Equal (t , bld .UpdatedAt .UTC (), updatedBuild .UpdatedAt .UTC (), "should not have bumped updated_at" )
206+ require .Equal (t , bld .Deadline .UTC (), updatedBuild .Deadline .UTC (), "should not have bumped deadline" )
207+ return
208+ }
199209 require .NotEqual (t , bld .UpdatedAt .UTC (), updatedBuild .UpdatedAt .UTC (), "should have bumped updated_at" )
200- expectedDeadline := approxBumpTime .Add (tt .expectedBump ).UTC ()
201- // Note: if CI is especially slow, this test may fail. There is an internal 15-second
202- // deadline in activityBumpWorkspace, so we allow the same window here.
203- require .WithinDuration (t , expectedDeadline , updatedBuild .Deadline .UTC (), 15 * time .Second , "unexpected deadline after bump" )
204- }
205- })
210+ if tt .maxDeadlineOffset != nil {
211+ require .Equal (t , bld .MaxDeadline .UTC (), updatedBuild .MaxDeadline .UTC (), "new deadline must equal original max deadline" )
212+ return
213+ }
214+
215+ // Assert that the bump occurred between start and end.
216+ expectedDeadlineStart := start .Add (tt .expectedBump )
217+ expectedDeadlineEnd := end .Add (tt .expectedBump )
218+ require .GreaterOrEqual (t , updatedBuild .Deadline , expectedDeadlineStart , "new deadline should be greater than or equal to start" )
219+ require .LessOrEqual (t , updatedBuild .Deadline , expectedDeadlineEnd , "new deadline should be lesser than or equal to end" )
220+ })
221+ }
206222 }
207223}
208224
@@ -223,11 +239,3 @@ func insertPrevWorkspaceBuild(t *testing.T, db database.Store, orgID, tvID, work
223239 Transition : transition ,
224240 })
225241}
226-
227- func workaroundWindowsTimeResolution (t * testing.T ) {
228- t .Helper ()
229- if runtime .GOOS == "windows" {
230- t .Logf ("workaround: sleeping for a short time to avoid time resolution issues on Windows" )
231- <- time .After (testutil .IntervalSlow )
232- }
233- }
0 commit comments