From a13bce59ad62ec30f322da85310128f1531c2757 Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Tue, 5 Mar 2019 11:54:04 -0500 Subject: [PATCH 1/4] updateDB should not return a pointer --- server/events/command_runner.go | 4 ++-- server/events/db/boltdb.go | 17 +++++++------- server/events/db/boltdb_test.go | 40 +++++++++++++++------------------ 3 files changed, 29 insertions(+), 32 deletions(-) diff --git a/server/events/command_runner.go b/server/events/command_runner.go index df73b59355..3662d90994 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -225,7 +225,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead } } -func (c *DefaultCommandRunner) automerge(ctx *CommandContext, pullStatus *models.PullStatus) { +func (c *DefaultCommandRunner) automerge(ctx *CommandContext, pullStatus models.PullStatus) { // We only automerge if all projects have been successfully applied. for _, p := range pullStatus.Projects { if p.Status != models.AppliedPlanStatus { @@ -364,7 +364,7 @@ func (c *DefaultCommandRunner) deletePlans(ctx *CommandContext) { } } -func (c *DefaultCommandRunner) updateDB(ctx *CommandContext, pull models.PullRequest, results []models.ProjectResult) (*models.PullStatus, error) { +func (c *DefaultCommandRunner) updateDB(ctx *CommandContext, pull models.PullRequest, results []models.ProjectResult) (models.PullStatus, error) { // Filter out results that errored due to the directory not existing. We // don't store these in the database because they would never be "applyable" // and so the pull request would always have errors. diff --git a/server/events/db/boltdb.go b/server/events/db/boltdb.go index 98e771068d..a7badcf1e9 100644 --- a/server/events/db/boltdb.go +++ b/server/events/db/boltdb.go @@ -215,13 +215,13 @@ func (b *BoltDB) GetLock(p models.Project, workspace string) (*models.ProjectLoc // UpdatePullWithResults updates pull's status with the latest project results. // It returns the new PullStatus object. -func (b *BoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []models.ProjectResult) (*models.PullStatus, error) { +func (b *BoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []models.ProjectResult) (models.PullStatus, error) { key, err := b.pullKey(pull) if err != nil { - return nil, err + return models.PullStatus{}, err } - var newStatus *models.PullStatus + var newStatus models.PullStatus err = b.db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket(b.pullsBucketName) currStatus, err := b.getPullFromBucket(bucket, key) @@ -236,7 +236,7 @@ func (b *BoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []mod for _, r := range newResults { statuses = append(statuses, b.projectResultToProject(r)) } - newStatus = &models.PullStatus{ + newStatus = models.PullStatus{ Pull: pull, Projects: statuses, } @@ -246,7 +246,7 @@ func (b *BoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []mod // because it's possible a user is just applying a single project // in this command and so we don't want to delete our data about // other projects that aren't affected by this command. - newStatus = currStatus + newStatus = *currStatus for _, res := range newResults { // First, check if we should update any existing projects. updatedExisting := false @@ -317,13 +317,14 @@ func (b *BoltDB) DeleteProjectStatus(pull models.PullRequest, workspace string, } err = b.db.Update(func(tx *bolt.Tx) error { bucket := tx.Bucket(b.pullsBucketName) - currStatus, err := b.getPullFromBucket(bucket, key) + currStatusPtr, err := b.getPullFromBucket(bucket, key) if err != nil { return err } - if currStatus == nil { + if currStatusPtr == nil { return nil } + currStatus := *currStatusPtr // Create a new projectStatuses array without the ones we want to // delete. @@ -373,7 +374,7 @@ func (b *BoltDB) getPullFromBucket(bucket *bolt.Bucket, key []byte) (*models.Pul return &p, nil } -func (b *BoltDB) writePullToBucket(bucket *bolt.Bucket, key []byte, pull *models.PullStatus) error { +func (b *BoltDB) writePullToBucket(bucket *bolt.Bucket, key []byte, pull models.PullStatus) error { serialized, err := json.Marshal(pull) if err != nil { return errors.Wrap(err, "serializing") diff --git a/server/events/db/boltdb_test.go b/server/events/db/boltdb_test.go index b65777db07..3be86635bb 100644 --- a/server/events/db/boltdb_test.go +++ b/server/events/db/boltdb_test.go @@ -387,11 +387,11 @@ func TestPullStatus_UpdateGet(t *testing.T) { }, }) Ok(t, err) - Assert(t, status != nil, "exp non-nil") - status, err = b.GetPullStatus(pull) + maybeStatus, err := b.GetPullStatus(pull) Ok(t, err) - Equals(t, pull, status.Pull) + Assert(t, maybeStatus != nil, "exp non-nil") + Equals(t, pull, maybeStatus.Pull) Equals(t, []models.ProjectStatus{ { Workspace: "default", @@ -428,7 +428,7 @@ func TestPullStatus_UpdateDeleteGet(t *testing.T) { }, }, } - status, err := b.UpdatePullWithResults( + _, err := b.UpdatePullWithResults( pull, []models.ProjectResult{ { @@ -438,14 +438,13 @@ func TestPullStatus_UpdateDeleteGet(t *testing.T) { }, }) Ok(t, err) - Assert(t, status != nil, "exp non-nil") err = b.DeletePullStatus(pull) Ok(t, err) - status, err = b.GetPullStatus(pull) + maybeStatus, err := b.GetPullStatus(pull) Ok(t, err) - Assert(t, status == nil, "exp nil") + Assert(t, maybeStatus == nil, "exp nil") } // Test we can create a status, delete a specific project's status within that @@ -475,7 +474,7 @@ func TestPullStatus_UpdateDeleteProject(t *testing.T) { }, }, } - status, err := b.UpdatePullWithResults( + _, err := b.UpdatePullWithResults( pull, []models.ProjectResult{ { @@ -490,13 +489,13 @@ func TestPullStatus_UpdateDeleteProject(t *testing.T) { }, }) Ok(t, err) - Assert(t, status != nil, "exp non-nil") err = b.DeleteProjectStatus(pull, "default", ".") Ok(t, err) - status, err = b.GetPullStatus(pull) + status, err := b.GetPullStatus(pull) Ok(t, err) + Assert(t, status != nil, "exp non-nil") Equals(t, pull, status.Pull) Equals(t, []models.ProjectStatus{ { @@ -534,7 +533,7 @@ func TestPullStatus_UpdateNewCommit(t *testing.T) { }, }, } - status, err := b.UpdatePullWithResults( + _, err := b.UpdatePullWithResults( pull, []models.ProjectResult{ { @@ -544,10 +543,9 @@ func TestPullStatus_UpdateNewCommit(t *testing.T) { }, }) Ok(t, err) - Assert(t, status != nil, "exp non-nil") pull.HeadCommit = "newsha" - status, err = b.UpdatePullWithResults(pull, + status, err := b.UpdatePullWithResults(pull, []models.ProjectResult{ { RepoRelDir: ".", @@ -559,9 +557,9 @@ func TestPullStatus_UpdateNewCommit(t *testing.T) { Ok(t, err) Equals(t, 1, len(status.Projects)) - status, err = b.GetPullStatus(pull) + maybeStatus, err := b.GetPullStatus(pull) Ok(t, err) - Equals(t, pull, status.Pull) + Equals(t, pull, maybeStatus.Pull) Equals(t, []models.ProjectStatus{ { Workspace: "staging", @@ -569,7 +567,7 @@ func TestPullStatus_UpdateNewCommit(t *testing.T) { ProjectName: "", Status: models.AppliedPlanStatus, }, - }, status.Projects) + }, maybeStatus.Projects) } // Test that if we update an existing pull status and our new status is for a @@ -598,7 +596,7 @@ func TestPullStatus_UpdateMerge(t *testing.T) { }, }, } - status, err := b.UpdatePullWithResults( + _, err := b.UpdatePullWithResults( pull, []models.ProjectResult{ { @@ -618,9 +616,8 @@ func TestPullStatus_UpdateMerge(t *testing.T) { }, }) Ok(t, err) - Assert(t, status != nil, "exp non-nil") - status, err = b.UpdatePullWithResults(pull, + updateStatus, err := b.UpdatePullWithResults(pull, []models.ProjectResult{ { RepoRelDir: "mergeme", @@ -633,7 +630,6 @@ func TestPullStatus_UpdateMerge(t *testing.T) { ApplySuccess: "success!", }, }) - Ok(t, err) getStatus, err := b.GetPullStatus(pull) @@ -641,7 +637,7 @@ func TestPullStatus_UpdateMerge(t *testing.T) { // Test both the pull state returned from the update call *and* the get // call. - for _, s := range []*models.PullStatus{status, getStatus} { + for _, s := range []models.PullStatus{updateStatus, *getStatus} { Equals(t, pull, s.Pull) Equals(t, []models.ProjectStatus{ { @@ -659,7 +655,7 @@ func TestPullStatus_UpdateMerge(t *testing.T) { Workspace: "default", Status: models.AppliedPlanStatus, }, - }, status.Projects) + }, updateStatus.Projects) } } From 393af51e4fb4f7b02bab8a8945b6ec371234f741 Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Tue, 5 Mar 2019 12:01:06 -0500 Subject: [PATCH 2/4] Add Command field to ProjectResult --- server/events/models/models.go | 1 + server/events/project_command_runner.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/server/events/models/models.go b/server/events/models/models.go index 73a61afdfc..6abb87e76f 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -335,6 +335,7 @@ func (p *ProjectCommandContext) GetProjectName() string { // ProjectResult is the result of executing a plan/apply for a specific project. type ProjectResult struct { + Command CommandName RepoRelDir string Workspace string Error error diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 77789d86a7..f2a9df3d83 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -94,6 +94,7 @@ type DefaultProjectCommandRunner struct { func (p *DefaultProjectCommandRunner) Plan(ctx models.ProjectCommandContext) models.ProjectResult { planSuccess, failure, err := p.doPlan(ctx) return models.ProjectResult{ + Command: models.PlanCommand, PlanSuccess: planSuccess, Error: err, Failure: failure, @@ -107,6 +108,7 @@ func (p *DefaultProjectCommandRunner) Plan(ctx models.ProjectCommandContext) mod func (p *DefaultProjectCommandRunner) Apply(ctx models.ProjectCommandContext) models.ProjectResult { applyOut, failure, err := p.doApply(ctx) return models.ProjectResult{ + Command: models.ApplyCommand, Failure: failure, Error: err, ApplySuccess: applyOut, From f98fe8ef80ee577e8da9858069936277dc4c9b57 Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Tue, 5 Mar 2019 12:08:11 -0500 Subject: [PATCH 3/4] Add new status to ProjectPlanStatus --- server/events/commit_status_updater.go | 2 +- server/events/db/boltdb.go | 20 ++------------ server/events/models/models.go | 37 +++++++++++++++++++++++--- 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/server/events/commit_status_updater.go b/server/events/commit_status_updater.go index 31977e53d1..2da397a1f6 100644 --- a/server/events/commit_status_updater.go +++ b/server/events/commit_status_updater.go @@ -56,7 +56,7 @@ func (d *DefaultCommitStatusUpdater) UpdateProjectResult(ctx *CommandContext, co } else { var statuses []models.CommitStatus for _, p := range res.ProjectResults { - statuses = append(statuses, p.Status()) + statuses = append(statuses, p.CommitStatus()) } status = d.worstStatus(statuses) } diff --git a/server/events/db/boltdb.go b/server/events/db/boltdb.go index a7badcf1e9..088dde75c6 100644 --- a/server/events/db/boltdb.go +++ b/server/events/db/boltdb.go @@ -258,7 +258,7 @@ func (b *BoltDB) UpdatePullWithResults(pull models.PullRequest, newResults []mod res.RepoRelDir == proj.RepoRelDir && res.ProjectName == proj.ProjectName { - proj.Status = b.getPlanStatus(res) + proj.Status = res.PlanStatus() updatedExisting = true break } @@ -382,27 +382,11 @@ func (b *BoltDB) writePullToBucket(bucket *bolt.Bucket, key []byte, pull models. return bucket.Put(key, serialized) } -func (b *BoltDB) getPlanStatus(p models.ProjectResult) models.ProjectPlanStatus { - if p.Error != nil { - return models.ErroredPlanStatus - } - if p.Failure != "" { - return models.ErroredPlanStatus - } - if p.PlanSuccess != nil { - return models.PlannedPlanStatus - } - if p.ApplySuccess != "" { - return models.AppliedPlanStatus - } - return models.ErroredPlanStatus -} - func (b *BoltDB) projectResultToProject(p models.ProjectResult) models.ProjectStatus { return models.ProjectStatus{ Workspace: p.Workspace, RepoRelDir: p.RepoRelDir, ProjectName: p.ProjectName, - Status: b.getPlanStatus(p), + Status: p.PlanStatus(), } } diff --git a/server/events/models/models.go b/server/events/models/models.go index 6abb87e76f..4bfba808e4 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -345,8 +345,8 @@ type ProjectResult struct { ProjectName string } -// Status returns the vcs commit status of this project result. -func (p ProjectResult) Status() CommitStatus { +// CommitStatus returns the vcs commit status of this project result. +func (p ProjectResult) CommitStatus() CommitStatus { if p.Error != nil { return FailedCommitStatus } @@ -356,6 +356,30 @@ func (p ProjectResult) Status() CommitStatus { return SuccessCommitStatus } +// PlanStatus returns the plan status. +func (p ProjectResult) PlanStatus() ProjectPlanStatus { + switch p.Command { + + case PlanCommand: + if p.Error != nil { + return ErroredPlanStatus + } else if p.Failure != "" { + return ErroredPlanStatus + } + return PlannedPlanStatus + + case ApplyCommand: + if p.Error != nil { + return ErroredApplyStatus + } else if p.Failure != "" { + return ErroredApplyStatus + } + return AppliedPlanStatus + } + + panic("PlanStatus() missing a combination") +} + // IsSuccessful returns true if this project result had no errors. func (p ProjectResult) IsSuccessful() bool { return p.PlanSuccess != nil || p.ApplySuccess != "" @@ -401,6 +425,9 @@ const ( // PlannedPlanStatus means that a plan has been successfully generated but // not yet applied. PlannedPlanStatus + // ErrorApplyStatus means that a plan has been generated but there was an + // error while applying it. + ErroredApplyStatus // AppliedPlanStatus means that a plan has been generated and applied // successfully. AppliedPlanStatus @@ -410,13 +437,15 @@ const ( func (p ProjectPlanStatus) String() string { switch p { case ErroredPlanStatus: - return "errored" + return "plan_errored" case PlannedPlanStatus: return "planned" + case ErroredApplyStatus: + return "apply_errored" case AppliedPlanStatus: return "applied" default: - return "errored" + panic("missing String() impl for ProjectPlanStatus") } } From 4fc22aedeb7238ed23f887d8b42ca41d28fd4a07 Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Tue, 5 Mar 2019 13:37:47 -0500 Subject: [PATCH 4/4] Split status checks into plan and apply. Split single top-level status check into one plan and one apply check. These checks will also now track the whole pull request status rather than just the last action. --- e2e/e2e.go | 2 +- server/events/command_runner.go | 59 +++++- server/events/command_runner_internal_test.go | 145 ++++++++++++++ server/events/command_runner_test.go | 4 +- server/events/commit_status_updater.go | 56 +++--- server/events/commit_status_updater_test.go | 179 +++++++++--------- server/events/db/boltdb_test.go | 25 +++ .../mocks/mock_commit_status_updater.go | 65 ++++--- server/events/models/models.go | 11 ++ server/events/models/models_test.go | 80 ++++++++ server/events_controller_e2e_test.go | 2 +- 11 files changed, 470 insertions(+), 158 deletions(-) create mode 100644 server/events/command_runner_internal_test.go diff --git a/e2e/e2e.go b/e2e/e2e.go index 5c3ef22b5e..eadd88e3e0 100644 --- a/e2e/e2e.go +++ b/e2e/e2e.go @@ -166,7 +166,7 @@ func getAtlantisStatus(t *E2ETester, branchName string) (string, error) { } for _, status := range combinedStatus.Statuses { - if status.GetContext() == "Atlantis" { + if status.GetContext() == "plan/atlantis" { return status.GetState(), nil } } diff --git a/server/events/command_runner.go b/server/events/command_runner.go index 3662d90994..7501ea8e5a 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -91,18 +91,26 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo if !c.validateCtxAndComment(ctx) { return } - if err := c.CommitStatusUpdater.Update(ctx.BaseRepo, ctx.Pull, models.PendingCommitStatus, models.PlanCommand); err != nil { + + if err := c.CommitStatusUpdater.UpdateCombined(ctx.BaseRepo, ctx.Pull, models.PendingCommitStatus, models.PlanCommand); err != nil { ctx.Log.Warn("unable to update commit status: %s", err) } projectCmds, err := c.ProjectCommandBuilder.BuildAutoplanCommands(ctx) if err != nil { + if statusErr := c.CommitStatusUpdater.UpdateCombined(ctx.BaseRepo, ctx.Pull, models.FailedCommitStatus, models.PlanCommand); statusErr != nil { + ctx.Log.Warn("unable to update commit status: %s", statusErr) + } + c.updatePull(ctx, AutoplanCommand{}, CommandResult{Error: err}) return } if len(projectCmds) == 0 { log.Info("determined there was no project to run plan in") - if err := c.CommitStatusUpdater.Update(baseRepo, pull, models.SuccessCommitStatus, models.PlanCommand); err != nil { + // If there were no projects modified, we set a successful commit status + // with 0/0 projects planned successfully because we've already set an + // in-progress status and we don't want that to be "in progress" forever. + if err := c.CommitStatusUpdater.UpdateCombinedCount(baseRepo, pull, models.SuccessCommitStatus, models.PlanCommand, 0, 0); err != nil { ctx.Log.Warn("unable to update commit status: %s", err) } return @@ -115,10 +123,12 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo result.PlansDeleted = true } c.updatePull(ctx, AutoplanCommand{}, result) - _, err = c.updateDB(ctx, ctx.Pull, result.ProjectResults) + pullStatus, err := c.updateDB(ctx, ctx.Pull, result.ProjectResults) if err != nil { c.Logger.Err("writing results: %s", err) } + + c.updateCommitStatus(ctx, models.PlanCommand, pullStatus) } // RunCommentCommand executes the command. @@ -184,7 +194,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead ctx.Log.Info("pull request mergeable status: %t", ctx.PullMergeable) } - if err = c.CommitStatusUpdater.Update(baseRepo, pull, models.PendingCommitStatus, cmd.CommandName()); err != nil { + if err = c.CommitStatusUpdater.UpdateCombined(baseRepo, pull, models.PendingCommitStatus, cmd.CommandName()); err != nil { ctx.Log.Warn("unable to update commit status: %s", err) } @@ -199,6 +209,9 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead return } if err != nil { + if statusErr := c.CommitStatusUpdater.UpdateCombined(ctx.BaseRepo, ctx.Pull, models.FailedCommitStatus, cmd.CommandName()); statusErr != nil { + ctx.Log.Warn("unable to update commit status: %s", statusErr) + } c.updatePull(ctx, cmd, CommandResult{Error: err}) return } @@ -220,11 +233,45 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead return } + c.updateCommitStatus(ctx, cmd.Name, pullStatus) + if cmd.Name == models.ApplyCommand && c.automergeEnabled(ctx, projectCmds) { c.automerge(ctx, pullStatus) } } +func (c *DefaultCommandRunner) updateCommitStatus(ctx *CommandContext, cmd models.CommandName, pullStatus models.PullStatus) { + var numSuccess int + var status models.CommitStatus + + if cmd == models.PlanCommand { + // We consider anything that isn't a plan error as a plan success. + // For example, if there is an apply error, that means that at least a + // plan was generated successfully. + numSuccess = len(pullStatus.Projects) - pullStatus.StatusCount(models.ErroredPlanStatus) + status = models.SuccessCommitStatus + if numSuccess != len(pullStatus.Projects) { + status = models.FailedCommitStatus + } + } else { + numSuccess = pullStatus.StatusCount(models.AppliedPlanStatus) + + numErrored := pullStatus.StatusCount(models.ErroredApplyStatus) + status = models.SuccessCommitStatus + if numErrored > 0 { + status = models.FailedCommitStatus + } else if numSuccess < len(pullStatus.Projects) { + // If there are plans that haven't been applied yet, we'll use a pending + // status. + status = models.PendingCommitStatus + } + } + + if err := c.CommitStatusUpdater.UpdateCombinedCount(ctx.BaseRepo, ctx.Pull, status, cmd, numSuccess, len(pullStatus.Projects)); err != nil { + ctx.Log.Warn("unable to update commit status: %s", err) + } +} + func (c *DefaultCommandRunner) automerge(ctx *CommandContext, pullStatus models.PullStatus) { // We only automerge if all projects have been successfully applied. for _, p := range pullStatus.Projects { @@ -328,10 +375,6 @@ func (c *DefaultCommandRunner) updatePull(ctx *CommandContext, command PullComma ctx.Log.Warn(res.Failure) } - // Update the pull request's status icon and comment back. - if err := c.CommitStatusUpdater.UpdateProjectResult(ctx, command.CommandName(), res); err != nil { - ctx.Log.Warn("unable to update commit status: %s", err) - } comment := c.MarkdownRenderer.Render(res, command.CommandName(), ctx.Log.History.String(), command.IsVerbose(), ctx.BaseRepo.VCSHost.Type) if err := c.VCSClient.CreateComment(ctx.BaseRepo, ctx.Pull.Num, comment); err != nil { ctx.Log.Err("unable to comment: %s", err) diff --git a/server/events/command_runner_internal_test.go b/server/events/command_runner_internal_test.go new file mode 100644 index 0000000000..9ee1a415e8 --- /dev/null +++ b/server/events/command_runner_internal_test.go @@ -0,0 +1,145 @@ +package events + +import ( + "github.com/runatlantis/atlantis/server/events/models" + . "github.com/runatlantis/atlantis/testing" + "testing" +) + +func TestUpdateCommitStatus(t *testing.T) { + cases := map[string]struct { + cmd models.CommandName + pullStatus models.PullStatus + expStatus models.CommitStatus + expNumSuccess int + expNumTotal int + }{ + "single plan success": { + cmd: models.PlanCommand, + pullStatus: models.PullStatus{ + Projects: []models.ProjectStatus{ + { + Status: models.PlannedPlanStatus, + }, + }, + }, + expStatus: models.SuccessCommitStatus, + expNumSuccess: 1, + expNumTotal: 1, + }, + "one plan error, other errors": { + cmd: models.PlanCommand, + pullStatus: models.PullStatus{ + Projects: []models.ProjectStatus{ + { + Status: models.ErroredPlanStatus, + }, + { + Status: models.PlannedPlanStatus, + }, + { + Status: models.AppliedPlanStatus, + }, + { + Status: models.ErroredApplyStatus, + }, + }, + }, + expStatus: models.FailedCommitStatus, + expNumSuccess: 3, + expNumTotal: 4, + }, + "apply, one pending": { + cmd: models.ApplyCommand, + pullStatus: models.PullStatus{ + Projects: []models.ProjectStatus{ + { + Status: models.PlannedPlanStatus, + }, + { + Status: models.AppliedPlanStatus, + }, + }, + }, + expStatus: models.PendingCommitStatus, + expNumSuccess: 1, + expNumTotal: 2, + }, + "apply, all successful": { + cmd: models.ApplyCommand, + pullStatus: models.PullStatus{ + Projects: []models.ProjectStatus{ + { + Status: models.AppliedPlanStatus, + }, + { + Status: models.AppliedPlanStatus, + }, + }, + }, + expStatus: models.SuccessCommitStatus, + expNumSuccess: 2, + expNumTotal: 2, + }, + "apply, one errored, one pending": { + cmd: models.ApplyCommand, + pullStatus: models.PullStatus{ + Projects: []models.ProjectStatus{ + { + Status: models.AppliedPlanStatus, + }, + { + Status: models.ErroredApplyStatus, + }, + { + Status: models.PlannedPlanStatus, + }, + }, + }, + expStatus: models.FailedCommitStatus, + expNumSuccess: 1, + expNumTotal: 3, + }, + } + + for name, c := range cases { + t.Run(name, func(t *testing.T) { + csu := &MockCSU{} + cr := &DefaultCommandRunner{ + CommitStatusUpdater: csu, + } + cr.updateCommitStatus(&CommandContext{}, c.cmd, c.pullStatus) + Equals(t, models.Repo{}, csu.CalledRepo) + Equals(t, models.PullRequest{}, csu.CalledPull) + Equals(t, c.expStatus, csu.CalledStatus) + Equals(t, c.cmd, csu.CalledCommand) + Equals(t, c.expNumSuccess, csu.CalledNumSuccess) + Equals(t, c.expNumTotal, csu.CalledNumTotal) + }) + } +} + +type MockCSU struct { + CalledRepo models.Repo + CalledPull models.PullRequest + CalledStatus models.CommitStatus + CalledCommand models.CommandName + CalledNumSuccess int + CalledNumTotal int +} + +func (m *MockCSU) UpdateCombinedCount(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName, numSuccess int, numTotal int) error { + m.CalledRepo = repo + m.CalledPull = pull + m.CalledStatus = status + m.CalledCommand = command + m.CalledNumSuccess = numSuccess + m.CalledNumTotal = numTotal + return nil +} +func (m *MockCSU) UpdateCombined(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) error { + return nil +} +func (m *MockCSU) UpdateProject(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus, url string) error { + return nil +} diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index 098dbe3426..69d9af019c 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -36,7 +36,6 @@ import ( var projectCommandBuilder *mocks.MockProjectCommandBuilder var projectCommandRunner *mocks.MockProjectCommandRunner var eventParsing *mocks.MockEventParsing -var ghStatus *mocks.MockCommitStatusUpdater var githubGetter *mocks.MockGithubPullGetter var gitlabGetter *mocks.MockGitlabMergeRequestGetter var ch events.DefaultCommandRunner @@ -48,7 +47,6 @@ func setup(t *testing.T) *vcsmocks.MockClient { RegisterMockTestingT(t) projectCommandBuilder = mocks.NewMockProjectCommandBuilder() eventParsing = mocks.NewMockEventParsing() - ghStatus = mocks.NewMockCommitStatusUpdater() vcsClient := vcsmocks.NewMockClient() githubGetter = mocks.NewMockGithubPullGetter() gitlabGetter = mocks.NewMockGitlabMergeRequestGetter() @@ -62,7 +60,7 @@ func setup(t *testing.T) *vcsmocks.MockClient { ThenReturn(pullLogger) ch = events.DefaultCommandRunner{ VCSClient: vcsClient, - CommitStatusUpdater: ghStatus, + CommitStatusUpdater: &events.DefaultCommitStatusUpdater{vcsClient}, EventParser: eventParsing, MarkdownRenderer: &events.MarkdownRenderer{}, GithubPullGetter: githubGetter, diff --git a/server/events/commit_status_updater.go b/server/events/commit_status_updater.go index 2da397a1f6..c142155c7c 100644 --- a/server/events/commit_status_updater.go +++ b/server/events/commit_status_updater.go @@ -26,12 +26,12 @@ import ( // CommitStatusUpdater updates the status of a commit with the VCS host. We set // the status to signify whether the plan/apply succeeds. type CommitStatusUpdater interface { - // Update updates the status of the head commit of pull. - Update(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) error - // UpdateProjectResult updates the status of the head commit given the - // state of response. - // todo: rename this so it doesn't conflict with UpdateProject - UpdateProjectResult(ctx *CommandContext, commandName models.CommandName, res CommandResult) error + // UpdateCombined updates the combined status of the head commit of pull. + // A combined status represents all the projects modified in the pull. + UpdateCombined(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) error + // UpdateCombinedCount updates the combined status to reflect the + // numSuccess out of numTotal. + UpdateCombinedCount(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName, numSuccess int, numTotal int) error // UpdateProject sets the commit status for the project represented by // ctx. UpdateProject(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus, url string) error @@ -42,25 +42,28 @@ type DefaultCommitStatusUpdater struct { Client vcs.Client } -// Update updates the commit status. -func (d *DefaultCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) error { - description := fmt.Sprintf("%s %s", strings.Title(command.String()), strings.Title(status.String())) - return d.Client.UpdateStatus(repo, pull, status, "Atlantis", description, "") +func (d *DefaultCommitStatusUpdater) UpdateCombined(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) error { + src := fmt.Sprintf("%s/atlantis", command.String()) + var descripWords string + switch status { + case models.PendingCommitStatus: + descripWords = "in progress..." + case models.FailedCommitStatus: + descripWords = "failed." + case models.SuccessCommitStatus: + descripWords = "succeeded." + } + descrip := fmt.Sprintf("%s %s", strings.Title(command.String()), descripWords) + return d.Client.UpdateStatus(repo, pull, status, src, descrip, "") } -// UpdateProjectResult updates the commit status based on the status of res. -func (d *DefaultCommitStatusUpdater) UpdateProjectResult(ctx *CommandContext, commandName models.CommandName, res CommandResult) error { - var status models.CommitStatus - if res.Error != nil || res.Failure != "" { - status = models.FailedCommitStatus - } else { - var statuses []models.CommitStatus - for _, p := range res.ProjectResults { - statuses = append(statuses, p.CommitStatus()) - } - status = d.worstStatus(statuses) +func (d *DefaultCommitStatusUpdater) UpdateCombinedCount(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName, numSuccess int, numTotal int) error { + src := fmt.Sprintf("%s/atlantis", command.String()) + cmdVerb := "planned" + if command == models.ApplyCommand { + cmdVerb = "applied" } - return d.Update(ctx.BaseRepo, ctx.Pull, status, commandName) + return d.Client.UpdateStatus(repo, pull, status, src, fmt.Sprintf("%d/%d projects %s successfully.", numSuccess, numTotal, cmdVerb), "") } func (d *DefaultCommitStatusUpdater) UpdateProject(ctx models.ProjectCommandContext, cmdName models.CommandName, status models.CommitStatus, url string) error { @@ -81,12 +84,3 @@ func (d *DefaultCommitStatusUpdater) UpdateProject(ctx models.ProjectCommandCont descrip := fmt.Sprintf("%s %s", strings.Title(cmdName.String()), descripWords) return d.Client.UpdateStatus(ctx.BaseRepo, ctx.Pull, status, src, descrip, url) } - -func (d *DefaultCommitStatusUpdater) worstStatus(ss []models.CommitStatus) models.CommitStatus { - for _, s := range ss { - if s == models.FailedCommitStatus { - return models.FailedCommitStatus - } - } - return models.SuccessCommitStatus -} diff --git a/server/events/commit_status_updater_test.go b/server/events/commit_status_updater_test.go index 4a16a3e9e5..27c78718f8 100644 --- a/server/events/commit_status_updater_test.go +++ b/server/events/commit_status_updater_test.go @@ -14,10 +14,8 @@ package events_test import ( - "errors" "fmt" "github.com/runatlantis/atlantis/server/events/yaml/valid" - "strings" "testing" . "github.com/petergtz/pegomock" @@ -27,113 +25,120 @@ import ( . "github.com/runatlantis/atlantis/testing" ) -var repoModel = models.Repo{} -var pullModel = models.PullRequest{} -var status = models.SuccessCommitStatus - -func TestUpdate(t *testing.T) { - RegisterMockTestingT(t) - client := mocks.NewMockClient() - s := events.DefaultCommitStatusUpdater{Client: client} - err := s.Update(repoModel, pullModel, status, models.PlanCommand) - Ok(t, err) - client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, status, "Atlantis", "Plan Success", "") -} - -func TestUpdateProjectResult_Error(t *testing.T) { - RegisterMockTestingT(t) - ctx := &events.CommandContext{ - BaseRepo: repoModel, - Pull: pullModel, - } - client := mocks.NewMockClient() - s := events.DefaultCommitStatusUpdater{Client: client} - err := s.UpdateProjectResult(ctx, models.PlanCommand, events.CommandResult{Error: errors.New("err")}) - Ok(t, err) - client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, models.FailedCommitStatus, "Atlantis", "Plan Failed", "") -} - -func TestUpdateProjectResult_Failure(t *testing.T) { - RegisterMockTestingT(t) - ctx := &events.CommandContext{ - BaseRepo: repoModel, - Pull: pullModel, +func TestUpdateCombined(t *testing.T) { + cases := []struct { + status models.CommitStatus + command models.CommandName + expDescrip string + }{ + { + status: models.PendingCommitStatus, + command: models.PlanCommand, + expDescrip: "Plan in progress...", + }, + { + status: models.FailedCommitStatus, + command: models.PlanCommand, + expDescrip: "Plan failed.", + }, + { + status: models.SuccessCommitStatus, + command: models.PlanCommand, + expDescrip: "Plan succeeded.", + }, + { + status: models.PendingCommitStatus, + command: models.ApplyCommand, + expDescrip: "Apply in progress...", + }, + { + status: models.FailedCommitStatus, + command: models.ApplyCommand, + expDescrip: "Apply failed.", + }, + { + status: models.SuccessCommitStatus, + command: models.ApplyCommand, + expDescrip: "Apply succeeded.", + }, } - client := mocks.NewMockClient() - s := events.DefaultCommitStatusUpdater{Client: client} - err := s.UpdateProjectResult(ctx, models.PlanCommand, events.CommandResult{Failure: "failure"}) - Ok(t, err) - client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, models.FailedCommitStatus, "Atlantis", "Plan Failed", "") -} -func TestUpdateProjectResult(t *testing.T) { - RegisterMockTestingT(t) + for _, c := range cases { + t.Run(c.expDescrip, func(t *testing.T) { + RegisterMockTestingT(t) + client := mocks.NewMockClient() + s := events.DefaultCommitStatusUpdater{Client: client} + err := s.UpdateCombined(models.Repo{}, models.PullRequest{}, c.status, c.command) + Ok(t, err) - ctx := &events.CommandContext{ - BaseRepo: repoModel, - Pull: pullModel, + expSrc := fmt.Sprintf("%s/atlantis", c.command) + client.VerifyWasCalledOnce().UpdateStatus(models.Repo{}, models.PullRequest{}, c.status, expSrc, c.expDescrip, "") + }) } +} +func TestUpdateCombinedCount(t *testing.T) { cases := []struct { - Statuses []string - Expected models.CommitStatus + status models.CommitStatus + command models.CommandName + numSuccess int + numTotal int + expDescrip string }{ { - []string{"success", "failure", "error"}, - models.FailedCommitStatus, - }, - { - []string{"failure", "error", "success"}, - models.FailedCommitStatus, + status: models.PendingCommitStatus, + command: models.PlanCommand, + numSuccess: 0, + numTotal: 2, + expDescrip: "0/2 projects planned successfully.", }, { - []string{"success", "failure"}, - models.FailedCommitStatus, + status: models.FailedCommitStatus, + command: models.PlanCommand, + numSuccess: 1, + numTotal: 2, + expDescrip: "1/2 projects planned successfully.", }, { - []string{"success", "error"}, - models.FailedCommitStatus, + status: models.SuccessCommitStatus, + command: models.PlanCommand, + numSuccess: 2, + numTotal: 2, + expDescrip: "2/2 projects planned successfully.", }, { - []string{"failure", "error"}, - models.FailedCommitStatus, + status: models.FailedCommitStatus, + command: models.ApplyCommand, + numSuccess: 0, + numTotal: 2, + expDescrip: "0/2 projects applied successfully.", }, { - []string{"success"}, - models.SuccessCommitStatus, + status: models.PendingCommitStatus, + command: models.ApplyCommand, + numSuccess: 1, + numTotal: 2, + expDescrip: "1/2 projects applied successfully.", }, { - []string{"success", "success"}, - models.SuccessCommitStatus, + status: models.SuccessCommitStatus, + command: models.ApplyCommand, + numSuccess: 2, + numTotal: 2, + expDescrip: "2/2 projects applied successfully.", }, } for _, c := range cases { - t.Run(strings.Join(c.Statuses, "-"), func(t *testing.T) { - var results []models.ProjectResult - for _, statusStr := range c.Statuses { - var result models.ProjectResult - switch statusStr { - case "failure": - result = models.ProjectResult{ - Failure: "failure", - } - case "error": - result = models.ProjectResult{ - Error: errors.New("err"), - } - default: - result = models.ProjectResult{} - } - results = append(results, result) - } - resp := events.CommandResult{ProjectResults: results} - + t.Run(c.expDescrip, func(t *testing.T) { + RegisterMockTestingT(t) client := mocks.NewMockClient() s := events.DefaultCommitStatusUpdater{Client: client} - err := s.UpdateProjectResult(ctx, models.PlanCommand, resp) + err := s.UpdateCombinedCount(models.Repo{}, models.PullRequest{}, c.status, c.command, c.numSuccess, c.numTotal) Ok(t, err) - client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, c.Expected, "Atlantis", "Plan "+strings.Title(c.Expected.String()), "") + + expSrc := fmt.Sprintf("%s/atlantis", c.command) + client.VerifyWasCalledOnce().UpdateStatus(models.Repo{}, models.PullRequest{}, c.status, expSrc, c.expDescrip, "") }) } } @@ -177,13 +182,13 @@ func TestDefaultCommitStatusUpdater_UpdateProjectSrc(t *testing.T) { models.PendingCommitStatus, "url") Ok(t, err) - client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, models.PendingCommitStatus, c.expSrc, "Plan in progress...", "url") + client.VerifyWasCalledOnce().UpdateStatus(models.Repo{}, models.PullRequest{}, models.PendingCommitStatus, c.expSrc, "Plan in progress...", "url") }) } } // Test that it uses the right words in the description. -func TestDefaultCommitStatusUpdater_UpdateProjectStatus(t *testing.T) { +func TestDefaultCommitStatusUpdater_UpdateProject(t *testing.T) { RegisterMockTestingT(t) cases := []struct { status models.CommitStatus @@ -234,7 +239,7 @@ func TestDefaultCommitStatusUpdater_UpdateProjectStatus(t *testing.T) { c.status, "url") Ok(t, err) - client.VerifyWasCalledOnce().UpdateStatus(repoModel, pullModel, c.status, fmt.Sprintf("%s/atlantis: ./default", c.cmd.String()), c.expDescrip, "url") + client.VerifyWasCalledOnce().UpdateStatus(models.Repo{}, models.PullRequest{}, c.status, fmt.Sprintf("%s/atlantis: ./default", c.cmd.String()), c.expDescrip, "url") }) } } diff --git a/server/events/db/boltdb_test.go b/server/events/db/boltdb_test.go index 3be86635bb..4656dadb2c 100644 --- a/server/events/db/boltdb_test.go +++ b/server/events/db/boltdb_test.go @@ -381,6 +381,7 @@ func TestPullStatus_UpdateGet(t *testing.T) { pull, []models.ProjectResult{ { + Command: models.PlanCommand, RepoRelDir: ".", Workspace: "default", Failure: "failure", @@ -600,11 +601,20 @@ func TestPullStatus_UpdateMerge(t *testing.T) { pull, []models.ProjectResult{ { + Command: models.PlanCommand, RepoRelDir: "mergeme", Workspace: "default", Failure: "failure", }, { + Command: models.PlanCommand, + RepoRelDir: "projectname", + Workspace: "default", + ProjectName: "projectname", + Failure: "failure", + }, + { + Command: models.PlanCommand, RepoRelDir: "staythesame", Workspace: "default", PlanSuccess: &models.PlanSuccess{ @@ -620,11 +630,20 @@ func TestPullStatus_UpdateMerge(t *testing.T) { updateStatus, err := b.UpdatePullWithResults(pull, []models.ProjectResult{ { + Command: models.ApplyCommand, RepoRelDir: "mergeme", Workspace: "default", ApplySuccess: "applied!", }, { + Command: models.ApplyCommand, + RepoRelDir: "projectname", + Workspace: "default", + ProjectName: "projectname", + Error: errors.New("apply error"), + }, + { + Command: models.ApplyCommand, RepoRelDir: "newresult", Workspace: "default", ApplySuccess: "success!", @@ -645,6 +664,12 @@ func TestPullStatus_UpdateMerge(t *testing.T) { Workspace: "default", Status: models.AppliedPlanStatus, }, + { + RepoRelDir: "projectname", + Workspace: "default", + ProjectName: "projectname", + Status: models.ErroredApplyStatus, + }, { RepoRelDir: "staythesame", Workspace: "default", diff --git a/server/events/mocks/mock_commit_status_updater.go b/server/events/mocks/mock_commit_status_updater.go index dc5c5f4a59..13d225b259 100644 --- a/server/events/mocks/mock_commit_status_updater.go +++ b/server/events/mocks/mock_commit_status_updater.go @@ -5,7 +5,6 @@ package mocks import ( pegomock "github.com/petergtz/pegomock" - events "github.com/runatlantis/atlantis/server/events" models "github.com/runatlantis/atlantis/server/events/models" "reflect" "time" @@ -19,12 +18,12 @@ func NewMockCommitStatusUpdater() *MockCommitStatusUpdater { return &MockCommitStatusUpdater{fail: pegomock.GlobalFailHandler} } -func (mock *MockCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) error { +func (mock *MockCommitStatusUpdater) UpdateCombined(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockCommitStatusUpdater().") } params := []pegomock.Param{repo, pull, status, command} - result := pegomock.GetGenericMockFrom(mock).Invoke("Update", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + result := pegomock.GetGenericMockFrom(mock).Invoke("UpdateCombined", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) var ret0 error if len(result) != 0 { if result[0] != nil { @@ -34,12 +33,12 @@ func (mock *MockCommitStatusUpdater) Update(repo models.Repo, pull models.PullRe return ret0 } -func (mock *MockCommitStatusUpdater) UpdateProjectResult(ctx *events.CommandContext, commandName models.CommandName, res events.CommandResult) error { +func (mock *MockCommitStatusUpdater) UpdateCombinedCount(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName, numSuccess int, numTotal int) error { if mock == nil { panic("mock must not be nil. Use myMock := NewMockCommitStatusUpdater().") } - params := []pegomock.Param{ctx, commandName, res} - result := pegomock.GetGenericMockFrom(mock).Invoke("UpdateProjectResult", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) + params := []pegomock.Param{repo, pull, status, command, numSuccess, numTotal} + result := pegomock.GetGenericMockFrom(mock).Invoke("UpdateCombinedCount", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) var ret0 error if len(result) != 0 { if result[0] != nil { @@ -101,23 +100,23 @@ type VerifierCommitStatusUpdater struct { timeout time.Duration } -func (verifier *VerifierCommitStatusUpdater) Update(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) *CommitStatusUpdater_Update_OngoingVerification { +func (verifier *VerifierCommitStatusUpdater) UpdateCombined(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName) *CommitStatusUpdater_UpdateCombined_OngoingVerification { params := []pegomock.Param{repo, pull, status, command} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Update", params, verifier.timeout) - return &CommitStatusUpdater_Update_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UpdateCombined", params, verifier.timeout) + return &CommitStatusUpdater_UpdateCombined_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } -type CommitStatusUpdater_Update_OngoingVerification struct { +type CommitStatusUpdater_UpdateCombined_OngoingVerification struct { mock *MockCommitStatusUpdater methodInvocations []pegomock.MethodInvocation } -func (c *CommitStatusUpdater_Update_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, models.CommitStatus, models.CommandName) { +func (c *CommitStatusUpdater_UpdateCombined_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, models.CommitStatus, models.CommandName) { repo, pull, status, command := c.GetAllCapturedArguments() return repo[len(repo)-1], pull[len(pull)-1], status[len(status)-1], command[len(command)-1] } -func (c *CommitStatusUpdater_Update_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []models.CommitStatus, _param3 []models.CommandName) { +func (c *CommitStatusUpdater_UpdateCombined_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []models.CommitStatus, _param3 []models.CommandName) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { _param0 = make([]models.Repo, len(params[0])) @@ -140,36 +139,48 @@ func (c *CommitStatusUpdater_Update_OngoingVerification) GetAllCapturedArguments return } -func (verifier *VerifierCommitStatusUpdater) UpdateProjectResult(ctx *events.CommandContext, commandName models.CommandName, res events.CommandResult) *CommitStatusUpdater_UpdateProjectResult_OngoingVerification { - params := []pegomock.Param{ctx, commandName, res} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UpdateProjectResult", params, verifier.timeout) - return &CommitStatusUpdater_UpdateProjectResult_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +func (verifier *VerifierCommitStatusUpdater) UpdateCombinedCount(repo models.Repo, pull models.PullRequest, status models.CommitStatus, command models.CommandName, numSuccess int, numTotal int) *CommitStatusUpdater_UpdateCombinedCount_OngoingVerification { + params := []pegomock.Param{repo, pull, status, command, numSuccess, numTotal} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UpdateCombinedCount", params, verifier.timeout) + return &CommitStatusUpdater_UpdateCombinedCount_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } -type CommitStatusUpdater_UpdateProjectResult_OngoingVerification struct { +type CommitStatusUpdater_UpdateCombinedCount_OngoingVerification struct { mock *MockCommitStatusUpdater methodInvocations []pegomock.MethodInvocation } -func (c *CommitStatusUpdater_UpdateProjectResult_OngoingVerification) GetCapturedArguments() (*events.CommandContext, models.CommandName, events.CommandResult) { - ctx, commandName, res := c.GetAllCapturedArguments() - return ctx[len(ctx)-1], commandName[len(commandName)-1], res[len(res)-1] +func (c *CommitStatusUpdater_UpdateCombinedCount_OngoingVerification) GetCapturedArguments() (models.Repo, models.PullRequest, models.CommitStatus, models.CommandName, int, int) { + repo, pull, status, command, numSuccess, numTotal := c.GetAllCapturedArguments() + return repo[len(repo)-1], pull[len(pull)-1], status[len(status)-1], command[len(command)-1], numSuccess[len(numSuccess)-1], numTotal[len(numTotal)-1] } -func (c *CommitStatusUpdater_UpdateProjectResult_OngoingVerification) GetAllCapturedArguments() (_param0 []*events.CommandContext, _param1 []models.CommandName, _param2 []events.CommandResult) { +func (c *CommitStatusUpdater_UpdateCombinedCount_OngoingVerification) GetAllCapturedArguments() (_param0 []models.Repo, _param1 []models.PullRequest, _param2 []models.CommitStatus, _param3 []models.CommandName, _param4 []int, _param5 []int) { params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) if len(params) > 0 { - _param0 = make([]*events.CommandContext, len(params[0])) + _param0 = make([]models.Repo, len(params[0])) for u, param := range params[0] { - _param0[u] = param.(*events.CommandContext) + _param0[u] = param.(models.Repo) } - _param1 = make([]models.CommandName, len(params[1])) + _param1 = make([]models.PullRequest, len(params[1])) for u, param := range params[1] { - _param1[u] = param.(models.CommandName) + _param1[u] = param.(models.PullRequest) } - _param2 = make([]events.CommandResult, len(params[2])) + _param2 = make([]models.CommitStatus, len(params[2])) for u, param := range params[2] { - _param2[u] = param.(events.CommandResult) + _param2[u] = param.(models.CommitStatus) + } + _param3 = make([]models.CommandName, len(params[3])) + for u, param := range params[3] { + _param3[u] = param.(models.CommandName) + } + _param4 = make([]int, len(params[4])) + for u, param := range params[4] { + _param4[u] = param.(int) + } + _param5 = make([]int, len(params[5])) + for u, param := range params[5] { + _param5[u] = param.(int) } } return diff --git a/server/events/models/models.go b/server/events/models/models.go index 4bfba808e4..9f2bd4c240 100644 --- a/server/events/models/models.go +++ b/server/events/models/models.go @@ -405,6 +405,17 @@ type PullStatus struct { Pull PullRequest } +// StatusCount returns the number of projects that have status. +func (p PullStatus) StatusCount(status ProjectPlanStatus) int { + c := 0 + for _, pr := range p.Projects { + if pr.Status == status { + c++ + } + } + return c +} + // ProjectStatus is the status of a specific project. type ProjectStatus struct { Workspace string diff --git a/server/events/models/models_test.go b/server/events/models/models_test.go index 85c8864fd8..fe6857cb5f 100644 --- a/server/events/models/models_test.go +++ b/server/events/models/models_test.go @@ -294,3 +294,83 @@ func TestProjectResult_IsSuccessful(t *testing.T) { }) } } + +func TestProjectResult_PlanStatus(t *testing.T) { + cases := []struct { + p models.ProjectResult + expStatus models.ProjectPlanStatus + }{ + { + p: models.ProjectResult{ + Command: models.PlanCommand, + Error: errors.New("err"), + }, + expStatus: models.ErroredPlanStatus, + }, + { + p: models.ProjectResult{ + Command: models.PlanCommand, + Failure: "failure", + }, + expStatus: models.ErroredPlanStatus, + }, + { + p: models.ProjectResult{ + Command: models.PlanCommand, + PlanSuccess: &models.PlanSuccess{}, + }, + expStatus: models.PlannedPlanStatus, + }, + { + p: models.ProjectResult{ + Command: models.ApplyCommand, + Error: errors.New("err"), + }, + expStatus: models.ErroredApplyStatus, + }, + { + p: models.ProjectResult{ + Command: models.ApplyCommand, + Failure: "failure", + }, + expStatus: models.ErroredApplyStatus, + }, + { + p: models.ProjectResult{ + Command: models.ApplyCommand, + ApplySuccess: "success", + }, + expStatus: models.AppliedPlanStatus, + }, + } + + for _, c := range cases { + t.Run(c.expStatus.String(), func(t *testing.T) { + Equals(t, c.expStatus, c.p.PlanStatus()) + }) + } +} + +func TestPullStatus_StatusCount(t *testing.T) { + ps := models.PullStatus{ + Projects: []models.ProjectStatus{ + { + Status: models.PlannedPlanStatus, + }, + { + Status: models.PlannedPlanStatus, + }, + { + Status: models.AppliedPlanStatus, + }, + { + Status: models.ErroredApplyStatus, + }, + }, + } + + Equals(t, 2, ps.StatusCount(models.PlannedPlanStatus)) + Equals(t, 1, ps.StatusCount(models.AppliedPlanStatus)) + Equals(t, 1, ps.StatusCount(models.ErroredApplyStatus)) + Equals(t, 0, ps.StatusCount(models.ErroredPlanStatus)) +} diff --git a/server/events_controller_e2e_test.go b/server/events_controller_e2e_test.go index 1f115f0efb..b1c78d49f7 100644 --- a/server/events_controller_e2e_test.go +++ b/server/events_controller_e2e_test.go @@ -355,7 +355,7 @@ func setupE2E(t *testing.T) (server.EventsController, *vcsmocks.MockClient, *moc // Mocks. e2eVCSClient := vcsmocks.NewMockClient() - e2eStatusUpdater := mocks.NewMockCommitStatusUpdater() + e2eStatusUpdater := &events.DefaultCommitStatusUpdater{Client: e2eVCSClient} e2eGithubGetter := mocks.NewMockGithubPullGetter() e2eGitlabGetter := mocks.NewMockGitlabMergeRequestGetter()