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