Skip to content

Commit

Permalink
[e2e] init suite session output dir at suite setup
Browse files Browse the repository at this point in the history
Remove not needed CreateTestSession dir function, centralise session output dir creation within a suite
  • Loading branch information
pducolin committed Dec 27, 2024
1 parent 00093c0 commit 2200dd8
Show file tree
Hide file tree
Showing 17 changed files with 136 additions and 178 deletions.
54 changes: 20 additions & 34 deletions test/new-e2e/pkg/e2e/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,6 @@ import (
"errors"
"fmt"
"reflect"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -199,8 +198,7 @@ type BaseSuite[Env any] struct {
endTime time.Time
initOnly bool

testSessionOutputDir string
onceTestSessionOutputDir sync.Once
outputDir string
}

//
Expand Down Expand Up @@ -487,6 +485,13 @@ func (bs *BaseSuite[Env]) providerContext(opTimeout time.Duration) (context.Cont
// [testify Suite]: https://pkg.go.dev/github.com/stretchr/testify/suite
func (bs *BaseSuite[Env]) SetupSuite() {
bs.startTime = time.Now()
// Create the root output directory for the test suite session
sessionDirectory, err := runner.GetProfile().CreateOutputSubDir(bs.getSuiteSessionSubdirectory())
if err != nil {
bs.T().Errorf("unable to create session output directory: %v", err)
}
bs.outputDir = sessionDirectory
bs.T().Logf("Suite session output directory: %s", bs.outputDir)
// In `SetupSuite` we cannot fail as `TearDownSuite` will not be called otherwise.
// Meaning that stack clean up may not be called.
// We do implement an explicit recover to handle this manuallay.
Expand All @@ -497,7 +502,7 @@ func (bs *BaseSuite[Env]) SetupSuite() {
}

bs.T().Logf("Caught panic in SetupSuite, err: %v. Will try to TearDownSuite", err)
bs.firstFailTest = "Initial provisioiningin SetupSuite" // This is required to handle skipDeleteOnFailure
bs.firstFailTest = "Initial provisioning SetupSuite" // This is required to handle skipDeleteOnFailure
bs.TearDownSuite()

// As we need to call `recover` to know if there was a panic, we wrap and forward the original panic to,
Expand All @@ -522,6 +527,12 @@ func (bs *BaseSuite[Env]) SetupSuite() {
}
}

func (bs *BaseSuite[Env]) getSuiteSessionSubdirectory() string {
suiteStartTimePart := bs.startTime.Format("2006_01_02_15_04_05")
testPart := common.SanitizeDirectoryName(bs.T().Name())
return fmt.Sprintf("%s_%s", testPart, suiteStartTimePart)
}

// BeforeTest is executed right before the test starts and receives the suite and test names as input.
// This function is called by [testify Suite].
//
Expand All @@ -537,7 +548,7 @@ func (bs *BaseSuite[Env]) BeforeTest(string, string) {
}
}

// AfterTest is executed right after the test finishes and receives the suite and test names as input.
// AfterTest is executed right after each test finishes and receives the suite and test names as input.
// This function is called by [testify Suite].
//
// If you override AfterTest in your custom test suite type, the function must call [test.BaseSuite.AfterTest].
Expand Down Expand Up @@ -604,35 +615,10 @@ func (bs *BaseSuite[Env]) TearDownSuite() {
}
}

// GetRootOutputDir returns the root output directory for tests to store output files and artifacts.
// The directory is created on the first call to this function and reused in future calls.
//
// See BaseSuite.CreateTestOutputDir() for a function that returns a directory for the current test.
//
// See CreateRootOutputDir() for details on the root directory creation.
func (bs *BaseSuite[Env]) GetRootOutputDir() (string, error) {
var err error
bs.onceTestSessionOutputDir.Do(func() {
var outputRoot string
outputRoot, err = runner.GetProfile().GetOutputDir()
if err != nil {
return
}
// Store the timestamped directory to be used by all tests in the suite
bs.testSessionOutputDir, err = common.CreateTestSessionOutputDir(outputRoot)
})
return bs.testSessionOutputDir, err
}

