351eab3743
Replace POSIX shell-based hub fixtures with a helper executable that resolves correctly on both Windows and Linux. This prevents Windows tests from falling through to a real hub.exe and hanging on network access. Document PowerShell installation and Windows path usage, require the .exe suffix for explicit Windows build output, and ignore generated binaries.
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)
|
|
}
|
|
}
|