Merge branch 'master' into 9372-container-display
[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         "log"
18         "os"
19         "os/exec"
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 += 1
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 += 1
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 // CalledWith returns the parameters from the first API call whose
206 // parameters match jpath/string. E.g., CalledWith(c, "foo.bar",
207 // "baz") returns parameters with parameters["foo"]["bar"]=="baz". If
208 // no call matches, it returns nil.
209 func (client *ArvTestClient) CalledWith(jpath, expect string) arvadosclient.Dict {
210 call:
211         for _, content := range client.Content {
212                 var v interface{} = content
213                 for _, k := range strings.Split(jpath, ".") {
214                         if dict, ok := v.(arvadosclient.Dict); !ok {
215                                 continue call
216                         } else {
217                                 v = dict[k]
218                         }
219                 }
220                 if v, ok := v.(string); ok && v == expect {
221                         return content
222                 }
223         }
224         return nil
225 }
226
227 func (client *KeepTestClient) PutHB(hash string, buf []byte) (string, int, error) {
228         client.Content = buf
229         return fmt.Sprintf("%s+%d", hash, len(buf)), len(buf), nil
230 }
231
232 type FileWrapper struct {
233         io.ReadCloser
234         len uint64
235 }
236
237 func (fw FileWrapper) Len() uint64 {
238         return fw.len
239 }
240
241 func (client *KeepTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
242         if filename == hwImageId+".tar" {
243                 rdr := ioutil.NopCloser(&bytes.Buffer{})
244                 client.Called = true
245                 return FileWrapper{rdr, 1321984}, nil
246         }
247         return nil, nil
248 }
249
250 func (s *TestSuite) TestLoadImage(c *C) {
251         kc := &KeepTestClient{}
252         docker := NewTestDockerClient()
253         cr := NewContainerRunner(&ArvTestClient{}, kc, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
254
255         _, err := cr.Docker.RemoveImage(hwImageId, true)
256
257         _, err = cr.Docker.InspectImage(hwImageId)
258         c.Check(err, NotNil)
259
260         cr.Container.ContainerImage = hwPDH
261
262         // (1) Test loading image from keep
263         c.Check(kc.Called, Equals, false)
264         c.Check(cr.ContainerConfig.Image, Equals, "")
265
266         err = cr.LoadImage()
267
268         c.Check(err, IsNil)
269         defer func() {
270                 cr.Docker.RemoveImage(hwImageId, true)
271         }()
272
273         c.Check(kc.Called, Equals, true)
274         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
275
276         _, err = cr.Docker.InspectImage(hwImageId)
277         c.Check(err, IsNil)
278
279         // (2) Test using image that's already loaded
280         kc.Called = false
281         cr.ContainerConfig.Image = ""
282
283         err = cr.LoadImage()
284         c.Check(err, IsNil)
285         c.Check(kc.Called, Equals, false)
286         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
287
288 }
289
290 type ArvErrorTestClient struct{}
291
292 func (ArvErrorTestClient) Create(resourceType string,
293         parameters arvadosclient.Dict,
294         output interface{}) error {
295         return nil
296 }
297
298 func (ArvErrorTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
299         return errors.New("ArvError")
300 }
301
302 func (ArvErrorTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
303         return errors.New("ArvError")
304 }
305
306 func (ArvErrorTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
307         return nil
308 }
309
310 type KeepErrorTestClient struct{}
311
312 func (KeepErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
313         return "", 0, errors.New("KeepError")
314 }
315
316 func (KeepErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
317         return nil, errors.New("KeepError")
318 }
319
320 type KeepReadErrorTestClient struct{}
321
322 func (KeepReadErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
323         return "", 0, nil
324 }
325
326 type ErrorReader struct{}
327
328 func (ErrorReader) Read(p []byte) (n int, err error) {
329         return 0, errors.New("ErrorReader")
330 }
331
332 func (ErrorReader) Close() error {
333         return nil
334 }
335
336 func (ErrorReader) Len() uint64 {
337         return 0
338 }
339
340 func (KeepReadErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
341         return ErrorReader{}, nil
342 }
343
344 func (s *TestSuite) TestLoadImageArvError(c *C) {
345         // (1) Arvados error
346         cr := NewContainerRunner(ArvErrorTestClient{}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
347         cr.Container.ContainerImage = hwPDH
348
349         err := cr.LoadImage()
350         c.Check(err.Error(), Equals, "While getting container image collection: ArvError")
351 }
352
353 func (s *TestSuite) TestLoadImageKeepError(c *C) {
354         // (2) Keep error
355         docker := NewTestDockerClient()
356         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
357         cr.Container.ContainerImage = hwPDH
358
359         err := cr.LoadImage()
360         c.Check(err.Error(), Equals, "While creating ManifestFileReader for container image: KeepError")
361 }
362
363 func (s *TestSuite) TestLoadImageCollectionError(c *C) {
364         // (3) Collection doesn't contain image
365         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
366         cr.Container.ContainerImage = otherPDH
367
368         err := cr.LoadImage()
369         c.Check(err.Error(), Equals, "First file in the container image collection does not end in .tar")
370 }
371
372 func (s *TestSuite) TestLoadImageKeepReadError(c *C) {
373         // (4) Collection doesn't contain image
374         docker := NewTestDockerClient()
375         cr := NewContainerRunner(&ArvTestClient{}, KeepReadErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
376         cr.Container.ContainerImage = hwPDH
377
378         err := cr.LoadImage()
379         c.Check(err, NotNil)
380 }
381
382 type ClosableBuffer struct {
383         bytes.Buffer
384 }
385
386 func (*ClosableBuffer) Close() error {
387         return nil
388 }
389
390 type TestLogs struct {
391         Stdout ClosableBuffer
392         Stderr ClosableBuffer
393 }
394
395 func (tl *TestLogs) NewTestLoggingWriter(logstr string) io.WriteCloser {
396         if logstr == "stdout" {
397                 return &tl.Stdout
398         }
399         if logstr == "stderr" {
400                 return &tl.Stderr
401         }
402         return nil
403 }
404
405 func dockerLog(fd byte, msg string) []byte {
406         by := []byte(msg)
407         header := make([]byte, 8+len(by))
408         header[0] = fd
409         header[7] = byte(len(by))
410         copy(header[8:], by)
411         return header
412 }
413
414 func (s *TestSuite) TestRunContainer(c *C) {
415         docker := NewTestDockerClient()
416         docker.fn = func(t *TestDockerClient) {
417                 t.logWriter.Write(dockerLog(1, "Hello world\n"))
418                 t.logWriter.Close()
419                 t.finish <- dockerclient.WaitResult{}
420         }
421         cr := NewContainerRunner(&ArvTestClient{}, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
422
423         var logs TestLogs
424         cr.NewLogWriter = logs.NewTestLoggingWriter
425         cr.Container.ContainerImage = hwPDH
426         cr.Container.Command = []string{"./hw"}
427         err := cr.LoadImage()
428         c.Check(err, IsNil)
429
430         err = cr.CreateContainer()
431         c.Check(err, IsNil)
432
433         err = cr.StartContainer()
434         c.Check(err, IsNil)
435
436         err = cr.WaitFinish()
437         c.Check(err, IsNil)
438
439         c.Check(strings.HasSuffix(logs.Stdout.String(), "Hello world\n"), Equals, true)
440         c.Check(logs.Stderr.String(), Equals, "")
441 }
442
443 func (s *TestSuite) TestCommitLogs(c *C) {
444         api := &ArvTestClient{}
445         kc := &KeepTestClient{}
446         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
447         cr.CrunchLog.Timestamper = (&TestTimestamper{}).Timestamp
448
449         cr.CrunchLog.Print("Hello world!")
450         cr.CrunchLog.Print("Goodbye")
451         cr.finalState = "Complete"
452
453         err := cr.CommitLogs()
454         c.Check(err, IsNil)
455
456         c.Check(api.Calls, Equals, 2)
457         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["name"], Equals, "logs for zzzzz-zzzzz-zzzzzzzzzzzzzzz")
458         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["manifest_text"], Equals, ". 744b2e4553123b02fa7b452ec5c18993+123 0:123:crunch-run.txt\n")
459         c.Check(*cr.LogsPDH, Equals, "63da7bdacf08c40f604daad80c261e9a+60")
460 }
461
462 func (s *TestSuite) TestUpdateContainerRunning(c *C) {
463         api := &ArvTestClient{}
464         kc := &KeepTestClient{}
465         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
466
467         err := cr.UpdateContainerRunning()
468         c.Check(err, IsNil)
469
470         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Running")
471 }
472
473 func (s *TestSuite) TestUpdateContainerComplete(c *C) {
474         api := &ArvTestClient{}
475         kc := &KeepTestClient{}
476         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
477
478         cr.LogsPDH = new(string)
479         *cr.LogsPDH = "d3a229d2fe3690c2c3e75a71a153c6a3+60"
480
481         cr.ExitCode = new(int)
482         *cr.ExitCode = 42
483         cr.finalState = "Complete"
484
485         err := cr.UpdateContainerFinal()
486         c.Check(err, IsNil)
487
488         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], Equals, *cr.LogsPDH)
489         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], Equals, *cr.ExitCode)
490         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
491 }
492
493 func (s *TestSuite) TestUpdateContainerCancelled(c *C) {
494         api := &ArvTestClient{}
495         kc := &KeepTestClient{}
496         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
497         cr.Cancelled = true
498         cr.finalState = "Cancelled"
499
500         err := cr.UpdateContainerFinal()
501         c.Check(err, IsNil)
502
503         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], IsNil)
504         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], IsNil)
505         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Cancelled")
506 }
507
508 // Used by the TestFullRun*() test below to DRY up boilerplate setup to do full
509 // dress rehearsal of the Run() function, starting from a JSON container record.
510 func FullRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner) {
511         rec := arvados.Container{}
512         err := json.Unmarshal([]byte(record), &rec)
513         c.Check(err, IsNil)
514
515         docker := NewTestDockerClient()
516         docker.fn = fn
517         docker.RemoveImage(hwImageId, true)
518
519         api = &ArvTestClient{Container: rec}
520         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
521         am := &ArvMountCmdLine{}
522         cr.RunArvMount = am.ArvMountTest
523
524         err = cr.Run()
525         c.Check(err, IsNil)
526         c.Check(api.WasSetRunning, Equals, true)
527
528         c.Check(api.Content[api.Calls-1]["container"].(arvadosclient.Dict)["log"], NotNil)
529
530         if err != nil {
531                 for k, v := range api.Logs {
532                         c.Log(k)
533                         c.Log(v.String())
534                 }
535         }
536
537         return
538 }
539
540 func (s *TestSuite) TestFullRunHello(c *C) {
541         api, _ := FullRunHelper(c, `{
542     "command": ["echo", "hello world"],
543     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
544     "cwd": ".",
545     "environment": {},
546     "mounts": {"/tmp": {"kind": "tmp"} },
547     "output_path": "/tmp",
548     "priority": 1,
549     "runtime_constraints": {}
550 }`, func(t *TestDockerClient) {
551                 t.logWriter.Write(dockerLog(1, "hello world\n"))
552                 t.logWriter.Close()
553                 t.finish <- dockerclient.WaitResult{}
554         })
555
556         c.Check(api.Calls, Equals, 7)
557         c.Check(api.Content[6]["container"].(arvadosclient.Dict)["exit_code"], Equals, 0)
558         c.Check(api.Content[6]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
559
560         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello world\n"), Equals, true)
561
562 }
563
564 func (s *TestSuite) TestFullRunStderr(c *C) {
565         api, _ := FullRunHelper(c, `{
566     "command": ["/bin/sh", "-c", "echo hello ; echo world 1>&2 ; exit 1"],
567     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
568     "cwd": ".",
569     "environment": {},
570     "mounts": {"/tmp": {"kind": "tmp"} },
571     "output_path": "/tmp",
572     "priority": 1,
573     "runtime_constraints": {}
574 }`, func(t *TestDockerClient) {
575                 t.logWriter.Write(dockerLog(1, "hello\n"))
576                 t.logWriter.Write(dockerLog(2, "world\n"))
577                 t.logWriter.Close()
578                 t.finish <- dockerclient.WaitResult{ExitCode: 1}
579         })
580
581         c.Assert(api.Calls, Equals, 8)
582         c.Check(api.Content[7]["container"].(arvadosclient.Dict)["log"], NotNil)
583         c.Check(api.Content[7]["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
584         c.Check(api.Content[7]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
585
586         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello\n"), Equals, true)
587         c.Check(strings.HasSuffix(api.Logs["stderr"].String(), "world\n"), Equals, true)
588 }
589
590 func (s *TestSuite) TestFullRunDefaultCwd(c *C) {
591         api, _ := FullRunHelper(c, `{
592     "command": ["pwd"],
593     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
594     "cwd": ".",
595     "environment": {},
596     "mounts": {"/tmp": {"kind": "tmp"} },
597     "output_path": "/tmp",
598     "priority": 1,
599     "runtime_constraints": {}
600 }`, func(t *TestDockerClient) {
601                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
602                 t.logWriter.Close()
603                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
604         })
605
606         c.Check(api.Calls, Equals, 7)
607         c.Check(api.Content[6]["container"].(arvadosclient.Dict)["exit_code"], Equals, 0)
608         c.Check(api.Content[6]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
609
610         log.Print(api.Logs["stdout"].String())
611
612         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/\n"), Equals, true)
613 }
614
615 func (s *TestSuite) TestFullRunSetCwd(c *C) {
616         api, _ := FullRunHelper(c, `{
617     "command": ["pwd"],
618     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
619     "cwd": "/bin",
620     "environment": {},
621     "mounts": {"/tmp": {"kind": "tmp"} },
622     "output_path": "/tmp",
623     "priority": 1,
624     "runtime_constraints": {}
625 }`, func(t *TestDockerClient) {
626                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
627                 t.logWriter.Close()
628                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
629         })
630
631         c.Check(api.Calls, Equals, 7)
632         c.Check(api.Content[6]["container"].(arvadosclient.Dict)["exit_code"], Equals, 0)
633         c.Check(api.Content[6]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
634
635         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/bin\n"), Equals, true)
636 }
637
638 func (s *TestSuite) TestCancel(c *C) {
639         record := `{
640     "command": ["/bin/sh", "-c", "echo foo && sleep 30 && echo bar"],
641     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
642     "cwd": ".",
643     "environment": {},
644     "mounts": {"/tmp": {"kind": "tmp"} },
645     "output_path": "/tmp",
646     "priority": 1,
647     "runtime_constraints": {}
648 }`
649
650         rec := arvados.Container{}
651         err := json.Unmarshal([]byte(record), &rec)
652         c.Check(err, IsNil)
653
654         docker := NewTestDockerClient()
655         docker.fn = func(t *TestDockerClient) {
656                 <-t.stop
657                 t.logWriter.Write(dockerLog(1, "foo\n"))
658                 t.logWriter.Close()
659                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
660         }
661         docker.RemoveImage(hwImageId, true)
662
663         api := &ArvTestClient{Container: rec}
664         cr := NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
665         am := &ArvMountCmdLine{}
666         cr.RunArvMount = am.ArvMountTest
667
668         go func() {
669                 for cr.ContainerID == "" {
670                         time.Sleep(time.Millisecond)
671                 }
672                 cr.SigChan <- syscall.SIGINT
673         }()
674
675         err = cr.Run()
676
677         c.Check(err, IsNil)
678         if err != nil {
679                 for k, v := range api.Logs {
680                         c.Log(k)
681                         c.Log(v.String())
682                 }
683         }
684
685         c.Assert(api.Calls, Equals, 6)
686         c.Check(api.Content[5]["container"].(arvadosclient.Dict)["log"], IsNil)
687         c.Check(api.Content[5]["container"].(arvadosclient.Dict)["state"], Equals, "Cancelled")
688         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "foo\n"), Equals, true)
689
690 }
691
692 func (s *TestSuite) TestFullRunSetEnv(c *C) {
693         api, _ := FullRunHelper(c, `{
694     "command": ["/bin/sh", "-c", "echo $FROBIZ"],
695     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
696     "cwd": "/bin",
697     "environment": {"FROBIZ": "bilbo"},
698     "mounts": {"/tmp": {"kind": "tmp"} },
699     "output_path": "/tmp",
700     "priority": 1,
701     "runtime_constraints": {}
702 }`, func(t *TestDockerClient) {
703                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
704                 t.logWriter.Close()
705                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
706         })
707
708         c.Check(api.Calls, Equals, 7)
709         c.Check(api.Content[6]["container"].(arvadosclient.Dict)["exit_code"], Equals, 0)
710         c.Check(api.Content[6]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
711
712         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "bilbo\n"), Equals, true)
713 }
714
715 type ArvMountCmdLine struct {
716         Cmd   []string
717         token string
718 }
719
720 func (am *ArvMountCmdLine) ArvMountTest(c []string, token string) (*exec.Cmd, error) {
721         am.Cmd = c
722         am.token = token
723         return nil, nil
724 }
725
726 func (s *TestSuite) TestSetupMounts(c *C) {
727         api := &ArvTestClient{}
728         kc := &KeepTestClient{}
729         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
730         am := &ArvMountCmdLine{}
731         cr.RunArvMount = am.ArvMountTest
732
733         i := 0
734         cr.MkTempDir = func(string, string) (string, error) {
735                 i += 1
736                 d := fmt.Sprintf("/tmp/mktmpdir%d", i)
737                 os.Mkdir(d, os.ModePerm)
738                 return d, nil
739         }
740
741         {
742                 cr.Container.Mounts = make(map[string]arvados.Mount)
743                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
744                 cr.OutputPath = "/tmp"
745
746                 err := cr.SetupMounts()
747                 c.Check(err, IsNil)
748                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-by-pdh", "by_id", "/tmp/mktmpdir1"})
749                 c.Check(cr.Binds, DeepEquals, []string{"/tmp/mktmpdir2:/tmp"})
750                 cr.CleanupDirs()
751         }
752
753         {
754                 i = 0
755                 cr.Container.Mounts = make(map[string]arvados.Mount)
756                 cr.Container.Mounts["/keeptmp"] = arvados.Mount{Kind: "collection", Writable: true}
757                 cr.OutputPath = "/keeptmp"
758
759                 os.MkdirAll("/tmp/mktmpdir1/tmp0", os.ModePerm)
760
761                 err := cr.SetupMounts()
762                 c.Check(err, IsNil)
763                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", "/tmp/mktmpdir1"})
764                 c.Check(cr.Binds, DeepEquals, []string{"/tmp/mktmpdir1/tmp0:/keeptmp"})
765                 cr.CleanupDirs()
766         }
767
768         {
769                 i = 0
770                 cr.Container.Mounts = make(map[string]arvados.Mount)
771                 cr.Container.Mounts["/keepinp"] = arvados.Mount{Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"}
772                 cr.Container.Mounts["/keepout"] = arvados.Mount{Kind: "collection", Writable: true}
773                 cr.OutputPath = "/keepout"
774
775                 os.MkdirAll("/tmp/mktmpdir1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
776                 os.MkdirAll("/tmp/mktmpdir1/tmp0", os.ModePerm)
777
778                 err := cr.SetupMounts()
779                 c.Check(err, IsNil)
780                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", "/tmp/mktmpdir1"})
781                 var ss sort.StringSlice = cr.Binds
782                 ss.Sort()
783                 c.Check(cr.Binds, DeepEquals, []string{"/tmp/mktmpdir1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
784                         "/tmp/mktmpdir1/tmp0:/keepout"})
785                 cr.CleanupDirs()
786         }
787 }
788
789 func (s *TestSuite) TestStdout(c *C) {
790         helperRecord := `{`
791         helperRecord += `"command": ["/bin/sh", "-c", "echo $FROBIZ"],`
792         helperRecord += `"container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",`
793         helperRecord += `"cwd": "/bin",`
794         helperRecord += `"environment": {"FROBIZ": "bilbo"},`
795         helperRecord += `"mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"} },`
796         helperRecord += `"output_path": "/tmp",`
797         helperRecord += `"priority": 1,`
798         helperRecord += `"runtime_constraints": {}`
799         helperRecord += `}`
800
801         api, _ := FullRunHelper(c, helperRecord, func(t *TestDockerClient) {
802                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
803                 t.logWriter.Close()
804                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
805         })
806
807         c.Assert(api.Calls, Equals, 6)
808         c.Check(api.Content[5]["container"].(arvadosclient.Dict)["exit_code"], Equals, 0)
809         c.Check(api.Content[5]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
810         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), Not(IsNil))
811 }
812
813 // Used by the TestStdoutWithWrongPath*()
814 func StdoutErrorRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, err error) {
815         rec := arvados.Container{}
816         err = json.Unmarshal([]byte(record), &rec)
817         c.Check(err, IsNil)
818
819         docker := NewTestDockerClient()
820         docker.fn = fn
821         docker.RemoveImage(hwImageId, true)
822
823         api = &ArvTestClient{Container: rec}
824         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
825         am := &ArvMountCmdLine{}
826         cr.RunArvMount = am.ArvMountTest
827
828         err = cr.Run()
829         return
830 }
831
832 func (s *TestSuite) TestStdoutWithWrongPath(c *C) {
833         _, _, err := StdoutErrorRunHelper(c, `{
834     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path":"/tmpa.out"} },
835     "output_path": "/tmp"
836 }`, func(t *TestDockerClient) {})
837
838         c.Check(err, NotNil)
839         c.Check(strings.Contains(err.Error(), "Stdout path does not start with OutputPath"), Equals, true)
840 }
841
842 func (s *TestSuite) TestStdoutWithWrongKindTmp(c *C) {
843         _, _, err := StdoutErrorRunHelper(c, `{
844     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "tmp", "path":"/tmp/a.out"} },
845     "output_path": "/tmp"
846 }`, func(t *TestDockerClient) {})
847
848         c.Check(err, NotNil)
849         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'tmp' for stdout"), Equals, true)
850 }
851
852 func (s *TestSuite) TestStdoutWithWrongKindCollection(c *C) {
853         _, _, err := StdoutErrorRunHelper(c, `{
854     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "collection", "path":"/tmp/a.out"} },
855     "output_path": "/tmp"
856 }`, func(t *TestDockerClient) {})
857
858         c.Check(err, NotNil)
859         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'collection' for stdout"), Equals, true)
860 }