package main import ( "io" "log/slog" "os" "path/filepath" "sort" "strings" "testing" "go-doc/storage" ) func TestScanDetectsNewProjectsAndEditsButSettlesWhenIdle(t *testing.T) { t.Parallel() root := t.TempDir() writeProject(t, filepath.Join(root, "go-alpha")) im := newImporter(options{root: root}, newEngine(t), discardLogger()) // A project nobody has seen counts as changed. changed := scan(t, im) if want := []string{"go-alpha"}; !equal(changed, want) { t.Fatalf("first scan = %v, want %v", changed, want) } // An untouched project must not re-import on every poll. if changed := scan(t, im); len(changed) != 0 { t.Errorf("idle scan reported %v, want no changes", changed) } // A project that appears mid-run is picked up. writeProject(t, filepath.Join(root, "go-beta")) if changed := scan(t, im); !equal(changed, []string{"go-beta"}) { t.Errorf("scan after a new project = %v, want [go-beta]", changed) } // An edit to an existing project is picked up, once. writeProject(t, filepath.Join(root, "go-alpha")) if changed := scan(t, im); !equal(changed, []string{"go-alpha"}) { t.Errorf("scan after an edit = %v, want [go-alpha]", changed) } if changed := scan(t, im); len(changed) != 0 { t.Errorf("the edit was reported twice: %v", changed) } } func TestScanForgetsProjectsThatLeaveTheWorkspace(t *testing.T) { t.Parallel() root := t.TempDir() project := filepath.Join(root, "go-alpha") writeProject(t, project) var logs strings.Builder im := newImporter(options{root: root}, newEngine(t), slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))) if changed := scan(t, im); len(changed) != 1 { t.Fatalf("first scan = %v, want one project", changed) } logs.Reset() if err := os.RemoveAll(project); err != nil { t.Fatalf("remove project: %v", err) } if changed := scan(t, im); len(changed) != 0 { t.Errorf("a departed project was treated as changed: %v", changed) } if !strings.Contains(logs.String(), "project left the workspace") { t.Errorf("a departed project was not reported:\n%s", logs.String()) } // Coming back is a change again rather than a silent no-op. writeProject(t, project) if changed := scan(t, im); !equal(changed, []string{"go-alpha"}) { t.Errorf("scan after the project returned = %v, want [go-alpha]", changed) } } func TestDiscoverHonoursAppsAndSkip(t *testing.T) { t.Parallel() root := t.TempDir() for _, name := range []string{"go-alpha", "go-beta", "go-doc", "not-a-go-app"} { writeProject(t, filepath.Join(root, name)) } cases := []struct { name string opts options want []string }{ {name: "default skips go-doc", opts: options{root: root, skip: "go-doc"}, want: []string{"go-alpha", "go-beta"}}, {name: "-apps narrows the run", opts: options{root: root, skip: "", apps: "go-beta"}, want: []string{"go-beta"}}, {name: "no skip at all", opts: options{root: root}, want: []string{"go-alpha", "go-beta", "go-doc"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { im := newImporter(tc.opts, newEngine(t), discardLogger()) got, err := im.Discover() if err != nil { t.Fatalf("discover: %v", err) } if strings.Join(got, ",") != strings.Join(tc.want, ",") { t.Errorf("discover = %v, want %v", got, tc.want) } }) } } func TestDiscoverReportsAMissingWorkspace(t *testing.T) { t.Parallel() im := newImporter(options{root: filepath.Join(t.TempDir(), "absent")}, newEngine(t), discardLogger()) if _, err := im.Discover(); err == nil { t.Fatal("a missing workspace should be reported, not silently ignored") } } func scan(t *testing.T, im *importer) []string { t.Helper() changed, err := im.Scan() if err != nil { t.Fatalf("scan: %v", err) } names := make([]string, 0, len(changed)) for name := range changed { names = append(names, name) } sort.Strings(names) return names } func equal(got, want []string) bool { return strings.Join(got, ",") == strings.Join(want, ",") } func newEngine(t *testing.T) *storage.Engine { t.Helper() engine, err := storage.New(t.TempDir()) if err != nil { t.Fatalf("provision engine: %v", err) } return engine } func discardLogger() *slog.Logger { return slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug})) } func writeProject(t *testing.T, root string) { t.Helper() if err := os.MkdirAll(filepath.Join(root, "internal"), 0o755); err != nil { t.Fatalf("mkdir %s: %v", root, err) } files := map[string]string{ "go.mod": "module example.com/" + filepath.Base(root) + "\n\ngo 1.27.1\n", "README.md": "# " + filepath.Base(root) + "\n\nIt serves an HTTP API.\n", "internal/app.go": "package internal\n\n// Run starts the app.\nfunc Run() error { return nil }\n", "internal/app_t.go": "placeholder\n", } for name, content := range files { if err := os.WriteFile(filepath.Join(root, filepath.FromSlash(name)), []byte(content), 0o644); err != nil { t.Fatalf("write %s: %v", name, err) } } }