// CreateTestOutputDir returns an output directory for the current test.
//
// See also CreateTestOutputDir()
func (bs *BaseSuite[Env]) CreateTestOutputDir() (string, error) {
root, err := bs.GetRootOutputDir()
if err != nil {
return "", err
}
return common.CreateTestOutputDir(root, bs.T())
// SessionOutputDir returns the root output directory for tests to store output files and artifacts.
// The directory is created at SetupSuite time.
func (bs *BaseSuite[Env]) SessionOutputDir() string {
return bs.outputDir
}

// Run is a helper function to run a test suite.
Expand Down
2 changes: 2 additions & 0 deletions test/new-e2e/pkg/runner/ci_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type ciProfile struct {
ciUniqueID string
}

var _ Profile = ciProfile{}

// NewCIProfile creates a new CI profile
func NewCIProfile() (Profile, error) {
ciSecretPrefix := os.Getenv("CI_SECRET_PREFIX")
Expand Down
25 changes: 25 additions & 0 deletions test/new-e2e/pkg/runner/local_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os"
"os/user"
"path"
"path/filepath"
"strings"

"github.com/DataDog/datadog-agent/test/new-e2e/pkg/runner/parameters"
Expand Down Expand Up @@ -80,6 +81,8 @@ type localProfile struct {
baseProfile
}

var _ Profile = localProfile{}

// NamePrefix returns a prefix to name objects based on local username
func (p localProfile) NamePrefix() string {
// Stack names may only contain alphanumeric characters, hyphens, underscores, or periods.
Expand Down Expand Up @@ -118,3 +121,25 @@ func (p localProfile) NamePrefix() string {
func (p localProfile) AllowDevMode() bool {
return true
}

// CreateOutputSubDir creates an output directory inside the runner root directory for tests to store output files and artifacts.
func (p ciProfile) CreateOutputSubDir(subdirectory string) (string, error) {
outputDir, err := p.baseProfile.CreateOutputSubDir(subdirectory)
if err != nil {
return "", err
}
// Create a symlink to the latest run for user convenience
latestLink := filepath.Join(filepath.Dir(outputDir), "latest")
// Remove the symlink if it already exists
if _, err := os.Lstat(latestLink); err == nil {
err = os.Remove(latestLink)
if err != nil {
return "", err
}
}
err = os.Symlink(outputDir, latestLink)
if err != nil {
return "", err
}
return outputDir, nil
}
29 changes: 26 additions & 3 deletions test/new-e2e/pkg/runner/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ type Profile interface {
AllowDevMode() bool
// GetOutputDir returns the root output directory for tests to store output files and artifacts.
// e.g. /tmp/e2e-output/ or ~/e2e-output/
//
// It is recommended to use GetTestOutputDir to create a subdirectory for a specific test.
GetOutputDir() (string, error)
// CreateOutputSubDir creates an output directory inside the runner root directory for tests to store output files and artifacts.
CreateOutputSubDir(subdirectory string) (string, error)
}

// Shared implementations for common profiles methods
Expand Down Expand Up @@ -162,7 +162,7 @@ func (p baseProfile) GetOutputDir() (string, error) {
return filepath.Join(os.TempDir(), "e2e-output"), nil
}

// GetWorkspacePath returns the directory for CI Pulumi workspace.
// GetWorkspacePath returns the directory for Pulumi workspace.
// Since one Workspace supports one single program and we have one program per stack,
// the path should be unique for each stack.
func (p baseProfile) GetWorkspacePath(stackName string) string {
Expand All @@ -175,6 +175,29 @@ func hashString(s string) string {
return fmt.Sprintf("%016x", hasher.Sum64())
}

// CreateOutputSubDir creates an output directory inside the runner root directory for tests to store output files and artifacts.
func (p baseProfile) CreateOutputSubDir(subdirectory string) (string, error) {
rootOutputDir, err := p.GetOutputDir()
if err != nil {
return "", err
}
fullPath := filepath.Join(rootOutputDir, subdirectory)
// create all directories in the path, excluding the last one
parentDir := filepath.Dir(fullPath)
finalDir := filepath.Base(fullPath)
err = os.MkdirAll(parentDir, 0755)
if err != nil {
return "", err
}
// Create final output directory
// Use MkdirTemp to avoid name collisions between parallel runs
outputDir, err := os.MkdirTemp(parentDir, fmt.Sprintf("%s_*", finalDir))
if err != nil {
return "", err
}
return outputDir, nil
}

// GetProfile return a profile initialising it at first call
func GetProfile() Profile {
initProfile.Do(func() {
Expand Down
66 changes: 4 additions & 62 deletions test/new-e2e/pkg/utils/common/output_dirs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,74 +6,16 @@
package common

import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"time"
)

// CreateTestSessionOutputDir creates and returns a directory for tests to store output files and artifacts.
// A timestamp is included in the path to distinguish between multiple runs, and os.MkdirTemp() is
// used to avoid name collisions between parallel runs.
//
// A new directory is created on each call to this function, it is recommended to save this result
// and use it for all tests in a run. For example see BaseSuite.GetRootOutputDir().
//
// See CreateTestOutputDir and BaseSuite.CreateTestOutputDir for a function that returns a subdirectory for a specific test.
func CreateTestSessionOutputDir(outputRoot string) (string, error) {
// Append timestamp to distinguish between multiple runs
// Format: YYYY-MM-DD_HH-MM-SS
// Use a custom timestamp format because Windows paths can't contain ':' characters
// and we don't need the timezone information.
timePart := time.Now().Format("2006-01-02_15-04-05")
// create root directory
err := os.MkdirAll(outputRoot, 0755)
if err != nil {
return "", err
}
// Create final output directory
// Use MkdirTemp to avoid name collisions between parallel runs
outputRoot, err = os.MkdirTemp(outputRoot, fmt.Sprintf("%s_*", timePart))
if err != nil {
return "", err
}
if os.Getenv("CI") == "" {
// Create a symlink to the latest run for user convenience
// TODO: Is there a standard "ci" vs "local" check?
// This code used to be in localProfile.GetOutputDir()
latestLink := filepath.Join(filepath.Dir(outputRoot), "latest")
// Remove the symlink if it already exists
if _, err := os.Lstat(latestLink); err == nil {
err = os.Remove(latestLink)
if err != nil {
return "", err
}
}
err = os.Symlink(outputRoot, latestLink)
if err != nil {
return "", err
}
}
return outputRoot, nil
}

// CreateTestOutputDir creates a local directory for a specific test that can be used to store output files and artifacts.
// The test name is used in the directory name, and invalid characters are replaced with underscores.
// SanitizeDirectoryName replace invalid characters in a directory name underscores.
//
// Example:
// - test name: TestInstallSuite/TestInstall/install_version=7.50.0
// - name: TestInstallSuite/TestInstall/install_version=7.50.0
// - output directory: <root>/TestInstallSuite/TestInstall/install_version_7_50_0
func CreateTestOutputDir(root string, t *testing.T) (string, error) {
func SanitizeDirectoryName(name string) string {
// https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
invalidPathChars := strings.Join([]string{"?", "%", "*", ":", "|", "\"", "<", ">", ".", ",", ";", "="}, "")

testPart := strings.ReplaceAll(t.Name(), invalidPathChars, "_")
path := filepath.Join(root, testPart)
err := os.MkdirAll(path, 0755)
if err != nil {
return "", err
}
return path, nil
return strings.ReplaceAll(name, invalidPathChars, "_")
}
1 change: 1 addition & 0 deletions test/new-e2e/pkg/utils/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import "testing"
// Context defines an interface that allows to get information about current test context
type Context interface {
T() *testing.T
SessionOutputDir() string
}

// Initializable defines the interface for an object that needs to be initialized
Expand Down
30 changes: 13 additions & 17 deletions test/new-e2e/pkg/utils/e2e/client/agent_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ package client
import (
"fmt"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
Expand All @@ -19,7 +21,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/DataDog/datadog-agent/test/new-e2e/pkg/runner"
"github.com/DataDog/datadog-agent/test/new-e2e/pkg/utils/common"
"github.com/DataDog/datadog-agent/test/new-e2e/pkg/utils/e2e/client/agentclient"
"github.com/DataDog/datadog-agent/test/new-e2e/pkg/utils/e2e/client/agentclientparams"
Expand All @@ -43,7 +44,7 @@ func NewHostAgentClient(context common.Context, hostOutput remote.HostOutput, wa
commandRunner := newAgentCommandRunner(context.T(), ae)

if params.ShouldWaitForReady {
if err := waitForReadyTimeout(context.T(), host, commandRunner, agentReadyTimeout); err != nil {
if err := waitForReadyTimeout(context, host, commandRunner, agentReadyTimeout); err != nil {
return nil, err
}
}
Expand All @@ -64,7 +65,7 @@ func NewHostAgentClientWithParams(context common.Context, hostOutput remote.Host
commandRunner := newAgentCommandRunner(context.T(), ae)

if params.ShouldWaitForReady {
if err := waitForReadyTimeout(context.T(), host, commandRunner, agentReadyTimeout); err != nil {
if err := waitForReadyTimeout(context, host, commandRunner, agentReadyTimeout); err != nil {
return nil, err
}
}
Expand Down Expand Up @@ -181,35 +182,30 @@ func fetchAuthTokenCommand(authTokenPath string, osFamily osComp.Family) string
return fmt.Sprintf("sudo cat %s", authTokenPath)
}

func waitForReadyTimeout(t *testing.T, host *Host, commandRunner *agentCommandRunner, timeout time.Duration) error {
func waitForReadyTimeout(ctx common.Context, host *Host, commandRunner *agentCommandRunner, timeout time.Duration) error {
err := commandRunner.waitForReadyTimeout(timeout)

if err != nil {
// Propagate the original error if we have another error here
localErr := generateAndDownloadFlare(t, commandRunner, host)
localErr := generateAndDownloadFlare(ctx, commandRunner, host)

if localErr != nil {
t.Errorf("Could not generate and get a flare: %v", localErr)
ctx.T().Errorf("Could not generate and get a flare: %v", localErr)
}
}

return err
}

func generateAndDownloadFlare(t *testing.T, commandRunner *agentCommandRunner, host *Host) error {
outputRoot, err := runner.GetProfile().GetOutputDir()
func generateAndDownloadFlare(ctx common.Context, commandRunner *agentCommandRunner, host *Host) error {
testPart := common.SanitizeDirectoryName(ctx.T().Name())
outputDir := filepath.Join(ctx.SessionOutputDir(), testPart)
err := os.MkdirAll(outputDir, 0755)
if err != nil {
return fmt.Errorf("could not get root output directory: %w", err)
}
root, err := common.CreateTestSessionOutputDir(outputRoot)
if err != nil {
return fmt.Errorf("could not get test session output directory: %w", err)
}
outputDir, err := common.CreateTestOutputDir(root, t)
if err != nil {
return fmt.Errorf("could not get output directory: %w", err)
return fmt.Errorf("could not create output directory: %w", err)
}
flareFound := false
t := ctx.T()

_, err = commandRunner.FlareWithError(agentclient.WithArgs([]string{"--email", "[email protected]", "--send", "--local"}))
if err != nil {
Expand Down
Loading

0 comments on commit 2200dd8

Please sign in to comment.