Skip to content

Commit 348ab40

Browse files
authored
Fix incorrect deployment times (#34)
* Removed unused releaseTime variable * Introduce time interface so sleeps now current time can be mocked in tests * Add simple test for single branch deployment * Fix bug where wrong interval is used during release
1 parent 552988c commit 348ab40

File tree

6 files changed

+177
-14
lines changed

6 files changed

+177
-14
lines changed

ergo.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package ergo
22

3-
import "context"
3+
import (
4+
"context"
5+
"time"
6+
)
47

58
// MessageLevel defines the level of output message.
69
type MessageLevel string
@@ -29,6 +32,12 @@ type CLI interface {
2932
Input() (string, error)
3033
}
3134

35+
// Time describes actions around time and waiting.
36+
type Time interface {
37+
Sleep(duration time.Duration)
38+
Now() time.Time
39+
}
40+
3241
// Deploy describes the deploy process.
3342
type Deploy interface {
3443
Do(ctx context.Context, releaseIntervalInput, releaseOffsetInput string, allowForcePush bool) error

mock/cli.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package mock
22

33
import (
4+
"fmt"
5+
"strings"
46
"sync"
57

68
"github.com/beatlabs/ergo"
@@ -13,6 +15,7 @@ type CLI struct {
1315
mu sync.Mutex
1416
ConfirmationCalls int
1517
PrintTableCalls []PrintTableVal
18+
PrintLines []string
1619
}
1720

1821
// PrintTableVal represents the values send to the PrintTable method.
@@ -32,7 +35,13 @@ func (c *CLI) PrintTable(header []string, values [][]string) {
3235
func (c *CLI) PrintColorizedLine(title, content string, level ergo.MessageLevel) {}
3336

3437
// PrintLine is a mock implementation.
35-
func (c *CLI) PrintLine(content ...interface{}) {}
38+
func (c *CLI) PrintLine(content ...interface{}) {
39+
words := make([]string, 0, len(content))
40+
for _, c := range content {
41+
words = append(words, fmt.Sprintf("%v", c))
42+
}
43+
c.PrintLines = append(c.PrintLines, strings.Join(words, " "))
44+
}
3645

3746
// Confirmation is a mock implementation.
3847
func (c *CLI) Confirmation(actionText, cancellationMessage, successMessage string) (bool, error) {

mock/time.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package mock
2+
3+
import "time"
4+
5+
// Time is a mock implementation.
6+
type Time struct {
7+
CurrentTime time.Time
8+
}
9+
10+
// NewMockedTime creates a new mocked time implementation.
11+
func NewMockedTime(initialTime time.Time) *Time {
12+
return &Time{CurrentTime: initialTime}
13+
}
14+
15+
// Sleep mocks the sleep action, adding the duration to the time.
16+
func (t *Time) Sleep(duration time.Duration) {
17+
t.CurrentTime = t.CurrentTime.Add(duration)
18+
}
19+
20+
// Now returns the mocked time.
21+
func (t *Time) Now() time.Time {
22+
return t.CurrentTime
23+
}

release/deploy.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/beatlabs/ergo"
1111
"github.com/beatlabs/ergo/cli"
12+
ergoTime "github.com/beatlabs/ergo/time"
1213
)
1314

1415
// Deploy is responsible to describe the release process.
@@ -20,6 +21,7 @@ type Deploy struct {
2021
releaseBodyReplace string
2122
releaseBranches []string
2223
releaseBodyBranches map[string]string
24+
time ergo.Time
2325
}
2426

2527
// NewDeploy initialize and return a new Deploy object.
@@ -38,6 +40,7 @@ func NewDeploy(
3840
releaseBodyReplace: releaseBodyReplace,
3941
releaseBranches: releaseBranches,
4042
releaseBodyBranches: releaseBodyBranches,
43+
time: ergoTime.Time{},
4144
}
4245
}
4346

@@ -78,7 +81,7 @@ func (r *Deploy) Do(
7881
r.printReleaseTimeBoard(releaseTime, r.releaseBranches, intervalDurations)
7982

8083
if skipConfirm {
81-
return r.deployToAllReleaseBranches(ctx, intervalDurations, releaseTime, release, allowForcePush)
84+
return r.deployToAllReleaseBranches(ctx, intervalDurations, release, allowForcePush)
8285
}
8386

8487
confirm, err := r.c.Confirmation("Deployment", "No deployment", "")
@@ -95,35 +98,35 @@ func (r *Deploy) Do(
9598

9699
untilReleaseTime := time.Until(releaseTime)
97100
r.c.PrintLine("Deployment will start in", untilReleaseTime.String())
98-
time.Sleep(untilReleaseTime)
101+
r.time.Sleep(untilReleaseTime)
99102

100-
return r.deployToAllReleaseBranches(ctx, intervalDurations, releaseTime, release, allowForcePush)
103+
return r.deployToAllReleaseBranches(ctx, intervalDurations, release, allowForcePush)
101104
}
102105

103106
func (r *Deploy) deployToAllReleaseBranches(
104107
ctx context.Context,
105108
intervalDurations []time.Duration,
106-
releaseTime time.Time,
107109
release *ergo.Release,
108110
allowForcePush bool,
109111
) error {
110112
for i, branch := range r.releaseBranches {
111-
intervalDuration := intervalDurations[i%len(intervalDurations)]
112-
if i != 0 {
113-
time.Sleep(intervalDuration)
114-
releaseTime = releaseTime.Add(intervalDuration)
115-
}
116-
r.c.PrintLine("Deploying", time.Now().Format("15:04:05"), branch)
113+
r.c.PrintLine("Deploying", r.time.Now().Format("15:04:05"), branch)
117114

118115
if errRelease := r.host.UpdateBranchFromTag(ctx, release.TagName, branch, allowForcePush); errRelease != nil {
119116
return errRelease
120117
}
121-
r.c.PrintLine(time.Now().Format("15:04:05"), "Triggered Successfully")
118+
r.c.PrintLine(r.time.Now().Format("15:04:05"), "Triggered Successfully")
122119

123120
err := r.updateHostReleaseBody(ctx, r.releaseBodyBranches, branch, r.releaseBodyFind, r.releaseBodyReplace)
124121
if err != nil {
125122
return err
126123
}
124+
125+
// Don't sleep after the last deployment
126+
if i < (len(r.releaseBranches) - 1) {
127+
intervalDuration := intervalDurations[i%len(intervalDurations)]
128+
r.time.Sleep(intervalDuration)
129+
}
127130
}
128131
return nil
129132
}

release/deploy_test.go

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -354,7 +354,7 @@ func TestNonLinearIntervals(t *testing.T) {
354354
}
355355
}
356356

357-
func TestNonLinearIntervalHandling(t *testing.T) {
357+
func TestPrintReleaseTimeBoard_NonLinearIntervalHandling(t *testing.T) {
358358
ts := func(t time.Time) string {
359359
return t.Format("15:04 MST")
360360
}
@@ -436,3 +436,109 @@ func TestNonLinearIntervalHandling(t *testing.T) {
436436
})
437437
}
438438
}
439+
440+
func TestDeployToAllReleaseBranches_NonLinearIntervalHandling(t *testing.T) {
441+
ts := func(t time.Time) string {
442+
return t.Format("15:04:05")
443+
}
444+
releaseTime := time.Date(2022, 8, 4, 13, 37, 0, 0, time.UTC)
445+
tests := []struct {
446+
name string
447+
branches []string
448+
intervals string
449+
expectedPrints []string
450+
expectedFinalTime time.Time
451+
}{
452+
{
453+
name: "single branch, single interval",
454+
branches: []string{"branch1"},
455+
intervals: "10m",
456+
expectedPrints: []string{
457+
fmt.Sprintf("Deploying %v branch1", ts(releaseTime)),
458+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime)),
459+
},
460+
expectedFinalTime: releaseTime,
461+
},
462+
{
463+
name: "multiple branches, single interval",
464+
branches: []string{"branch1", "branch2"},
465+
intervals: "10m",
466+
expectedPrints: []string{
467+
fmt.Sprintf("Deploying %v branch1", ts(releaseTime)),
468+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime)),
469+
fmt.Sprintf("Deploying %v branch2", ts(releaseTime.Add(10*time.Minute))),
470+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(10*time.Minute))),
471+
},
472+
expectedFinalTime: releaseTime.Add(10 * time.Minute),
473+
},
474+
{
475+
name: "multiple branches, decreasing interval",
476+
branches: []string{"branch1", "branch2", "branch3", "branch4", "branch5"},
477+
intervals: "10m,5m,1m,1m",
478+
expectedPrints: []string{
479+
fmt.Sprintf("Deploying %v branch1", ts(releaseTime)),
480+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime)),
481+
fmt.Sprintf("Deploying %v branch2", ts(releaseTime.Add(10*time.Minute))), // 10
482+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(10*time.Minute))),
483+
fmt.Sprintf("Deploying %v branch3", ts(releaseTime.Add(15*time.Minute))), // 10 + 5
484+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(15*time.Minute))),
485+
fmt.Sprintf("Deploying %v branch4", ts(releaseTime.Add(16*time.Minute))), // 10 + 5 + 1
486+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(16*time.Minute))),
487+
fmt.Sprintf("Deploying %v branch5", ts(releaseTime.Add(17*time.Minute))), // 10 + 5 + 1 + 1
488+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(17*time.Minute))),
489+
},
490+
expectedFinalTime: releaseTime.Add(17 * time.Minute),
491+
},
492+
{
493+
name: "multiple branches, fewer intervals",
494+
branches: []string{"branch1", "branch2", "branch3", "branch4", "branch5"},
495+
intervals: "10m,5m",
496+
expectedPrints: []string{
497+
fmt.Sprintf("Deploying %v branch1", ts(releaseTime)),
498+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime)),
499+
fmt.Sprintf("Deploying %v branch2", ts(releaseTime.Add(10*time.Minute))), // 10
500+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(10*time.Minute))),
501+
fmt.Sprintf("Deploying %v branch3", ts(releaseTime.Add(15*time.Minute))), // 10 + 5
502+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(15*time.Minute))),
503+
fmt.Sprintf("Deploying %v branch4", ts(releaseTime.Add(25*time.Minute))), // 10 + 5 + 10
504+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(25*time.Minute))),
505+
fmt.Sprintf("Deploying %v branch5", ts(releaseTime.Add(30*time.Minute))), // 10 + 5 + 10 + 5
506+
fmt.Sprintf("%v Triggered Successfully", ts(releaseTime.Add(30*time.Minute))),
507+
},
508+
expectedFinalTime: releaseTime.Add(30 * time.Minute),
509+
},
510+
}
511+
for _, test := range tests {
512+
t.Run(test.name, func(t *testing.T) {
513+
cliMock := &mock.CLI{}
514+
timeMock := mock.NewMockedTime(releaseTime)
515+
deploy := &Deploy{
516+
c: cliMock,
517+
releaseBranches: test.branches,
518+
host: &mock.RepositoryClient{},
519+
time: timeMock,
520+
}
521+
intervalDurations, _, err := deploy.calculateReleaseTime(test.intervals, "1ms")
522+
if err != nil {
523+
t.Errorf("NewDeploy().Do() returned error: %v", err)
524+
}
525+
err = deploy.deployToAllReleaseBranches(context.Background(), intervalDurations, &ergo.Release{}, false)
526+
if err != nil {
527+
t.Errorf("Deploy.deployToAllReleaseBranches() returned error %v", err)
528+
}
529+
if len(test.expectedPrints) != len(cliMock.PrintLines) {
530+
t.Errorf("Expected %d number of prints to the mock, got %d", len(test.expectedPrints), len(cliMock.PrintLines))
531+
} else {
532+
for i, expected := range test.expectedPrints {
533+
got := cliMock.PrintLines[i]
534+
if !reflect.DeepEqual(expected, got) {
535+
t.Errorf("Expected print %d: '%v' to equal '%v'", i, expected, got)
536+
}
537+
}
538+
}
539+
if test.expectedFinalTime != timeMock.CurrentTime {
540+
t.Errorf("Expected final time to equal %v, got %v", test.expectedFinalTime, timeMock.CurrentTime)
541+
}
542+
})
543+
}
544+
}

time/time.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package time
2+
3+
import "time"
4+
5+
type Time struct{}
6+
7+
func (w Time) Sleep(duration time.Duration) {
8+
time.Sleep(duration)
9+
}
10+
11+
func (w Time) Now() time.Time {
12+
return time.Now()
13+
}

0 commit comments

Comments
 (0)