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