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