1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
16
17 package test
18
19 import (
20 "bufio"
21 "bytes"
22 "errors"
23 "fmt"
24 "io"
25 "log"
26 "net"
27 "net/http"
28 "os"
29 "os/exec"
30 "path/filepath"
31 "runtime"
32 "strings"
33 "sync/atomic"
34 "testing"
35 "time"
36
37 "perkeep.org/internal/osutil"
38 "perkeep.org/pkg/blob"
39 )
40
41
42
43
44
45
46 type World struct {
47 srcRoot string
48 config string
49 tempDir string
50 gobin string
51
52 addr string
53
54 server *exec.Cmd
55 isRunning int32
56 serverErr error
57 }
58
59
60 func pkSourceRoot() (string, error) {
61 root, err := osutil.GoPackagePath("perkeep.org")
62 if err == os.ErrNotExist {
63 return "", errors.New("directory \"perkeep.org\" not found under GOPATH/src; can't run Perkeep integration tests")
64 }
65 return root, nil
66 }
67
68
69
70 func NewWorld() (*World, error) {
71 return WorldFromConfig("server-config.json")
72 }
73
74
75
76
77 func WorldFromConfig(cfg string) (*World, error) {
78 root, err := pkSourceRoot()
79 if err != nil {
80 return nil, err
81 }
82 return &World{
83 srcRoot: root,
84 config: cfg,
85 }, nil
86 }
87
88 func (w *World) Addr() string {
89 return w.addr
90 }
91
92
93 func (w *World) SourceRoot() string {
94 return w.srcRoot
95 }
96
97
98 func (w *World) Build() error {
99 var err error
100 w.tempDir, err = os.MkdirTemp("", "perkeep-test-")
101 if err != nil {
102 return err
103 }
104 w.gobin = filepath.Join(w.tempDir, "bin")
105 if err := os.MkdirAll(w.gobin, 0700); err != nil {
106 return err
107 }
108
109 {
110 cmd := exec.Command("go", "run", "make.go",
111 "--stampversion=false",
112 "--targets="+strings.Join([]string{
113 "perkeep.org/server/perkeepd",
114 "perkeep.org/cmd/pk",
115 "perkeep.org/cmd/pk-get",
116 "perkeep.org/cmd/pk-put",
117 "perkeep.org/cmd/pk-mount",
118 }, ","))
119 if testing.Verbose() {
120
121
122 cmd.Args = append(cmd.Args, "-v=true")
123 }
124 cmd.Dir = w.srcRoot
125 cmd.Env = append(os.Environ(), "GOBIN="+w.gobin)
126 log.Print("Running make.go to build perkeep binaries for testing...")
127 out, err := cmd.CombinedOutput()
128 if err != nil {
129 return fmt.Errorf("Error building world: %v, %s", err, string(out))
130 }
131 if testing.Verbose() {
132 log.Printf("%s\n", out)
133 }
134 log.Print("Ran make.go.")
135 }
136 return nil
137 }
138
139
140 func (w *World) Help() ([]byte, error) {
141 if err := w.Build(); err != nil {
142 return nil, err
143 }
144 pkdbin := w.lookPathGobin("perkeepd")
145
146 cmd := exec.Command(pkdbin, "-help")
147 return cmd.CombinedOutput()
148 }
149
150
151 func (w *World) Start() error {
152 if err := w.Build(); err != nil {
153 return err
154 }
155
156 {
157 pkdbin := w.lookPathGobin("perkeepd")
158 w.server = exec.Command(
159 pkdbin,
160 "--openbrowser=false",
161 "--configfile="+filepath.Join(w.srcRoot, "pkg", "test", "testdata", w.config),
162 "--pollparent=true",
163 "--listen=127.0.0.1:0",
164 )
165 var buf bytes.Buffer
166 if testing.Verbose() {
167 w.server.Stdout = os.Stdout
168 w.server.Stderr = os.Stderr
169 } else {
170 w.server.Stdout = &buf
171 w.server.Stderr = &buf
172 }
173
174 getPortListener, err := net.Listen("tcp", "127.0.0.1:0")
175 if err != nil {
176 return err
177 }
178 defer getPortListener.Close()
179
180 w.server.Dir = w.tempDir
181 w.server.Env = append(os.Environ(),
182
183 "CAMLI_MORE_FLAGS=1",
184 "CAMLI_ROOT="+w.tempDir,
185 "CAMLI_SECRET_RING="+filepath.Join(w.srcRoot, filepath.FromSlash("pkg/jsonsign/testdata/test-secring.gpg")),
186 "CAMLI_BASE_URL=http://127.0.0.0:tbd",
187 "CAMLI_SET_BASE_URL_AND_SEND_ADDR_TO="+getPortListener.Addr().String(),
188 )
189
190 if err := w.server.Start(); err != nil {
191 w.serverErr = fmt.Errorf("starting perkeepd: %v", err)
192 return w.serverErr
193 }
194
195 atomic.StoreInt32(&w.isRunning, 1)
196 waitc := make(chan error, 1)
197 go func() {
198 err := w.server.Wait()
199 w.serverErr = fmt.Errorf("%v: %s", err, buf.String())
200 atomic.StoreInt32(&w.isRunning, 0)
201 waitc <- w.serverErr
202 }()
203 upc := make(chan bool)
204 upErr := make(chan error, 1)
205 go func() {
206 c, err := getPortListener.Accept()
207 if err != nil {
208 upErr <- fmt.Errorf("waiting for child to report its port: %v", err)
209 return
210 }
211 defer c.Close()
212 br := bufio.NewReader(c)
213 addr, err := br.ReadString('\n')
214 if err != nil {
215 upErr <- fmt.Errorf("ReadString: %v", err)
216 return
217 }
218 w.addr = strings.TrimSpace(addr)
219
220 for i := 0; i < 100; i++ {
221 res, err := http.Get("http://" + w.addr)
222 if err == nil {
223 res.Body.Close()
224 upc <- true
225 return
226 }
227 time.Sleep(50 * time.Millisecond)
228 }
229 w.serverErr = errors.New(buf.String())
230 atomic.StoreInt32(&w.isRunning, 0)
231 upErr <- fmt.Errorf("server never became reachable: %v", w.serverErr)
232 }()
233
234 select {
235 case <-waitc:
236 return fmt.Errorf("server exited: %v", w.serverErr)
237 case err := <-upErr:
238 return err
239 case <-upc:
240 if err := w.Ping(); err != nil {
241 return err
242 }
243
244 }
245 }
246 return nil
247 }
248
249
250 func (w *World) Ping() error {
251 if atomic.LoadInt32(&w.isRunning) != 1 {
252 return fmt.Errorf("perkeepd not running: %v", w.serverErr)
253 }
254 return nil
255 }
256
257 func (w *World) Stop() {
258 if w == nil {
259 return
260 }
261 if err := w.server.Process.Kill(); err != nil {
262 log.Fatalf("killed failed: %v", err)
263 }
264
265 if d := w.tempDir; d != "" {
266 os.RemoveAll(d)
267 }
268 }
269
270 func (w *World) NewPermanode(t *testing.T) blob.Ref {
271 if err := w.Ping(); err != nil {
272 t.Fatal(err)
273 }
274 out := MustRunCmd(t, w.Cmd("pk-put", "permanode"))
275 br, ok := blob.Parse(strings.TrimSpace(out))
276 if !ok {
277 t.Fatalf("Expected permanode in pk-put stdout; got %q", out)
278 }
279 return br
280 }
281
282 func (w *World) PutFile(t *testing.T, name string) blob.Ref {
283 out := MustRunCmd(t, w.Cmd("pk-put", "file", name))
284 br, ok := blob.Parse(strings.TrimSpace(out))
285 if !ok {
286 t.Fatalf("Expected blobref in pk-put stdout; got %q", out)
287 }
288 return br
289 }
290
291 func (w *World) Cmd(binary string, args ...string) *exec.Cmd {
292 return w.CmdWithEnv(binary, os.Environ(), args...)
293 }
294
295 func (w *World) CmdWithEnv(binary string, env []string, args ...string) *exec.Cmd {
296 hasVerbose := func() bool {
297 for _, v := range args {
298 if v == "-verbose" || v == "--verbose" {
299 return true
300 }
301 }
302 return false
303 }
304 var cmd *exec.Cmd
305 switch binary {
306 case "pk-get", "pk-put", "pk", "pk-mount":
307
308 if binary == "pk-put" && !hasVerbose() {
309
310
311 args = append([]string{"-verbose"}, args...)
312 }
313 binary := w.lookPathGobin(binary)
314
315 cmd = exec.Command(binary, args...)
316 clientConfigDir := filepath.Join(w.srcRoot, "config", "dev-client-dir")
317 cmd.Env = append(env,
318 "CAMLI_CONFIG_DIR="+clientConfigDir,
319
320 "CAMLI_SERVER="+w.ServerBaseURL(),
321 "CAMLI_SECRET_RING="+w.SecretRingFile(),
322 "CAMLI_KEYID="+w.ClientIdentity(),
323 "CAMLI_AUTH=userpass:testuser:passTestWorld",
324 )
325 default:
326 panic("Unknown binary " + binary)
327 }
328 return cmd
329 }
330
331 func (w *World) ServerBaseURL() string {
332 return fmt.Sprintf("http://" + w.addr)
333 }
334
335 var theWorld *World
336
337
338
339 func GetWorld(t *testing.T) *World {
340 w := theWorld
341 if w == nil {
342 var err error
343 w, err = NewWorld()
344 if err != nil {
345 t.Fatalf("Error finding test world: %v", err)
346 }
347 err = w.Start()
348 if err != nil {
349 t.Fatalf("Error starting test world: %v", err)
350 }
351 theWorld = w
352 }
353 return w
354 }
355
356
357 func GetWorldMaybe(t *testing.T) *World {
358 return theWorld
359 }
360
361
362
363
364
365
366 func RunCmd(c *exec.Cmd) (output string, err error) {
367 var stdout, stderr bytes.Buffer
368 if testing.Verbose() {
369 c.Stderr = io.MultiWriter(os.Stderr, &stderr)
370 c.Stdout = io.MultiWriter(os.Stdout, &stdout)
371 } else {
372 c.Stderr = &stderr
373 c.Stdout = &stdout
374 }
375 err = c.Run()
376 if err != nil {
377 return "", fmt.Errorf("Error running command %+v: Stdout:\n%s\nStderr:\n%s\n", c, stdout.String(), stderr.String())
378 }
379 return stdout.String(), nil
380 }
381
382
383 func MustRunCmd(t testing.TB, c *exec.Cmd) string {
384 out, err := RunCmd(c)
385 if err != nil {
386 t.Fatal(err)
387 }
388 return out
389 }
390
391
392
393 func (w *World) ClientIdentity() string {
394 return "26F5ABDA"
395 }
396
397
398
399 func (w *World) SecretRingFile() string {
400 return filepath.Join(w.srcRoot, "pkg", "jsonsign", "testdata", "test-secring.gpg")
401 }
402
403
404 func (w *World) SearchHandlerPath() string { return "/my-search/" }
405
406
407 func (w *World) ServerBinary() string {
408 return w.lookPathGobin("perkeepd")
409 }
410
411 func (w *World) lookPathGobin(binName string) string {
412 if runtime.GOOS == "windows" && !strings.HasSuffix(binName, ".exe") {
413 return filepath.Join(w.gobin, binName+".exe")
414 }
415 return filepath.Join(w.gobin, binName)
416 }