Files
atlantis/server/jobs/project_command_output_handler_test.go
Finn Arne Gangstad 739d6e435b test(command-output): stabilize server jobs test (#3194)
* Bugfix: TestProjectCommandOutputHandler is unstable

Use buffered channels in TestProjectCommandOutputHandler

The output handler registered with
ProjectCommandOutputHandler.Register() needs the channel that
is given to not block writes.  If the channel
blocks writes, the channel is removed from the list
of active receivers on that JobID.

The tests are set up with unbuffered channels, and there
is a race condition where the channel reader does
not hit `for msg := range ch` before the message that
is sent is handled by AsyncProjectCommandOutputHandler.writeLogLine

By making the channel buffered it does not matter
that the reader isn't reading yet.

* Add missing wg.Wait() in TestProjectCommandOutputHandler

There is a rare race condition where messages come in
while the asserts run in the
"clean up all jobs when PR is closed" test.

Added  wg.Wait() to make sure all messages are processed
before running the final asserts.

---------

Co-authored-by: Finn Arne Gangstad <finnag@gmail.com>
2023-03-07 09:24:37 -06:00

253 lines
6.6 KiB
Go

package jobs_test
import (
"sync"
"testing"
"time"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/jobs"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
"github.com/stretchr/testify/assert"
)
func createTestProjectCmdContext(t *testing.T) command.ProjectContext {
logger := logging.NewNoopLogger(t)
return command.ProjectContext{
BaseRepo: models.Repo{
Name: "test-repo",
Owner: "test-org",
},
HeadRepo: models.Repo{
Name: "test-repo",
Owner: "test-org",
},
Pull: models.PullRequest{
Num: 1,
HeadBranch: "main",
BaseBranch: "main",
Author: "test-user",
HeadCommit: "234r232432",
},
User: models.User{
Username: "test-user",
},
Log: logger,
Workspace: "myworkspace",
RepoRelDir: "test-dir",
ProjectName: "test-project",
JobID: "1234",
}
}
func createProjectCommandOutputHandler(t *testing.T) jobs.ProjectCommandOutputHandler {
logger := logging.NewNoopLogger(t)
prjCmdOutputChan := make(chan *jobs.ProjectCmdOutputLine)
prjCmdOutputHandler := jobs.NewAsyncProjectCommandOutputHandler(
prjCmdOutputChan,
logger,
)
go func() {
prjCmdOutputHandler.Handle()
}()
return prjCmdOutputHandler
}
func TestProjectCommandOutputHandler(t *testing.T) {
Msg := "Test Terraform Output"
ctx := createTestProjectCmdContext(t)
t.Run("receive message from main channel", func(t *testing.T) {
var wg sync.WaitGroup
var expectedMsg string
projectOutputHandler := createProjectCommandOutputHandler(t)
ch := make(chan string, 1)
// register channel and backfill from buffer
// Note: We call this synchronously because otherwise
// there could be a race where we are unable to register the channel
// before sending messages due to the way we lock our buffer memory cache
projectOutputHandler.Register(ctx.JobID, ch)
wg.Add(1)
// read from channel
go func() {
for msg := range ch {
expectedMsg = msg
wg.Done()
}
}()
projectOutputHandler.Send(ctx, Msg, false)
wg.Wait()
close(ch)
// Wait for the msg to be read.
wg.Wait()
Equals(t, expectedMsg, Msg)
})
t.Run("copies buffer to new channels", func(t *testing.T) {
var wg sync.WaitGroup
projectOutputHandler := createProjectCommandOutputHandler(t)
// send first message to populated the buffer
projectOutputHandler.Send(ctx, Msg, false)
ch := make(chan string, 2)
receivedMsgs := []string{}
wg.Add(1)
// read from channel asynchronously
go func() {
for msg := range ch {
receivedMsgs = append(receivedMsgs, msg)
// we're only expecting two messages here.
if len(receivedMsgs) >= 2 {
wg.Done()
}
}
}()
// register channel and backfill from buffer
// Note: We call this synchronously because otherwise
// there could be a race where we are unable to register the channel
// before sending messages due to the way we lock our buffer memory cache
projectOutputHandler.Register(ctx.JobID, ch)
projectOutputHandler.Send(ctx, Msg, false)
wg.Wait()
close(ch)
expectedMsgs := []string{Msg, Msg}
assert.Equal(t, len(expectedMsgs), len(receivedMsgs))
for i := range expectedMsgs {
assert.Equal(t, expectedMsgs[i], receivedMsgs[i])
}
})
t.Run("clean up all jobs when PR is closed", func(t *testing.T) {
var wg sync.WaitGroup
projectOutputHandler := createProjectCommandOutputHandler(t)
ch := make(chan string, 2)
// register channel and backfill from buffer
// Note: We call this synchronously because otherwise
// there could be a race where we are unable to register the channel
// before sending messages due to the way we lock our buffer memory cache
projectOutputHandler.Register(ctx.JobID, ch)
wg.Add(1)
// read from channel
go func() {
for msg := range ch {
if msg == "Complete" {
wg.Done()
}
}
}()
projectOutputHandler.Send(ctx, Msg, false)
projectOutputHandler.Send(ctx, "Complete", false)
pullContext := jobs.PullInfo{
PullNum: ctx.Pull.Num,
Repo: ctx.BaseRepo.Name,
ProjectName: ctx.ProjectName,
Workspace: ctx.Workspace,
}
wg.Wait() // Must finish reading messages before cleaning up
projectOutputHandler.CleanUp(pullContext)
// Check all the resources are cleaned up.
dfProjectOutputHandler, ok := projectOutputHandler.(*jobs.AsyncProjectCommandOutputHandler)
assert.True(t, ok)
assert.Empty(t, dfProjectOutputHandler.GetProjectOutputBuffer(ctx.JobID))
assert.Empty(t, dfProjectOutputHandler.GetReceiverBufferForPull(ctx.JobID))
assert.Empty(t, dfProjectOutputHandler.GetJobIDMapForPull(pullContext))
})
t.Run("mark operation status complete and close conn buffers for the job", func(t *testing.T) {
projectOutputHandler := createProjectCommandOutputHandler(t)
ch := make(chan string, 2)
// register channel and backfill from buffer
// Note: We call this synchronously because otherwise
// there could be a race where we are unable to register the channel
// before sending messages due to the way we lock our buffer memory cache
projectOutputHandler.Register(ctx.JobID, ch)
// read from channel
go func() {
for range ch {
}
}()
projectOutputHandler.Send(ctx, Msg, false)
projectOutputHandler.Send(ctx, "", true)
// Wait for the handler to process the message
time.Sleep(10 * time.Millisecond)
dfProjectOutputHandler, ok := projectOutputHandler.(*jobs.AsyncProjectCommandOutputHandler)
assert.True(t, ok)
outputBuffer := dfProjectOutputHandler.GetProjectOutputBuffer(ctx.JobID)
assert.True(t, outputBuffer.OperationComplete)
_, ok = (<-ch)
assert.False(t, ok)
})
t.Run("close conn buffer after streaming logs for completed operation", func(t *testing.T) {
projectOutputHandler := createProjectCommandOutputHandler(t)
ch := make(chan string)
// register channel and backfill from buffer
// Note: We call this synchronously because otherwise
// there could be a race where we are unable to register the channel
// before sending messages due to the way we lock our buffer memory cache
projectOutputHandler.Register(ctx.JobID, ch)
// read from channel
go func() {
for range ch {
}
}()
projectOutputHandler.Send(ctx, Msg, false)
projectOutputHandler.Send(ctx, "", true)
// Wait for the handler to process the message
time.Sleep(10 * time.Millisecond)
ch2 := make(chan string, 2)
opComplete := make(chan bool)
// buffer channel will be closed immediately after logs are streamed
go func() {
for range ch2 {
}
opComplete <- true
}()
projectOutputHandler.Register(ctx.JobID, ch2)
assert.True(t, <-opComplete)
})
}