package main import ( "os" "os/exec" "path/filepath" "runtime" "strings" "syscall" "testing" ) // runInstaller runs scripts/install.sh with the given arguments and returns // everything it printed, failing the test if it did not exit cleanly. func runInstaller(t *testing.T, args ...string) string { t.Helper() output, err := exec.Command("bash", append([]string{"scripts/install.sh"}, args...)...).CombinedOutput() if err != nil { t.Fatalf("scripts/install.sh %v failed: %v\n%s", args, err, output) } return string(output) } // skipUnlessShellIsAvailable skips a test where the installer cannot run. func skipUnlessShellIsAvailable(t *testing.T) { t.Helper() if testing.Short() { t.Skip("-short: the installer compiles the whole editor") } if runtime.GOOS == "windows" { t.Skip("the installer is a shell script") } if _, err := exec.LookPath("bash"); err != nil { t.Skip("bash is not available") } } func TestTheInstallerBuildsAWorkingBinary(t *testing.T) { skipUnlessShellIsAvailable(t) prefix := t.TempDir() output := runInstaller(t, "--prefix", prefix) binary := filepath.Join(prefix, "turbo-js") info, err := os.Stat(binary) if err != nil { t.Fatalf("nothing was installed at %s: %v\n%s", binary, err, output) } if info.Mode().Perm()&0o111 == 0 { t.Errorf("the installed file has permissions %o, want it executable", info.Mode().Perm()) } version, err := exec.Command(binary, "-version").Output() if err != nil { t.Fatalf("the installed binary does not run: %v", err) } if !strings.Contains(string(version), "Turbo JS") { t.Errorf("-version printed %q", version) } } func TestTheInstallerSaysWhereItPutThings(t *testing.T) { skipUnlessShellIsAvailable(t) prefix := t.TempDir() output := runInstaller(t, "--prefix", prefix) for _, want := range []string{"Turbo JS", prefix, "PATH", "typescript-language-server"} { if !strings.Contains(output, want) { t.Errorf("the installer never mentions %q:\n%s", want, output) } } } func TestTheInstallerWarnsWhenThePrefixIsNotOnPath(t *testing.T) { skipUnlessShellIsAvailable(t) prefix := t.TempDir() // a fresh temporary directory is never on PATH output := runInstaller(t, "--prefix", prefix) if !strings.Contains(output, "not on your PATH") { t.Errorf("the installer did not warn about the PATH:\n%s", output) } if !strings.Contains(output, "export PATH=") { t.Errorf("the installer warned without saying how to fix it:\n%s", output) } } func TestTheInstallerRemovesWhatItInstalled(t *testing.T) { skipUnlessShellIsAvailable(t) prefix := t.TempDir() runInstaller(t, "--prefix", prefix) runInstaller(t, "--prefix", prefix, "--uninstall") if _, err := os.Stat(filepath.Join(prefix, "turbo-js")); !os.IsNotExist(err) { t.Error("the binary is still there after --uninstall") } } func TestUninstallingNothingIsNotAFailure(t *testing.T) { skipUnlessShellIsAvailable(t) output := runInstaller(t, "--prefix", t.TempDir(), "--uninstall") if !strings.Contains(output, "nothing installed") { t.Errorf("the installer did not say there was nothing to remove:\n%s", output) } } func TestTheInstallerExplainsItself(t *testing.T) { skipUnlessShellIsAvailable(t) output := runInstaller(t, "--help") for _, want := range []string{"--prefix", "--with-server", "--uninstall"} { if !strings.Contains(output, want) { t.Errorf("--help does not document %q:\n%s", want, output) } } } func TestTheInstallerRefusesAnUnknownOption(t *testing.T) { skipUnlessShellIsAvailable(t) output, err := exec.Command("bash", "scripts/install.sh", "--nonsense").CombinedOutput() if err == nil { t.Fatal("the installer accepted an option it does not have") } if !strings.Contains(string(output), "unknown option") { t.Errorf("the installer did not say what was wrong:\n%s", output) } } func TestTheInstallerNeedsADirectoryAfterPrefix(t *testing.T) { skipUnlessShellIsAvailable(t) output, err := exec.Command("bash", "scripts/install.sh", "--prefix").CombinedOutput() if err == nil { t.Fatal("--prefix was accepted with nothing after it") } if !strings.Contains(string(output), "needs a directory") { t.Errorf("the installer did not say what was wrong:\n%s", output) } } func TestTheInstallerRunsFromAnyDirectory(t *testing.T) { skipUnlessShellIsAvailable(t) repo, err := filepath.Abs(".") if err != nil { t.Fatalf("Abs() error = %v", err) } prefix := t.TempDir() // It is invoked by absolute path from somewhere else entirely, as it would // be from a shell alias or another script. command := exec.Command("bash", filepath.Join(repo, "scripts", "install.sh"), "--prefix", prefix) command.Dir = t.TempDir() if output, err := command.CombinedOutput(); err != nil { t.Fatalf("the installer failed when run from elsewhere: %v\n%s", err, output) } if _, err := os.Stat(filepath.Join(prefix, "turbo-js")); err != nil { t.Errorf("nothing was installed: %v", err) } } func TestAFailedBuildLeavesTheInstalledBinaryAlone(t *testing.T) { skipUnlessShellIsAvailable(t) prefix := t.TempDir() runInstaller(t, "--prefix", prefix) binary := filepath.Join(prefix, "turbo-js") before, err := os.Stat(binary) if err != nil { t.Fatalf("the first install produced nothing: %v", err) } // A stray file in package main is exactly what a user's own scratch file // does to this repository, and it must not cost them their installation. stray := filepath.Join("scripts", "..", "zz_broken_on_purpose.go") if err := os.WriteFile(stray, []byte("package main\n\nfunc main() {}\n"), 0o644); err != nil { t.Fatalf("writing the stray file: %v", err) } t.Cleanup(func() { os.Remove(stray) }) output, err := exec.Command("bash", "scripts/install.sh", "--prefix", prefix).CombinedOutput() if err == nil { t.Fatal("the installer reported success on a build that cannot succeed") } if !strings.Contains(string(output), "nothing was installed") { t.Errorf("the installer did not say the installation was untouched:\n%s", output) } after, err := os.Stat(binary) if err != nil { t.Fatalf("the failed build removed the installed binary: %v", err) } if !after.ModTime().Equal(before.ModTime()) { t.Error("the failed build replaced the installed binary") } } func TestReinstallingReplacesTheFileRatherThanOverwritingIt(t *testing.T) { // macOS caches a binary's code signature against its inode. Writing new // bytes into the same inode — which is what cp does — leaves the cached // signature describing something else, and the kernel then refuses to // execute it: builds fine, installs fine, "does not run". Replacing the // directory entry with a fresh inode is what avoids that, and it makes the // install atomic besides. skipUnlessShellIsAvailable(t) prefix := t.TempDir() binary := filepath.Join(prefix, "turbo-js") runInstaller(t, "--prefix", prefix) first := inodeOf(t, binary) runInstaller(t, "--prefix", prefix) second := inodeOf(t, binary) if first == second { t.Errorf("the reinstall wrote into the same inode (%d); it must replace the file", first) } } func TestReinstallingLeavesAWorkingBinary(t *testing.T) { skipUnlessShellIsAvailable(t) prefix := t.TempDir() binary := filepath.Join(prefix, "turbo-js") runInstaller(t, "--prefix", prefix) runInstaller(t, "--prefix", prefix) if _, err := exec.Command(binary, "-version").Output(); err != nil { t.Fatalf("the reinstalled binary does not run: %v", err) } } func TestABinaryThatWillNotRunIsReportedWithItsOwnError(t *testing.T) { // "the installed binary does not run" on its own tells whoever hit it // nothing they can act on. Whatever the system said has to come through. skipUnlessShellIsAvailable(t) if !strings.Contains(readInstaller(t), "$verify") { t.Error("the installer discards what the binary said when it will not run") } } // readInstaller returns the installer's source. func readInstaller(t *testing.T) string { t.Helper() data, err := os.ReadFile("scripts/install.sh") if err != nil { t.Fatalf("reading the installer: %v", err) } return string(data) } // inodeOf returns a file's inode number. func inodeOf(t *testing.T, path string) uint64 { t.Helper() info, err := os.Stat(path) if err != nil { t.Fatalf("stat %s: %v", path, err) } stat, ok := info.Sys().(*syscall.Stat_t) if !ok { t.Skip("inode numbers are not available on this platform") } return uint64(stat.Ino) } func TestTheInstalledBinaryReportsTheCommitItWasBuiltFrom(t *testing.T) { // The point of stamping: an installed editor must name the commit it came // from, not a constant somebody forgot to bump before releasing. skipUnlessShellIsAvailable(t) prefix := t.TempDir() runInstaller(t, "--prefix", prefix) reported, err := exec.Command(filepath.Join(prefix, "turbo-js"), "-version").Output() if err != nil { t.Fatalf("the installed binary does not run: %v", err) } commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() if err != nil { t.Skip("not a git checkout, so there is no commit to stamp") } if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) { t.Errorf("-version printed %q, which never mentions the commit %s", reported, want) } } func TestTheInstalledBinaryDoesNotReportAnUnknownVersion(t *testing.T) { // "unknown" is what the binary says when *no* source could name it, and // seeing it here would mean the installer's ldflags never reached the // linker. "devel" is a different thing: it is what a correct build of a // checkout with no tags reports, so a checkout that has never been tagged // must not fail this. // // What proves the stamp arrived either way is the commit, which only the // linker can have supplied. skipUnlessShellIsAvailable(t) // Outside a git checkout the installer has nothing to stamp *with*, and // "unknown" is then the correct answer rather than a failure — so the // premise is checked before anything is asserted on. commit, err := exec.Command("git", "rev-parse", "--short", "HEAD").Output() if err != nil { t.Skip("not a git checkout, so there is nothing for the installer to stamp") } prefix := t.TempDir() runInstaller(t, "--prefix", prefix) reported, err := exec.Command(filepath.Join(prefix, "turbo-js"), "-version").Output() if err != nil { t.Fatalf("the installed binary does not run: %v", err) } if strings.Contains(string(reported), "unknown") { t.Errorf("-version printed %q, so nothing reached the linker at all", reported) } if want := strings.TrimSpace(string(commit)); !strings.Contains(string(reported), want) { t.Errorf("-version printed %q, want it to carry the commit %q", reported, want) } } func TestTheInstallerStampsThroughTheLinker(t *testing.T) { // A build outside a git checkout has nothing to describe, and must still // build rather than passing a half-built -X flag to the linker. script := readInstaller(t) for _, want := range []string{"turbo-core/version", "-ldflags", "describe --tags --dirty"} { if !strings.Contains(script, want) { t.Errorf("the installer never mentions %q", want) } } if !strings.Contains(script, `ldflags=""`) { t.Error("the installer has no path for a checkout git cannot describe") } }