feat(14.3-02): add the sunrise bootstrap script module
- modules/sunrise/new.sh: curl | bash installer wrapped in main(), checks Go 1.27+/git/Docker, clones or fast-forwards the framework at the newest release tag (SUMMERCMS_REPO/DIR/REF overrides), installs summer and runs summer new next to the checkout, prints the cd line and a PATH hint - sunrise.Script() returns a fresh copy of the embedded script - smoke, shape, truncation and gated end-to-end tests; README and modules row
This commit is contained in:
216
modules/sunrise/sunrise_test.go
Normal file
216
modules/sunrise/sunrise_test.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package sunrise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestScriptEmbedded(t *testing.T) {
|
||||
want, err := os.ReadFile("new.sh")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := Script()
|
||||
if !bytes.Equal(got, want) {
|
||||
t.Fatal("Script() differs from new.sh")
|
||||
}
|
||||
for i := range got {
|
||||
got[i] = 'x'
|
||||
}
|
||||
if !bytes.Equal(Script(), want) {
|
||||
t.Fatal("modifying a returned slice changed the next Script() call")
|
||||
}
|
||||
}
|
||||
|
||||
func TestScriptShape(t *testing.T) {
|
||||
s := string(Script())
|
||||
lines := strings.Split(strings.TrimRight(s, "\n"), "\n")
|
||||
if lines[0] != "#!/usr/bin/env bash" {
|
||||
t.Fatalf("first line = %q, want the bash shebang", lines[0])
|
||||
}
|
||||
if last := lines[len(lines)-1]; last != `main "$@"` {
|
||||
t.Fatalf("last non-empty line = %q, want %q", last, `main "$@"`)
|
||||
}
|
||||
if strings.Count(s, "\nmain \"$@\"") != 1 {
|
||||
t.Fatal("main must be called exactly once, on the last line")
|
||||
}
|
||||
bash := lookBash(t)
|
||||
if out, err := exec.Command(bash, "-n", "new.sh").CombinedOutput(); err != nil {
|
||||
t.Fatalf("bash -n new.sh: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestScriptTruncationRunsNothing runs every prefix of the script that ends
|
||||
// on a line boundary and every byte prefix of its final line, as a cut-short
|
||||
// download would arrive, and asserts none of them invokes a command or
|
||||
// creates a file. A positive control proves the stubs do record calls.
|
||||
func TestScriptTruncationRunsNothing(t *testing.T) {
|
||||
bash := lookBash(t)
|
||||
stubs := t.TempDir()
|
||||
marker := filepath.Join(t.TempDir(), "invoked")
|
||||
for _, name := range []string{"git", "go", "docker", "curl", "summer"} {
|
||||
body := "#!/bin/sh\necho \"" + name + " $*\" >>'" + marker + "'\n"
|
||||
if err := os.WriteFile(filepath.Join(stubs, name), []byte(body), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
home := t.TempDir()
|
||||
scripts := t.TempDir()
|
||||
run := func(t *testing.T, prefix []byte) (work string) {
|
||||
t.Helper()
|
||||
file := filepath.Join(scripts, "new.sh")
|
||||
if err := os.WriteFile(file, prefix, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
work = t.TempDir()
|
||||
cmd := exec.Command(bash, file, "demo")
|
||||
cmd.Dir = work
|
||||
cmd.Env = []string{"PATH=" + stubs, "HOME=" + home, "PWD=" + work, "LC_ALL=C"}
|
||||
_ = cmd.Run()
|
||||
return work
|
||||
}
|
||||
|
||||
s := Script()
|
||||
body := bytes.TrimRight(s, "\n")
|
||||
lastStart := bytes.LastIndexByte(body, '\n') + 1
|
||||
var prefixes []int
|
||||
for i, c := range s[:lastStart] {
|
||||
if c == '\n' {
|
||||
prefixes = append(prefixes, i+1)
|
||||
}
|
||||
}
|
||||
prefixes = append([]int{0}, prefixes...)
|
||||
for k := 1; k < len(body)-lastStart; k++ {
|
||||
prefixes = append(prefixes, lastStart+k)
|
||||
}
|
||||
for _, n := range prefixes {
|
||||
work := run(t, s[:n])
|
||||
if out, err := os.ReadFile(marker); err == nil {
|
||||
t.Fatalf("prefix of %d bytes (ending %q) invoked: %s", n, tail(s[:n]), out)
|
||||
}
|
||||
if entries, err := os.ReadDir(work); err != nil || len(entries) != 0 {
|
||||
t.Fatalf("prefix of %d bytes (ending %q) changed the working directory: %v %v", n, tail(s[:n]), entries, err)
|
||||
}
|
||||
}
|
||||
if len(prefixes) < 50 {
|
||||
t.Fatalf("only %d prefixes tested", len(prefixes))
|
||||
}
|
||||
|
||||
run(t, s)
|
||||
if _, err := os.Stat(marker); err != nil {
|
||||
t.Fatalf("the complete script did not reach a stub, so the harness proves nothing: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func tail(b []byte) string {
|
||||
if len(b) > 30 {
|
||||
b = b[len(b)-30:]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func lookBash(t *testing.T) string {
|
||||
t.Helper()
|
||||
bash, err := exec.LookPath("bash")
|
||||
if err != nil {
|
||||
t.Skip("bash not installed")
|
||||
}
|
||||
return bash
|
||||
}
|
||||
|
||||
// TestBootstrapEndToEnd serves Script() over HTTP, pipes it into bash with
|
||||
// curl the way the one-liner does, clones this repository at HEAD, installs
|
||||
// summer into a GOBIN that is not on PATH and builds the generated app. It
|
||||
// needs network access for Go modules and runs only with
|
||||
// SUMMERCMS_BOOTSTRAP_CHECK=1.
|
||||
func TestBootstrapEndToEnd(t *testing.T) {
|
||||
if os.Getenv("SUMMERCMS_BOOTSTRAP_CHECK") != "1" {
|
||||
t.Skip("set SUMMERCMS_BOOTSTRAP_CHECK=1 to run the bootstrap script end to end")
|
||||
}
|
||||
for _, tool := range []string{"bash", "curl", "git", "go"} {
|
||||
if _, err := exec.LookPath(tool); err != nil {
|
||||
t.Fatalf("%s is required: %v", tool, err)
|
||||
}
|
||||
}
|
||||
root, err := filepath.Abs(filepath.Join("..", ".."))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
head := gitOut(t, root, "rev-parse", "HEAD")
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write(Script())
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
work := t.TempDir()
|
||||
gobin := t.TempDir()
|
||||
env := []string{"SUMMERCMS_REPO=" + root, "SUMMERCMS_REF=" + head, "GOBIN=" + gobin}
|
||||
for _, kv := range os.Environ() {
|
||||
key, _, _ := strings.Cut(kv, "=")
|
||||
switch {
|
||||
case strings.HasPrefix(key, "SUMMER_"), strings.HasPrefix(key, "SUMMERCMS_"),
|
||||
key == "GOWORK", key == "GOBIN", key == "PWD", key == "GOFLAGS":
|
||||
continue
|
||||
}
|
||||
env = append(env, kv)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "bash", "-c", `set -o pipefail; curl -fsSL "$1" | bash -s -- demo`, "bash", srv.URL+"/new")
|
||||
cmd.Dir = work
|
||||
cmd.Env = append(env[:len(env):len(env)], "PWD="+work)
|
||||
out, err := cmd.CombinedOutput()
|
||||
t.Logf("bootstrap output:\n%s", out)
|
||||
if err != nil {
|
||||
t.Fatalf("curl | bash: %v", err)
|
||||
}
|
||||
|
||||
checkout := filepath.Join(work, "summercms")
|
||||
if got := gitOut(t, checkout, "rev-parse", "HEAD"); got != head {
|
||||
t.Fatalf("checkout HEAD = %s, want %s", got, head)
|
||||
}
|
||||
if err := exec.Command("git", "-C", checkout, "symbolic-ref", "-q", "HEAD").Run(); err == nil {
|
||||
t.Fatal("checkout is on a branch, want a detached HEAD")
|
||||
}
|
||||
app := filepath.Join(work, "sm-demo-app")
|
||||
gomod, err := os.ReadFile(filepath.Join(app, "go.mod"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !strings.Contains(string(gomod), "replace git.golem15.com/golem15/summercms => ../summercms\n") {
|
||||
t.Fatalf("go.mod lacks the relative framework replace:\n%s", gomod)
|
||||
}
|
||||
if !strings.Contains(string(out), " cd "+app+"\n") {
|
||||
t.Fatalf("output lacks the cd line for %s", app)
|
||||
}
|
||||
if !strings.Contains(string(out), `export PATH="`+gobin+`:$PATH"`) {
|
||||
t.Fatalf("output lacks the PATH hint for %s", gobin)
|
||||
}
|
||||
|
||||
b := exec.CommandContext(ctx, filepath.Join(gobin, "summer"), "build")
|
||||
b.Dir = app
|
||||
b.Env = env
|
||||
if out, err := b.CombinedOutput(); err != nil {
|
||||
t.Fatalf("summer build in the generated app: %v\n%s", err, out)
|
||||
}
|
||||
}
|
||||
|
||||
func gitOut(t *testing.T, dir string, args ...string) string {
|
||||
t.Helper()
|
||||
out, err := exec.Command("git", append([]string{"-C", dir}, args...)...).Output()
|
||||
if err != nil {
|
||||
t.Fatalf("git %v in %s: %v", args, dir, err)
|
||||
}
|
||||
return strings.TrimSpace(string(out))
|
||||
}
|
||||
Reference in New Issue
Block a user