From bf59aa037e43d71b98690327fa2cdc256bae66f8 Mon Sep 17 00:00:00 2001 From: Will Roden Date: Sat, 9 Dec 2023 16:06:19 -0600 Subject: [PATCH 1/4] golden tests --- cmd/bindown/bootstrap_test.go | 14 +-- .../bindown/testdata/bootstrap}/checksums.txt | 0 .../testdata/golden/bootstrap/bootstrap.sh | 0 internal/testutil/golden.go | 117 ++++++++++++++++++ script/lint | 4 +- 5 files changed, 123 insertions(+), 12 deletions(-) rename {internal/bindown/testdata/build-bootstrapper => cmd/bindown/testdata/bootstrap}/checksums.txt (100%) rename internal/bindown/testdata/build-bootstrapper/bootstrap-bindown.sh => cmd/bindown/testdata/golden/bootstrap/bootstrap.sh (100%) create mode 100644 internal/testutil/golden.go diff --git a/cmd/bindown/bootstrap_test.go b/cmd/bindown/bootstrap_test.go index bcaec79..578fbd9 100644 --- a/cmd/bindown/bootstrap_test.go +++ b/cmd/bindown/bootstrap_test.go @@ -1,29 +1,25 @@ package main import ( - "os" "path/filepath" "testing" - "github.com/stretchr/testify/require" "github.com/willabides/bindown/v4/internal/testutil" ) func Test_bootstrapCmd(t *testing.T) { - output := filepath.Join(t.TempDir(), "foo", "bootstrap.sh") + targetDir := filepath.Join(t.TempDir(), "target") + output := filepath.Join(targetDir, "bootstrap.sh") runner := newCmdRunner(t) server := testutil.ServeFile( t, - testdataPath("build-bootstrapper/checksums.txt"), + "testdata/bootstrap/checksums.txt", "/WillAbides/bindown/releases/download/v4.8.0/checksums.txt", "", ) - want, err := os.ReadFile(testdataPath("build-bootstrapper/bootstrap-bindown.sh")) - require.NoError(t, err) result := runner.run("bootstrap", "--output", output, "--tag", "4.8.0", "--base-url", server.URL) result.assertState(resultState{}) - got, err := os.ReadFile(output) - require.NoError(t, err) - require.Equal(t, string(want), string(got)) + + testutil.CheckGoldenDir(t, targetDir, filepath.FromSlash("testdata/golden/bootstrap")) } diff --git a/internal/bindown/testdata/build-bootstrapper/checksums.txt b/cmd/bindown/testdata/bootstrap/checksums.txt similarity index 100% rename from internal/bindown/testdata/build-bootstrapper/checksums.txt rename to cmd/bindown/testdata/bootstrap/checksums.txt diff --git a/internal/bindown/testdata/build-bootstrapper/bootstrap-bindown.sh b/cmd/bindown/testdata/golden/bootstrap/bootstrap.sh similarity index 100% rename from internal/bindown/testdata/build-bootstrapper/bootstrap-bindown.sh rename to cmd/bindown/testdata/golden/bootstrap/bootstrap.sh diff --git a/internal/testutil/golden.go b/internal/testutil/golden.go new file mode 100644 index 0000000..979457d --- /dev/null +++ b/internal/testutil/golden.go @@ -0,0 +1,117 @@ +package testutil + +import ( + "bytes" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func updateGoldenDir(t *testing.T, resultDir, goldenDir string) { + t.Helper() + if os.Getenv("UPDATE_GOLDEN") == "" { + return + } + require.NoError(t, os.RemoveAll(goldenDir)) + err := filepath.WalkDir(resultDir, func(path string, d fs.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + fmt.Println(path) + relName := mustRel(t, resultDir, path) + return copyFile(path, filepath.Join(goldenDir, relName)) + }) + require.NoError(t, err) +} + +func copyFile(src, dst string) (errOut error) { + err := os.MkdirAll(filepath.Dir(dst), 0o777) + if err != nil { + return err + } + dstFile, err := os.Create(dst) + if err != nil { + return err + } + defer func() { + e := dstFile.Close() + if errOut == nil { + errOut = e + } + }() + srcFile, err := os.Open(src) + if err != nil { + return err + } + defer func() { + e := srcFile.Close() + if errOut == nil { + errOut = e + } + }() + _, err = io.Copy(dstFile, srcFile) + return err +} + +func CheckGoldenDir(t *testing.T, resultDir, goldenDir string) { + t.Helper() + golden := true + t.Cleanup(func() { + t.Helper() + if !golden { + t.Log("To regenerate golden files run `UPDATE_GOLDEN=1 script/test`") + } + }) + updateGoldenDir(t, resultDir, goldenDir) + checked := map[string]bool{} + _, err := os.Stat(goldenDir) + if err == nil { + assert.NoError(t, filepath.WalkDir(goldenDir, func(wantPath string, info fs.DirEntry, err error) error { + relPath := mustRel(t, goldenDir, wantPath) + if err != nil || info.IsDir() { + return err + } + if !assertEqualFiles(t, wantPath, filepath.Join(resultDir, relPath)) { + golden = false + } + checked[relPath] = true + return nil + })) + } + assert.NoError(t, filepath.Walk(resultDir, func(resultPath string, info fs.FileInfo, err error) error { + relPath := mustRel(t, resultDir, resultPath) + if err != nil || info.IsDir() || checked[relPath] { + return err + } + golden = false + return fmt.Errorf("found unexpected file:\n%s", relPath) + })) +} + +func mustRel(t *testing.T, base, target string) string { + t.Helper() + rel, err := filepath.Rel(base, target) + require.NoError(t, err) + return rel +} + +func assertEqualFiles(t *testing.T, want, got string) bool { + t.Helper() + wantBytes, err := os.ReadFile(want) + if !assert.NoError(t, err) { + return false + } + wantBytes = bytes.ReplaceAll(wantBytes, []byte("\r\n"), []byte("\n")) + gotBytes, err := os.ReadFile(got) + if !assert.NoError(t, err) { + return false + } + gotBytes = bytes.ReplaceAll(gotBytes, []byte("\r\n"), []byte("\n")) + return assert.Equal(t, string(wantBytes), string(gotBytes)) +} diff --git a/script/lint b/script/lint index ab93476..1610641 100755 --- a/script/lint +++ b/script/lint @@ -4,11 +4,9 @@ set -e CDPATH="" cd -- "$(dirname -- "$0")/.." -script/bindown -q install golangci-lint shellcheck shfmt +script/bindown -q install golangci-lint shellcheck bin/golangci-lint run # Don't check bootstrap-bindown.sh because it's dynamically generated find script -type f -not -name 'bootstrap-bindown.sh' -print0 | xargs -0 bin/shellcheck bin/shellcheck -s sh internal/build-bootstrapper/assets/*.sh -bin/shellcheck -s sh internal/bindown/testdata/build-bootstrapper/bootstrap-bindown.sh -bin/shfmt -i 2 -ci -sr --diff internal/bindown/testdata/build-bootstrapper/bootstrap-bindown.sh From acc07cb090c46d51e4da1bf1a0f5b012a49237e6 Mon Sep 17 00:00:00 2001 From: Will Roden Date: Sat, 9 Dec 2023 16:32:21 -0600 Subject: [PATCH 2/4] update testutil --- cmd/bindown/cli_test.go | 16 ++----- internal/bindown/config_test.go | 6 +-- internal/testutil/testutil.go | 77 +++++++++++++++++++++++++++++++-- 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/cmd/bindown/cli_test.go b/cmd/bindown/cli_test.go index 985c0a5..5f4ed84 100644 --- a/cmd/bindown/cli_test.go +++ b/cmd/bindown/cli_test.go @@ -416,10 +416,7 @@ url_checksums: stdout: `installed foo to`, }) wantBin := filepath.Join(runner.tmpDir, "bin", "foo") - require.FileExists(t, wantBin) - stat, err := os.Stat(wantBin) - require.NoError(t, err) - testutil.AssertExecutable(t, stat.Mode()) + testutil.AssertFile(t, wantBin, true, false) }) t.Run("link raw file", func(t *testing.T) { @@ -440,11 +437,7 @@ url_checksums: stdout: `installed foo to`, }) wantBin := filepath.Join(runner.tmpDir, "bin", "foo") - require.FileExists(t, wantBin) - stat, err := os.Lstat(wantBin) - require.NoError(t, err) - testutil.AssertExecutable(t, stat.Mode()) - require.True(t, stat.Mode()&os.ModeSymlink != 0) + testutil.AssertFile(t, wantBin, true, true) }) t.Run("bin in root", func(t *testing.T) { @@ -464,10 +457,7 @@ url_checksums: stdout: `installed foo to`, }) wantBin := filepath.Join(runner.tmpDir, "bin", "foo") - require.FileExists(t, wantBin) - stat, err := os.Stat(wantBin) - require.NoError(t, err) - testutil.AssertExecutable(t, stat.Mode()) + testutil.AssertFile(t, wantBin, true, false) }) t.Run("wrong checksum", func(t *testing.T) { diff --git a/internal/bindown/config_test.go b/internal/bindown/config_test.go index 0a862c6..e63e91c 100644 --- a/internal/bindown/config_test.go +++ b/internal/bindown/config_test.go @@ -220,11 +220,7 @@ dependencies: }) require.NoError(t, err) require.Equal(t, wantStdout, stdout.String()) - require.True(t, FileExists(wantBin)) - stat, err := os.Stat(wantBin) - require.NoError(t, err) - require.False(t, stat.IsDir()) - testutil.AssertExecutable(t, stat.Mode()) + testutil.AssertFile(t, wantBin, true, false) }) t.Run("bin in root", func(t *testing.T) { diff --git a/internal/testutil/testutil.go b/internal/testutil/testutil.go index 96a58ef..c85351d 100644 --- a/internal/testutil/testutil.go +++ b/internal/testutil/testutil.go @@ -2,18 +2,53 @@ package testutil import ( "bytes" + "fmt" "io/fs" "net/http" "net/http/httptest" "os" + "os/exec" "path/filepath" "runtime" + "sync" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +var bindownBinOnce sync.Once + +func BindownBin() string { + bindownBinPath := filepath.Join(RepoRoot(), "tmp", "_test", "bindown") + bindownBinOnce.Do(func() { + cmd := exec.Command(goExec(), "build", "-o", bindownBinPath, "./cmd/bindown") + cmd.Dir = RepoRoot() + err := cmd.Run() + if err != nil { + panic(fmt.Sprintf("error building bindown: %v", err)) + } + }) + return bindownBinPath +} + +// goExec returns te path to the go executable to use for tests. +func goExec() string { + goRoot := runtime.GOROOT() + if goRoot != "" { + p := filepath.Join(goRoot, "bin", "go") + info, err := os.Stat(p) + if err == nil && !info.IsDir() { + return p + } + } + p, err := exec.LookPath("go") + if err != nil { + panic("unable to find go executable") + } + return p +} + // ServeFile starts an HTTP server func ServeFile(t *testing.T, file, path, query string) *httptest.Server { t.Helper() @@ -58,11 +93,45 @@ func ServeFiles(t *testing.T, files map[string]string) *httptest.Server { return ts } -func AssertExecutable(t *testing.T, mode fs.FileMode) { +// AssertFile asserts that the file at filename exists and has the given properties. +func AssertFile(t *testing.T, filename string, wantExecutable, wantLink bool) bool { t.Helper() - // Windows doesn't have executable bits + linfo, err := os.Lstat(filename) + if !assert.NoError(t, err) { + return false + } + var ok bool + if wantLink { + ok = assert.True(t, linfo.Mode()&fs.ModeSymlink != 0, "expected %s to be a symlink", filename) + } else { + ok = assert.False(t, linfo.Mode()&fs.ModeSymlink != 0, "expected %s to not be a symlink", filename) + } + if !ok { + return false + } + // windows doesn't have executable bit so we can't check it if runtime.GOOS == "windows" { - return + return false + } + info, err := os.Stat(filename) + if !assert.NoError(t, err) { + return false + } + if wantExecutable { + ok = assert.True(t, info.Mode()&0o110 != 0, "expected %s to be executable", filename) + } else { + ok = assert.False(t, info.Mode()&0o110 != 0, "expected %s to not be executable", filename) + } + return ok +} + +// RepoRoot returns the absolute path to the root of this repo +func RepoRoot() string { + _, filename, _, _ := runtime.Caller(0) + dir := filepath.Join(filepath.Dir(filename), "..", "..") + abs, err := filepath.Abs(dir) + if err != nil { + panic(err) } - assert.Equal(t, fs.FileMode(0o110), mode&0o110) + return abs } From 88aa45dda2a6c74275bb3c5ca2d0d3e9e14df2dc Mon Sep 17 00:00:00 2001 From: Will Roden Date: Sat, 9 Dec 2023 16:35:21 -0600 Subject: [PATCH 3/4] Test_wrapCmd --- cmd/bindown/cli_test.go | 30 +++++++++++++++++++ .../golden/wrap/bindown-path/runnable | 15 ++++++++++ 2 files changed, 45 insertions(+) create mode 100644 cmd/bindown/testdata/golden/wrap/bindown-path/runnable diff --git a/cmd/bindown/cli_test.go b/cmd/bindown/cli_test.go index 5f4ed84..0488dc4 100644 --- a/cmd/bindown/cli_test.go +++ b/cmd/bindown/cli_test.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "os/exec" "path/filepath" "runtime" "strings" @@ -478,3 +479,32 @@ url_checksums: require.NoFileExists(t, filepath.Join(runner.tmpDir, "bin", "foo")) }) } + +func Test_wrapCmd(t *testing.T) { + t.Run("bindown path", func(t *testing.T) { + runner := newCmdRunner(t) + servePath := testdataPath("downloadables/runnable.tar.gz") + ts := testutil.ServeFile(t, servePath, "/runnable/runnable.tar.gz", "") + depURL := ts.URL + "/runnable/runnable.tar.gz" + runner.writeConfigYaml(fmt.Sprintf(` +dependencies: + runnable: + archive_path: bin/runnable.sh + url: %s +url_checksums: + %s: fb2fe41a34b77ee180def0cb9a222d8776a6e581106009b64f35983da291ab6e +`, depURL, depURL)) + outputDir := filepath.Join(runner.tmpDir, "output") + runnable := filepath.Join(outputDir, "runnable") + result := runner.run("wrap", "runnable", "--bindown", testutil.BindownBin(), "--output", runnable) + result.assertState(resultState{stdout: runnable}) + testutil.AssertFile(t, runnable, true, false) + testutil.CheckGoldenDir(t, outputDir, filepath.FromSlash("testdata/golden/wrap/bindown-path")) + + // make sure it runs + cmd := exec.Command("sh", "-c", filepath.ToSlash(runnable)) + out, err := cmd.Output() + require.NoError(t, err) + require.Equal(t, "Hello world", strings.TrimSpace(string(out))) + }) +} diff --git a/cmd/bindown/testdata/golden/wrap/bindown-path/runnable b/cmd/bindown/testdata/golden/wrap/bindown-path/runnable new file mode 100644 index 0000000..1fbacc4 --- /dev/null +++ b/cmd/bindown/testdata/golden/wrap/bindown-path/runnable @@ -0,0 +1,15 @@ +#!/bin/sh +# Code generated by bindown. DO NOT EDIT. + +set -e + +bindown_bin="$( + CDPATH="" cd -- "$(dirname -- "$0")" + + "../../../../../../../../../Users/wroden/repos/WillAbides/bindown/tmp/_test/bindown" install "runnable" \ + --to-cache \ + --configfile "../.bindown.yaml" \ + --cache "../cache" +)" + +exec "$bindown_bin" "$@" From d58599dddfc68510bd8295d80af8f5a5b944a4b8 Mon Sep 17 00:00:00 2001 From: Will Roden Date: Sat, 9 Dec 2023 17:44:57 -0600 Subject: [PATCH 4/4] testTmp --- .../golden/wrap/bindown-path/runnable | 2 +- cmd/bindown/testutil_test.go | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/cmd/bindown/testdata/golden/wrap/bindown-path/runnable b/cmd/bindown/testdata/golden/wrap/bindown-path/runnable index 1fbacc4..f34a546 100644 --- a/cmd/bindown/testdata/golden/wrap/bindown-path/runnable +++ b/cmd/bindown/testdata/golden/wrap/bindown-path/runnable @@ -6,7 +6,7 @@ set -e bindown_bin="$( CDPATH="" cd -- "$(dirname -- "$0")" - "../../../../../../../../../Users/wroden/repos/WillAbides/bindown/tmp/_test/bindown" install "runnable" \ + "../../bindown" install "runnable" \ --to-cache \ --configfile "../.bindown.yaml" \ --cache "../cache" diff --git a/cmd/bindown/testutil_test.go b/cmd/bindown/testutil_test.go index 7b0fadc..4a073fe 100644 --- a/cmd/bindown/testutil_test.go +++ b/cmd/bindown/testutil_test.go @@ -38,7 +38,7 @@ type cmdRunner struct { func newCmdRunner(t testing.TB) *cmdRunner { t.Helper() - dir := t.TempDir() + dir := testTmp(t) cacheDir := filepath.Join(dir, "cache") configfile := filepath.Join(dir, ".bindown.yaml") runner := &cmdRunner{ @@ -243,6 +243,24 @@ func testInDir(t testing.TB, dir string) { assert.NoError(t, os.Chdir(dir)) } +// testTmp is like t.TempDir but it uses a directory in this repo's tmp directory. +// This is useful so that there can be a relative path from the resulting directory to +// directories in this repo. +func testTmp(t testing.TB) string { + t.Helper() + tmpDir := filepath.FromSlash("../../tmp/_test") + err := os.MkdirAll(tmpDir, 0o777) + require.NoError(t, err) + dir, err := os.MkdirTemp(tmpDir, "") + require.NoError(t, err) + t.Cleanup(func() { + assert.NoError(t, os.RemoveAll(dir)) + }) + abs, err := filepath.Abs(dir) + require.NoError(t, err) + return abs +} + func testdataPath(f string) string { return filepath.Join( filepath.FromSlash("../../internal/bindown/testdata"),