Skip to content

Commit

Permalink
Move Deploy and Waiter to pubic packages (#662)
Browse files Browse the repository at this point in the history
  • Loading branch information
kegsay authored Oct 9, 2023
1 parent 00b3326 commit 0e35eaa
Show file tree
Hide file tree
Showing 13 changed files with 182 additions and 197 deletions.
81 changes: 81 additions & 0 deletions helpers/test_package.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package helpers

import (
"context"
"fmt"
"log"
"sync/atomic"
"testing"
"time"

"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/internal/config"
"github.com/matrix-org/complement/internal/docker"
"github.com/sirupsen/logrus"
)

// TestPackage represents the configuration for a package of tests. A package of tests
// are all tests in the same Go package (directory).
type TestPackage struct {
// the config used for this package.
Config *config.Complement
// the builder we'll use to make containers
complementBuilder *docker.Builder
// a counter to stop tests from allocating the same container name
namespaceCounter uint64
}

// NewTestPackage creates a new test package which can be used to deploy containers for all tests
// in a single package. This should be called from `TestMain` which is the Go-provided entry point
// before any tests run. After the tests have run, call `TestPackage.Cleanup`. Tests can deploy
// containers by calling `TestPackage.Deploy`.
func NewTestPackage(pkgNamespace string) (*TestPackage, error) {
cfg := config.NewConfigFromEnvVars(pkgNamespace, "")
log.Printf("config: %+v", cfg)
builder, err := docker.NewBuilder(cfg)
if err != nil {
return nil, fmt.Errorf("failed to make docker builder: %w", err)
}
// remove any old images/containers/networks in case we died horribly before
builder.Cleanup()

// we use GMSL which uses logrus by default. We don't want those logs in our test output unless they are Serious.
logrus.SetLevel(logrus.ErrorLevel)

return &TestPackage{
complementBuilder: builder,
namespaceCounter: 0,
Config: cfg,
}, nil
}

func (tp *TestPackage) Cleanup() {
tp.complementBuilder.Cleanup()
}

// Deploy will deploy the given blueprint or terminate the test.
// It will construct the blueprint if it doesn't already exist in the docker image cache.
// This function is the main setup function for all tests as it provides a deployment with
// which tests can interact with.
func (tp *TestPackage) Deploy(t *testing.T, blueprint b.Blueprint) *docker.Deployment {
t.Helper()
timeStartBlueprint := time.Now()
if tp.complementBuilder == nil {
t.Fatalf("complementBuilder not set, did you forget to call TestMain?")
}
if err := tp.complementBuilder.ConstructBlueprintIfNotExist(blueprint); err != nil {
t.Fatalf("Deploy: Failed to construct blueprint: %s", err)
}
namespace := fmt.Sprintf("%d", atomic.AddUint64(&tp.namespaceCounter, 1))
d, err := docker.NewDeployer(namespace, tp.complementBuilder.Config)
if err != nil {
t.Fatalf("Deploy: NewDeployer returned error %s", err)
}
timeStartDeploy := time.Now()
dep, err := d.Deploy(context.Background(), blueprint.Name)
if err != nil {
t.Fatalf("Deploy: Deploy returned error %s", err)
}
t.Logf("Deploy times: %v blueprints, %v containers", timeStartDeploy.Sub(timeStartBlueprint), time.Since(timeStartDeploy))
return dep
}
60 changes: 60 additions & 0 deletions helpers/waiter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package helpers

import (
"fmt"
"sync"
"testing"
"time"
)

// Waiter is a simple primitive to wait for a signal asynchronously. It is preferred
// over other sync primitives due to having more sensible defaults such as built-in timeouts
// if the signal does not appear and ability to signal more than once without panicking.
type Waiter struct {
mu sync.Mutex
ch chan bool
closed bool
}

// NewWaiter returns a generic struct which can be waited on until `Waiter.Finish` is called.
// A Waiter is similar to a `sync.WaitGroup` of size 1, but without the ability to underflow and
// with built-in timeouts.
func NewWaiter() *Waiter {
return &Waiter{
ch: make(chan bool),
mu: sync.Mutex{},
}
}

// Wait blocks until Finish() is called or until the timeout is reached.
// If the timeout is reached, the test is failed.
func (w *Waiter) Wait(t *testing.T, timeout time.Duration) {
t.Helper()
w.Waitf(t, timeout, "Wait")
}

// Waitf blocks until Finish() is called or until the timeout is reached.
// If the timeout is reached, the test is failed with the given error message.
func (w *Waiter) Waitf(t *testing.T, timeout time.Duration, errFormat string, args ...interface{}) {
t.Helper()
select {
case <-w.ch:
return
case <-time.After(timeout):
errmsg := fmt.Sprintf(errFormat, args...)
t.Fatalf("%s: timed out after %f seconds.", errmsg, timeout.Seconds())
}
}

// Finish will cause all goroutines waiting via Wait to stop waiting and return.
// Once this function has been called, subsequent calls to Wait will return immediately.
// To begin waiting again, make a new Waiter.
func (w *Waiter) Finish() {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return
}
w.closed = true
close(w.ch)
}
91 changes: 6 additions & 85 deletions tests/csapi/main_test.go
Original file line number Diff line number Diff line change
@@ -1,48 +1,30 @@
package csapi_tests

