Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add COMPLEMENT_ENABLE_DIRTY_RUNS to vastly speed up test runs #677

Merged
merged 4 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ jobs:
- homeserver: Synapse
tags: synapse_blacklist
packages: ./tests/msc3874 ./tests/msc3902
env: "COMPLEMENT_SHARE_ENV_PREFIX=PASS_ PASS_SYNAPSE_COMPLEMENT_DATABASE=sqlite"
env: "COMPLEMENT_ENABLE_DIRTY_RUNS=1 COMPLEMENT_SHARE_ENV_PREFIX=PASS_ PASS_SYNAPSE_COMPLEMENT_DATABASE=sqlite"
timeout: 20m

- homeserver: Dendrite
tags: dendrite_blacklist
packages: ""
env: ""
env: "COMPLEMENT_ENABLE_DIRTY_RUNS=1"
timeout: 10m

steps:
Expand Down
9 changes: 7 additions & 2 deletions ENVIRONMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Complement is configured exclusively through the use of environment variables. These variables are described below.

#### `COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS`
If 1, always prints the Homeserver container logs even on success.
If 1, always prints the Homeserver container logs even on success. When used with COMPLEMENT_ENABLE_DIRTY_RUNS, server logs are only printed once for reused deployments, at the very end of the test suite.
- Type: `bool`
- Default: 0

Expand All @@ -21,6 +21,11 @@ If 1, prints out more verbose logging such as HTTP request/response bodies.
- Type: `bool`
- Default: 0

#### `COMPLEMENT_ENABLE_DIRTY_RUNS`
If 1, eligible tests will be provided with reusable deployments rather than a clean deployment. Eligible tests are tests run with `Deploy(t, numHomeservers)`. If enabled, COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS and COMPLEMENT_POST_TEST_SCRIPT are run exactly once, at the end of all tests in the package. The post test script is run with the test name "COMPLEMENT_ENABLE_DIRTY_RUNS", and failed=false. Enabling dirty runs can greatly speed up tests, at the cost of clear server logs and the chance of tests polluting each other. Tests using `OldDeploy` and blueprints will still have a fresh image for each test. Fresh images can still be desirable e.g user directory tests need a clean homeserver else search results can be polluted, tests which can blacklist a server over federation also need isolated deployments to stop failures impacting other tests. For these reasons, there will always be a way for a test to override this setting and get a dedicated deployment. Eventually, dirty runs will become the default running mode of Complement, with an environment variable to disable this behaviour being added later, once this has stablised.
- Type: `bool`
- Default: 0

#### `COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT`
The hostname of Complement from the perspective of a Homeserver running inside a container. This can be useful for container runtimes using another hostname to access the host from a container, like Podman that uses `host.containers.internal` instead.
- Type: `string`
Expand All @@ -35,7 +40,7 @@ A list of space separated blueprint names to not clean up after running. For exa
- Type: `[]string`

#### `COMPLEMENT_POST_TEST_SCRIPT`
An arbitrary script to execute after a test was executed and before the container is removed. This can be used to extract, for example, server logs or database files. The script is passed the parameters: ContainerID, TestName, TestFailed (true/false)
An arbitrary script to execute after a test was executed and before the container is removed. This can be used to extract, for example, server logs or database files. The script is passed the parameters: ContainerID, TestName, TestFailed (true/false). When combined with COMPLEMENT_ENABLE_DIRTY_RUNS, the script is called exactly once at the end of the test suite, and is called with the TestName of "COMPLEMENT_ENABLE_DIRTY_RUNS" and TestFailed=false.
- Type: `string`
- Default: ""

