|
|
|
|
@@ -2,6 +2,8 @@ package build
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"go/token"
|
|
|
|
|
"io/fs"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
@@ -12,19 +14,109 @@ import (
|
|
|
|
|
// testAppKey is a fixed, valid app key (base64 of 32 bytes) for hermetic tests.
|
|
|
|
|
const testAppKey = "c3VtbWVyY21zLXRlc3Qta2V5LTMyLWJ5dGVzLWxvbmc="
|
|
|
|
|
|
|
|
|
|
// repoRoot is the absolute path of this framework checkout.
|
|
|
|
|
func repoRoot(t *testing.T) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
repo, err := filepath.Abs(filepath.Join("..", ".."))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return repo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// parentWithFramework returns a temp parent holding a summercms symlink to
|
|
|
|
|
// this checkout, so NewApp finds the framework as a sibling.
|
|
|
|
|
func parentWithFramework(t *testing.T) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
if err := os.Symlink(repoRoot(t), filepath.Join(parent, "summercms")); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
return parent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fakeFramework writes dir/go.mod declaring module and returns dir.
|
|
|
|
|
func fakeFramework(t *testing.T, dir, module string) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
write(t, filepath.Join(dir, "go.mod"), "module "+module+"\n\ngo 1.27.0\n")
|
|
|
|
|
return dir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// stubBinaryFramework replaces frameworkFromBinary for the test.
|
|
|
|
|
func stubBinaryFramework(t *testing.T, dir string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
orig := frameworkFromBinary
|
|
|
|
|
frameworkFromBinary = func() string { return dir }
|
|
|
|
|
t.Cleanup(func() { frameworkFromBinary = orig })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// gitOut runs git in dir and returns its trimmed output.
|
|
|
|
|
func gitOut(t *testing.T, dir string, args ...string) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
cmd := exec.CommandContext(t.Context(), "git", args...)
|
|
|
|
|
cmd.Dir = dir
|
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
|
|
|
}
|
|
|
|
|
return strings.TrimSpace(string(out))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// requireGit skips when git is not installed.
|
|
|
|
|
func requireGit(t *testing.T) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
if _, err := exec.LookPath("git"); err != nil {
|
|
|
|
|
t.Skip("git not installed")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// isolateGit points git at an empty configuration (no system or global
|
|
|
|
|
// file, temp HOME) and clears every identity variable, while keeping the Go
|
|
|
|
|
// caches where they are so the starter plugin's go mod tidy stays offline.
|
|
|
|
|
func isolateGit(t *testing.T) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
requireGit(t)
|
|
|
|
|
envOut, err := exec.CommandContext(t.Context(), "go", "env", "GOMODCACHE", "GOCACHE", "GOPATH", "GOENV").Output()
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("go env: %v", err)
|
|
|
|
|
}
|
|
|
|
|
vals := strings.Split(strings.TrimSpace(string(envOut)), "\n")
|
|
|
|
|
if len(vals) != 4 {
|
|
|
|
|
t.Fatalf("go env output %q", envOut)
|
|
|
|
|
}
|
|
|
|
|
for i, k := range []string{"GOMODCACHE", "GOCACHE", "GOPATH", "GOENV"} {
|
|
|
|
|
t.Setenv(k, vals[i])
|
|
|
|
|
}
|
|
|
|
|
home := t.TempDir()
|
|
|
|
|
t.Setenv("HOME", home)
|
|
|
|
|
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, ".config"))
|
|
|
|
|
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
|
|
|
|
|
t.Setenv("GIT_CONFIG_GLOBAL", os.DevNull)
|
|
|
|
|
// Recent git runs auto maintenance detached after a commit; it would
|
|
|
|
|
// write into .git while the temp dir is being removed.
|
|
|
|
|
t.Setenv("GIT_CONFIG_COUNT", "2")
|
|
|
|
|
t.Setenv("GIT_CONFIG_KEY_0", "maintenance.auto")
|
|
|
|
|
t.Setenv("GIT_CONFIG_VALUE_0", "false")
|
|
|
|
|
t.Setenv("GIT_CONFIG_KEY_1", "gc.auto")
|
|
|
|
|
t.Setenv("GIT_CONFIG_VALUE_1", "0")
|
|
|
|
|
for _, k := range []string{"GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL", "EMAIL", "GIT_DIR", "GIT_WORK_TREE"} {
|
|
|
|
|
t.Setenv(k, "")
|
|
|
|
|
os.Unsetenv(k)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestNewAppSmoke writes an app next to a symlinked framework checkout,
|
|
|
|
|
// builds it and lists the starter route.
|
|
|
|
|
func TestNewAppSmoke(t *testing.T) {
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
t.Skip("runs go mod tidy and go build")
|
|
|
|
|
}
|
|
|
|
|
repo, err := filepath.Abs(filepath.Join("..", ".."))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
fw := filepath.Join(parent, "summercms")
|
|
|
|
|
if err := os.Symlink(repo, fw); err != nil {
|
|
|
|
|
if err := os.Symlink(repoRoot(t), fw); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
@@ -73,3 +165,480 @@ func TestNewAppSmoke(t *testing.T) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TestNewNoPluginBuilds builds an app created with --no-plugin: an empty
|
|
|
|
|
// plugin list must compile.
|
|
|
|
|
func TestNewNoPluginBuilds(t *testing.T) {
|
|
|
|
|
if testing.Short() {
|
|
|
|
|
t.Skip("runs go build")
|
|
|
|
|
}
|
|
|
|
|
parent := parentWithFramework(t)
|
|
|
|
|
ctx := t.Context()
|
|
|
|
|
dir, err := NewApp(ctx, NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
gen := readAppFile(t, dir, "plugins.gen.go")
|
|
|
|
|
if !bytes.Contains(gen, []byte("var PluginIDs = []string{}")) {
|
|
|
|
|
t.Fatalf("plugins.gen.go has plugins:\n%s", gen)
|
|
|
|
|
}
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
if err := App(ctx, dir, &out); err != nil {
|
|
|
|
|
t.Fatalf("build: %v\n%s", err, out.String())
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(filepath.Join(dir, "bin", "demo")); err != nil {
|
|
|
|
|
t.Fatalf("binary missing: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewValidateAppName(t *testing.T) {
|
|
|
|
|
var keywords []string
|
|
|
|
|
for tok := token.Token(0); tok < 256; tok++ {
|
|
|
|
|
if tok.IsKeyword() {
|
|
|
|
|
keywords = append(keywords, tok.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if len(keywords) != 25 {
|
|
|
|
|
t.Fatalf("found %d Go keywords, want 25: %v", len(keywords), keywords)
|
|
|
|
|
}
|
|
|
|
|
for _, name := range []string{"", "Demo", "1x", "a-b", "a.b", "a/b", "a_b", "demo app", "main", "internal"} {
|
|
|
|
|
if err := ValidateAppName(name); err == nil {
|
|
|
|
|
t.Errorf("ValidateAppName(%q) = nil, want error", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, kw := range keywords {
|
|
|
|
|
err := ValidateAppName(kw)
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "keyword") {
|
|
|
|
|
t.Errorf("ValidateAppName(%q) = %v, want a keyword error", kw, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, name := range []string{"main", "internal"} {
|
|
|
|
|
if err := ValidateAppName(name); err == nil || !strings.Contains(err.Error(), "reserved") {
|
|
|
|
|
t.Errorf("ValidateAppName(%q) = %v, want a reserved error", name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
for _, name := range []string{"init", "string", "vendor", "testdata", "a1", "demo"} {
|
|
|
|
|
if err := ValidateAppName(name); err != nil {
|
|
|
|
|
t.Errorf("ValidateAppName(%q) = %v, want nil", name, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// NewApp applies the same rule before touching the filesystem.
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
if _, err := NewApp(t.Context(), NewOptions{Name: "func", Parent: parent, AppKey: testAppKey, NoGit: true}, nil); err == nil {
|
|
|
|
|
t.Fatal("NewApp accepted a keyword name")
|
|
|
|
|
}
|
|
|
|
|
assertDirEntries(t, parent, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// assertDirEntries requires dir to hold exactly want (names, sorted).
|
|
|
|
|
func assertDirEntries(t *testing.T, dir string, want []string) {
|
|
|
|
|
t.Helper()
|
|
|
|
|
entries, err := os.ReadDir(dir)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
var got []string
|
|
|
|
|
for _, e := range entries {
|
|
|
|
|
got = append(got, e.Name())
|
|
|
|
|
}
|
|
|
|
|
if strings.Join(got, ",") != strings.Join(want, ",") {
|
|
|
|
|
t.Fatalf("%s holds %v, want %v", dir, got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewRefusesExistingTarget(t *testing.T) {
|
|
|
|
|
cases := []struct {
|
|
|
|
|
name string
|
|
|
|
|
setup func(t *testing.T, target string)
|
|
|
|
|
check func(t *testing.T, target string)
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "directory",
|
|
|
|
|
setup: func(t *testing.T, target string) {
|
|
|
|
|
if err := os.Mkdir(target, 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
write(t, filepath.Join(target, "keep.txt"), "mine\n")
|
|
|
|
|
},
|
|
|
|
|
check: func(t *testing.T, target string) {
|
|
|
|
|
assertDirEntries(t, target, []string{"keep.txt"})
|
|
|
|
|
if got := string(readAppFile(t, target, "keep.txt")); got != "mine\n" {
|
|
|
|
|
t.Fatalf("keep.txt = %q", got)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "file",
|
|
|
|
|
setup: func(t *testing.T, target string) { write(t, target, "mine\n") },
|
|
|
|
|
check: func(t *testing.T, target string) {
|
|
|
|
|
data, err := os.ReadFile(target)
|
|
|
|
|
if err != nil || string(data) != "mine\n" {
|
|
|
|
|
t.Fatalf("file = %q, %v", data, err)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "symlink to a directory",
|
|
|
|
|
setup: func(t *testing.T, target string) {
|
|
|
|
|
dest := filepath.Join(filepath.Dir(target), "elsewhere")
|
|
|
|
|
if err := os.Mkdir(dest, 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.Symlink("elsewhere", target); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
check: func(t *testing.T, target string) {
|
|
|
|
|
if link, err := os.Readlink(target); err != nil || link != "elsewhere" {
|
|
|
|
|
t.Fatalf("symlink = %q, %v", link, err)
|
|
|
|
|
}
|
|
|
|
|
assertDirEntries(t, filepath.Join(filepath.Dir(target), "elsewhere"), nil)
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "dangling symlink",
|
|
|
|
|
setup: func(t *testing.T, target string) { _ = os.Symlink("missing", target) },
|
|
|
|
|
check: func(t *testing.T, target string) {
|
|
|
|
|
if link, err := os.Readlink(target); err != nil || link != "missing" {
|
|
|
|
|
t.Fatalf("symlink = %q, %v", link, err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(filepath.Join(filepath.Dir(target), "missing")); !os.IsNotExist(err) {
|
|
|
|
|
t.Fatalf("dangling symlink target was created: %v", err)
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
for _, tc := range cases {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
parent := parentWithFramework(t)
|
|
|
|
|
target := filepath.Join(parent, "sm-demo-app")
|
|
|
|
|
tc.setup(t, target)
|
|
|
|
|
_, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true}, nil)
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "already exists") {
|
|
|
|
|
t.Fatalf("NewApp error = %v, want already exists", err)
|
|
|
|
|
}
|
|
|
|
|
tc.check(t, target)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewFrameworkResolution(t *testing.T) {
|
|
|
|
|
const fwModule = "git.golem15.com/golem15/summercms"
|
|
|
|
|
|
|
|
|
|
t.Run("flag wins over siblings", func(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, "")
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
fakeFramework(t, filepath.Join(parent, "summercms"), fwModule)
|
|
|
|
|
flagDir := fakeFramework(t, filepath.Join(t.TempDir(), "fw"), fwModule)
|
|
|
|
|
got, err := resolveFramework(parent, flagDir)
|
|
|
|
|
if err != nil || got != flagDir {
|
|
|
|
|
t.Fatalf("resolveFramework = %q, %v; want %q", got, err, flagDir)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("relative flag resolves against the parent", func(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, "")
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
want := fakeFramework(t, filepath.Join(parent, "vendor", "fw"), fwModule)
|
|
|
|
|
got, err := resolveFramework(parent, "vendor/fw")
|
|
|
|
|
if err != nil || got != want {
|
|
|
|
|
t.Fatalf("resolveFramework = %q, %v; want %q", got, err, want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("wrong module in the flag is an error, not a fall-through", func(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, repoRoot(t))
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
fakeFramework(t, filepath.Join(parent, "summercms"), fwModule)
|
|
|
|
|
flagDir := fakeFramework(t, filepath.Join(t.TempDir(), "other"), "example.com/other")
|
|
|
|
|
got, err := resolveFramework(parent, flagDir)
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "--framework") || !strings.Contains(err.Error(), "example.com/other") {
|
|
|
|
|
t.Fatalf("resolveFramework = %q, %v; want a --framework module error", got, err)
|
|
|
|
|
}
|
|
|
|
|
missing := filepath.Join(t.TempDir(), "nothing")
|
|
|
|
|
if got, err := resolveFramework(parent, missing); err == nil {
|
|
|
|
|
t.Fatalf("resolveFramework(missing flag) = %q, want error", got)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("summercms before summercms.go", func(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, "")
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
want := fakeFramework(t, filepath.Join(parent, "summercms"), fwModule)
|
|
|
|
|
fakeFramework(t, filepath.Join(parent, "summercms.go"), fwModule)
|
|
|
|
|
got, err := resolveFramework(parent, "")
|
|
|
|
|
if err != nil || got != want {
|
|
|
|
|
t.Fatalf("resolveFramework = %q, %v; want %q", got, err, want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("sibling with another module is skipped", func(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, "")
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
fakeFramework(t, filepath.Join(parent, "summercms"), "example.com/notframework")
|
|
|
|
|
want := fakeFramework(t, filepath.Join(parent, "summercms.go"), fwModule)
|
|
|
|
|
got, err := resolveFramework(parent, "")
|
|
|
|
|
if err != nil || got != want {
|
|
|
|
|
t.Fatalf("resolveFramework = %q, %v; want %q", got, err, want)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("binary source is used last", func(t *testing.T) {
|
|
|
|
|
bin := fakeFramework(t, filepath.Join(t.TempDir(), "src"), fwModule)
|
|
|
|
|
stubBinaryFramework(t, bin)
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
got, err := resolveFramework(parent, "")
|
|
|
|
|
if err != nil || got != bin {
|
|
|
|
|
t.Fatalf("resolveFramework = %q, %v; want %q", got, err, bin)
|
|
|
|
|
}
|
|
|
|
|
// A sibling still wins over the binary source.
|
|
|
|
|
sib := fakeFramework(t, filepath.Join(parent, "summercms.go"), fwModule)
|
|
|
|
|
if got, err := resolveFramework(parent, ""); err != nil || got != sib {
|
|
|
|
|
t.Fatalf("resolveFramework = %q, %v; want sibling %q", got, err, sib)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("binary source with another module is ignored", func(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, fakeFramework(t, filepath.Join(t.TempDir(), "src"), "example.com/other"))
|
|
|
|
|
if got, err := resolveFramework(t.TempDir(), ""); err == nil {
|
|
|
|
|
t.Fatalf("resolveFramework = %q, want error", got)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
t.Run("nothing found names --framework and writes nothing", func(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, "")
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
_, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true}, nil)
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "--framework") {
|
|
|
|
|
t.Fatalf("NewApp error = %v, want one naming --framework", err)
|
|
|
|
|
}
|
|
|
|
|
assertDirEntries(t, parent, nil)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewRefusesTargetInsideFramework(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, "")
|
|
|
|
|
fw := fakeFramework(t, filepath.Join(t.TempDir(), "summercms"), frameworkModule)
|
|
|
|
|
for _, tc := range []struct {
|
|
|
|
|
name, parent, flag string
|
|
|
|
|
}{
|
|
|
|
|
{"framework root", fw, "."},
|
|
|
|
|
{"subdirectory", filepath.Join(fw, "apps"), ".."},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
|
|
|
if err := os.MkdirAll(tc.parent, 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
before, err := os.ReadDir(tc.parent)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
_, err = NewApp(t.Context(), NewOptions{Name: "demo", Parent: tc.parent, Framework: tc.flag, AppKey: testAppKey, NoGit: true}, nil)
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "inside the framework checkout") {
|
|
|
|
|
t.Fatalf("NewApp error = %v, want inside the framework checkout", err)
|
|
|
|
|
}
|
|
|
|
|
after, err := os.ReadDir(tc.parent)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if len(after) != len(before) {
|
|
|
|
|
t.Fatalf("%s changed: %d entries before, %d after", tc.parent, len(before), len(after))
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewCleansUpOnFailure(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, "")
|
|
|
|
|
// The framework go.mod is valid but the checkout has no packages, so the
|
|
|
|
|
// starter plugin's go mod tidy fails after the target was created.
|
|
|
|
|
t.Setenv("GOPROXY", "off")
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
fw := fakeFramework(t, filepath.Join(parent, "summercms"), frameworkModule)
|
|
|
|
|
write(t, filepath.Join(parent, "notes.txt"), "keep me\n")
|
|
|
|
|
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
_, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, Framework: fw, AppKey: testAppKey, NoGit: true}, &out)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("NewApp succeeded against an empty framework\n%s", out.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(err.Error(), "removed it") {
|
|
|
|
|
t.Fatalf("error %v does not say the target was removed", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Lstat(filepath.Join(parent, "sm-demo-app")); !os.IsNotExist(err) {
|
|
|
|
|
t.Fatalf("sm-demo-app remains after a failed run: %v", err)
|
|
|
|
|
}
|
|
|
|
|
assertDirEntries(t, parent, []string{"notes.txt", "summercms"})
|
|
|
|
|
if got := string(readAppFile(t, parent, "notes.txt")); got != "keep me\n" {
|
|
|
|
|
t.Fatalf("notes.txt = %q", got)
|
|
|
|
|
}
|
|
|
|
|
assertDirEntries(t, fw, []string{"go.mod"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewRequiresValidAppKey(t *testing.T) {
|
|
|
|
|
stubBinaryFramework(t, "")
|
|
|
|
|
// No framework is reachable: a key error must come first.
|
|
|
|
|
parent := t.TempDir()
|
|
|
|
|
for _, key := range []string{"", "AAAA", "c2hvcnQ=", "not base64 at all!", testAppKey[:43]} {
|
|
|
|
|
_, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: key, NoGit: true}, nil)
|
|
|
|
|
if err == nil || !strings.Contains(err.Error(), "application key") {
|
|
|
|
|
t.Errorf("key %q: error = %v, want an application key error", key, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
assertDirEntries(t, parent, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewEnvFileMode(t *testing.T) {
|
|
|
|
|
parent := parentWithFramework(t)
|
|
|
|
|
dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
for rel, want := range map[string]fs.FileMode{".env": 0o600, ".env.example": 0o644, "compose.yaml": 0o644} {
|
|
|
|
|
st, err := os.Stat(filepath.Join(dir, rel))
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if got := st.Mode().Perm(); got != want {
|
|
|
|
|
t.Errorf("%s mode = %o, want %o", rel, got, want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
env := string(readAppFile(t, dir, ".env"))
|
|
|
|
|
if !strings.Contains(env, "SUMMER_APP__KEY="+testAppKey+"\n") {
|
|
|
|
|
t.Fatalf(".env lacks the key:\n%s", env)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewAgentsSymlink(t *testing.T) {
|
|
|
|
|
parent := parentWithFramework(t)
|
|
|
|
|
dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, nil)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
link, err := os.Readlink(filepath.Join(dir, "AGENTS.md"))
|
|
|
|
|
if err != nil || link != "CLAUDE.md" {
|
|
|
|
|
t.Fatalf("AGENTS.md -> %q, %v; want CLAUDE.md", link, err)
|
|
|
|
|
}
|
|
|
|
|
if !bytes.Equal(readAppFile(t, dir, "AGENTS.md"), readAppFile(t, dir, "CLAUDE.md")) {
|
|
|
|
|
t.Fatal("AGENTS.md does not resolve to CLAUDE.md")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewGitFirstCommit(t *testing.T) {
|
|
|
|
|
isolateGit(t)
|
|
|
|
|
parent := parentWithFramework(t)
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey}, &out)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("NewApp: %v\n%s", err, out.String())
|
|
|
|
|
}
|
|
|
|
|
if got := gitOut(t, dir, "rev-list", "--count", "HEAD"); got != "1" {
|
|
|
|
|
t.Fatalf("commit count = %s, want 1", got)
|
|
|
|
|
}
|
|
|
|
|
if got := gitOut(t, dir, "symbolic-ref", "--short", "HEAD"); got != "main" {
|
|
|
|
|
t.Fatalf("branch = %s, want main", got)
|
|
|
|
|
}
|
|
|
|
|
if got := gitOut(t, dir, "log", "-1", "--format=%an <%ae>|%cn <%ce>"); got != "SummerCMS <summer@localhost>|SummerCMS <summer@localhost>" {
|
|
|
|
|
t.Fatalf("identity = %s", got)
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(out.String(), "warning: git user.name and user.email not configured") {
|
|
|
|
|
t.Fatalf("no fallback identity warning:\n%s", out.String())
|
|
|
|
|
}
|
|
|
|
|
// The fallback is passed per command, never written to the repository.
|
|
|
|
|
for _, key := range []string{"user.name", "user.email"} {
|
|
|
|
|
cmd := exec.CommandContext(t.Context(), "git", "config", key)
|
|
|
|
|
cmd.Dir = dir
|
|
|
|
|
if res, err := cmd.CombinedOutput(); err == nil {
|
|
|
|
|
t.Fatalf("git config %s is set after summer new: %s", key, res)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
tracked := strings.Split(gitOut(t, dir, "ls-files"), "\n")
|
|
|
|
|
has := func(name string) bool {
|
|
|
|
|
for _, f := range tracked {
|
|
|
|
|
if f == name {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
for _, want := range []string{".env.example", ".gitignore", "go.work", "main.go", "plugins.gen.go", "AGENTS.md", "CLAUDE.md", "summer.yaml", "plugins/demo/plugin.go"} {
|
|
|
|
|
if !has(want) {
|
|
|
|
|
t.Errorf("%s is not tracked: %v", want, tracked)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if has(".env") {
|
|
|
|
|
t.Errorf(".env is tracked: %v", tracked)
|
|
|
|
|
}
|
|
|
|
|
if got := gitOut(t, dir, "status", "--porcelain"); got != "" {
|
|
|
|
|
t.Errorf("working tree not clean after the first commit:\n%s", got)
|
|
|
|
|
}
|
|
|
|
|
if got := gitOut(t, dir, "log", "-1", "--format=%B"); strings.Contains(got, "Co-Authored-By") || got != "Create sm-demo-app with summer new" {
|
|
|
|
|
t.Errorf("commit message = %q", got)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewGitUsesConfiguredIdentity(t *testing.T) {
|
|
|
|
|
isolateGit(t)
|
|
|
|
|
t.Setenv("GIT_AUTHOR_NAME", "Ada Lovelace")
|
|
|
|
|
t.Setenv("GIT_AUTHOR_EMAIL", "ada@example.com")
|
|
|
|
|
t.Setenv("GIT_COMMITTER_NAME", "Ada Lovelace")
|
|
|
|
|
t.Setenv("GIT_COMMITTER_EMAIL", "ada@example.com")
|
|
|
|
|
parent := parentWithFramework(t)
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoPlugin: true}, &out)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("NewApp: %v\n%s", err, out.String())
|
|
|
|
|
}
|
|
|
|
|
if got := gitOut(t, dir, "log", "-1", "--format=%an <%ae>|%cn <%ce>"); got != "Ada Lovelace <ada@example.com>|Ada Lovelace <ada@example.com>" {
|
|
|
|
|
t.Fatalf("identity = %s", got)
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(out.String(), "warning") {
|
|
|
|
|
t.Fatalf("unexpected warning with a configured identity:\n%s", out.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewGitMissingIsWarning(t *testing.T) {
|
|
|
|
|
goBin, err := exec.LookPath("go")
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
goBin, err = filepath.Abs(goBin)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
tools := t.TempDir()
|
|
|
|
|
if err := os.Symlink(goBin, filepath.Join(tools, "go")); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Setenv("PATH", tools)
|
|
|
|
|
if _, err := exec.LookPath("git"); err == nil {
|
|
|
|
|
t.Fatal("git still on PATH")
|
|
|
|
|
}
|
|
|
|
|
parent := parentWithFramework(t)
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey}, &out)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("NewApp without git: %v\n%s", err, out.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(out.String(), "warning: git not found on PATH") {
|
|
|
|
|
t.Fatalf("no git warning:\n%s", out.String())
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(filepath.Join(dir, "summer.yaml")); err != nil {
|
|
|
|
|
t.Fatalf("app incomplete: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Lstat(filepath.Join(dir, ".git")); !os.IsNotExist(err) {
|
|
|
|
|
t.Fatalf(".git exists without git: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewNoGit(t *testing.T) {
|
|
|
|
|
requireGit(t)
|
|
|
|
|
parent := parentWithFramework(t)
|
|
|
|
|
var out bytes.Buffer
|
|
|
|
|
dir, err := NewApp(t.Context(), NewOptions{Name: "demo", Parent: parent, AppKey: testAppKey, NoGit: true, NoPlugin: true}, &out)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Lstat(filepath.Join(dir, ".git")); !os.IsNotExist(err) {
|
|
|
|
|
t.Fatalf("--no-git created .git: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(out.String(), "warning") {
|
|
|
|
|
t.Fatalf("--no-git printed a warning:\n%s", out.String())
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(out.String(), "created "+dir) {
|
|
|
|
|
t.Fatalf("output lacks the created line:\n%s", out.String())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|