133728e barerepo 1mo
1
package thread
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"fmt"
133728e barerepo 1mo
6
"os"
133728e barerepo 1mo
7
"os/exec"
133728e barerepo 1mo
8
"strconv"
133728e barerepo 1mo
9
"strings"
133728e barerepo 1mo
10
"sync"
133728e barerepo 1mo
11
"testing"
133728e barerepo 1mo
12
"time"
133728e barerepo 1mo
14
"github.com/barerepo/server/internal/gitx"
133728e barerepo 1mo
17
func repoWithCommit(t *testing.T) (dir, sha string) {
133728e barerepo 1mo
18
t.Helper()
133728e barerepo 1mo
19
if _, err := gitx.Version(context.Background()); err != nil {
133728e barerepo 1mo
20
t.Skip("git is not installed")
133728e barerepo 1mo
22
dir = t.TempDir()
133728e barerepo 1mo
23
run := func(args ...string) string {
133728e barerepo 1mo
24
t.Helper()
133728e barerepo 1mo
25
cmd := exec.Command(gitx.Bin, args...)
133728e barerepo 1mo
26
cmd.Dir = dir
133728e barerepo 1mo
27
cmd.Env = append(cmd.Environ(),
133728e barerepo 1mo
28
"GIT_AUTHOR_NAME=lisa", "GIT_AUTHOR_EMAIL=m@x",
133728e barerepo 1mo
29
"GIT_COMMITTER_NAME=lisa", "GIT_COMMITTER_EMAIL=m@x")
133728e barerepo 1mo
30
out, err := cmd.CombinedOutput()
133728e barerepo 1mo
31
if err != nil {
133728e barerepo 1mo
32
t.Fatalf("git %v: %v\n%s", args, err, out)
133728e barerepo 1mo
34
return strings.TrimSpace(string(out))
133728e barerepo 1mo
36
run("init", "-q", "-b", "master")
133728e barerepo 1mo
37
run("commit", "-q", "--allow-empty", "-m", "first")
133728e barerepo 1mo
38
return dir, run("rev-parse", "HEAD")
133728e barerepo 1mo
41
func TestThreadRoundTrip(t *testing.T) {
133728e barerepo 1mo
42
ctx := context.Background()
133728e barerepo 1mo
43
dir, sha := repoWithCommit(t)
133728e barerepo 1mo
45
m := Meta{
133728e barerepo 1mo
46
Title: "panic when config file is empty", State: Open,
133728e barerepo 1mo
47
Ref: "refs/proposals/47", Author: "lisa", Opened: time.Unix(1787074650, 0),
133728e barerepo 1mo
49
first := Comment{Author: "lisa", Time: time.Unix(1787074650, 0),
133728e barerepo 1mo
50
Body: "fresh install, empty config.toml, immediate nil deref on line 44."}
133728e barerepo 1mo
51
if err := Create(ctx, dir, 47, m, sha, first); err != nil {
133728e barerepo 1mo
52
t.Fatal(err)
133728e barerepo 1mo
54
if err := Create(ctx, dir, 47, m, sha, first); err == nil {
133728e barerepo 1mo
55
t.Error("a thread was opened twice with the same number")
133728e barerepo 1mo
58
got, exists, err := ReadMeta(ctx, dir, 47)
133728e barerepo 1mo
59
if err != nil || !exists {
133728e barerepo 1mo
60
t.Fatalf("ReadMeta: %v %v", exists, err)
133728e barerepo 1mo
62
if got.Title != m.Title || got.State != Open || got.Ref != m.Ref || got.Author != "lisa" {
133728e barerepo 1mo
63
t.Errorf("meta round trip lost something: %+v", got)
133728e barerepo 1mo
66
if err := Reply(ctx, dir, 47, sha, Comment{
133728e barerepo 1mo
67
Author: "john", Time: time.Unix(1787078250, 0), Anchor: "config.go:43",
133728e barerepo 1mo
68
Body: "Default() allocates every call. make it a package var?",
133728e barerepo 1mo
69
}); err != nil {
133728e barerepo 1mo
70
t.Fatal(err)
133728e barerepo 1mo
73
// The whole point: git itself can read this, with no barerepo involved.
133728e barerepo 1mo
74
out, err := gitx.Run(ctx, dir, "log", "--show-notes=threads/47", "--max-count=1")
133728e barerepo 1mo
75
if err != nil {
133728e barerepo 1mo
76
t.Fatal(err)
133728e barerepo 1mo
78
for _, want := range []string{"Notes (threads/47)", "immediate nil deref", "package var?"} {
133728e barerepo 1mo
79
if !strings.Contains(out, want) {
133728e barerepo 1mo
80
t.Errorf("git log --show-notes did not show %q\n%s", want, out)
133728e barerepo 1mo
83
// And so can git notes show, by the object's hash.
133728e barerepo 1mo
84
note, err := gitx.Run(ctx, dir, "notes", "--ref=threads/47", "show", sha)
133728e barerepo 1mo
85
if err != nil {
133728e barerepo 1mo
86
t.Fatalf("git notes show: %v", err)
133728e barerepo 1mo
88
comments := ParseComments(note)
133728e barerepo 1mo
89
if len(comments) != 2 {
133728e barerepo 1mo
90
t.Fatalf("got %d comments, want 2: %+v", len(comments), comments)
133728e barerepo 1mo
92
if comments[0].Author != "lisa" || comments[1].Author != "john" {
133728e barerepo 1mo
93
t.Errorf("comments are out of order: %+v", comments)
133728e barerepo 1mo
95
if comments[1].Anchor != "config.go:43" {
133728e barerepo 1mo
96
t.Errorf("the anchor was lost: %+v", comments[1])
133728e barerepo 1mo
100
// Chapter 45.3's thousand, at full count outside short mode, since each write costs processes.
133728e barerepo 1mo
101
func TestConcurrentRepliesAtScale(t *testing.T) {
133728e barerepo 1mo
102
if testing.Short() {
133728e barerepo 1mo
103
t.Skip("chapter 45.3's full count takes minutes")
133728e barerepo 1mo
104
}
133728e barerepo 1mo
105
ctx := context.Background()
133728e barerepo 1mo
106
dir, sha := repoWithCommit(t)
133728e barerepo 1mo
107
if err := Create(ctx, dir, 1, Meta{Title: "scale", State: Open}, "", Comment{}); err != nil {
133728e barerepo 1mo
108
t.Fatal(err)
133728e barerepo 1mo
109
}
133728e barerepo 1mo
111
const rounds, writers = 50, 20
133728e barerepo 1mo
112
for round := range rounds {
133728e barerepo 1mo
113
var wg sync.WaitGroup
133728e barerepo 1mo
114
errs := make([]error, writers)
133728e barerepo 1mo
115
start := make(chan struct{})
133728e barerepo 1mo
116
for i := range writers {
133728e barerepo 1mo
117
wg.Add(1)
133728e barerepo 1mo
118
go func(i int) {
133728e barerepo 1mo
119
defer wg.Done()
133728e barerepo 1mo
120
<-start
133728e barerepo 1mo
121
errs[i] = Reply(ctx, dir, 1, sha, Comment{
133728e barerepo 1mo
122
Author: "writer",
133728e barerepo 1mo
123
Time: time.Unix(int64(1787074650+round*writers+i), 0),
133728e barerepo 1mo
124
Body: fmt.Sprintf("round %d writer %d", round, i),
133728e barerepo 1mo
125
})
133728e barerepo 1mo
126
}(i)
133728e barerepo 1mo
127
}
133728e barerepo 1mo
128
close(start)
133728e barerepo 1mo
129
wg.Wait()
133728e barerepo 1mo
130
for i, err := range errs {
133728e barerepo 1mo
131
if err != nil {
133728e barerepo 1mo
132
t.Fatalf("round %d writer %d: %v", round, i, err)
133728e barerepo 1mo
133
}
133728e barerepo 1mo
134
}
133728e barerepo 1mo
135
}
133728e barerepo 1mo
137
note, err := gitx.Run(ctx, dir, "notes", "--ref=threads/1", "show", sha)
133728e barerepo 1mo
138
if err != nil {
133728e barerepo 1mo
139
t.Fatal(err)
133728e barerepo 1mo
140
}
133728e barerepo 1mo
141
if got := len(ParseComments(note)); got != rounds*writers {
133728e barerepo 1mo
142
t.Errorf("kept %d comments of %d: %d were dropped",
133728e barerepo 1mo
143
got, rounds*writers, rounds*writers-got)
133728e barerepo 1mo
144
}
133728e barerepo 1mo
147
// Chapter 45.3: two comments at one moment, both surviving, because the loser reads again.
133728e barerepo 1mo
148
func TestConcurrentReplies(t *testing.T) {
133728e barerepo 1mo
149
ctx := context.Background()
133728e barerepo 1mo
150
dir, sha := repoWithCommit(t)
133728e barerepo 1mo
151
if err := Create(ctx, dir, 1, Meta{Title: "concurrency", State: Open}, "", Comment{}); err != nil {
133728e barerepo 1mo
152
t.Fatal(err)
133728e barerepo 1mo
153
}
133728e barerepo 1mo
155
const n = 12
133728e barerepo 1mo
156
var wg sync.WaitGroup
133728e barerepo 1mo
157
errs := make([]error, n)
133728e barerepo 1mo
158
start := make(chan struct{})
133728e barerepo 1mo
159
for i := 0; i < n; i++ {
133728e barerepo 1mo
160
wg.Add(1)
133728e barerepo 1mo
161
go func(i int) {
133728e barerepo 1mo
162
defer wg.Done()
133728e barerepo 1mo
163
<-start
133728e barerepo 1mo
164
errs[i] = Reply(ctx, dir, 1, sha, Comment{
133728e barerepo 1mo
165
Author: "writer", Time: time.Unix(int64(1787074650+i), 0),
133728e barerepo 1mo
166
Body: "comment " + string(rune('a'+i)),
133728e barerepo 1mo
167
})
133728e barerepo 1mo
168
}(i)
133728e barerepo 1mo
169
}
133728e barerepo 1mo
170
close(start)
133728e barerepo 1mo
171
wg.Wait()
133728e barerepo 1mo
172
for i, err := range errs {
133728e barerepo 1mo
173
if err != nil {
133728e barerepo 1mo
174
t.Fatalf("reply %d failed: %v", i, err)
133728e barerepo 1mo
175
}
133728e barerepo 1mo
176
}
133728e barerepo 1mo
177
note, err := gitx.Run(ctx, dir, "notes", "--ref=threads/1", "show", sha)
133728e barerepo 1mo
178
if err != nil {
133728e barerepo 1mo
179
t.Fatal(err)
133728e barerepo 1mo
180
}
133728e barerepo 1mo
181
if got := len(ParseComments(note)); got != n {
133728e barerepo 1mo
182
t.Errorf("kept %d comments of %d; the rest were dropped", got, n)
133728e barerepo 1mo
183
}
133728e barerepo 1mo
186
func TestSetState(t *testing.T) {
133728e barerepo 1mo
187
ctx := context.Background()
133728e barerepo 1mo
188
dir, _ := repoWithCommit(t)
133728e barerepo 1mo
189
if err := Create(ctx, dir, 5, Meta{Title: "t", State: Open}, "", Comment{}); err != nil {
133728e barerepo 1mo
190
t.Fatal(err)
133728e barerepo 1mo
191
}
133728e barerepo 1mo
192
if err := SetState(ctx, dir, 5, Merged, "a3f9c2d"); err != nil {
133728e barerepo 1mo
193
t.Fatal(err)
133728e barerepo 1mo
194
}
133728e barerepo 1mo
195
m, _, _ := ReadMeta(ctx, dir, 5)
133728e barerepo 1mo
196
if m.State != Merged || m.Merged != "a3f9c2d" {
133728e barerepo 1mo
197
t.Errorf("state = %+v", m)
133728e barerepo 1mo
198
}
133728e barerepo 1mo
199
if m.Title != "t" {
133728e barerepo 1mo
200
t.Error("changing the state lost the title")
133728e barerepo 1mo
201
}
133728e barerepo 1mo
204
func TestParseCommentsIgnoresJunk(t *testing.T) {
133728e barerepo 1mo
205
if got := ParseComments("no headers here"); len(got) != 0 {
133728e barerepo 1mo
206
t.Errorf("parsed %d comments from junk", len(got))
133728e barerepo 1mo
207
}
133728e barerepo 1mo
208
if got := ParseComments(""); len(got) != 0 {
133728e barerepo 1mo
209
t.Errorf("parsed %d comments from nothing", len(got))
133728e barerepo 1mo
210
}
133728e barerepo 1mo
213
// helperEnv carries one reply into the child process started below.
133728e barerepo 1mo
214
const helperEnv = "BAREREPO_THREAD_HELPER"
133728e barerepo 1mo
216
// TestReplyHelper is a child process: it writes one comment and exits.
133728e barerepo 1mo
217
func TestReplyHelper(t *testing.T) {
133728e barerepo 1mo
218
spec := os.Getenv(helperEnv)
133728e barerepo 1mo
219
if spec == "" {
133728e barerepo 1mo
220
t.Skip("not a helper process")
133728e barerepo 1mo
221
}