Expand Down
27 changes: 25 additions & 2 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ type Complement struct {
DebugLoggingEnabled bool
// Name: COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS
// Default: 0
// Description: If 1, always prints the Homeserver container logs even on success.
// Description: If 1, always prints the Homeserver container logs even on success. When used with
// COMPLEMENT_ENABLE_DIRTY_RUNS, server logs are only printed once for reused deployments, at the very
// end of the test suite.
AlwaysPrintServerLogs bool
// Name: COMPLEMENT_SHARE_ENV_PREFIX
// Description: If set, all environment variables on the host with this prefix will be shared with
Expand Down Expand Up @@ -86,13 +88,33 @@ type Complement struct {
// like Podman that uses `host.containers.internal` instead.
HostnameRunningComplement string

// Name: COMPLEMENT_ENABLE_DIRTY_RUNS
// Default: 0
// Description: If 1, eligible tests will be provided with reusable deployments rather than a clean deployment.
// Eligible tests are tests run with `Deploy(t, numHomeservers)`. If enabled, COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS
// and COMPLEMENT_POST_TEST_SCRIPT are run exactly once, at the end of all tests in the package. The post test script
// is run with the test name "COMPLEMENT_ENABLE_DIRTY_RUNS", and failed=false.
//
// Enabling dirty runs can greatly speed up tests, at the cost of clear server logs and the chance of tests
// polluting each other. Tests using `OldDeploy` and blueprints will still have a fresh image for each test.
// Fresh images can still be desirable e.g user directory tests need a clean homeserver else search results can
// be polluted, tests which can blacklist a server over federation also need isolated deployments to stop failures
// impacting other tests. For these reasons, there will always be a way for a test to override this setting and
// get a dedicated deployment.
//
// Eventually, dirty runs will become the default running mode of Complement, with an environment variable to
// disable this behaviour being added later, once this has stablised.
EnableDirtyRuns bool

HSPortBindingIP string

// Name: COMPLEMENT_POST_TEST_SCRIPT
// Default: ""
// Description: An arbitrary script to execute after a test was executed and before the container is removed.
// This can be used to extract, for example, server logs or database files. The script is passed the parameters:
// ContainerID, TestName, TestFailed (true/false)
// ContainerID, TestName, TestFailed (true/false). When combined with COMPLEMENT_ENABLE_DIRTY_RUNS, the script is
// called exactly once at the end of the test suite, and is called with the TestName of "COMPLEMENT_ENABLE_DIRTY_RUNS"
// and TestFailed=false.
PostTestScript string
}

Expand All @@ -106,6 +128,7 @@ func NewConfigFromEnvVars(pkgNamespace, baseImageURI string) *Complement {
}
cfg.DebugLoggingEnabled = os.Getenv("COMPLEMENT_DEBUG") == "1"
cfg.AlwaysPrintServerLogs = os.Getenv("COMPLEMENT_ALWAYS_PRINT_SERVER_LOGS") == "1"
cfg.EnableDirtyRuns = os.Getenv("COMPLEMENT_ENABLE_DIRTY_RUNS") == "1"
cfg.EnvVarsPropagatePrefix = os.Getenv("COMPLEMENT_SHARE_ENV_PREFIX")
cfg.PostTestScript = os.Getenv("COMPLEMENT_POST_TEST_SCRIPT")
cfg.SpawnHSTimeout = time.Duration(parseEnvWithDefault("COMPLEMENT_SPAWN_HS_TIMEOUT_SECS", 30)) * time.Second
Expand Down
6 changes: 6 additions & 0 deletions internal/docker/deployer.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,12 @@ func (d *Deployer) Deploy(ctx context.Context, blueprintName string) (*Deploymen
return dep, lastErr
}

func (d *Deployer) PrintLogs(dep *Deployment) {
for _, hsDep := range dep.HS {
printLogs(d.Docker, hsDep.ContainerID, hsDep.ContainerID)
}
}

// Destroy a deployment. This will kill all running containers.
func (d *Deployer) Destroy(dep *Deployment, printServerLogs bool, testName string, failed bool) {
for _, hsDep := range dep.HS {
Expand Down
18 changes: 18 additions & 0 deletions internal/docker/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type Deployment struct {
Deployer *Deployer
// The name of the deployed blueprint
BlueprintName string
// Set to true if this deployment is a dirty deployment and so should not be destroyed.
Dirty bool
// A map of HS name to a HomeserverDeployment
HS map[string]*HomeserverDeployment
Config *config.Complement
Expand Down Expand Up @@ -52,10 +54,26 @@ func (hsDep *HomeserverDeployment) SetEndpoints(baseURL string, fedBaseURL strin
}
}

// DestroyAtCleanup destroys the entire deployment. It should be called at cleanup time for dirty
// deployments only. Handles configuration options for things which should run at container destroy
// time, like post-run scripts and printing logs.
func (d *Deployment) DestroyAtCleanup() {
if !d.Dirty {
return
}
d.Deployer.Destroy(d, d.Deployer.config.AlwaysPrintServerLogs, "COMPLEMENT_ENABLE_DIRTY_RUNS", false)
}

// Destroy the entire deployment. Destroys all running containers. If `printServerLogs` is true,
// will print container logs before killing the container.
func (d *Deployment) Destroy(t *testing.T) {
t.Helper()
if d.Dirty {
if t.Failed() {
d.Deployer.PrintLogs(d)
}
return
}
d.Deployer.Destroy(d, d.Deployer.config.AlwaysPrintServerLogs || t.Failed(), t.Name(), t.Failed())
}

Expand Down
34 changes: 31 additions & 3 deletions test_package.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"net/http"
"sync"
"sync/atomic"
"testing"
"time"
Expand Down Expand Up @@ -49,6 +50,11 @@ type TestPackage struct {
complementBuilder *docker.Builder
// a counter to stop tests from allocating the same container name
namespaceCounter uint64

// pointers to existing deployments for Deploy(t, 1) style deployments which are reused when run
// in dirty mode.
existingDeployments map[int]*docker.Deployment
existingDeploymentsMu *sync.Mutex
}

// NewTestPackage creates a new test package which can be used to deploy containers for all tests
Expand All @@ -69,13 +75,21 @@ func NewTestPackage(pkgNamespace string) (*TestPackage, error) {
logrus.SetLevel(logrus.ErrorLevel)

return &TestPackage{
complementBuilder: builder,
namespaceCounter: 0,
Config: cfg,
complementBuilder: builder,
namespaceCounter: 0,
Config: cfg,
existingDeployments: make(map[int]*docker.Deployment),
existingDeploymentsMu: &sync.Mutex{},
}, nil
}

func (tp *TestPackage) Cleanup() {
// any dirty deployments need logs printed and post scripts run
tp.existingDeploymentsMu.Lock()
for _, dep := range tp.existingDeployments {
dep.DestroyAtCleanup()
}
tp.existingDeploymentsMu.Unlock()
tp.complementBuilder.Cleanup()
}

Expand Down Expand Up @@ -105,6 +119,14 @@ func (tp *TestPackage) OldDeploy(t *testing.T, blueprint b.Blueprint) Deployment

func (tp *TestPackage) Deploy(t *testing.T, numServers int) Deployment {
t.Helper()
if tp.Config.EnableDirtyRuns {
tp.existingDeploymentsMu.Lock()
existingDep := tp.existingDeployments[numServers]
tp.existingDeploymentsMu.Unlock()
if existingDep != nil {
return existingDep
}
}
blueprint := mapServersToBlueprint(numServers)
timeStartBlueprint := time.Now()
if err := tp.complementBuilder.ConstructBlueprintIfNotExist(blueprint); err != nil {
Expand All @@ -121,6 +143,12 @@ func (tp *TestPackage) Deploy(t *testing.T, numServers int) Deployment {
t.Fatalf("Deploy: Deploy returned error %s", err)
}
t.Logf("Deploy times: %v blueprints, %v containers", timeStartDeploy.Sub(timeStartBlueprint), time.Since(timeStartDeploy))
if tp.Config.EnableDirtyRuns {
dep.Dirty = true // stop this deployment being destroyed.
tp.existingDeploymentsMu.Lock()
tp.existingDeployments[numServers] = dep
tp.existingDeploymentsMu.Unlock()
}
return dep
}

Expand Down
4 changes: 3 additions & 1 deletion tests/csapi/user_directory_display_names_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/matrix-org/complement"
"github.com/matrix-org/complement/b"
"github.com/matrix-org/complement/client"
"github.com/matrix-org/complement/helpers"
"github.com/matrix-org/complement/match"
Expand Down Expand Up @@ -41,7 +42,8 @@ func setupUsers(t *testing.T) (*client.CSAPI, *client.CSAPI, *client.CSAPI, func
// - Eve knows about Alice,
// - Alice reveals a private name to another friend Bob
// - Eve shouldn't be able to see that private name via the directory.
deployment := complement.Deploy(t, 1)
// Use a clean deployment for these tests so the user directory isn't polluted.
deployment := complement.OldDeploy(t, b.BlueprintCleanHS)
Comment on lines +45 to +46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha. Thanks.

cleanup := func(t *testing.T) {
deployment.Destroy(t)
}
Expand Down
Loading