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

Move output dir logic to suite #29580

Merged
merged 12 commits into from
Oct 9, 2024
30 changes: 30 additions & 0 deletions test/new-e2e/pkg/e2e/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ import (
"errors"
"fmt"
"reflect"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -189,6 +190,9 @@ type BaseSuite[Env any] struct {
currentProvisioners ProvisionerMap

firstFailTest string

testSessionOutputDir string
onceTestSessionOutputDir sync.Once
}

//
Expand Down Expand Up @@ -529,6 +533,32 @@ 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() {
// Store the timestamped directory to be used by all tests in the suite
bs.testSessionOutputDir, err = CreateRootOutputDir()
})
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 CreateTestOutputDir(root, bs.T())
}

// Run is a helper function to run a test suite.
// Unfortunately, we cannot use `s Suite[Env]` as Go is not able to match it with a struct
// However it's able to verify the same constraint on T
Expand Down
82 changes: 81 additions & 1 deletion test/new-e2e/pkg/e2e/suite_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,17 @@

package e2e

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

"github.com/DataDog/datadog-agent/test/new-e2e/pkg/runner"

"testing"
)

type testLogger struct {
t *testing.T
Expand All @@ -20,3 +30,73 @@ func (tl testLogger) Write(p []byte) (n int, err error) {
tl.t.Log(string(p))
return len(p), nil
}

// CreateRootOutputDir 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 runner.GetProfile().GetOutputDir() for the root output directory selection logic.
//
// See CreateTestOutputDir and BaseSuite.CreateTestOutputDir for a function that returns a subdirectory for a specific test.
func CreateRootOutputDir() (string, error) {
outputRoot, err := runner.GetProfile().GetOutputDir()
if err != nil {
return "", err
}
// 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
}
}
Comment on lines +65 to +81
Copy link
Contributor

Choose a reason for hiding this comment

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

💬 suggestion
What about passing the profile, and having the profile passing a path to the output root directory. Then each suite would create a directory inside the root directory, while the generic one is static.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure I'm understanding.
profile.GetOutputDir() already returns static output dir, e.g. ~/e2e-output. The suite calls GetRootOutputDir() to create a new subdirectory for itself.

Copy link
Contributor

Choose a reason for hiding this comment

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

The rename to Create fixes this too

Copy link
Contributor

Choose a reason for hiding this comment

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

What I meant in my message is that is profile that knows where you are running. You might have a generic profile GetLatestLink that cleans up symlinks only on localprofile and that does nothing on ci profile. You would call it here with runner.GetProfile().CleanupOldLinks() or a better name.

return outputRoot, nil
}

// CreateTestOutputDir creates a 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.
//
// Example:
// - test 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) {
// 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
}
25 changes: 0 additions & 25 deletions test/new-e2e/pkg/runner/local_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"os"
"os/user"
"path"
"path/filepath"
"strings"

