get.porter.sh/porter@v1.3.0/pkg/portercontext/helpers.go (about)

     1  package portercontext
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"path/filepath"
    12  	"runtime"
    13  	"strings"
    14  	"testing"
    15  
    16  	"get.porter.sh/porter/pkg"
    17  	"get.porter.sh/porter/pkg/test"
    18  	"get.porter.sh/porter/pkg/yaml"
    19  	"github.com/carolynvs/aferox"
    20  	"github.com/spf13/afero"
    21  	"github.com/stretchr/testify/require"
    22  	"go.uber.org/zap/zapcore"
    23  )
    24  
    25  type TestContext struct {
    26  	*Context
    27  
    28  	cleanupDirs []string
    29  	capturedErr *bytes.Buffer
    30  	capturedOut *bytes.Buffer
    31  	captureLogs *bytes.Buffer
    32  	T           *testing.T
    33  }
    34  
    35  // NewTestContext initializes a configuration suitable for testing, with the
    36  // output buffered, and an in-memory file system, using the specified
    37  // environment variables.
    38  func NewTestContext(t *testing.T) *TestContext {
    39  	// Provide a way for tests to provide and capture stdin and stdout
    40  	// Copy output to the test log simultaneously, use go test -v to see the output
    41  	logs := &bytes.Buffer{}
    42  	err := &bytes.Buffer{}
    43  	aggErr := io.MultiWriter(err, test.Logger{T: t}, logs)
    44  	out := &bytes.Buffer{}
    45  	aggOut := io.MultiWriter(out, test.Logger{T: t}, logs)
    46  
    47  	innerContext := New()
    48  	innerContext.correlationId = "0"
    49  	innerContext.timestampLogs = false
    50  	innerContext.environ = getEnviron()
    51  	innerContext.FileSystem = aferox.NewAferox("/", afero.NewMemMapFs())
    52  	innerContext.In = &bytes.Buffer{}
    53  	innerContext.Out = aggOut
    54  	innerContext.Err = aggErr
    55  	innerContext.ConfigureLogging(context.Background(), LogConfiguration{
    56  		LogLevel:  zapcore.DebugLevel,
    57  		Verbosity: zapcore.DebugLevel,
    58  	})
    59  	innerContext.PlugInDebugContext = &PluginDebugContext{
    60  		DebuggerPort:           "2735",
    61  		RunPlugInInDebugger:    "",
    62  		PlugInWorkingDirectory: "",
    63  	}
    64  
    65  	c := &TestContext{
    66  		Context:     innerContext,
    67  		capturedOut: out,
    68  		capturedErr: err,
    69  		captureLogs: logs,
    70  		T:           t,
    71  	}
    72  
    73  	c.NewCommand = c.NewTestCommand
    74  
    75  	return c
    76  }
    77  
    78  func (c *TestContext) NewTestCommand(ctx context.Context, name string, args ...string) *exec.Cmd {
    79  	testArgs := append([]string{name}, args...)
    80  	cmd := exec.CommandContext(ctx, os.Args[0], testArgs...)
    81  	cmd.Dir = c.Getwd()
    82  
    83  	cmd.Env = []string{
    84  		fmt.Sprintf("%s=true", test.MockedCommandEnv),
    85  	}
    86  	if val, ok := c.LookupEnv(test.ExpectedCommandEnv); ok {
    87  		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", test.ExpectedCommandEnv, val))
    88  	}
    89  	if val, ok := c.LookupEnv(test.ExpectedCommandExitCodeEnv); ok {
    90  		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", test.ExpectedCommandExitCodeEnv, val))
    91  	}
    92  	if val, ok := c.LookupEnv(test.ExpectedCommandOutputEnv); ok {
    93  		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", test.ExpectedCommandOutputEnv, val))
    94  	}
    95  	if val, ok := c.LookupEnv(test.ExpectedCommandErrorEnv); ok {
    96  		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", test.ExpectedCommandErrorEnv, val))
    97  	}
    98  	return cmd
    99  }
   100  
   101  func (c *TestContext) GetTestDefinitionDirectory() string {
   102  	for i := 0; true; i++ {
   103  		_, filename, _, ok := runtime.Caller(i)
   104  		if !ok {
   105  			c.T.Fatal("could not determine calling test directory")
   106  		}
   107  		if strings.HasSuffix(filename, "_test.go") {
   108  			return filepath.Dir(filename)
   109  		}
   110  	}
   111  	return ""
   112  }
   113  
   114  // UseFilesystem has porter's context use the OS filesystem instead of an in-memory filesystem
   115  // Returns the test directory, and the temp porter home directory.
   116  func (c *TestContext) UseFilesystem() (testDir string, homeDir string) {
   117  	homeDir, err := os.MkdirTemp("", "porter-test")
   118  	require.NoError(c.T, err)
   119  	c.cleanupDirs = append(c.cleanupDirs, homeDir)
   120  
   121  	testDir = c.GetTestDefinitionDirectory()
   122  	c.FileSystem = aferox.NewAferox(testDir, afero.NewOsFs())
   123  	c.defaultNewCommand()
   124  	c.DisableUmask()
   125  
   126  	return testDir, homeDir
   127  }
   128  
   129  func (c *TestContext) AddCleanupDir(dir string) {
   130  	c.cleanupDirs = append(c.cleanupDirs, dir)
   131  }
   132  
   133  func (c *TestContext) Close() {
   134  	for _, dir := range c.cleanupDirs {
   135  		_ = c.FileSystem.RemoveAll(dir)
   136  	}
   137  }
   138  
   139  // AddTestFileFromRoot should be used when the testfile you are referencing is in a different directory than the test.
   140  func (c *TestContext) AddTestFileFromRoot(src, dest string) []byte {
   141  	pathFromRoot := filepath.Join(c.FindRepoRoot(), src)
   142  	return c.AddTestFile(pathFromRoot, dest)
   143  }
   144  
   145  // AddTestFile adds a test file where the filepath is relative to the test directory.
   146  // mode is optional and only the first one passed is used.
   147  func (c *TestContext) AddTestFile(src, dest string, mode ...os.FileMode) []byte {
   148  	if strings.Contains(src, "..") {
   149  		c.T.Fatal(errors.New("use AddTestFileFromRoot when referencing a test file in a different directory than the test"))
   150  	}
   151  
   152  	data, err := os.ReadFile(src)
   153  	if err != nil {
   154  		c.T.Fatal(fmt.Errorf("error reading file %s from host filesystem: %w", src, err))
   155  	}
   156  
   157  	var perms os.FileMode
   158  	if len(mode) == 0 {
   159  		ext := filepath.Ext(dest)
   160  		if ext == ".sh" || ext == "" {
   161  			perms = pkg.FileModeExecutable
   162  		} else {
   163  			perms = pkg.FileModeWritable
   164  		}
   165  	} else {
   166  		perms = mode[0]
   167  	}
   168  
   169  	err = c.FileSystem.WriteFile(dest, data, perms)
   170  	if err != nil {
   171  		c.T.Fatal(fmt.Errorf("error writing file %s to test filesystem: %w", dest, err))
   172  	}
   173  
   174  	return data
   175  }
   176  
   177  func (c *TestContext) AddTestFileContents(file []byte, dest string) error {
   178  	return c.FileSystem.WriteFile(dest, file, pkg.FileModeWritable)
   179  }
   180  
   181  // Use this when the directory you are referencing is in a different directory than the test.
   182  func (c *TestContext) AddTestDirectoryFromRoot(srcDir, destDir string) {
   183  	pathFromRoot := filepath.Join(c.FindRepoRoot(), srcDir)
   184  	c.AddTestDirectory(pathFromRoot, destDir)
   185  }
   186  
   187  // AddTestDirectory adds a test directory where the filepath is relative to the test directory.
   188  // mode is optional and should only be specified once
   189  func (c *TestContext) AddTestDirectory(srcDir, destDir string, mode ...os.FileMode) {
   190  	if strings.Contains(srcDir, "..") {
   191  		c.T.Fatal(errors.New("use AddTestDirectoryFromRoot when referencing a test directory in a different directory than the test"))
   192  	}
   193  
   194  	err := filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error {
   195  		if err != nil {
   196  			return err
   197  		}
   198  
   199  		// Skip the root src directory
   200  		if path == srcDir {
   201  			return nil
   202  		}
   203  
   204  		// Translate the path from the src to the final destination
   205  		dest := filepath.Join(destDir, strings.TrimPrefix(path, srcDir))
   206  
   207  		if info.IsDir() {
   208  			return c.FileSystem.MkdirAll(dest, pkg.FileModeDirectory)
   209  		}
   210  
   211  		c.AddTestFile(path, dest, mode...)
   212  		return nil
   213  	})
   214  	if err != nil {
   215  		c.T.Fatal(err)
   216  	}
   217  }
   218  
   219  func (c *TestContext) AddTestDriver(src, name string) string {
   220  	data, err := os.ReadFile(src)
   221  	if err != nil {
   222  		c.T.Fatal(err)
   223  	}
   224  
   225  	dirname, err := c.FileSystem.TempDir("", "porter")
   226  	if err != nil {
   227  		c.T.Fatal(err)
   228  	}
   229  
   230  	// filename in accordance with cnab-go's command driver expectations
   231  	filename := fmt.Sprintf("%s/cnab-%s", dirname, name)
   232  
   233  	newfile, err := c.FileSystem.Create(filename)
   234  	if err != nil {
   235  		c.T.Fatal(err)
   236  	}
   237  
   238  	if len(data) > 0 {
   239  		_, err := newfile.Write(data)
   240  		if err != nil {
   241  			c.T.Fatal(err)
   242  		}
   243  	}
   244  
   245  	err = c.FileSystem.Chmod(newfile.Name(), pkg.FileModeExecutable)
   246  	if err != nil {
   247  		c.T.Fatal(err)
   248  	}
   249  	err = newfile.Close()
   250  	if err != nil {
   251  		c.T.Fatal(err)
   252  	}
   253  
   254  	path := c.Getenv("PATH")
   255  	pathlist := []string{dirname, path}
   256  	newpath := strings.Join(pathlist, string(os.PathListSeparator))
   257  	c.Setenv("PATH", newpath)
   258  
   259  	return dirname
   260  }
   261  
   262  // GetOutput returns all text printed to stdout.
   263  func (c *TestContext) GetOutput() string {
   264  	return c.capturedOut.String()
   265  }
   266  
   267  // GetError returns all text printed to stderr.
   268  func (c *TestContext) GetError() string {
   269  	return c.capturedErr.String()
   270  }
   271  
   272  // GetAllLogs returns all text logged both on stdout and stderr
   273  func (c *TestContext) GetAllLogs() string {
   274  	return c.captureLogs.String()
   275  }
   276  
   277  func (c *TestContext) ClearOutputs() {
   278  	c.capturedOut.Truncate(0)
   279  	c.capturedErr.Truncate(0)
   280  }
   281  
   282  // FindRepoRoot returns the path to the porter repository where the test is currently running
   283  func (c *TestContext) FindRepoRoot() string {
   284  	goMod := c.findRepoFile("go.mod")
   285  	return filepath.Dir(goMod)
   286  }
   287  
   288  // FindBinDir returns the path to the bin directory of the repository where the test is currently running
   289  func (c *TestContext) FindBinDir() string {
   290  	return c.findRepoFile("bin")
   291  }
   292  
   293  // Finds a file in the porter repository, does not use the mock filesystem
   294  func (c *TestContext) findRepoFile(wantFile string) string {
   295  	d := c.GetTestDefinitionDirectory()
   296  	for {
   297  		if foundFile, ok := c.hasChild(d, wantFile); ok {
   298  			return foundFile
   299  		}
   300  
   301  		d = filepath.Dir(d)
   302  		if d == "." || d == "" || d == filepath.Dir(d) {
   303  			c.T.Fatalf("could not find %s", wantFile)
   304  		}
   305  	}
   306  }
   307  
   308  func (c *TestContext) hasChild(dir string, childName string) (string, bool) {
   309  	children, err := os.ReadDir(dir)
   310  	if err != nil {
   311  		c.T.Fatal(err)
   312  	}
   313  	for _, child := range children {
   314  		if child.Name() == childName {
   315  			return filepath.Join(dir, child.Name()), true
   316  		}
   317  	}
   318  	return "", false
   319  }
   320  
   321  // CompareGoldenFile checks if the specified string matches the content of a golden test file.
   322  // When they are different and PORTER_UPDATE_TEST_FILES is true, the file is updated to match
   323  // the new test output.
   324  func (c *TestContext) CompareGoldenFile(goldenFile string, got string) {
   325  	test.CompareGoldenFile(c.T, goldenFile, got)
   326  }
   327  
   328  func (c *TestContext) EditYaml(path string, transformations ...func(yq *yaml.Editor) error) {
   329  	c.T.Log("Editing", path)
   330  	yq := yaml.NewEditor(c.FileSystem)
   331  
   332  	require.NoError(c.T, yq.ReadFile(path))
   333  	for _, transform := range transformations {
   334  		require.NoError(c.T, transform(yq))
   335  	}
   336  	require.NoError(c.T, yq.WriteFile(path))
   337  }