diff --git a/modules/sunrise/script_behavior_test.go b/modules/sunrise/script_behavior_test.go
new file mode 100644
index 0000000..5024ae9
--- /dev/null
+++ b/modules/sunrise/script_behavior_test.go
@@ -0,0 +1,629 @@
+package sunrise
+
+import (
+ "bytes"
+ "errors"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+// scriptHarness runs new.sh with bash on a PATH that holds only a symlink to
+// the real git plus stub go and docker commands. The go stub answers
+// `go env GOVERSION|GOBIN|GOPATH` from the harness and, for
+// `go -C
install ./cmd/summer`, writes a stub summer into the Go bin
+// directory that records its working directory, arguments and stdin.
+type scriptHarness struct {
+ t *testing.T
+ bash string
+ script string
+ root string // per-case temp root
+ work string // the directory the script runs in
+ tools string // the PATH directory
+ logs string // stub call records
+ gobin string
+ gopath string
+ remote *fakeRemote
+
+ goVersion string // go env GOVERSION
+ useGOBIN bool // go env GOBIN prints gobin; otherwise empty, GOPATH/bin is used
+ gobinOnPath bool
+ env []string // extra environment
+}
+
+type scriptResult struct {
+ code int
+ stdout, stderr string
+}
+
+// stdinProbe is piped into every run: the script must never let a child
+// read it.
+const stdinProbe = "SHOULD-NOT-BE-READ\n"
+
+// fwModule is the framework module path the script requires.
+const fwModule = "git.golem15.com/golem15/summercms"
+
+// gitTestEnv isolates git from the user's configuration and supplies an
+// identity for the commits the harness itself makes.
+func gitTestEnv(home string) []string {
+ return []string{
+ "HOME=" + home,
+ "XDG_CONFIG_HOME=" + filepath.Join(home, ".config"),
+ "GIT_CONFIG_NOSYSTEM=1",
+ "GIT_CONFIG_GLOBAL=" + os.DevNull,
+ // Detached auto maintenance would write into temp repos during cleanup.
+ "GIT_CONFIG_COUNT=2",
+ "GIT_CONFIG_KEY_0=maintenance.auto", "GIT_CONFIG_VALUE_0=false",
+ "GIT_CONFIG_KEY_1=gc.auto", "GIT_CONFIG_VALUE_1=0",
+ "GIT_AUTHOR_NAME=Test", "GIT_AUTHOR_EMAIL=test@example.com",
+ "GIT_COMMITTER_NAME=Test", "GIT_COMMITTER_EMAIL=test@example.com",
+ "LC_ALL=C",
+ }
+}
+
+// fakeRemote is a bare framework repository fed from a work clone.
+type fakeRemote struct {
+ t *testing.T
+ env []string
+ bare string
+ src string
+ n int
+ tags map[string]string // tag -> commit
+}
+
+func (r *fakeRemote) git(dir string, args ...string) string {
+ r.t.Helper()
+ cmd := exec.Command("git", args...)
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ()[:0:0], append([]string{"PATH=" + os.Getenv("PATH")}, r.env...)...)
+ out, err := cmd.CombinedOutput()
+ if err != nil {
+ r.t.Fatalf("git %v in %s: %v\n%s", args, dir, err, out)
+ }
+ return strings.TrimSpace(string(out))
+}
+
+// commit adds a commit on main and returns its hash.
+func (r *fakeRemote) commit() string {
+ r.t.Helper()
+ r.n++
+ if err := os.WriteFile(filepath.Join(r.src, "VERSION"), []byte(strings.Repeat("x", r.n)+"\n"), 0o644); err != nil {
+ r.t.Fatal(err)
+ }
+ r.git(r.src, "add", "-A")
+ r.git(r.src, "commit", "-q", "-m", "commit")
+ r.git(r.src, "push", "-q", "origin", "main")
+ return r.git(r.src, "rev-parse", "HEAD")
+}
+
+// tag commits and tags the new commit, then pushes the tag.
+func (r *fakeRemote) tag(tag string) string {
+ r.t.Helper()
+ c := r.commit()
+ r.git(r.src, "tag", tag)
+ r.git(r.src, "push", "-q", "origin", "refs/tags/"+tag)
+ r.tags[tag] = c
+ return c
+}
+
+// moveTag recreates tag on a new commit upstream.
+func (r *fakeRemote) moveTag(tag string) string {
+ r.t.Helper()
+ c := r.commit()
+ r.git(r.src, "tag", "-f", tag)
+ r.git(r.src, "push", "-q", "--force", "origin", "refs/tags/"+tag)
+ r.tags[tag] = c
+ return c
+}
+
+func newFakeRemote(t *testing.T, root string, env []string) *fakeRemote {
+ t.Helper()
+ r := &fakeRemote{t: t, env: env, bare: filepath.Join(root, "remote.git"), src: filepath.Join(root, "remote-src"), tags: map[string]string{}}
+ r.git(root, "init", "-q", "--bare", "--initial-branch=main", r.bare)
+ r.git(root, "init", "-q", "--initial-branch=main", r.src)
+ if err := os.WriteFile(filepath.Join(r.src, "go.mod"), []byte("module "+fwModule+"\n\ngo 1.27.0\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ r.git(r.src, "remote", "add", "origin", r.bare)
+ r.git(r.src, "add", "-A")
+ r.git(r.src, "commit", "-q", "-m", "init")
+ r.git(r.src, "push", "-q", "origin", "main")
+ r.tag("v0.1.0")
+ r.tag("v0.2.0")
+ r.tag("v0.10.0")
+ // A pre-release newer than every release: never selected.
+ r.tag("v1.0.0-rc1")
+ return r
+}
+
+func newScriptHarness(t *testing.T) *scriptHarness {
+ t.Helper()
+ bash := lookBash(t)
+ gitBin, err := exec.LookPath("git")
+ if err != nil {
+ t.Skip("git not installed")
+ }
+ gitBin, err = filepath.Abs(gitBin)
+ if err != nil {
+ t.Fatal(err)
+ }
+ chmod, err := exec.LookPath("chmod")
+ if err != nil {
+ t.Skip("chmod not found")
+ }
+ root := t.TempDir()
+ h := &scriptHarness{
+ t: t,
+ bash: bash,
+ root: root,
+ work: filepath.Join(root, "work"),
+ tools: filepath.Join(root, "tools"),
+ logs: filepath.Join(root, "logs"),
+ gobin: filepath.Join(root, "gobin"),
+ gopath: filepath.Join(root, "gopath"),
+ goVersion: "go1.27.0",
+ useGOBIN: true,
+ }
+ for _, d := range []string{h.work, h.tools, h.logs, h.gobin, filepath.Join(h.gopath, "bin"), filepath.Join(root, "home")} {
+ if err := os.MkdirAll(d, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ h.script = filepath.Join(root, "new.sh")
+ if err := os.WriteFile(h.script, Script(), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(gitBin, filepath.Join(h.tools, "git")); err != nil {
+ t.Fatal(err)
+ }
+ summerStub := "#!" + bash + `
+{ printf 'cwd=%s\n' "$(pwd)"; printf 'arg=%s\n' "$@"; } >"$STUB_LOG/summer.call"
+data=""
+while IFS= read -r line || [[ -n $line ]]; do data+="$line"; done
+printf '%s' "$data" >"$STUB_LOG/summer.stdin"
+`
+ goStub := "#!" + bash + `
+printf '%s\n' "$*" >>"$STUB_LOG/go.calls"
+case "$1 ${2:-}" in
+"env GOVERSION") printf '%s\n' "$STUB_GOVERSION"; exit 0 ;;
+"env GOBIN") printf '%s\n' "$STUB_GOBIN"; exit 0 ;;
+"env GOPATH") printf '%s\n' "$STUB_GOPATH"; exit 0 ;;
+esac
+if [[ $1 == -C && $3 == install && $4 == ./cmd/summer ]]; then
+ printf 'GOWORK=%s dir=%s\n' "${GOWORK-unset}" "$2" >>"$STUB_LOG/go.install"
+ bin="$STUB_GOBIN"
+ [[ -n $bin ]] || bin="${STUB_GOPATH%%:*}/bin"
+ printf '%s' "$SUMMER_STUB" >"$bin/summer"
+ '` + chmod + `' 0755 "$bin/summer"
+ exit 0
+fi
+printf 'unexpected go %s\n' "$*" >&2
+exit 3
+`
+ dockerStub := "#!" + bash + "\nprintf '%s\\n' \"$*\" >>\"$STUB_LOG/docker.calls\"\nexit 0\n"
+ write := func(name, body string) {
+ if err := os.WriteFile(filepath.Join(h.tools, name), []byte(body), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ }
+ write("go", goStub)
+ write("docker", dockerStub)
+ h.env = append(h.env, "SUMMER_STUB="+summerStub)
+ h.remote = newFakeRemote(t, root, append(gitTestEnv(filepath.Join(root, "home")), "GIT_CEILING_DIRECTORIES="+root))
+ return h
+}
+
+// without removes a tool from the stub PATH.
+func (h *scriptHarness) without(tool string) {
+ h.t.Helper()
+ if err := os.Remove(filepath.Join(h.tools, tool)); err != nil {
+ h.t.Fatal(err)
+ }
+}
+
+// run executes the script with args in h.work.
+func (h *scriptHarness) run(args ...string) scriptResult {
+ h.t.Helper()
+ path := h.tools
+ if h.gobinOnPath {
+ path += ":" + h.gobin
+ }
+ gobin := ""
+ if h.useGOBIN {
+ gobin = h.gobin
+ }
+ env := append(gitTestEnv(filepath.Join(h.root, "home")),
+ "PATH="+path,
+ "PWD="+h.work,
+ "GIT_CEILING_DIRECTORIES="+h.root,
+ "SUMMERCMS_REPO="+h.remote.bare,
+ "STUB_LOG="+h.logs,
+ "STUB_GOVERSION="+h.goVersion,
+ "STUB_GOBIN="+gobin,
+ "STUB_GOPATH="+h.gopath,
+ )
+ // The script must not depend on the harness identity: drop it.
+ filtered := env[:0]
+ for _, kv := range env {
+ if strings.HasPrefix(kv, "GIT_AUTHOR_") || strings.HasPrefix(kv, "GIT_COMMITTER_") {
+ continue
+ }
+ filtered = append(filtered, kv)
+ }
+ env = append(filtered, h.env...)
+ cmd := exec.Command(h.bash, append([]string{h.script}, args...)...)
+ cmd.Dir = h.work
+ cmd.Env = env
+ cmd.Stdin = strings.NewReader(stdinProbe)
+ var stdout, stderr bytes.Buffer
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ err := cmd.Run()
+ res := scriptResult{stdout: stdout.String(), stderr: stderr.String()}
+ var exitErr *exec.ExitError
+ switch {
+ case err == nil:
+ case errors.As(err, &exitErr):
+ res.code = exitErr.ExitCode()
+ default:
+ h.t.Fatalf("run script: %v", err)
+ }
+ return res
+}
+
+// mustRun runs the script and requires success.
+func (h *scriptHarness) mustRun(args ...string) scriptResult {
+ h.t.Helper()
+ res := h.run(args...)
+ if res.code != 0 {
+ h.t.Fatalf("script exit %d\nstdout:\n%s\nstderr:\n%s", res.code, res.stdout, res.stderr)
+ }
+ return res
+}
+
+func (h *scriptHarness) checkout() string { return filepath.Join(h.work, "summercms") }
+
+func (h *scriptHarness) head() string {
+ h.t.Helper()
+ return h.remote.git(h.checkout(), "rev-parse", "HEAD")
+}
+
+func (h *scriptHarness) log(name string) string {
+ data, err := os.ReadFile(filepath.Join(h.logs, name))
+ if err != nil {
+ return ""
+ }
+ return string(data)
+}
+
+func (h *scriptHarness) exists(rel string) bool {
+ _, err := os.Lstat(filepath.Join(h.work, rel))
+ return err == nil
+}
+
+func expectCode(t *testing.T, res scriptResult, code int) {
+ t.Helper()
+ if res.code != code {
+ t.Fatalf("exit %d, want %d\nstdout:\n%s\nstderr:\n%s", res.code, code, res.stdout, res.stderr)
+ }
+}
+
+func expectContains(t *testing.T, what, got string, wants ...string) {
+ t.Helper()
+ for _, want := range wants {
+ if !strings.Contains(got, want) {
+ t.Fatalf("%s lacks %q:\n%s", what, want, got)
+ }
+ }
+}
+
+// expectNothingStarted asserts no clone and no go or summer call happened.
+func expectNothingStarted(t *testing.T, h *scriptHarness) {
+ t.Helper()
+ if h.exists("summercms") {
+ t.Fatal("the framework was cloned")
+ }
+ if c := h.log("go.calls"); c != "" {
+ t.Fatalf("go was called:\n%s", c)
+ }
+ if c := h.log("summer.call"); c != "" {
+ t.Fatalf("summer was called:\n%s", c)
+ }
+}
+
+// expectRefusedUnchanged asserts a refusal with exit 1 that left HEAD at
+// before and never reached go install or summer.
+func expectRefusedUnchanged(t *testing.T, h *scriptHarness, res scriptResult, before string, msg string) {
+ t.Helper()
+ expectCode(t, res, 1)
+ expectContains(t, "stderr", res.stderr, msg)
+ if got := h.head(); got != before {
+ t.Fatalf("HEAD moved from %s to %s", before, got)
+ }
+ if c := h.log("summer.call"); c != "" {
+ t.Fatalf("summer was called after a refusal:\n%s", c)
+ }
+}
+
+// resetLogs clears stub records between runs of one case.
+func (h *scriptHarness) resetLogs() {
+ h.t.Helper()
+ for _, n := range []string{"go.calls", "go.install", "summer.call", "summer.stdin", "docker.calls"} {
+ _ = os.Remove(filepath.Join(h.logs, n))
+ }
+}
+
+func TestScriptBehavior(t *testing.T) {
+ cases := []struct {
+ name string
+ run func(t *testing.T, h *scriptHarness)
+ }{
+ {"help prints usage and exits 0", func(t *testing.T, h *scriptHarness) {
+ res := h.run("--help")
+ expectCode(t, res, 0)
+ expectContains(t, "stdout", res.stdout, "Usage: curl -fsSL https://summercms.io/new | bash -s -- ", "SUMMERCMS_REF")
+ expectNothingStarted(t, h)
+ }},
+ {"no argument is a usage error", func(t *testing.T, h *scriptHarness) {
+ res := h.run()
+ expectCode(t, res, 2)
+ expectContains(t, "stderr", res.stderr, "Usage:")
+ expectNothingStarted(t, h)
+ }},
+ {"two arguments are a usage error", func(t *testing.T, h *scriptHarness) {
+ res := h.run("demo", "extra")
+ expectCode(t, res, 2)
+ expectNothingStarted(t, h)
+ }},
+ {"upper-case name is rejected", func(t *testing.T, h *scriptHarness) {
+ res := h.run("Demo")
+ expectCode(t, res, 2)
+ expectContains(t, "stderr", res.stderr, "invalid application name")
+ expectNothingStarted(t, h)
+ }},
+ {"name with shell metacharacters is rejected", func(t *testing.T, h *scriptHarness) {
+ res := h.run("demo;touch pwned")
+ expectCode(t, res, 2)
+ if h.exists("pwned") {
+ t.Fatal("the name was executed")
+ }
+ expectNothingStarted(t, h)
+ }},
+ {"existing app stops before any clone", func(t *testing.T, h *scriptHarness) {
+ if err := os.Mkdir(filepath.Join(h.work, "sm-demo-app"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ res := h.run("demo")
+ expectCode(t, res, 1)
+ expectContains(t, "stderr", res.stderr, "sm-demo-app already exists")
+ expectNothingStarted(t, h)
+ }},
+ {"Go missing names go.dev/dl", func(t *testing.T, h *scriptHarness) {
+ h.without("go")
+ res := h.run("demo")
+ expectCode(t, res, 1)
+ expectContains(t, "stderr", res.stderr, "Go is not installed", "https://go.dev/dl/")
+ expectNothingStarted(t, h)
+ }},
+ {"Go older than 1.27 is refused", func(t *testing.T, h *scriptHarness) {
+ h.goVersion = "go1.26.3"
+ res := h.run("demo")
+ expectCode(t, res, 1)
+ expectContains(t, "stderr", res.stderr, "found go1.26.3", "https://go.dev/dl/")
+ if h.exists("summercms") {
+ t.Fatal("cloned despite an old Go")
+ }
+ }},
+ {"devel Go version only warns", func(t *testing.T, h *scriptHarness) {
+ h.goVersion = "devel go1.28-0123456789ab"
+ res := h.mustRun("demo")
+ expectContains(t, "stderr", res.stderr, "warning: could not read the Go version")
+ if h.log("summer.call") == "" {
+ t.Fatal("summer was not called")
+ }
+ }},
+ {"git missing is an error", func(t *testing.T, h *scriptHarness) {
+ h.without("git")
+ res := h.run("demo")
+ expectCode(t, res, 1)
+ expectContains(t, "stderr", res.stderr, "git is not installed")
+ expectNothingStarted(t, h)
+ }},
+ {"Docker missing only warns", func(t *testing.T, h *scriptHarness) {
+ h.without("docker")
+ res := h.mustRun("demo")
+ expectContains(t, "stderr", res.stderr, "warning: Docker is not installed")
+ if h.log("summer.call") == "" {
+ t.Fatal("summer was not called")
+ }
+ }},
+ {"fresh clone at the newest release", func(t *testing.T, h *scriptHarness) {
+ res := h.mustRun("demo")
+ if got, want := h.head(), h.remote.tags["v0.10.0"]; got != want {
+ t.Fatalf("HEAD = %s, want v0.10.0 %s (numeric order, pre-release ignored)", got, want)
+ }
+ cmd := exec.Command("git", "-C", h.checkout(), "symbolic-ref", "-q", "HEAD")
+ if err := cmd.Run(); err == nil {
+ t.Fatal("checkout is on a branch, want detached")
+ }
+ fw := filepath.Join(h.work, "summercms")
+ call := h.log("summer.call")
+ want := "cwd=" + h.work + "\narg=new\narg=demo\narg=--framework\narg=" + fw + "\n"
+ if call != want {
+ t.Fatalf("summer call:\n%s\nwant:\n%s", call, want)
+ }
+ if got := h.log("summer.stdin"); got != "" {
+ t.Fatalf("summer read stdin %q", got)
+ }
+ if got, want := h.log("go.install"), "GOWORK=off dir="+fw+"\n"; got != want {
+ t.Fatalf("go install = %q, want %q", got, want)
+ }
+ expectContains(t, "stdout", res.stdout, " cd "+filepath.Join(h.work, "sm-demo-app")+"\n")
+ expectContains(t, "stderr", res.stderr, "framework target: v0.10.0")
+ }},
+ {"SUMMERCMS_REF selects a tag", func(t *testing.T, h *scriptHarness) {
+ h.env = append(h.env, "SUMMERCMS_REF=v0.2.0")
+ res := h.mustRun("demo")
+ if got, want := h.head(), h.remote.tags["v0.2.0"]; got != want {
+ t.Fatalf("HEAD = %s, want v0.2.0 %s", got, want)
+ }
+ expectContains(t, "stderr", res.stderr, "framework target: v0.2.0")
+ }},
+ {"SUMMERCMS_REF starting with a dash is refused", func(t *testing.T, h *scriptHarness) {
+ h.env = append(h.env, "SUMMERCMS_REF=--upload-pack=x")
+ res := h.run("demo")
+ expectCode(t, res, 1)
+ expectContains(t, "stderr", res.stderr, "SUMMERCMS_REF must be a tag or commit")
+ if h.log("summer.call") != "" {
+ t.Fatal("summer was called")
+ }
+ }},
+ {"re-run moves forward to a new release", func(t *testing.T, h *scriptHarness) {
+ h.mustRun("demo")
+ next := h.remote.tag("v0.11.0")
+ h.resetLogs()
+ res := h.mustRun("demo")
+ if got := h.head(); got != next {
+ t.Fatalf("HEAD = %s, want v0.11.0 %s", got, next)
+ }
+ expectContains(t, "stderr", res.stderr, "updating the framework checkout", "framework target: v0.11.0")
+ }},
+ {"re-run at the newest release changes nothing", func(t *testing.T, h *scriptHarness) {
+ h.mustRun("demo")
+ before := h.head()
+ h.resetLogs()
+ res := h.mustRun("demo")
+ if got := h.head(); got != before {
+ t.Fatalf("HEAD moved from %s to %s", before, got)
+ }
+ expectContains(t, "stderr", res.stderr, "already at the target")
+ }},
+ {"tracked change refuses and is kept", func(t *testing.T, h *scriptHarness) {
+ h.mustRun("demo")
+ before := h.head()
+ mod := filepath.Join(h.checkout(), "go.mod")
+ edited := "module " + fwModule + "\n\ngo 1.27.0\n// local edit\n"
+ if err := os.WriteFile(mod, []byte(edited), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ h.resetLogs()
+ res := h.run("demo")
+ expectRefusedUnchanged(t, h, res, before, "uncommitted changes to tracked files")
+ if got, _ := os.ReadFile(mod); string(got) != edited {
+ t.Fatalf("go.mod changed: %q", got)
+ }
+ }},
+ {"branch checkout refuses", func(t *testing.T, h *scriptHarness) {
+ h.mustRun("demo")
+ h.remote.git(h.checkout(), "switch", "-q", "-c", "mine")
+ before := h.head()
+ h.resetLogs()
+ res := h.run("demo")
+ expectRefusedUnchanged(t, h, res, before, "is on branch mine")
+ if got := h.remote.git(h.checkout(), "symbolic-ref", "--short", "HEAD"); got != "mine" {
+ t.Fatalf("branch = %s", got)
+ }
+ }},
+ {"non-fast-forward refuses", func(t *testing.T, h *scriptHarness) {
+ h.mustRun("demo")
+ h.remote.git(h.checkout(), "commit", "-q", "--allow-empty", "-m", "local work")
+ before := h.head()
+ h.resetLogs()
+ res := h.run("demo")
+ expectRefusedUnchanged(t, h, res, before, "is not a fast-forward")
+ }},
+ {"moved upstream tag refuses", func(t *testing.T, h *scriptHarness) {
+ h.mustRun("demo")
+ before := h.head()
+ moved := h.remote.moveTag("v0.10.0")
+ h.resetLogs()
+ res := h.run("demo")
+ if res.code == 0 {
+ t.Fatalf("script followed a moved tag\nstderr:\n%s", res.stderr)
+ }
+ if got := h.head(); got != before {
+ t.Fatalf("HEAD moved from %s to %s", before, got)
+ }
+ if got := h.remote.git(h.checkout(), "rev-parse", "refs/tags/v0.10.0^{commit}"); got == moved || got != before {
+ t.Fatalf("local v0.10.0 = %s, want the original %s (upstream moved to %s)", got, before, moved)
+ }
+ if h.log("summer.call") != "" {
+ t.Fatal("summer was called")
+ }
+ }},
+ {"another module in the directory refuses", func(t *testing.T, h *scriptHarness) {
+ dir := h.checkout()
+ if err := os.Mkdir(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/other\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ res := h.run("demo")
+ expectCode(t, res, 1)
+ expectContains(t, "stderr", res.stderr, "is not a SummerCMS framework checkout", "example.com/other")
+ if h.log("summer.call") != "" || h.log("go.install") != "" {
+ t.Fatal("the script went on after refusing the directory")
+ }
+ }},
+ {"framework directory that is not a git repository refuses", func(t *testing.T, h *scriptHarness) {
+ dir := h.checkout()
+ if err := os.Mkdir(dir, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module "+fwModule+"\n"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ res := h.run("demo")
+ expectCode(t, res, 1)
+ expectContains(t, "stderr", res.stderr, "is not a git repository")
+ if _, err := os.Stat(filepath.Join(dir, ".git")); !os.IsNotExist(err) {
+ t.Fatalf("the script created .git: %v", err)
+ }
+ }},
+ {"GOBIN off PATH prints the hint", func(t *testing.T, h *scriptHarness) {
+ res := h.mustRun("demo")
+ expectContains(t, "stdout", res.stdout, "not on your PATH", `export PATH="`+h.gobin+`:$PATH"`)
+ }},
+ {"GOBIN on PATH prints no hint", func(t *testing.T, h *scriptHarness) {
+ h.gobinOnPath = true
+ res := h.mustRun("demo")
+ if strings.Contains(res.stdout, "export PATH") || strings.Contains(res.stdout, "not on your PATH") {
+ t.Fatalf("hint printed although GOBIN is on PATH:\n%s", res.stdout)
+ }
+ }},
+ {"empty GOBIN installs into GOPATH/bin", func(t *testing.T, h *scriptHarness) {
+ h.useGOBIN = false
+ res := h.mustRun("demo")
+ bin := filepath.Join(h.gopath, "bin")
+ if _, err := os.Stat(filepath.Join(bin, "summer")); err != nil {
+ t.Fatalf("summer not installed in GOPATH/bin: %v", err)
+ }
+ expectContains(t, "stderr", res.stderr, "installing summer into "+bin)
+ expectContains(t, "stdout", res.stdout, `export PATH="`+bin+`:$PATH"`)
+ }},
+ }
+ if len(cases) < 18 {
+ t.Fatalf("only %d cases", len(cases))
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ tc.run(t, newScriptHarness(t))
+ })
+ }
+}
+
+// TestScriptShellcheck runs shellcheck on new.sh; it is skipped only when
+// shellcheck is not installed.
+func TestScriptShellcheck(t *testing.T) {
+ sc, err := exec.LookPath("shellcheck")
+ if err != nil {
+ t.Skip("shellcheck not installed")
+ }
+ out, err := exec.Command(sc, "-s", "bash", "new.sh").CombinedOutput()
+ if err != nil {
+ t.Fatalf("shellcheck -s bash new.sh: %v\n%s", err, out)
+ }
+}