"github.com/DataDog/datadog-agent/test/new-e2e/pkg/runner/parameters"
Expand Down Expand Up @@ -119,27 +118,3 @@ func (p localProfile) NamePrefix() string {
func (p localProfile) AllowDevMode() bool {
return true
}

// GetOutputDir extends baseProfile.GetOutputDir to create a symlink to the latest run
func (p localProfile) GetOutputDir() (string, error) {
outDir, err := p.baseProfile.GetOutputDir()
if err != nil {
return "", err
}

// Create a symlink to the latest run for user convenience
latestLink := filepath.Join(filepath.Dir(outDir), "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(outDir, latestLink)
if err != nil {
return "", err
}

return outDir, nil
}
83 changes: 18 additions & 65 deletions test/new-e2e/pkg/runner/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@ import (
"strconv"
"strings"
"sync"
"time"

"github.com/DataDog/datadog-agent/test/new-e2e/pkg/runner/parameters"

"testing"
)

// CloudProvider alias to string
Expand Down Expand Up @@ -64,9 +61,9 @@ type Profile interface {
// AllowDevMode returns if DevMode is allowed
AllowDevMode() bool
// GetOutputDir returns the root output directory for tests to store output files and artifacts.
// e.g. /tmp/e2e-output/2020-01-01_00-00-00_<random>
// e.g. /tmp/e2e-output/ or ~/e2e-output/
//
// See GetTestOutputDir for a function that returns a subdirectory for a specific test.
// It is recommended to use GetTestOutputDir to create a subdirectory for a specific test.
GetOutputDir() (string, error)
}

Expand All @@ -78,7 +75,6 @@ type baseProfile struct {
secretStore parameters.Store
workspaceRootFolder string
defaultOutputRootFolder string
outputRootFolder string
}

func newProfile(projectName string, environments []string, store parameters.Store, secretStore *parameters.Store, defaultOutputRoot string) baseProfile {
Expand Down Expand Up @@ -140,55 +136,30 @@ func (p baseProfile) SecretStore() parameters.Store {
return p.secretStore
}

// GetOutputDir returns the root output directory for tests to store output files and artifacts.
// The directory is created on the first call to this function, normally this will be when a
// test calls GetTestOutputDir.
// GetOutputDir returns the root output directory to be used to store output files and artifacts.
// A path is returned but the directory is not created.
//
// The root output directory is chosen in the following order:
// - outputDir parameter from the runner configuration, or E2E_OUTPUT_DIR environment variable
// - default provided by a parent profile, <defaultOutputRootFolder>/e2e-output, e.g. $CI_PROJECT_DIR/e2e-output
// - default provided by profile, <defaultOutputRootFolder>/e2e-output, e.g. $CI_PROJECT_DIR/e2e-output
// - os.TempDir()/e2e-output
//
// A timestamp is appended to the root output directory to distinguish between multiple runs,
// and os.MkdirTemp() is used to avoid name collisions between parallel runs.
//
// See GetTestOutputDir for a function that returns a subdirectory for a specific test.
func (p baseProfile) GetOutputDir() (string, error) {
if p.outputRootFolder == "" {
var outputRoot string
configOutputRoot, err := p.store.GetWithDefault(parameters.OutputDir, "")
if err != nil {
return "", err
}
if configOutputRoot != "" {
// If outputRoot is provided in the config file, use it as the root directory
outputRoot = configOutputRoot
} else if p.defaultOutputRootFolder != "" {
// If a default outputRoot was provided, use it as the root directory
outputRoot = filepath.Join(p.defaultOutputRootFolder, "e2e-output")
} else if outputRoot == "" {
// If outputRoot is not provided, use os.TempDir() as the root directory
outputRoot = filepath.Join(os.TempDir(), "e2e-output")
}
// 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
}
p.outputRootFolder = outputRoot
configOutputRoot, err := p.store.GetWithDefault(parameters.OutputDir, "")
if err != nil {
return "", err
}
return p.outputRootFolder, nil
if configOutputRoot != "" {
// If outputRoot is provided in the config file, use it as the root directory
return configOutputRoot, nil
}
if p.defaultOutputRootFolder != "" {
// If a default outputRoot was provided, use it as the root directory
return filepath.Join(p.defaultOutputRootFolder, "e2e-output"), nil
}
// as a final fallback, use os.TempDir() as the root directory
return filepath.Join(os.TempDir(), "e2e-output"), nil
}

// GetWorkspacePath returns the directory for CI Pulumi workspace.
Expand Down Expand Up @@ -222,21 +193,3 @@ func GetProfile() Profile {

return runProfile
}

// GetTestOutputDir returns the output directory for a specific test.
// The test name is sanitized to remove invalid characters, and the output directory is created.
func GetTestOutputDir(p Profile, t *testing.T) (string, error) {
// https://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words
invalidPathChars := strings.Join([]string{"?", "%", "*", ":", "|", "\"", "<", ">", ".", ",", ";", "="}, "")
root, err := p.GetOutputDir()
if err != nil {
return "", err
}
testPart := strings.ReplaceAll(t.Name(), invalidPathChars, "_")
path := filepath.Join(root, testPart)
err = os.MkdirAll(path, 0755)
if err != nil {
return "", err
}
return path, nil
}
29 changes: 15 additions & 14 deletions test/new-e2e/pkg/utils/e2e/client/agent_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import (
"github.com/stretchr/testify/require"

"github.com/DataDog/datadog-agent/test/new-e2e/pkg/e2e"
"github.com/DataDog/datadog-agent/test/new-e2e/pkg/runner"
"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 Down Expand Up @@ -197,13 +196,15 @@ func waitForReadyTimeout(t *testing.T, host *Host, commandRunner *agentCommandRu
}

func generateAndDownloadFlare(t *testing.T, commandRunner *agentCommandRunner, host *Host) error {
profile := runner.GetProfile()
outputDir, err := profile.GetOutputDir()
flareFound := false

root, err := e2e.CreateRootOutputDir()
if err != nil {
return fmt.Errorf("could not get output directory: %v", err)
return fmt.Errorf("could not get root output directory: %w", err)
}
outputDir, err := e2e.CreateTestOutputDir(root, t)
if err != nil {
return fmt.Errorf("could not get output directory: %w", err)
}
flareFound := false

_, err = commandRunner.FlareWithError(agentclient.WithArgs([]string{"--email", "e2e@test.com", "--send", "--local"}))
if err != nil {
Expand All @@ -213,17 +214,17 @@ func generateAndDownloadFlare(t *testing.T, commandRunner *agentCommandRunner, h

flareRegex, err := regexp.Compile(`datadog-agent-.*\.zip`)
if err != nil {
return fmt.Errorf("could not compile regex: %v", err)
return fmt.Errorf("could not compile regex: %w", err)
}

tmpFolder, err := host.GetTmpFolder()
if err != nil {
return fmt.Errorf("could not get tmp folder: %v", err)
return fmt.Errorf("could not get tmp folder: %w", err)
}

entries, err := host.ReadDir(tmpFolder)
if err != nil {
return fmt.Errorf("could not read directory: %v", err)
return fmt.Errorf("could not read directory: %w", err)
}

for _, entry := range entries {
Expand All @@ -233,15 +234,15 @@ func generateAndDownloadFlare(t *testing.T, commandRunner *agentCommandRunner, h
if host.osFamily != osComp.WindowsFamily {
_, err = host.Execute(fmt.Sprintf("sudo chmod 744 %s/%s", tmpFolder, entry.Name()))
if err != nil {
return fmt.Errorf("could not update permission of flare file %s/%s : %v", tmpFolder, entry.Name(), err)
return fmt.Errorf("could not update permission of flare file %s/%s : %w", tmpFolder, entry.Name(), err)
}
}

t.Logf("Downloading flare file in: %s", outputDir)
err = host.GetFile(fmt.Sprintf("%s/%s", tmpFolder, entry.Name()), fmt.Sprintf("%s/%s", outputDir, entry.Name()))

if err != nil {
return fmt.Errorf("could not download flare file from %s/%s : %v", tmpFolder, entry.Name(), err)
return fmt.Errorf("could not download flare file from %s/%s : %w", tmpFolder, entry.Name(), err)
}

flareFound = true
Expand All @@ -253,21 +254,21 @@ func generateAndDownloadFlare(t *testing.T, commandRunner *agentCommandRunner, h

logsFolder, err := host.GetLogsFolder()
if err != nil {
return fmt.Errorf("could not get logs folder: %v", err)
return fmt.Errorf("could not get logs folder: %w", err)
}

entries, err = host.ReadDir(logsFolder)

if err != nil {
return fmt.Errorf("could not read directory: %v", err)
return fmt.Errorf("could not read directory: %w", err)
}

for _, entry := range entries {
t.Logf("Found log file: %s. Downloading file in: %s", entry.Name(), outputDir)

err = host.GetFile(fmt.Sprintf("%s/%s", logsFolder, entry.Name()), fmt.Sprintf("%s/%s", outputDir, entry.Name()))
if err != nil {
return fmt.Errorf("could not download log file from %s/%s : %v", logsFolder, entry.Name(), err)
return fmt.Errorf("could not download log file from %s/%s : %w", logsFolder, entry.Name(), err)
}
}
}
Expand Down
Loading
Loading