- Build-time discovery (D-028, the Caddy pattern): a module self-registers from init(); internal/modules/imports_gen.go (go:generate) blank-imports every module directory, so THE DIRECTORY IS THE SOURCE OF TRUTH - The registry: deterministic registration order, config-backed enable/disable (D-038), required modules, name-prefixed migrations, Mount() into the door - The name-collision guard now also fails a module named for a reserved level-1 workflow (SPEC-009 §13 Q-1) — access · static · api · media · ws · dev — derived from routing.ReservedWorkflows, `home` excepted (D-2) - home: the reference dashboard module, and the core Modules page
135 lines
4 KiB
Go
135 lines
4 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// writeModule creates a minimal module directory (one non-test Go file).
|
|
func writeModule(t *testing.T, dir, name string) {
|
|
t.Helper()
|
|
d := filepath.Join(dir, name)
|
|
if err := os.MkdirAll(d, 0o755); err != nil {
|
|
t.Fatalf("mkdir %s: %v", d, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(d, "app.go"), []byte("package "+name+"\n"), 0o644); err != nil {
|
|
t.Fatalf("write %s: %v", d, err)
|
|
}
|
|
}
|
|
|
|
const importBase = "example.com/mod/internal/modules"
|
|
|
|
// TestGenerateSortedAndDeterministic: the aggregator is deterministic and the
|
|
// imports are sorted (AC-1/AC-2).
|
|
func TestGenerateSortedAndDeterministic(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for _, n := range []string{"time", "home", "expense"} {
|
|
writeModule(t, dir, n)
|
|
}
|
|
|
|
src, err := Generate(dir, importBase)
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
got := string(src)
|
|
if !strings.Contains(got, "Code generated") {
|
|
t.Error("the header is missing")
|
|
}
|
|
|
|
// Sorted, regardless of creation order.
|
|
iHome := strings.Index(got, importBase+"/home")
|
|
iExpense := strings.Index(got, importBase+"/expense")
|
|
iTime := strings.Index(got, importBase+"/time")
|
|
if iHome < 0 || iExpense < 0 || iTime < 0 {
|
|
t.Fatalf("not all modules were imported:\n%s", got)
|
|
}
|
|
if !(iExpense < iHome && iHome < iTime) {
|
|
t.Errorf("imports are not sorted (expense<home<time):\n%s", got)
|
|
}
|
|
|
|
// Deterministic across runs.
|
|
again, err := Generate(dir, importBase)
|
|
if err != nil {
|
|
t.Fatalf("Generate (again): %v", err)
|
|
}
|
|
if string(again) != got {
|
|
t.Error("Generate is not deterministic")
|
|
}
|
|
}
|
|
|
|
// TestGenerateDiscoversANewDirectory: a new directory alone changes the output
|
|
// with no edits elsewhere (AC-1).
|
|
func TestGenerateDiscoversANewDirectory(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeModule(t, dir, "home")
|
|
before, err := Generate(dir, importBase)
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
if strings.Contains(string(before), "/reports") {
|
|
t.Fatal("reports appeared before it existed")
|
|
}
|
|
|
|
writeModule(t, dir, "reports")
|
|
after, err := Generate(dir, importBase)
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
if !strings.Contains(string(after), importBase+"/reports") {
|
|
t.Errorf("a new directory was not discovered:\n%s", after)
|
|
}
|
|
}
|
|
|
|
// TestGenerateSkipsTooling: the generator dir, testdata, dot/underscore dirs
|
|
// and test-only packages are not modules.
|
|
func TestGenerateSkipsTooling(t *testing.T) {
|
|
dir := t.TempDir()
|
|
writeModule(t, dir, "home")
|
|
for _, n := range []string{"gen", "testdata", "_scratch", ".hidden"} {
|
|
writeModule(t, dir, n)
|
|
}
|
|
// A directory that holds only a _test.go file is not a module either.
|
|
only := filepath.Join(dir, "onlytests")
|
|
if err := os.MkdirAll(only, 0o755); err != nil {
|
|
t.Fatalf("mkdir: %v", err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(only, "x_test.go"), []byte("package onlytests\n"), 0o644); err != nil {
|
|
t.Fatalf("write: %v", err)
|
|
}
|
|
|
|
src, err := Generate(dir, importBase)
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
got := string(src)
|
|
if !strings.Contains(got, importBase+"/home") {
|
|
t.Errorf("home was skipped:\n%s", got)
|
|
}
|
|
for _, n := range []string{"gen", "testdata", "_scratch", ".hidden", "onlytests"} {
|
|
if strings.Contains(got, importBase+"/"+n) {
|
|
t.Errorf("%s was imported, want it skipped:\n%s", n, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestImportsGenIsUpToDate guards the committed aggregator against drift: the
|
|
// real internal/modules tree must reproduce imports_gen.go exactly.
|
|
func TestImportsGenIsUpToDate(t *testing.T) {
|
|
const real = ".."
|
|
const realPath = "git.lohmar.co.uk/cclohmar/NextMyDay/internal/modules"
|
|
|
|
committed, err := os.ReadFile(filepath.Join(real, "imports_gen.go"))
|
|
if err != nil {
|
|
t.Fatalf("read imports_gen.go: %v", err)
|
|
}
|
|
want, err := Generate(real, realPath)
|
|
if err != nil {
|
|
t.Fatalf("Generate: %v", err)
|
|
}
|
|
if string(committed) != string(want) {
|
|
t.Errorf("imports_gen.go is stale — run `go generate ./internal/modules`.\n--- committed ---\n%s\n--- generated ---\n%s",
|
|
committed, want)
|
|
}
|
|
}
|