import (
"context"
"fmt"
"log"
"os"
"sync"
"sync/atomic"
"testing"
"time"

"github.com/sirupsen/logrus"

"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/internal/config"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/internal/docker"
)

var namespaceCounter uint64

// persist the complement builder which is set when the tests start via TestMain
var complementBuilder *docker.Builder
var testPackage *helpers.TestPackage

// TestMain is the main entry point for Complement.
//
// It will clean up any old containers/images/networks from the previous run, then run the tests, then clean up
// again. No blueprints are made at this point as they are lazily made on demand.
func TestMain(m *testing.M) {
cfg := config.NewConfigFromEnvVars("csapi", "")
log.Printf("config: %+v", cfg)
builder, err := docker.NewBuilder(cfg)
var err error
testPackage, err = helpers.NewTestPackage("csapi")
if err != nil {
fmt.Printf("Error: %s", err)
os.Exit(1)
}
complementBuilder = builder
// remove any old images/containers/networks in case we died horribly before
builder.Cleanup()

// we use GMSL which uses logrus by default. We don't want those logs in our test output unless they are Serious.
logrus.SetLevel(logrus.ErrorLevel)

exitCode := m.Run()
builder.Cleanup()
testPackage.Cleanup()
os.Exit(exitCode)
}

Expand All @@ -52,66 +34,5 @@ func TestMain(m *testing.M) {
// which tests can interact with.
func Deploy(t *testing.T, blueprint b.Blueprint) *docker.Deployment {
t.Helper()
timeStartBlueprint := time.Now()
if complementBuilder == nil {
t.Fatalf("complementBuilder not set, did you forget to call TestMain?")
}
if err := complementBuilder.ConstructBlueprintIfNotExist(blueprint); err != nil {
t.Fatalf("Deploy: Failed to construct blueprint: %s", err)
}
namespace := fmt.Sprintf("%d", atomic.AddUint64(&namespaceCounter, 1))
d, err := docker.NewDeployer(namespace, complementBuilder.Config)
if err != nil {
t.Fatalf("Deploy: NewDeployer returned error %s", err)
}
timeStartDeploy := time.Now()
dep, err := d.Deploy(context.Background(), blueprint.Name)
if err != nil {
t.Fatalf("Deploy: Deploy returned error %s", err)
}
t.Logf("Deploy times: %v blueprints, %v containers", timeStartDeploy.Sub(timeStartBlueprint), time.Since(timeStartDeploy))
return dep
}

// nolint:unused
type Waiter struct {
mu sync.Mutex
ch chan bool
closed bool
}

// NewWaiter returns a generic struct which can be waited on until `Waiter.Finish` is called.
// A Waiter is similar to a `sync.WaitGroup` of size 1, but without the ability to underflow and
// with built-in timeouts.
// nolint:unused
func NewWaiter() *Waiter {
return &Waiter{
ch: make(chan bool),
mu: sync.Mutex{},
}
}

// Wait blocks until Finish() is called or until the timeout is reached.
// If the timeout is reached, the test is failed.
func (w *Waiter) Wait(t *testing.T, timeout time.Duration) {
t.Helper()
select {
case <-w.ch:
return
case <-time.After(timeout):
t.Fatalf("Wait: timed out after %f seconds.", timeout.Seconds())
}
}

// Finish will cause all goroutines waiting via Wait to stop waiting and return.
// Once this function has been called, subsequent calls to Wait will return immediately.
// To begin waiting again, make a new Waiter.
func (w *Waiter) Finish() {
w.mu.Lock()
defer w.mu.Unlock()
if w.closed {
return
}
w.closed = true
close(w.ch)
return testPackage.Deploy(t, blueprint)
}
2 changes: 1 addition & 1 deletion tests/csapi/url_preview_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func TestUrlPreview(t *testing.T) {
deployment := Deploy(t, b.BlueprintAlice)
defer deployment.Destroy(t)

webServer := web.NewServer(t, complementBuilder.Config, func(router *mux.Router) {
webServer := web.NewServer(t, testPackage.Config, func(router *mux.Router) {
router.HandleFunc("/test.png", func(w http.ResponseWriter, req *http.Request) {
t.Log("/test.png fetched")

Expand Down
3 changes: 2 additions & 1 deletion tests/federation_event_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/internal/federation"
"github.com/matrix-org/complement/must"
"github.com/matrix-org/gomatrixserverlib/spec"
Expand Down Expand Up @@ -34,7 +35,7 @@ func TestEventAuth(t *testing.T) {

// create a remote homeserver which will make the /event_auth request
var joinRuleEvent gomatrixserverlib.PDU
waiter := NewWaiter()
waiter := helpers.NewWaiter()
srv := federation.NewServer(t, deployment,
federation.HandleKeyRequests(),
federation.HandleMakeSendJoinRequests(),
Expand Down
3 changes: 2 additions & 1 deletion tests/federation_redaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"time"

"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/internal/federation"
"github.com/matrix-org/complement/must"
"github.com/matrix-org/complement/runtime"
Expand All @@ -20,7 +21,7 @@ func TestFederationRedactSendsWithoutEvent(t *testing.T) {

alice := deployment.Client(t, "hs1", "@alice:hs1")

waiter := NewWaiter()
waiter := helpers.NewWaiter()
wantEventType := "m.room.redaction"

// create a remote homeserver
Expand Down
5 changes: 3 additions & 2 deletions tests/federation_room_get_missing_events_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/client"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/internal/federation"
"github.com/matrix-org/complement/match"
"github.com/matrix-org/complement/must"
Expand Down Expand Up @@ -246,7 +247,7 @@ func TestOutboundFederationIgnoresMissingEventWithBadJSONForRoomVersion6(t *test
})
room.AddEvent(sentEvent)

waiter := NewWaiter()
waiter := helpers.NewWaiter()
onGetMissingEvents = func(w http.ResponseWriter, req *http.Request) {
defer waiter.Finish()
must.MatchRequest(t, req, match.HTTPRequest{
Expand Down Expand Up @@ -300,7 +301,7 @@ func TestOutboundFederationIgnoresMissingEventWithBadJSONForRoomVersion6(t *test
})
room.AddEvent(message3)

waiter = NewWaiter()
waiter = helpers.NewWaiter()
onGetMissingEvents = func(w http.ResponseWriter, req *http.Request) {
must.MatchRequest(t, req, match.HTTPRequest{
JSON: []match.JSON{
Expand Down
7 changes: 4 additions & 3 deletions tests/federation_room_invite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/matrix-org/gomatrixserverlib"

"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/internal/federation"
)

Expand All @@ -22,7 +23,7 @@ func TestFederationRejectInvite(t *testing.T) {
charlie := deployment.Client(t, "hs2", "@charlie:hs2")

// we'll awaken this Waiter when we receive a membership event for Charlie
var waiter *Waiter
var waiter *helpers.Waiter

srv := federation.NewServer(t, deployment,
federation.HandleKeyRequests(),
Expand All @@ -47,13 +48,13 @@ func TestFederationRejectInvite(t *testing.T) {
room := srv.MustJoinRoom(t, deployment, "hs1", roomID, delia)

// Alice invites Charlie; Delia should see the invite
waiter = NewWaiter()
waiter = helpers.NewWaiter()
alice.InviteRoom(t, roomID, charlie.UserID)
waiter.Wait(t, 5*time.Second)
room.MustHaveMembershipForUser(t, charlie.UserID, "invite")

// Charlie rejects the invite; Delia should see the rejection.
waiter = NewWaiter()
waiter = helpers.NewWaiter()
charlie.LeaveRoom(t, roomID)
waiter.Wait(t, 5*time.Second)
room.MustHaveMembershipForUser(t, charlie.UserID, "leave")
Expand Down
Loading

0 comments on commit 0e35eaa

Please sign in to comment.