10194: Record git version in package
[arvados.git] / services / crunch-run / crunchrun_test.go
1 package main
2
3 import (
4         "bytes"
5         "crypto/md5"
6         "encoding/json"
7         "errors"
8         "fmt"
9         "git.curoverse.com/arvados.git/sdk/go/arvados"
10         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
11         "git.curoverse.com/arvados.git/sdk/go/keepclient"
12         "git.curoverse.com/arvados.git/sdk/go/manifest"
13         "github.com/curoverse/dockerclient"
14         . "gopkg.in/check.v1"
15         "io"
16         "io/ioutil"
17         "os"
18         "os/exec"
19         "path/filepath"
20         "sort"
21         "strings"
22         "sync"
23         "syscall"
24         "testing"
25         "time"
26 )
27
28 // Gocheck boilerplate
29 func TestCrunchExec(t *testing.T) {
30         TestingT(t)
31 }
32
33 type TestSuite struct{}
34
35 // Gocheck boilerplate
36 var _ = Suite(&TestSuite{})
37
38 type ArvTestClient struct {
39         Total   int64
40         Calls   int
41         Content []arvadosclient.Dict
42         arvados.Container
43         Logs          map[string]*bytes.Buffer
44         WasSetRunning bool
45         sync.Mutex
46 }
47
48 type KeepTestClient struct {
49         Called  bool
50         Content []byte
51 }
52
53 var hwManifest = ". 82ab40c24fc8df01798e57ba66795bb1+841216+Aa124ac75e5168396c73c0a18eda641a4f41791c0@569fa8c3 0:841216:9c31ee32b3d15268a0754e8edc74d4f815ee014b693bc5109058e431dd5caea7.tar\n"
54 var hwPDH = "a45557269dcb65a6b78f9ac061c0850b+120"
55 var hwImageId = "9c31ee32b3d15268a0754e8edc74d4f815ee014b693bc5109058e431dd5caea7"
56
57 var otherManifest = ". 68a84f561b1d1708c6baff5e019a9ab3+46+Ae5d0af96944a3690becb1decdf60cc1c937f556d@5693216f 0:46:md5sum.txt\n"
58 var otherPDH = "a3e8f74c6f101eae01fa08bfb4e49b3a+54"
59
60 var fakeAuthUUID = "zzzzz-gj3su-55pqoyepgi2glem"
61 var fakeAuthToken = "a3ltuwzqcu2u4sc0q7yhpc2w7s00fdcqecg5d6e0u3pfohmbjt"
62
63 type TestDockerClient struct {
64         imageLoaded string
65         logReader   io.ReadCloser
66         logWriter   io.WriteCloser
67         fn          func(t *TestDockerClient)
68         finish      chan dockerclient.WaitResult
69         stop        chan bool
70         cwd         string
71         env         []string
72 }
73
74 func NewTestDockerClient() *TestDockerClient {
75         t := &TestDockerClient{}
76         t.logReader, t.logWriter = io.Pipe()
77         t.finish = make(chan dockerclient.WaitResult)
78         t.stop = make(chan bool)
79         t.cwd = "/"
80         return t
81 }
82
83 func (t *TestDockerClient) StopContainer(id string, timeout int) error {
84         t.stop <- true
85         return nil
86 }
87
88 func (t *TestDockerClient) InspectImage(id string) (*dockerclient.ImageInfo, error) {
89         if t.imageLoaded == id {
90                 return &dockerclient.ImageInfo{}, nil
91         } else {
92                 return nil, errors.New("")
93         }
94 }
95
96 func (t *TestDockerClient) LoadImage(reader io.Reader) error {
97         _, err := io.Copy(ioutil.Discard, reader)
98         if err != nil {
99                 return err
100         } else {
101                 t.imageLoaded = hwImageId
102                 return nil
103         }
104 }
105
106 func (t *TestDockerClient) CreateContainer(config *dockerclient.ContainerConfig, name string, authConfig *dockerclient.AuthConfig) (string, error) {
107         if config.WorkingDir != "" {
108                 t.cwd = config.WorkingDir
109         }
110         t.env = config.Env
111         return "abcde", nil
112 }
113
114 func (t *TestDockerClient) StartContainer(id string, config *dockerclient.HostConfig) error {
115         if id == "abcde" {
116                 go t.fn(t)
117                 return nil
118         } else {
119                 return errors.New("Invalid container id")
120         }
121 }
122
123 func (t *TestDockerClient) AttachContainer(id string, options *dockerclient.AttachOptions) (io.ReadCloser, error) {
124         return t.logReader, nil
125 }
126
127 func (t *TestDockerClient) Wait(id string) <-chan dockerclient.WaitResult {
128         return t.finish
129 }
130
131 func (*TestDockerClient) RemoveImage(name string, force bool) ([]*dockerclient.ImageDelete, error) {
132         return nil, nil
133 }
134
135 func (client *ArvTestClient) Create(resourceType string,
136         parameters arvadosclient.Dict,
137         output interface{}) error {
138
139         client.Mutex.Lock()
140         defer client.Mutex.Unlock()
141
142         client.Calls++
143         client.Content = append(client.Content, parameters)
144
145         if resourceType == "logs" {
146                 et := parameters["log"].(arvadosclient.Dict)["event_type"].(string)
147                 if client.Logs == nil {
148                         client.Logs = make(map[string]*bytes.Buffer)
149                 }
150                 if client.Logs[et] == nil {
151                         client.Logs[et] = &bytes.Buffer{}
152                 }
153                 client.Logs[et].Write([]byte(parameters["log"].(arvadosclient.Dict)["properties"].(map[string]string)["text"]))
154         }
155
156         if resourceType == "collections" && output != nil {
157                 mt := parameters["collection"].(arvadosclient.Dict)["manifest_text"].(string)
158                 outmap := output.(*arvados.Collection)
159                 outmap.PortableDataHash = fmt.Sprintf("%x+%d", md5.Sum([]byte(mt)), len(mt))
160         }
161
162         return nil
163 }
164
165 func (client *ArvTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
166         switch {
167         case method == "GET" && resourceType == "containers" && action == "auth":
168                 return json.Unmarshal([]byte(`{
169                         "kind": "arvados#api_client_authorization",
170                         "uuid": "`+fakeAuthUUID+`",
171                         "api_token": "`+fakeAuthToken+`"
172                         }`), output)
173         default:
174                 return fmt.Errorf("Not found")
175         }
176 }
177
178 func (client *ArvTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
179         if resourceType == "collections" {
180                 if uuid == hwPDH {
181                         output.(*arvados.Collection).ManifestText = hwManifest
182                 } else if uuid == otherPDH {
183                         output.(*arvados.Collection).ManifestText = otherManifest
184                 }
185         }
186         if resourceType == "containers" {
187                 (*output.(*arvados.Container)) = client.Container
188         }
189         return nil
190 }
191
192 func (client *ArvTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
193         client.Mutex.Lock()
194         defer client.Mutex.Unlock()
195         client.Calls++
196         client.Content = append(client.Content, parameters)
197         if resourceType == "containers" {
198                 if parameters["container"].(arvadosclient.Dict)["state"] == "Running" {
199                         client.WasSetRunning = true
200                 }
201         }
202         return nil
203 }
204
205 var discoveryMap = map[string]interface{}{"defaultTrashLifetime": float64(1209600)}
206
207 func (client *ArvTestClient) Discovery(key string) (interface{}, error) {
208         return discoveryMap[key], nil
209 }
210
211 // CalledWith returns the parameters from the first API call whose
212 // parameters match jpath/string. E.g., CalledWith(c, "foo.bar",
213 // "baz") returns parameters with parameters["foo"]["bar"]=="baz". If
214 // no call matches, it returns nil.
215 func (client *ArvTestClient) CalledWith(jpath string, expect interface{}) arvadosclient.Dict {
216 call:
217         for _, content := range client.Content {
218                 var v interface{} = content
219                 for _, k := range strings.Split(jpath, ".") {
220                         if dict, ok := v.(arvadosclient.Dict); !ok {
221                                 continue call
222                         } else {
223                                 v = dict[k]
224                         }
225                 }
226                 if v == expect {
227                         return content
228                 }
229         }
230         return nil
231 }
232
233 func (client *KeepTestClient) PutHB(hash string, buf []byte) (string, int, error) {
234         client.Content = buf
235         return fmt.Sprintf("%s+%d", hash, len(buf)), len(buf), nil
236 }
237
238 type FileWrapper struct {
239         io.ReadCloser
240         len uint64
241 }
242
243 func (fw FileWrapper) Len() uint64 {
244         return fw.len
245 }
246
247 func (client *KeepTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
248         if filename == hwImageId+".tar" {
249                 rdr := ioutil.NopCloser(&bytes.Buffer{})
250                 client.Called = true
251                 return FileWrapper{rdr, 1321984}, nil
252         }
253         return nil, nil
254 }
255
256 func (s *TestSuite) TestLoadImage(c *C) {
257         kc := &KeepTestClient{}
258         docker := NewTestDockerClient()
259         cr := NewContainerRunner(&ArvTestClient{}, kc, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
260
261         _, err := cr.Docker.RemoveImage(hwImageId, true)
262
263         _, err = cr.Docker.InspectImage(hwImageId)
264         c.Check(err, NotNil)
265
266         cr.Container.ContainerImage = hwPDH
267
268         // (1) Test loading image from keep
269         c.Check(kc.Called, Equals, false)
270         c.Check(cr.ContainerConfig.Image, Equals, "")
271
272         err = cr.LoadImage()
273
274         c.Check(err, IsNil)
275         defer func() {
276                 cr.Docker.RemoveImage(hwImageId, true)
277         }()
278
279         c.Check(kc.Called, Equals, true)
280         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
281
282         _, err = cr.Docker.InspectImage(hwImageId)
283         c.Check(err, IsNil)
284
285         // (2) Test using image that's already loaded
286         kc.Called = false
287         cr.ContainerConfig.Image = ""
288
289         err = cr.LoadImage()
290         c.Check(err, IsNil)
291         c.Check(kc.Called, Equals, false)
292         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
293
294 }
295
296 type ArvErrorTestClient struct{}
297
298 func (ArvErrorTestClient) Create(resourceType string,
299         parameters arvadosclient.Dict,
300         output interface{}) error {
301         return nil
302 }
303
304 func (ArvErrorTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
305         return errors.New("ArvError")
306 }
307
308 func (ArvErrorTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
309         return errors.New("ArvError")
310 }
311
312 func (ArvErrorTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
313         return nil
314 }
315
316 func (ArvErrorTestClient) Discovery(key string) (interface{}, error) {
317         return discoveryMap[key], nil
318 }
319
320 type KeepErrorTestClient struct{}
321
322 func (KeepErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
323         return "", 0, errors.New("KeepError")
324 }
325
326 func (KeepErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
327         return nil, errors.New("KeepError")
328 }
329
330 type KeepReadErrorTestClient struct{}
331
332 func (KeepReadErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
333         return "", 0, nil
334 }
335
336 type ErrorReader struct{}
337
338 func (ErrorReader) Read(p []byte) (n int, err error) {
339         return 0, errors.New("ErrorReader")
340 }
341
342 func (ErrorReader) Close() error {
343         return nil
344 }
345
346 func (ErrorReader) Len() uint64 {
347         return 0
348 }
349
350 func (KeepReadErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
351         return ErrorReader{}, nil
352 }
353
354 func (s *TestSuite) TestLoadImageArvError(c *C) {
355         // (1) Arvados error
356         cr := NewContainerRunner(ArvErrorTestClient{}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
357         cr.Container.ContainerImage = hwPDH
358
359         err := cr.LoadImage()
360         c.Check(err.Error(), Equals, "While getting container image collection: ArvError")
361 }
362
363 func (s *TestSuite) TestLoadImageKeepError(c *C) {
364         // (2) Keep error
365         docker := NewTestDockerClient()
366         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
367         cr.Container.ContainerImage = hwPDH
368
369         err := cr.LoadImage()
370         c.Check(err.Error(), Equals, "While creating ManifestFileReader for container image: KeepError")
371 }
372
373 func (s *TestSuite) TestLoadImageCollectionError(c *C) {
374         // (3) Collection doesn't contain image
375         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
376         cr.Container.ContainerImage = otherPDH
377
378         err := cr.LoadImage()
379         c.Check(err.Error(), Equals, "First file in the container image collection does not end in .tar")
380 }
381
382 func (s *TestSuite) TestLoadImageKeepReadError(c *C) {
383         // (4) Collection doesn't contain image
384         docker := NewTestDockerClient()
385         cr := NewContainerRunner(&ArvTestClient{}, KeepReadErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
386         cr.Container.ContainerImage = hwPDH
387
388         err := cr.LoadImage()
389         c.Check(err, NotNil)
390 }
391
392 type ClosableBuffer struct {
393         bytes.Buffer
394 }
395
396 func (*ClosableBuffer) Close() error {
397         return nil
398 }
399
400 type TestLogs struct {
401         Stdout ClosableBuffer
402         Stderr ClosableBuffer
403 }
404
405 func (tl *TestLogs) NewTestLoggingWriter(logstr string) io.WriteCloser {
406         if logstr == "stdout" {
407                 return &tl.Stdout
408         }
409         if logstr == "stderr" {
410                 return &tl.Stderr
411         }
412         return nil
413 }
414
415 func dockerLog(fd byte, msg string) []byte {
416         by := []byte(msg)
417         header := make([]byte, 8+len(by))
418         header[0] = fd
419         header[7] = byte(len(by))
420         copy(header[8:], by)
421         return header
422 }
423
424 func (s *TestSuite) TestRunContainer(c *C) {
425         docker := NewTestDockerClient()
426         docker.fn = func(t *TestDockerClient) {
427                 t.logWriter.Write(dockerLog(1, "Hello world\n"))
428                 t.logWriter.Close()
429                 t.finish <- dockerclient.WaitResult{}
430         }
431         cr := NewContainerRunner(&ArvTestClient{}, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
432
433         var logs TestLogs
434         cr.NewLogWriter = logs.NewTestLoggingWriter
435         cr.Container.ContainerImage = hwPDH
436         cr.Container.Command = []string{"./hw"}
437         err := cr.LoadImage()
438         c.Check(err, IsNil)
439
440         err = cr.CreateContainer()
441         c.Check(err, IsNil)
442
443         err = cr.StartContainer()
444         c.Check(err, IsNil)
445
446         err = cr.WaitFinish()
447         c.Check(err, IsNil)
448
449         c.Check(strings.HasSuffix(logs.Stdout.String(), "Hello world\n"), Equals, true)
450         c.Check(logs.Stderr.String(), Equals, "")
451 }
452
453 func (s *TestSuite) TestCommitLogs(c *C) {
454         api := &ArvTestClient{}
455         kc := &KeepTestClient{}
456         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
457         cr.CrunchLog.Timestamper = (&TestTimestamper{}).Timestamp
458
459         cr.CrunchLog.Print("Hello world!")
460         cr.CrunchLog.Print("Goodbye")
461         cr.finalState = "Complete"
462
463         err := cr.CommitLogs()
464         c.Check(err, IsNil)
465
466         c.Check(api.Calls, Equals, 2)
467         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["name"], Equals, "logs for zzzzz-zzzzz-zzzzzzzzzzzzzzz")
468         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["manifest_text"], Equals, ". 744b2e4553123b02fa7b452ec5c18993+123 0:123:crunch-run.txt\n")
469         c.Check(*cr.LogsPDH, Equals, "63da7bdacf08c40f604daad80c261e9a+60")
470 }
471
472 func (s *TestSuite) TestUpdateContainerRunning(c *C) {
473         api := &ArvTestClient{}
474         kc := &KeepTestClient{}
475         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
476
477         err := cr.UpdateContainerRunning()
478         c.Check(err, IsNil)
479
480         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Running")
481 }
482
483 func (s *TestSuite) TestUpdateContainerComplete(c *C) {
484         api := &ArvTestClient{}
485         kc := &KeepTestClient{}
486         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
487
488         cr.LogsPDH = new(string)
489         *cr.LogsPDH = "d3a229d2fe3690c2c3e75a71a153c6a3+60"
490
491         cr.ExitCode = new(int)
492         *cr.ExitCode = 42
493         cr.finalState = "Complete"
494
495         err := cr.UpdateContainerFinal()
496         c.Check(err, IsNil)
497
498         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], Equals, *cr.LogsPDH)
499         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], Equals, *cr.ExitCode)
500         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
501 }
502
503 func (s *TestSuite) TestUpdateContainerCancelled(c *C) {
504         api := &ArvTestClient{}
505         kc := &KeepTestClient{}
506         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
507         cr.Cancelled = true
508         cr.finalState = "Cancelled"
509
510         err := cr.UpdateContainerFinal()
511         c.Check(err, IsNil)
512
513         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], IsNil)
514         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], IsNil)
515         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Cancelled")
516 }
517
518 // Used by the TestFullRun*() test below to DRY up boilerplate setup to do full
519 // dress rehearsal of the Run() function, starting from a JSON container record.
520 func FullRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner) {
521         rec := arvados.Container{}
522         err := json.Unmarshal([]byte(record), &rec)
523         c.Check(err, IsNil)
524
525         docker := NewTestDockerClient()
526         docker.fn = fn
527         docker.RemoveImage(hwImageId, true)
528
529         api = &ArvTestClient{Container: rec}
530         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
531         cr.statInterval = 100 * time.Millisecond
532         am := &ArvMountCmdLine{}
533         cr.RunArvMount = am.ArvMountTest
534
535         err = cr.Run()
536         c.Check(err, IsNil)
537         c.Check(api.WasSetRunning, Equals, true)
538
539         c.Check(api.Content[api.Calls-1]["container"].(arvadosclient.Dict)["log"], NotNil)
540
541         if err != nil {
542                 for k, v := range api.Logs {
543                         c.Log(k)
544                         c.Log(v.String())
545                 }
546         }
547
548         return
549 }
550
551 func (s *TestSuite) TestFullRunHello(c *C) {
552         api, _ := FullRunHelper(c, `{
553     "command": ["echo", "hello world"],
554     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
555     "cwd": ".",
556     "environment": {},
557     "mounts": {"/tmp": {"kind": "tmp"} },
558     "output_path": "/tmp",
559     "priority": 1,
560     "runtime_constraints": {}
561 }`, func(t *TestDockerClient) {
562                 t.logWriter.Write(dockerLog(1, "hello world\n"))
563                 t.logWriter.Close()
564                 t.finish <- dockerclient.WaitResult{}
565         })
566
567         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
568         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
569         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello world\n"), Equals, true)
570
571 }
572
573 func (s *TestSuite) TestCrunchstat(c *C) {
574         api, _ := FullRunHelper(c, `{
575                 "command": ["sleep", "1"],
576                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
577                 "cwd": ".",
578                 "environment": {},
579                 "mounts": {"/tmp": {"kind": "tmp"} },
580                 "output_path": "/tmp",
581                 "priority": 1,
582                 "runtime_constraints": {}
583         }`, func(t *TestDockerClient) {
584                 time.Sleep(time.Second)
585                 t.logWriter.Close()
586                 t.finish <- dockerclient.WaitResult{}
587         })
588
589         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
590         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
591
592         // We didn't actually start a container, so crunchstat didn't
593         // find accounting files and therefore didn't log any stats.
594         // It should have logged a "can't find accounting files"
595         // message after one poll interval, though, so we can confirm
596         // it's alive:
597         c.Assert(api.Logs["crunchstat"], NotNil)
598         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files have not appeared after 100ms.*`)
599
600         // The "files never appeared" log assures us that we called
601         // (*crunchstat.Reporter)Stop(), and that we set it up with
602         // the correct container ID "abcde":
603         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files never appeared for abcde\n`)
604 }
605
606 func (s *TestSuite) TestFullRunStderr(c *C) {
607         api, _ := FullRunHelper(c, `{
608     "command": ["/bin/sh", "-c", "echo hello ; echo world 1>&2 ; exit 1"],
609     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
610     "cwd": ".",
611     "environment": {},
612     "mounts": {"/tmp": {"kind": "tmp"} },
613     "output_path": "/tmp",
614     "priority": 1,
615     "runtime_constraints": {}
616 }`, func(t *TestDockerClient) {
617                 t.logWriter.Write(dockerLog(1, "hello\n"))
618                 t.logWriter.Write(dockerLog(2, "world\n"))
619                 t.logWriter.Close()
620                 t.finish <- dockerclient.WaitResult{ExitCode: 1}
621         })
622
623         final := api.CalledWith("container.state", "Complete")
624         c.Assert(final, NotNil)
625         c.Check(final["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
626         c.Check(final["container"].(arvadosclient.Dict)["log"], NotNil)
627
628         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello\n"), Equals, true)
629         c.Check(strings.HasSuffix(api.Logs["stderr"].String(), "world\n"), Equals, true)
630 }
631
632 func (s *TestSuite) TestFullRunDefaultCwd(c *C) {
633         api, _ := FullRunHelper(c, `{
634     "command": ["pwd"],
635     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
636     "cwd": ".",
637     "environment": {},
638     "mounts": {"/tmp": {"kind": "tmp"} },
639     "output_path": "/tmp",
640     "priority": 1,
641     "runtime_constraints": {}
642 }`, func(t *TestDockerClient) {
643                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
644                 t.logWriter.Close()
645                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
646         })
647
648         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
649         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
650         c.Log(api.Logs["stdout"])
651         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/\n"), Equals, true)
652 }
653
654 func (s *TestSuite) TestFullRunSetCwd(c *C) {
655         api, _ := FullRunHelper(c, `{
656     "command": ["pwd"],
657     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
658     "cwd": "/bin",
659     "environment": {},
660     "mounts": {"/tmp": {"kind": "tmp"} },
661     "output_path": "/tmp",
662     "priority": 1,
663     "runtime_constraints": {}
664 }`, func(t *TestDockerClient) {
665                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
666                 t.logWriter.Close()
667                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
668         })
669
670         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
671         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
672         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/bin\n"), Equals, true)
673 }
674
675 func (s *TestSuite) TestCancel(c *C) {
676         record := `{
677     "command": ["/bin/sh", "-c", "echo foo && sleep 30 && echo bar"],
678     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
679     "cwd": ".",
680     "environment": {},
681     "mounts": {"/tmp": {"kind": "tmp"} },
682     "output_path": "/tmp",
683     "priority": 1,
684     "runtime_constraints": {}
685 }`
686
687         rec := arvados.Container{}
688         err := json.Unmarshal([]byte(record), &rec)
689         c.Check(err, IsNil)
690
691         docker := NewTestDockerClient()
692         docker.fn = func(t *TestDockerClient) {
693                 <-t.stop
694                 t.logWriter.Write(dockerLog(1, "foo\n"))
695                 t.logWriter.Close()
696                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
697         }
698         docker.RemoveImage(hwImageId, true)
699
700         api := &ArvTestClient{Container: rec}
701         cr := NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
702         am := &ArvMountCmdLine{}
703         cr.RunArvMount = am.ArvMountTest
704
705         go func() {
706                 for cr.ContainerID == "" {
707                         time.Sleep(time.Millisecond)
708                 }
709                 cr.SigChan <- syscall.SIGINT
710         }()
711
712         err = cr.Run()
713
714         c.Check(err, IsNil)
715         if err != nil {
716                 for k, v := range api.Logs {
717                         c.Log(k)
718                         c.Log(v.String())
719                 }
720         }
721
722         c.Check(api.CalledWith("container.log", nil), NotNil)
723         c.Check(api.CalledWith("container.state", "Cancelled"), NotNil)
724         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "foo\n"), Equals, true)
725
726 }
727
728 func (s *TestSuite) TestFullRunSetEnv(c *C) {
729         api, _ := FullRunHelper(c, `{
730     "command": ["/bin/sh", "-c", "echo $FROBIZ"],
731     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
732     "cwd": "/bin",
733     "environment": {"FROBIZ": "bilbo"},
734     "mounts": {"/tmp": {"kind": "tmp"} },
735     "output_path": "/tmp",
736     "priority": 1,
737     "runtime_constraints": {}
738 }`, func(t *TestDockerClient) {
739                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
740                 t.logWriter.Close()
741                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
742         })
743
744         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
745         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
746         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "bilbo\n"), Equals, true)
747 }
748
749 type ArvMountCmdLine struct {
750         Cmd   []string
751         token string
752 }
753
754 func (am *ArvMountCmdLine) ArvMountTest(c []string, token string) (*exec.Cmd, error) {
755         am.Cmd = c
756         am.token = token
757         return nil, nil
758 }
759
760 func (s *TestSuite) TestSetupMounts(c *C) {
761         api := &ArvTestClient{}
762         kc := &KeepTestClient{}
763         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
764         am := &ArvMountCmdLine{}
765         cr.RunArvMount = am.ArvMountTest
766
767         realTemp, err := ioutil.TempDir("", "crunchrun_test-")
768         c.Assert(err, IsNil)
769         defer os.RemoveAll(realTemp)
770
771         i := 0
772         cr.MkTempDir = func(_ string, prefix string) (string, error) {
773                 i++
774                 d := fmt.Sprintf("%s/%s%d", realTemp, prefix, i)
775                 err := os.Mkdir(d, os.ModePerm)
776                 if err != nil && strings.Contains(err.Error(), ": file exists") {
777                         // Test case must have pre-populated the tempdir
778                         err = nil
779                 }
780                 return d, err
781         }
782
783         checkEmpty := func() {
784                 filepath.Walk(realTemp, func(path string, _ os.FileInfo, err error) error {
785                         c.Check(path, Equals, realTemp)
786                         c.Check(err, IsNil)
787                         return nil
788                 })
789         }
790
791         {
792                 i = 0
793                 cr.Container.Mounts = make(map[string]arvados.Mount)
794                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
795                 cr.OutputPath = "/tmp"
796
797                 err := cr.SetupMounts()
798                 c.Check(err, IsNil)
799                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
800                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp"})
801                 cr.CleanupDirs()
802                 checkEmpty()
803         }
804
805         {
806                 i = 0
807                 cr.Container.Mounts = map[string]arvados.Mount{
808                         "/keeptmp": {Kind: "collection", Writable: true},
809                 }
810                 cr.OutputPath = "/keeptmp"
811
812                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
813
814                 err := cr.SetupMounts()
815                 c.Check(err, IsNil)
816                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
817                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/tmp0:/keeptmp"})
818                 cr.CleanupDirs()
819                 checkEmpty()
820         }
821
822         {
823                 i = 0
824                 cr.Container.Mounts = map[string]arvados.Mount{
825                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
826                         "/keepout": {Kind: "collection", Writable: true},
827                 }
828                 cr.OutputPath = "/keepout"
829
830                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
831                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
832
833                 err := cr.SetupMounts()
834                 c.Check(err, IsNil)
835                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
836                 sort.StringSlice(cr.Binds).Sort()
837                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
838                         realTemp + "/keep1/tmp0:/keepout"})
839                 cr.CleanupDirs()
840                 checkEmpty()
841         }
842
843         for _, test := range []struct {
844                 in  interface{}
845                 out string
846         }{
847                 {in: "foo", out: `"foo"`},
848                 {in: nil, out: `null`},
849                 {in: map[string]int{"foo": 123}, out: `{"foo":123}`},
850         } {
851                 i = 0
852                 cr.Container.Mounts = map[string]arvados.Mount{
853                         "/mnt/test.json": {Kind: "json", Content: test.in},
854                 }
855                 err := cr.SetupMounts()
856                 c.Check(err, IsNil)
857                 sort.StringSlice(cr.Binds).Sort()
858                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2/mountdata.json:/mnt/test.json:ro"})
859                 content, err := ioutil.ReadFile(realTemp + "/2/mountdata.json")
860                 c.Check(err, IsNil)
861                 c.Check(content, DeepEquals, []byte(test.out))
862                 cr.CleanupDirs()
863                 checkEmpty()
864         }
865 }
866
867 func (s *TestSuite) TestStdout(c *C) {
868         helperRecord := `{
869                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
870                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
871                 "cwd": "/bin",
872                 "environment": {"FROBIZ": "bilbo"},
873                 "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"} },
874                 "output_path": "/tmp",
875                 "priority": 1,
876                 "runtime_constraints": {}
877         }`
878
879         api, _ := FullRunHelper(c, helperRecord, func(t *TestDockerClient) {
880                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
881                 t.logWriter.Close()
882                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
883         })
884
885         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
886         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
887         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
888 }
889
890 // Used by the TestStdoutWithWrongPath*()
891 func StdoutErrorRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, err error) {
892         rec := arvados.Container{}
893         err = json.Unmarshal([]byte(record), &rec)
894         c.Check(err, IsNil)
895
896         docker := NewTestDockerClient()
897         docker.fn = fn
898         docker.RemoveImage(hwImageId, true)
899
900         api = &ArvTestClient{Container: rec}
901         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
902         am := &ArvMountCmdLine{}
903         cr.RunArvMount = am.ArvMountTest
904
905         err = cr.Run()
906         return
907 }
908
909 func (s *TestSuite) TestStdoutWithWrongPath(c *C) {
910         _, _, err := StdoutErrorRunHelper(c, `{
911     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path":"/tmpa.out"} },
912     "output_path": "/tmp"
913 }`, func(t *TestDockerClient) {})
914
915         c.Check(err, NotNil)
916         c.Check(strings.Contains(err.Error(), "Stdout path does not start with OutputPath"), Equals, true)
917 }
918
919 func (s *TestSuite) TestStdoutWithWrongKindTmp(c *C) {
920         _, _, err := StdoutErrorRunHelper(c, `{
921     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "tmp", "path":"/tmp/a.out"} },
922     "output_path": "/tmp"
923 }`, func(t *TestDockerClient) {})
924
925         c.Check(err, NotNil)
926         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'tmp' for stdout"), Equals, true)
927 }
928
929 func (s *TestSuite) TestStdoutWithWrongKindCollection(c *C) {
930         _, _, err := StdoutErrorRunHelper(c, `{
931     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "collection", "path":"/tmp/a.out"} },
932     "output_path": "/tmp"
933 }`, func(t *TestDockerClient) {})
934
935         c.Check(err, NotNil)
936         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'collection' for stdout"), Equals, true)
937 }