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 }