mirror of https://github.com/databricks/cli.git
303 lines
7.2 KiB
Go
303 lines
7.2 KiB
Go
|
package acceptance_test
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"os"
|
||
|
"os/exec"
|
||
|
"path/filepath"
|
||
|
"runtime"
|
||
|
"slices"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
"time"
|
||
|
|
||
|
"github.com/databricks/cli/internal/testutil"
|
||
|
"github.com/databricks/cli/libs/env"
|
||
|
"github.com/databricks/cli/libs/testdiff"
|
||
|
"github.com/stretchr/testify/require"
|
||
|
)
|
||
|
|
||
|
var KeepTmp = os.Getenv("KEEP_TMP") != ""
|
||
|
|
||
|
const (
|
||
|
EntryPointScript = "script"
|
||
|
CleanupScript = "script.cleanup"
|
||
|
PrepareScript = "script.prepare"
|
||
|
)
|
||
|
|
||
|
var Scripts = map[string]bool{
|
||
|
EntryPointScript: true,
|
||
|
CleanupScript: true,
|
||
|
PrepareScript: true,
|
||
|
}
|
||
|
|
||
|
func TestAccept(t *testing.T) {
|
||
|
execPath := BuildCLI(t)
|
||
|
// $CLI is what test scripts are using
|
||
|
t.Setenv("CLI", execPath)
|
||
|
|
||
|
server := StartServer(t)
|
||
|
AddHandlers(server)
|
||
|
// Redirect API access to local server:
|
||
|
t.Setenv("DATABRICKS_HOST", fmt.Sprintf("http://127.0.0.1:%d", server.Port))
|
||
|
t.Setenv("DATABRICKS_TOKEN", "dapi1234")
|
||
|
|
||
|
homeDir := t.TempDir()
|
||
|
// Do not read user's ~/.databrickscfg
|
||
|
t.Setenv(env.HomeEnvVar(), homeDir)
|
||
|
|
||
|
testDirs := getTests(t)
|
||
|
require.NotEmpty(t, testDirs)
|
||
|
for _, dir := range testDirs {
|
||
|
t.Run(dir, func(t *testing.T) {
|
||
|
t.Parallel()
|
||
|
runTest(t, dir)
|
||
|
})
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func getTests(t *testing.T) []string {
|
||
|
testDirs := make([]string, 0, 128)
|
||
|
|
||
|
err := filepath.Walk(".", func(path string, info os.FileInfo, err error) error {
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
name := filepath.Base(path)
|
||
|
if name == EntryPointScript {
|
||
|
// Presence of 'script' marks a test case in this directory
|
||
|
testDirs = append(testDirs, filepath.Dir(path))
|
||
|
}
|
||
|
return nil
|
||
|
})
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
sort.Strings(testDirs)
|
||
|
return testDirs
|
||
|
}
|
||
|
|
||
|
func runTest(t *testing.T, dir string) {
|
||
|
var tmpDir string
|
||
|
var err error
|
||
|
if KeepTmp {
|
||
|
tempDirBase := filepath.Join(os.TempDir(), "acceptance")
|
||
|
_ = os.Mkdir(tempDirBase, 0o755)
|
||
|
tmpDir, err = os.MkdirTemp(tempDirBase, "")
|
||
|
require.NoError(t, err)
|
||
|
t.Logf("Created directory: %s", tmpDir)
|
||
|
} else {
|
||
|
tmpDir = t.TempDir()
|
||
|
}
|
||
|
|
||
|
scriptContents := readMergedScriptContents(t, dir)
|
||
|
testutil.WriteFile(t, filepath.Join(tmpDir, EntryPointScript), scriptContents)
|
||
|
|
||
|
inputs := make(map[string]bool, 2)
|
||
|
outputs := make(map[string]bool, 2)
|
||
|
err = CopyDir(dir, tmpDir, inputs, outputs)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
args := []string{"bash", "-euo", "pipefail", EntryPointScript}
|
||
|
cmd := exec.Command(args[0], args[1:]...)
|
||
|
cmd.Dir = tmpDir
|
||
|
outB, err := cmd.CombinedOutput()
|
||
|
|
||
|
out := formatOutput(string(outB), err)
|
||
|
out = strings.ReplaceAll(out, os.Getenv("CLI"), "$CLI")
|
||
|
doComparison(t, filepath.Join(dir, "output.txt"), "script output", out)
|
||
|
|
||
|
for key := range outputs {
|
||
|
if key == "output.txt" {
|
||
|
// handled above
|
||
|
continue
|
||
|
}
|
||
|
pathNew := filepath.Join(tmpDir, key)
|
||
|
newValBytes, err := os.ReadFile(pathNew)
|
||
|
if err != nil {
|
||
|
if errors.Is(err, os.ErrNotExist) {
|
||
|
t.Errorf("%s: expected to find this file but could not (%s)", key, tmpDir)
|
||
|
} else {
|
||
|
t.Errorf("%s: could not read: %s", key, err)
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
pathExpected := filepath.Join(dir, key)
|
||
|
doComparison(t, pathExpected, pathNew, string(newValBytes))
|
||
|
}
|
||
|
|
||
|
// Make sure there are not unaccounted for new files
|
||
|
files, err := os.ReadDir(tmpDir)
|
||
|
require.NoError(t, err)
|
||
|
|
||
|
for _, f := range files {
|
||
|
name := f.Name()
|
||
|
if _, ok := inputs[name]; ok {
|
||
|
continue
|
||
|
}
|
||
|
if _, ok := outputs[name]; ok {
|
||
|
continue
|
||
|
}
|
||
|
t.Errorf("Unexpected output: %s", f)
|
||
|
if strings.HasPrefix(name, "out") {
|
||
|
// We have a new file starting with "out"
|
||
|
// Show the contents & support overwrite mode for it:
|
||
|
pathNew := filepath.Join(tmpDir, name)
|
||
|
newVal := testutil.ReadFile(t, pathNew)
|
||
|
doComparison(t, filepath.Join(dir, name), filepath.Join(tmpDir, name), newVal)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func doComparison(t *testing.T, pathExpected, pathNew, valueNew string) {
|
||
|
valueNew = testdiff.NormalizeNewlines(valueNew)
|
||
|
valueExpected := string(readIfExists(t, pathExpected))
|
||
|
valueExpected = testdiff.NormalizeNewlines(valueExpected)
|
||
|
testdiff.AssertEqualTexts(t, pathExpected, pathNew, valueExpected, valueNew)
|
||
|
if testdiff.OverwriteMode {
|
||
|
if valueNew != "" {
|
||
|
t.Logf("Overwriting: %s", pathExpected)
|
||
|
testutil.WriteFile(t, pathExpected, valueNew)
|
||
|
} else {
|
||
|
t.Logf("Removing: %s", pathExpected)
|
||
|
_ = os.Remove(pathExpected)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Returns combined script.prepare (root) + script.prepare (parent) + ... + script + ... + script.cleanup (parent) + ...
|
||
|
// Note, cleanups are not executed if main script fails; that's not a huge issue, since it runs it temp dir.
|
||
|
func readMergedScriptContents(t *testing.T, dir string) string {
|
||
|
scriptContents := testutil.ReadFile(t, filepath.Join(dir, EntryPointScript))
|
||
|
prepares := []string{}
|
||
|
cleanups := []string{}
|
||
|
|
||
|
for {
|
||
|
x := readIfExists(t, filepath.Join(dir, CleanupScript))
|
||
|
if len(x) > 0 {
|
||
|
cleanups = append(cleanups, string(x))
|
||
|
}
|
||
|
|
||
|
x = readIfExists(t, filepath.Join(dir, PrepareScript))
|
||
|
if len(x) > 0 {
|
||
|
prepares = append(prepares, string(x))
|
||
|
}
|
||
|
|
||
|
if dir == "" || dir == "." {
|
||
|
break
|
||
|
}
|
||
|
|
||
|
dir = filepath.Dir(dir)
|
||
|
require.True(t, filepath.IsLocal(dir))
|
||
|
}
|
||
|
|
||
|
slices.Reverse(prepares)
|
||
|
prepares = append(prepares, scriptContents)
|
||
|
prepares = append(prepares, cleanups...)
|
||
|
return strings.Join(prepares, "\n")
|
||
|
}
|
||
|
|
||
|
func BuildCLI(t *testing.T) string {
|
||
|
cwd, err := os.Getwd()
|
||
|
require.NoError(t, err)
|
||
|
execPath := filepath.Join(cwd, "build", "databricks")
|
||
|
if runtime.GOOS == "windows" {
|
||
|
execPath += ".exe"
|
||
|
}
|
||
|
|
||
|
start := time.Now()
|
||
|
args := []string{"go", "build", "-mod", "vendor", "-o", execPath}
|
||
|
cmd := exec.Command(args[0], args[1:]...)
|
||
|
cmd.Dir = ".."
|
||
|
out, err := cmd.CombinedOutput()
|
||
|
elapsed := time.Since(start)
|
||
|
t.Logf("%s took %s", args, elapsed)
|
||
|
require.NoError(t, err, "go build failed: %s: %s\n%s", args, err, out)
|
||
|
if len(out) > 0 {
|
||
|
t.Logf("go build output: %s: %s", args, out)
|
||
|
}
|
||
|
|
||
|
// Quick check + warm up cache:
|
||
|
cmd = exec.Command(execPath, "--version")
|
||
|
out, err = cmd.CombinedOutput()
|
||
|
require.NoError(t, err, "%s --version failed: %s\n%s", execPath, err, out)
|
||
|
return execPath
|
||
|
}
|
||
|
|
||
|
func copyFile(src, dst string) error {
|
||
|
in, err := os.Open(src)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer in.Close()
|
||
|
|
||
|
out, err := os.Create(dst)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
defer out.Close()
|
||
|
|
||
|
_, err = io.Copy(out, in)
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
func formatOutput(out string, err error) string {
|
||
|
if err == nil {
|
||
|
return out
|
||
|
}
|
||
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
||
|
exitCode := exiterr.ExitCode()
|
||
|
out += fmt.Sprintf("\nExit code: %d\n", exitCode)
|
||
|
} else {
|
||
|
out += fmt.Sprintf("\nError: %s\n", err)
|
||
|
}
|
||
|
return out
|
||
|
}
|
||
|
|
||
|
func readIfExists(t *testing.T, path string) []byte {
|
||
|
data, err := os.ReadFile(path)
|
||
|
if err == nil {
|
||
|
return data
|
||
|
}
|
||
|
|
||
|
if !errors.Is(err, os.ErrNotExist) {
|
||
|
t.Fatalf("%s: %s", path, err)
|
||
|
}
|
||
|
return []byte{}
|
||
|
}
|
||
|
|
||
|
func CopyDir(src, dst string, inputs, outputs map[string]bool) error {
|
||
|
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
name := info.Name()
|
||
|
|
||
|
relPath, err := filepath.Rel(src, path)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
if strings.HasPrefix(name, "out") {
|
||
|
outputs[relPath] = true
|
||
|
return nil
|
||
|
} else {
|
||
|
inputs[relPath] = true
|
||
|
}
|
||
|
|
||
|
if _, ok := Scripts[name]; ok {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
destPath := filepath.Join(dst, relPath)
|
||
|
|
||
|
if info.IsDir() {
|
||
|
return os.MkdirAll(destPath, info.Mode())
|
||
|
}
|
||
|
|
||
|
return copyFile(path, destPath)
|
||
|
})
|
||
|
}
|