Skip to content

Commit ced390c

Browse files
committed
fix test racyness, add apimiddleware to coderdtest
1 parent e5bee0b commit ced390c

File tree

2 files changed

+57
-10
lines changed

2 files changed

+57
-10
lines changed

cli/ssh_test.go

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"os/exec"
1818
"path"
1919
"path/filepath"
20+
"regexp"
2021
"runtime"
2122
"strings"
2223
"testing"
@@ -148,8 +149,35 @@ func TestSSH(t *testing.T) {
148149
t.Run("StartStoppedWorkspaceConflict", func(t *testing.T) {
149150
t.Parallel()
150151

152+
// Intercept builds to synchronize execution of the SSH command.
153+
// The purpose here is to make sure all commands try to trigger
154+
// a start build of the workspace.
155+
isFirstBuild := true
156+
buildURL := regexp.MustCompile("/api/v2/workspaces/.*/builds")
157+
buildReq := make(chan struct{}) // Buffer to allow the initial workspace build.
158+
buildResume := make(chan struct{})
159+
buildSyncMW := func(next http.Handler) http.Handler {
160+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
161+
// Block execution until all commands are trying to build to synchronize execution.
162+
t.Log("buildSyncMW", r.Method, r.URL.Path)
163+
if r.Method == http.MethodPost && buildURL.MatchString(r.URL.Path) {
164+
if !isFirstBuild {
165+
t.Log("buildSyncMW: blocking build")
166+
buildReq <- struct{}{}
167+
<-buildResume
168+
t.Log("buildSyncMW: resuming build")
169+
}
170+
isFirstBuild = false
171+
}
172+
next.ServeHTTP(w, r)
173+
})
174+
}
175+
151176
authToken := uuid.NewString()
152-
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
177+
ownerClient := coderdtest.New(t, &coderdtest.Options{
178+
IncludeProvisionerDaemon: true,
179+
APIMiddleware: buildSyncMW,
180+
})
153181
owner := coderdtest.CreateFirstUser(t, ownerClient)
154182
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
155183
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
@@ -165,21 +193,28 @@ func TestSSH(t *testing.T) {
165193
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
166194
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
167195

168-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
196+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
169197
defer cancel()
170198

171-
ptys := make([]*ptytest.PTY, 3)
172-
for i := range ptys {
199+
var ptys []*ptytest.PTY
200+
for i := 0; i < 3; i++ {
173201
// SSH to the workspace which should autostart it
174202
inv, root := clitest.New(t, "ssh", workspace.Name)
175203

176-
ptys[i] = ptytest.New(t).Attach(inv)
204+
pty := ptytest.New(t).Attach(inv)
205+
ptys = append(ptys, pty)
177206
clitest.SetupConfig(t, client, root)
178-
clitest.StartWithAssert(t, inv, func(*testing.T, error) {
179-
// Noop.
207+
testutil.Go(t, func() {
208+
_ = inv.WithContext(ctx).Run()
180209
})
181210
}
182211

212+
for _, pty := range ptys {
213+
pty.ExpectMatchContext(ctx, "Workspace was stopped, starting workspace to allow connecting to")
214+
testutil.RequireRecvCtx(ctx, t, buildReq)
215+
}
216+
close(buildResume)
217+
183218
var foundConflict int
184219
for _, pty := range ptys {
185220
// Either allow the command to start the workspace or fail
@@ -191,8 +226,7 @@ func TestSSH(t *testing.T) {
191226
pty.ExpectMatchContext(ctx, "Waiting for the workspace agent to connect")
192227
}
193228
}
194-
// TODO(mafredri): Remove this if it's racy.
195-
require.Greater(t, foundConflict, 0, "expected at least one conflict")
229+
require.Equal(t, foundConflict, 2, "expected 2 conflicts")
196230
})
197231
t.Run("RequireActiveVersion", func(t *testing.T) {
198232
t.Parallel()

coderd/coderdtest/coderdtest.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333

3434
"cloud.google.com/go/compute/metadata"
3535
"github.com/fullsailor/pkcs7"
36+
"github.com/go-chi/chi/v5"
3637
"github.com/golang-jwt/jwt/v4"
3738
"github.com/google/uuid"
3839
"github.com/moby/moby/pkg/namesgenerator"
@@ -146,6 +147,11 @@ type Options struct {
146147
Database database.Store
147148
Pubsub pubsub.Pubsub
148149

150+
// APIMiddleware inserts middleware before api.RootHandler, this can be
151+
// useful in certain tests where you want to intercept requests before
152+
// passing them on to the API, e.g. for synchronization of execution.
153+
APIMiddleware func(http.Handler) http.Handler
154+
149155
ConfigSSH codersdk.SSHConfigResponse
150156

151157
SwaggerEndpoint bool
@@ -555,7 +561,14 @@ func NewWithAPI(t testing.TB, options *Options) (*codersdk.Client, io.Closer, *c
555561
setHandler, cancelFunc, serverURL, newOptions := NewOptions(t, options)
556562
// We set the handler after server creation for the access URL.
557563
coderAPI := coderd.New(newOptions)
558-
setHandler(coderAPI.RootHandler)
564+
rootHandler := coderAPI.RootHandler
565+
if options.APIMiddleware != nil {
566+
r := chi.NewRouter()
567+
r.Use(options.APIMiddleware)
568+
r.Mount("/", rootHandler)
569+
rootHandler = r
570+
}
571+
setHandler(rootHandler)
559572
var provisionerCloser io.Closer = nopcloser{}
560573
if options.IncludeProvisionerDaemon {
561574
provisionerCloser = NewTaggedProvisionerDaemon(t, coderAPI, "test", options.ProvisionerDaemonTags)

0 commit comments

Comments
 (0)