88143838dd
- replace the POSIX shell fixture with a compiled Go helper named hub or hub.exe - prepend the helper directory to PATH in TestMain so tests cannot invoke an installed hub binary - add PowerShell build, install, and Windows path examples to the README - ignore the generated stack-sync.exe binary
248 lines
6.6 KiB
Go
248 lines
6.6 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
if os.Getenv("STACK_SYNC_TEST_HELPER") == "hub" {
|
|
marker := os.Getenv("STACK_SYNC_TEST_MARKER")
|
|
if err := os.WriteFile(marker, []byte(strings.Join(os.Args[1:], " ")), 0o644); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
os.Exit(0)
|
|
}
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func installFakeHub(t *testing.T, dir string) string {
|
|
t.Helper()
|
|
source, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
name := "hub"
|
|
if runtime.GOOS == "windows" {
|
|
name += ".exe"
|
|
}
|
|
destination := filepath.Join(dir, name)
|
|
if err := os.Link(source, destination); err == nil {
|
|
return destination
|
|
}
|
|
|
|
input, err := os.Open(source)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer input.Close()
|
|
output, err := os.OpenFile(destination, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o755)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := io.Copy(output, input); err != nil {
|
|
_ = output.Close()
|
|
t.Fatal(err)
|
|
}
|
|
if err := output.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return destination
|
|
}
|
|
|
|
func git(t *testing.T, dir string, args ...string) {
|
|
t.Helper()
|
|
cmd := exec.Command("git", append([]string{"-C", dir}, args...)...)
|
|
if out, err := cmd.CombinedOutput(); err != nil {
|
|
t.Fatalf("git %v: %v\n%s", args, err, out)
|
|
}
|
|
}
|
|
|
|
func makeRepo(t *testing.T, path string) {
|
|
t.Helper()
|
|
if err := os.MkdirAll(path, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
git(t, path, "init", "-q")
|
|
git(t, path, "config", "user.name", "Test")
|
|
git(t, path, "config", "user.email", "test@example.com")
|
|
if err := os.WriteFile(filepath.Join(path, "tracked.txt"), []byte("clean\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
git(t, path, "add", "tracked.txt")
|
|
git(t, path, "commit", "-qm", "initial")
|
|
git(t, path, "remote", "add", "origin", "https://example.invalid/repo.git")
|
|
}
|
|
|
|
func TestDiscoverNestedRepositories(t *testing.T) {
|
|
root := t.TempDir()
|
|
parent := filepath.Join(root, "parent")
|
|
child := filepath.Join(parent, "apps", "child")
|
|
makeRepo(t, parent)
|
|
makeRepo(t, child)
|
|
|
|
paths, err := discover(root, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(paths) != 2 || paths[0] != parent || paths[1] != child {
|
|
t.Fatalf("discover() = %#v", paths)
|
|
}
|
|
}
|
|
|
|
func TestNestedRepoDoesNotDirtyParent(t *testing.T) {
|
|
root := t.TempDir()
|
|
parent := filepath.Join(root, "parent")
|
|
child := filepath.Join(parent, "child")
|
|
makeRepo(t, parent)
|
|
makeRepo(t, child)
|
|
|
|
paths, _ := discover(root, nil)
|
|
got := inspect(root, parent, paths)
|
|
if !got.Eligible {
|
|
t.Fatalf("parent blocked by nested repo: %+v", got)
|
|
}
|
|
if len(got.NestedRepoEntries) == 0 {
|
|
t.Fatalf("expected nested repository entry to be recorded: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestDirtyFileBlocksRepository(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "repo")
|
|
makeRepo(t, path)
|
|
if err := os.WriteFile(filepath.Join(path, "tracked.txt"), []byte("changed\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
got := inspect(root, path, []string{path})
|
|
if got.Eligible || len(got.Dirty) != 1 {
|
|
t.Fatalf("dirty repository was not blocked: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestUntrackedFileAlongsideNestedRepoStillBlocksParent(t *testing.T) {
|
|
root := t.TempDir()
|
|
parent := filepath.Join(root, "parent")
|
|
child := filepath.Join(parent, "child")
|
|
makeRepo(t, parent)
|
|
makeRepo(t, child)
|
|
if err := os.WriteFile(filepath.Join(parent, "notes.txt"), []byte("work\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
paths, _ := discover(root, nil)
|
|
got := inspect(root, parent, paths)
|
|
if got.Eligible || len(got.Dirty) != 1 {
|
|
t.Fatalf("untracked file should block parent: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSkippedDirectoriesAreNotSearched(t *testing.T) {
|
|
root := t.TempDir()
|
|
makeRepo(t, filepath.Join(root, "node_modules", "dependency"))
|
|
makeRepo(t, filepath.Join(root, "real"))
|
|
paths, err := discover(root, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(paths) != 1 || paths[0] != filepath.Join(root, "real") {
|
|
t.Fatalf("discover() = %#v", paths)
|
|
}
|
|
}
|
|
|
|
func TestSyncRechecksAndSkipsNewlyDirtyRepository(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "repo")
|
|
makeRepo(t, path)
|
|
planned := inspect(root, path, []string{path})
|
|
if !planned.Eligible {
|
|
t.Fatalf("fixture should initially be eligible: %+v", planned)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(path, "late-change.txt"), []byte("do not touch\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
bin := filepath.Join(root, "bin")
|
|
if err := os.Mkdir(bin, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
marker := filepath.Join(root, "hub-was-called")
|
|
installFakeHub(t, bin)
|
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
t.Setenv("STACK_SYNC_TEST_HELPER", "hub")
|
|
t.Setenv("STACK_SYNC_TEST_MARKER", marker)
|
|
|
|
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true)
|
|
if len(results) != 1 || !results[0].Skipped || results[0].Success {
|
|
t.Fatalf("syncAll() = %+v", results)
|
|
}
|
|
if _, err := os.Stat(marker); !os.IsNotExist(err) {
|
|
t.Fatalf("hub ran despite the newly dirty worktree")
|
|
}
|
|
}
|
|
|
|
func TestSyncInvokesHubForCleanRepository(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "repo")
|
|
makeRepo(t, path)
|
|
planned := inspect(root, path, []string{path})
|
|
|
|
bin := filepath.Join(root, "bin")
|
|
if err := os.Mkdir(bin, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
marker := filepath.Join(root, "hub-was-called")
|
|
installFakeHub(t, bin)
|
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
t.Setenv("STACK_SYNC_TEST_HELPER", "hub")
|
|
t.Setenv("STACK_SYNC_TEST_MARKER", marker)
|
|
|
|
results := syncAll(root, []repo{planned}, 1, time.Minute, io.Discard, true)
|
|
if len(results) != 1 || !results[0].Success || results[0].Skipped {
|
|
t.Fatalf("syncAll() = %+v", results)
|
|
}
|
|
args, err := os.ReadFile(marker)
|
|
if err != nil {
|
|
t.Fatalf("hub was not called: %v", err)
|
|
}
|
|
if string(args) != "sync --color=never" {
|
|
t.Fatalf("hub arguments = %q", args)
|
|
}
|
|
}
|
|
|
|
func TestScanWithBlockedRepositoryIsAValidReport(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "repo")
|
|
makeRepo(t, path)
|
|
if err := os.WriteFile(filepath.Join(path, "untracked.txt"), []byte("work\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
code := run([]string{"scan", "--root", root}, bytes.NewReader(nil), &stdout, &stderr)
|
|
if code != 0 {
|
|
t.Fatalf("run() code = %d, stderr = %q", code, stderr.String())
|
|
}
|
|
if !bytes.Contains(stdout.Bytes(), []byte("BLOCKED: dirty worktree")) {
|
|
t.Fatalf("scan did not report blocked repository:\n%s", stdout.String())
|
|
}
|
|
}
|
|
|
|
func TestIndentNormalizesGitProgressOutput(t *testing.T) {
|
|
got := indent("first\rsecond\r\nthird", " ")
|
|
want := " first\n second\n third"
|
|
if got != want {
|
|
t.Fatalf("indent() = %q, want %q", got, want)
|
|
}
|
|
}
|