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