Skip to content

Commit 042bd42

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

File tree

2 files changed

+55
-10
lines changed

2 files changed

+55
-10
lines changed

cli/ssh_test.go

Lines changed: 41 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,33 @@ 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{})
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+
if r.Method == http.MethodPost && buildURL.MatchString(r.URL.Path) {
162+
if !isFirstBuild {
163+
t.Log("buildSyncMW: blocking build")
164+
buildReq <- struct{}{}
165+
<-buildResume
166+
t.Log("buildSyncMW: resuming build")
167+
}
168+
isFirstBuild = false
169+
}
170+
next.ServeHTTP(w, r)
171+
})
172+
}
173+
151174
authToken := uuid.NewString()
152-
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
175+
ownerClient := coderdtest.New(t, &coderdtest.Options{
176+
IncludeProvisionerDaemon: true,
177+
APIMiddleware: buildSyncMW,
178+
})
153179
owner := coderdtest.CreateFirstUser(t, ownerClient)
154180
client, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
155181
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
@@ -165,21 +191,28 @@ func TestSSH(t *testing.T) {
165191
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
166192
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
167193

168-
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
194+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium)
169195
defer cancel()
170196

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

176-
ptys[i] = ptytest.New(t).Attach(inv)
202+
pty := ptytest.New(t).Attach(inv)
203+
ptys = append(ptys, pty)
177204
clitest.SetupConfig(t, client, root)
178-
clitest.StartWithAssert(t, inv, func(*testing.T, error) {
179-
// Noop.
205+
testutil.Go(t, func() {
206+
_ = inv.WithContext(ctx).Run()
180207
})
181208
}
182209

210+
for _, pty := range ptys {
211+
pty.ExpectMatchContext(ctx, "Workspace was stopped, starting workspace to allow connecting to")
212+
testutil.RequireRecvCtx(ctx, t, buildReq)
213+
}
214+
close(buildResume)
215+
183216
var foundConflict int
184217
for _, pty := range ptys {
185218
// Either allow the command to start the workspace or fail
@@ -191,8 +224,7 @@ func TestSSH(t *testing.T) {
191224
pty.ExpectMatchContext(ctx, "Waiting for the workspace agent to connect")
192225
}
193226
}
194-
// TODO(mafredri): Remove this if it's racy.
195-
require.Greater(t, foundConflict, 0, "expected at least one conflict")
227+
require.Equal(t, foundConflict, 2, "expected 2 conflicts")
196228
})
197229
t.Run("RequireActiveVersion", func(t *testing.T) {
198230
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)