package proposal import ( "context" "os/exec" "strings" "sync" "testing" "github.com/barerepo/server/internal/gitx" ) func bare(t *testing.T) string { t.Helper() if _, err := gitx.Version(context.Background()); err != nil { t.Skip("git is not installed") } dir := t.TempDir() cmd := exec.Command(gitx.Bin, "init", "--bare", "-q", "--initial-branch", "master", dir) if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("init: %v\n%s", err, out) } return dir } func TestAllocateCountsUp(t *testing.T) { ctx := context.Background() dir := bare(t) for want := 1; want <= 5; want++ { got, err := Allocate(ctx, dir) if err != nil { t.Fatal(err) } if got != want { t.Fatalf("Allocate = %d, want %d", got, want) } } // The number lives in the repository, so a mirror carries it. if _, _, err := readCounter(ctx, dir); err != nil { t.Fatal(err) } } // Chapter 45.2: two simultaneous pushes, two different numbers, twenty times over. func TestAllocateIsAtomic(t *testing.T) { ctx := context.Background() dir := bare(t) const n = 20 var wg sync.WaitGroup got := make([]int, n) errs := make([]error, n) start := make(chan struct{}) for i := 0; i < n; i++ { wg.Add(1) go func(i int) { defer wg.Done() <-start got[i], errs[i] = Allocate(ctx, dir) }(i) } close(start) wg.Wait() seen := map[int]bool{} for i, v := range got { if errs[i] != nil { t.Fatalf("allocation %d failed: %v", i, errs[i]) } if seen[v] { t.Fatalf("number %d was handed out twice", v) } seen[v] = true } for want := 1; want <= n; want++ { if !seen[want] { t.Errorf("number %d was skipped", want) } } } func TestNumber(t *testing.T) { for ref, want := range map[string]int{ "refs/proposals/47": 47, "refs/proposals/47/rev/2": 47, "refs/proposals/1": 1, "refs/proposals/new": 0, "refs/heads/master": 0, "refs/proposals/0": 0, "refs/proposals/-1": 0, "refs/proposals/": 0, "": 0, } { if got := Number(ref); got != want { t.Errorf("Number(%q) = %d, want %d", ref, got, want) } } } // A revision cannot live under the proposal ref, where git would need a file and directory both. func TestRevisionRefsCanCoexist(t *testing.T) { ctx := context.Background() dir := bare(t) empty, err := gitx.RunStdin(ctx, dir, "", "hash-object", "-w", "--stdin") if err != nil { t.Fatal(err) } sha := strings.TrimSpace(empty) if _, err := gitx.Run(ctx, dir, "update-ref", Ref(47), sha, ""); err != nil { t.Fatal(err) } if _, err := gitx.Run(ctx, dir, "update-ref", RevisionRef(47, 1), sha, ""); err != nil { t.Fatalf("a proposal and its retained revision cannot both exist: %v", err) } } func TestRefNames(t *testing.T) { if Ref(47) != "refs/proposals/47" { t.Error(Ref(47)) } if RevisionRef(47, 2) != "refs/revisions/47/2" { t.Error(RevisionRef(47, 2)) } // Appendix A: the whole layout has to be valid to git. for _, r := range []string{Ref(47), RevisionRef(47, 2), NewRef, CounterRef} { if !gitx.ValidRef(r) { t.Errorf("%q is not a usable ref name", r) } } } // A pushed thread leaves the counter behind, and reusing the number merges two conversations. func TestAllocateSkipsNumbersInUse(t *testing.T) { ctx := context.Background() dir := bare(t) empty, err := gitx.RunStdin(ctx, dir, "", "hash-object", "-w", "--stdin") if err != nil { t.Fatal(err) } sha := strings.TrimSpace(empty) // Somebody pushed thread 1 and thread 2 without asking the counter. for _, ref := range []string{"refs/notes/threads/1", "refs/notes/threads/2"} { if _, err := gitx.Run(ctx, dir, "update-ref", ref, sha, ""); err != nil { t.Fatal(err) } } got, err := Allocate(ctx, dir) if err != nil { t.Fatal(err) } if got != 3 { t.Errorf("Allocate = %d, want 3: it handed out a number already in use", got) } }