346 lines
9.4 KiB
Go
346 lines
9.4 KiB
Go
package versionscript_test
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
repoRoot = "../.."
|
|
scriptPath = "../../script/version"
|
|
makefilePath = "../../Makefile"
|
|
dockerfilePath = "../../Dockerfile"
|
|
|
|
// unknown is what a tree with no git metadata and no $VERSION must
|
|
// report: a source tarball has no way to know its version, and the
|
|
// one thing it must not do is name a tag it may not be at.
|
|
unknown = "unknown"
|
|
|
|
// scriptMode keeps the copied script runnable; dirMode and fileMode
|
|
// are the ordinary permissions for the throwaway tree around it.
|
|
scriptMode = 0o755
|
|
dirMode = 0o750
|
|
fileMode = 0o600
|
|
)
|
|
|
|
// checkout is a throwaway working tree carrying a copy of the script
|
|
// under test at the same path it lives at in this repository, since the
|
|
// script resolves the checkout root from its own location.
|
|
type checkout struct {
|
|
dir string
|
|
head string
|
|
}
|
|
|
|
func TestVersion_CleanTagReportsExactlyTheTag(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
require.Equal(t, "v1.2.3", c.version(t))
|
|
}
|
|
|
|
func TestVersion_CommitAfterTagCarriesDistanceAndSHA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
head := c.commit(t, "after the tag")
|
|
|
|
got := c.version(t)
|
|
|
|
require.NotEqual(t, "v1.2.3", got,
|
|
"a commit past the tag must not claim to be the tag")
|
|
require.True(t, strings.HasPrefix(got, "v1.2.3-1-g"),
|
|
"want describe form v1.2.3-1-g<sha>, got %q", got)
|
|
require.True(t, strings.HasPrefix(head, strings.TrimPrefix(
|
|
got, "v1.2.3-1-g")),
|
|
"%q must carry the abbreviated head SHA of %q", got, head)
|
|
}
|
|
|
|
func TestVersion_UntaggedHistoryReportsShortSHA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
|
|
got := c.version(t)
|
|
|
|
require.NotEmpty(t, got)
|
|
require.NotEqual(t, unknown, got)
|
|
require.True(t, strings.HasPrefix(c.head, got),
|
|
"%q must be an abbreviation of head %q", got, c.head)
|
|
}
|
|
|
|
func TestVersion_UncommittedChangesAreMarkedDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(c.dir, "tracked.txt"), []byte("edited\n"), fileMode,
|
|
))
|
|
|
|
require.Equal(t, "v1.2.3-dirty", c.version(t))
|
|
}
|
|
|
|
// A source tarball, or any build context without .git, still has to
|
|
// build. It reports "unknown" rather than failing or naming a tag.
|
|
func TestVersion_NoGitMetadataReportsUnknown(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
require.NoError(t, os.RemoveAll(filepath.Join(c.dir, ".git")))
|
|
|
|
require.Equal(t, unknown, c.version(t))
|
|
}
|
|
|
|
// An unpacked tarball can sit inside an unrelated working copy. The
|
|
// enclosing repository's version is not this tree's version.
|
|
func TestVersion_EnclosingRepositoryIsNotUsed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
outer := newCheckout(t)
|
|
outer.git(t, "tag", "v9.9.9")
|
|
|
|
inner := filepath.Join(outer.dir, "unpacked")
|
|
require.NoError(t, os.MkdirAll(filepath.Join(inner, "script"), dirMode))
|
|
copyScript(t, inner)
|
|
|
|
require.Equal(t, unknown, runScript(t, inner, nil))
|
|
}
|
|
|
|
// The Docker build has no git metadata, so the version arrives as an
|
|
// environment override. It wins over anything derivable.
|
|
func TestVersion_EnvironmentOverrideWins(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
require.Equal(t, "v4.5.6",
|
|
runScript(t, c.dir, []string{"VERSION=v4.5.6"}))
|
|
}
|
|
|
|
// An empty VERSION is treated as unset rather than stamping an empty
|
|
// string: the Dockerfile's build arg has a non-empty default, but a
|
|
// caller exporting VERSION= must not produce a binary reporting "".
|
|
func TestVersion_EmptyOverrideFallsBackToGit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
require.Equal(t, "v1.2.3", runScript(t, c.dir, []string{"VERSION="}))
|
|
}
|
|
|
|
// Two builds of the same commit must produce a byte-identical binary,
|
|
// which they cannot if the stamped value moves between invocations.
|
|
func TestVersion_IsStableAcrossInvocations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
first := c.version(t)
|
|
second := c.version(t)
|
|
|
|
require.Equal(t, first, second)
|
|
}
|
|
|
|
// The build target is the only place composing linker flags. If a
|
|
// future edit drops either half, the binary silently reports "dev"
|
|
// again (the defect this package exists for) or fails to link on
|
|
// Alpine.
|
|
func TestMakefile_BuildComposesVersionAndExtraFlags(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
makefile := read(t, makefilePath)
|
|
|
|
require.Contains(t, makefile, "-X main.version=$(VERSION)")
|
|
require.Contains(t, makefile, "$(GO_LDFLAGS)")
|
|
require.Contains(t, makefile, "VERSION ?= $(shell script/version)")
|
|
}
|
|
|
|
// A caller can define VERSION as the empty string -- `make build
|
|
// VERSION=`, or a `--build-arg VERSION=` reaching the Dockerfile's `make
|
|
// build VERSION="$VERSION"`. script/version's own guard does not cover
|
|
// that: the value never passes through the script. Stamping "" would
|
|
// leave the binary reporting no version and the footer on "dev", which
|
|
// is the defect this package exists for.
|
|
func TestMakefile_EmptyOverrideResolvesLikeAnUnsetOne(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// A plain assignment would be ignored here: a command-line
|
|
// definition outranks it, and that is the case being corrected.
|
|
require.Contains(t, read(t, makefilePath), "override VERSION :=")
|
|
|
|
requireMake(t)
|
|
|
|
derived := makeVersion(t)
|
|
require.NotEmpty(t, derived)
|
|
|
|
require.Equal(t, derived, makeVersion(t, "VERSION="),
|
|
"an empty VERSION must resolve the way an unset one does")
|
|
require.Equal(t, "v9.9.9", makeVersion(t, "VERSION=v9.9.9"),
|
|
"the empty guard must not clobber a real override")
|
|
}
|
|
|
|
// makeVersion runs this repository's `version` target, which prints the
|
|
// value `make build` would stamp, with the given command-line
|
|
// definitions.
|
|
func makeVersion(t *testing.T, defs ...string) string {
|
|
t.Helper()
|
|
|
|
//nolint:gosec // fixed argv, arguments are test constants
|
|
cmd := exec.CommandContext(t.Context(), "make",
|
|
append([]string{"--no-print-directory", "version"}, defs...)...)
|
|
cmd.Dir = repoRoot
|
|
|
|
// Only the command-line definitions may decide the outcome: an
|
|
// inherited VERSION would change what an unset one resolves to, and
|
|
// an inherited MAKEFLAGS carries the parent's jobserver.
|
|
cmd.Env = append(os.Environ(), "VERSION=", "MAKEFLAGS=", "MAKELEVEL=")
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, string(out))
|
|
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func requireMake(t *testing.T) {
|
|
t.Helper()
|
|
|
|
_, err := exec.LookPath("make")
|
|
if err != nil {
|
|
t.Skipf("make is not installed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Every compile in the image goes through the build target, so the
|
|
// static relink cannot replace the flags that carry the stamp.
|
|
func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dockerfile := read(t, dockerfilePath)
|
|
|
|
require.NotContains(t, dockerfile, "go build",
|
|
"a raw go build bypasses the Makefile's -X flag")
|
|
require.Contains(t, dockerfile, "ARG VERSION=")
|
|
require.Contains(t, dockerfile,
|
|
`make build VERSION="$VERSION" GO_LDFLAGS='-extldflags "-static"'`)
|
|
}
|
|
|
|
func read(t *testing.T, path string) string {
|
|
t.Helper()
|
|
|
|
//nolint:gosec // repo-local build file under test, fixed path
|
|
b, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
|
|
return string(b)
|
|
}
|
|
|
|
// version runs the script in this checkout with no overrides.
|
|
func (c checkout) version(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
return runScript(t, c.dir, nil)
|
|
}
|
|
|
|
func runScript(t *testing.T, dir string, env []string) string {
|
|
t.Helper()
|
|
|
|
//nolint:gosec // fixed argv, repo-local script under test
|
|
cmd := exec.CommandContext(t.Context(), "sh",
|
|
filepath.Join(dir, "script", "version"))
|
|
cmd.Dir = dir
|
|
|
|
cmd.Env = append(os.Environ(), env...)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, string(out))
|
|
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func (c checkout) git(t *testing.T, args ...string) string {
|
|
t.Helper()
|
|
|
|
//nolint:gosec // fixed argv, arguments are test constants
|
|
cmd := exec.CommandContext(t.Context(), "git", args...)
|
|
cmd.Dir = c.dir
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, string(out))
|
|
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func (c checkout) commit(t *testing.T, message string) string {
|
|
t.Helper()
|
|
|
|
c.git(t,
|
|
"-c", "user.email=ci@example.invalid",
|
|
"-c", "user.name=ci",
|
|
"-c", "commit.gpgsign=false",
|
|
"commit", "-q", "--allow-empty", "-m", message,
|
|
)
|
|
|
|
return c.git(t, "rev-parse", "HEAD")
|
|
}
|
|
|
|
// newCheckout builds a one-commit repository with a tracked file, so a
|
|
// later edit to that file makes the tree dirty, and with a copy of the
|
|
// script at the path it occupies in this repository.
|
|
func newCheckout(t *testing.T) checkout {
|
|
t.Helper()
|
|
|
|
requireGit(t)
|
|
|
|
dir := t.TempDir()
|
|
c := checkout{dir: dir}
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "script"), dirMode))
|
|
copyScript(t, dir)
|
|
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "tracked.txt"), []byte("original\n"), fileMode,
|
|
))
|
|
|
|
c.git(t, "init", "-q", "-b", "main")
|
|
c.git(t, "add", "tracked.txt")
|
|
c.head = c.commit(t, "initial")
|
|
|
|
return c
|
|
}
|
|
|
|
func copyScript(t *testing.T, dir string) {
|
|
t.Helper()
|
|
|
|
body, err := os.ReadFile(scriptPath)
|
|
require.NoError(t, err)
|
|
|
|
//nolint:gosec // the copy has to stay executable to be run
|
|
err = os.WriteFile(
|
|
filepath.Join(dir, "script", "version"), body, scriptMode,
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func requireGit(t *testing.T) {
|
|
t.Helper()
|
|
|
|
for _, tool := range []string{"sh", "git"} {
|
|
_, err := exec.LookPath(tool)
|
|
if err != nil {
|
|
t.Skipf("%s is not installed: %v", tool, err)
|
|
}
|
|
}
|
|
}
|