package main import ( "os" "os/exec" "path/filepath" "strings" "testing" ) // Linker flags are a string, and a wrong one is not an error: `-X` naming a // symbol that does not exist links happily and stamps nothing, so the binary // falls back to whatever the Go build system knows and reports a version the // build never meant. Nothing but running the binary catches that, which is why // `make build` runs it and why these tests do too. // buildStampedWith compiles the editor with the flags `make ldflags` produces // for a version, and returns the path to the binary. func buildStampedWith(t *testing.T, version string) string { t.Helper() flags, err := exec.Command("make", "--no-print-directory", "ldflags", "VERSION="+version).Output() if err != nil { t.Fatalf("make ldflags VERSION=%s: %v", version, err) } binary := filepath.Join(t.TempDir(), "turbo-moonbit") build := exec.Command("go", "build", "-ldflags", strings.TrimSpace(string(flags)), "-o", binary, ".") if out, err := build.CombinedOutput(); err != nil { t.Fatalf("building with those flags failed: %v\n%s", err, out) } return binary } // checkVersion runs the build's version check and returns what it said and // whether it was satisfied. func checkVersion(t *testing.T, args ...string) (string, bool) { t.Helper() out, err := exec.Command("./scripts/check-version.sh", args...).CombinedOutput() return string(out), err == nil } func TestTheVersionCheckAcceptsTheVersionTheBuildStamped(t *testing.T) { binary := buildStampedWith(t, "v9.9.9") out, ok := checkVersion(t, binary, "v9.9.9") if !ok { t.Fatalf("the check refused a correctly stamped binary:\n%s", out) } if !strings.Contains(out, "9.9.9") { t.Errorf("the check reported %q, want it to name the version", out) } } func TestTheVersionCheckRefusesAVersionThatMerelyContainsTheRightOne(t *testing.T) { // The reason this is an equality test and not a grep: "0.2.0" is a // substring of "10.2.0", so a substring check passes a release that ships // a binary naming an entirely different version. binary := buildStampedWith(t, "v10.2.0") out, ok := checkVersion(t, binary, "v0.2.0") if ok { t.Fatalf("the check accepted 10.2.0 as 0.2.0:\n%s", out) } if !strings.Contains(out, "10.2.0") { t.Errorf("the failure does not say what the binary actually reports:\n%s", out) } } func TestTheVersionCheckRefusesAStampThatNeverReachedTheLinker(t *testing.T) { // The failure this exists for. `-X` naming a symbol that is not there is // not an error: the binary links, runs, and reports the wrong thing. binary := filepath.Join(t.TempDir(), "turbo-moonbit") build := exec.Command("go", "build", "-ldflags", "-X 'rickub.com/turbo-editors/turbo-core/version.stampX=v9.9.9'", "-o", binary, ".") if out, err := build.CombinedOutput(); err != nil { t.Fatalf("the build with a misspelt -X failed, so there is nothing to catch: %v\n%s", err, out) } out, ok := checkVersion(t, binary, "v9.9.9") if ok { t.Fatalf("the check accepted a binary nothing was stamped into:\n%s", out) } } func TestTheVersionCheckRefusesABinaryThatDoesNotRun(t *testing.T) { binary := filepath.Join(t.TempDir(), "turbo-moonbit") if err := os.WriteFile(binary, []byte("#!/bin/sh\nexit 1\n"), 0o755); err != nil { t.Fatalf("cannot write the stand-in: %v", err) } if out, ok := checkVersion(t, binary, "v9.9.9"); ok { t.Fatalf("the check accepted a binary that does not run:\n%s", out) } } func TestTheVersionCheckNeedsSomethingToCheck(t *testing.T) { if out, ok := checkVersion(t); ok { t.Fatalf("the check accepted no arguments at all:\n%s", out) } if out, ok := checkVersion(t, filepath.Join(t.TempDir(), "not-there")); ok { t.Fatalf("the check accepted a path with no binary at it:\n%s", out) } } func TestAnUnstampedBuildIsAcceptedButAnUnnameableOneIsNot(t *testing.T) { // Installing from a tarball has no git checkout to describe, so there is no // version to expect. The claim left is that *some* source named it. binary := filepath.Join(t.TempDir(), "turbo-moonbit") build := exec.Command("go", "build", "-o", binary, ".") if out, err := build.CombinedOutput(); err != nil { t.Fatalf("an unstamped build failed: %v\n%s", err, out) } out, ok := checkVersion(t, binary) if !ok { t.Fatalf("the check refused an unstamped build, which is a legitimate one:\n%s", out) } if strings.Contains(out, "unknown") { t.Errorf("the binary cannot name its version and the check passed anyway:\n%s", out) } } func TestTheBuildTargetChecksWhatItStamped(t *testing.T) { // The wiring, not the script: a check nothing calls protects nothing. makefile, err := os.ReadFile("Makefile") if err != nil { t.Fatalf("cannot read the Makefile: %v", err) } recipe := buildRecipe(t, string(makefile)) if !strings.Contains(recipe, "check-version.sh") { t.Errorf("the build target never checks the version it stamped:\n%s", recipe) } } func TestTheInstallerChecksWhatItStampedBeforeItInstalls(t *testing.T) { // Before, not after: a binary that cannot name its own version must never // replace one that can. script, err := os.ReadFile("scripts/install.sh") if err != nil { t.Fatalf("cannot read the installer: %v", err) } text := string(script) check := strings.Index(text, "check-version.sh") install := strings.Index(text, "mv -f") switch { case check < 0: t.Fatal("the installer never checks the version it stamped") case install < 0: t.Fatal("the installer no longer installs by rename; this test is out of date") case check > install: t.Error("the installer checks the version after installing, so a bad build replaces a good one") } } // buildRecipe returns the lines of the Makefile's build target. func buildRecipe(t *testing.T, makefile string) string { t.Helper() start := strings.Index(makefile, "\nbuild:") if start < 0 { t.Fatal("the Makefile has no build target") } rest := makefile[start+1:] end := strings.Index(rest, "\n\n") if end < 0 { end = len(rest) } return rest[:end] }