internal/buildpacktestenv/buildpacktestenv.go (148 lines of code) (raw):

// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package buildpacktestenv contains utilities for setting up environments // for buildpack tests. package buildpacktestenv import ( "encoding/json" "fmt" "io/ioutil" "os" "os/exec" "path/filepath" "testing" ) const ( // EnvHelperMockProcessMap is the env var used to communicate the intended // behavior of the mock process for various commands. It contains a // map[string]MockProcess serialized to JSON. EnvHelperMockProcessMap = "HELPER_MOCK_PROCESS_MAP" ) // TempDirs represents temp directories used for buildpack environment setup. type TempDirs struct { LayersDir string PlatformDir string CodeDir string BuildpackDir string PlanFile string } // TempWorkingDir creates a temp dir, sets the current working directory to it, and returns a clean up function to restore everything back. func TempWorkingDir(t *testing.T) (string, func()) { t.Helper() oldwd, err := os.Getwd() if err != nil { t.Fatalf("getting working dir: %v", err) } newwd, err := ioutil.TempDir("", "source-") if err != nil { t.Fatalf("creating temp dir: %v", err) } if err := os.Chdir(newwd); err != nil { t.Fatalf("setting current dir to %q: %v", newwd, err) } return newwd, func() { if err := os.Chdir(oldwd); err != nil { t.Fatalf("restoring old current dir to %q: %v", oldwd, err) } if err := os.RemoveAll(newwd); err != nil { t.Fatalf("deleting temp dir %q: %v", newwd, err) } } } // SetUpTempDirs sets up temp directories that mimic the layers of buildpacks. func SetUpTempDirs(t *testing.T, customCodeDir string) TempDirs { t.Helper() layersDir := t.TempDir() platformDir := t.TempDir() codeDir := "" if customCodeDir != "" { err := os.Mkdir(filepath.Join(os.TempDir(), customCodeDir), 0700) if err != nil { t.Fatalf("creating code dir: %v", err) } codeDir = filepath.Join(os.TempDir(), customCodeDir) } else { codeDir = t.TempDir() } buildpackDir := t.TempDir() stack := "com.stack" buildpackTOML := fmt.Sprintf(` api = "0.9" [buildpack] id = "my-id" version = "my-version" name = "my-name" [[stacks]] id = "%s" `, stack) if err := ioutil.WriteFile(filepath.Join(buildpackDir, "buildpack.toml"), []byte(buildpackTOML), 0644); err != nil { t.Fatalf("writing buildpack.toml: %v", err) } planTOML := ` [[entries]] name = "entry-name" version = "entry-version" [entries.metadata] entry-meta-key = "entry-meta-value" ` if err := ioutil.WriteFile(filepath.Join(buildpackDir, "plan.toml"), []byte(planTOML), 0644); err != nil { t.Fatalf("writing plan.toml: %v", err) } if err := os.Setenv("CNB_STACK_ID", stack); err != nil { t.Fatalf("setting env var CNB_STACK_ID: %v", err) } if err := os.Setenv("CNB_BUILDPACK_DIR", buildpackDir); err != nil { t.Fatalf("setting env var CNB_BUILDPACK_DIR: %v", err) } // Set Plartform dir if err := os.Setenv("CNB_PLATFORM_DIR", platformDir); err != nil { t.Fatalf("setting env var CNB_PLATFORM_DIR: %v", err) } // Set Build plan path if err := os.Setenv("CNB_BUILD_PLAN_PATH", filepath.Join(buildpackDir, "plan.toml")); err != nil { t.Fatalf("setting env var CNB_BUILD_PLAN_PATH: %v", err) } // Set Layers dir if err := os.Setenv("CNB_LAYERS_DIR", layersDir); err != nil { t.Fatalf("setting env var CNB_LAYERS_DIR: %v", err) } // Set CNB_BP_PLAN_PATH if err := os.Setenv("CNB_BP_PLAN_PATH", filepath.Join(buildpackDir, "plan.toml")); err != nil { t.Fatalf("setting env var CNB_BP_PLAN_PATH: %v", err) } temps := TempDirs{ CodeDir: codeDir, LayersDir: layersDir, PlatformDir: platformDir, BuildpackDir: buildpackDir, PlanFile: filepath.Join(buildpackDir, "plan.toml"), } t.Cleanup(func() { if err := os.RemoveAll(codeDir); err != nil { t.Fatalf("removing code dir %q: %v", codeDir, err) } if err := os.Unsetenv("CNB_STACK_ID"); err != nil { t.Fatalf("unsetting CNB_STACK_ID: %v", err) } if err := os.Unsetenv("CNB_BUILDPACK_DIR"); err != nil { t.Fatalf("unsetting CNB_BUILDPACK_DIR: %v", err) } if err := os.Unsetenv("CNB_PLATFORM_DIR"); err != nil { t.Fatalf("unsetting CNB_PLATFORM_DIR: %v", err) } if err := os.Unsetenv("CNB_BUILD_PLAN_PATH"); err != nil { t.Fatalf("unsetting CNB_BUILD_PLAN_PATH: %v", err) } }) return temps } // MockProcess encapsulates the behavior of a mock process for test. // To add more behaviors to the mock process, expand this struct and implement // the corresponding handling in // internal/buildpacktest/mockprocess/mockprocess.go type MockProcess struct { // Stdout is the message that should be printed to stdout. Stdout string // Stderr is the message that should be printed to stderr. Stderr string // ExitCode is the exit code that the process should use. ExitCode int } // NewMockExecCmd constructs an ExecCmd that can replace standard exec.Cmd calls // with custom behavior for testing. It takes the path to the mock process // binary as the first argument, and a map of // { command : mock action to simulate the commands behavior }. The command // must be at least a partial match to the full command (binary name and all // args) that would have been executed by exec.Cmd. func NewMockExecCmd(t *testing.T, mockProcess string, commands map[string]*MockProcess) func(name string, args ...string) *exec.Cmd { t.Helper() b, err := json.Marshal(commands) if err != nil { t.Fatalf("unable to marshal MockProcess map to JSON: %v", err) } return func(name string, args ...string) *exec.Cmd { cmd := exec.Command(mockProcess, append([]string{name}, args...)...) cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", EnvHelperMockProcessMap, string(b))) return cmd } } // UnmarshalMockProcessMap is a utility function that marshals a // map[string]MockProcess from JSON. func UnmarshalMockProcessMap(data string) (map[string]*MockProcess, error) { var mocks map[string]*MockProcess if err := json.Unmarshal([]byte(data), &mocks); err != nil { return mocks, err } return mocks, nil }