9397: test with multiple mount points under subdirectories
[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 subdirManifest = ". 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0a18eda6411234567890@569fa8c3 0:9:file1_in_main.txt 9:18:file2_in_main.txt 27:5649:qr1hi-8i9sb-6tjg516fx3dhvnk.log.txt\n./subdir1 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0a18eda6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt\n./subdir1/subdir2 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0a18eda6415544332211@569fa8c5 0:9:file1_in_subdir2.txt 9:19:file2_in_subdir2.txt\n"
61 var subdirPDH = "a0def87f80dd594d4675809e83bd4f15+367"
62
63 var fakeAuthUUID = "zzzzz-gj3su-55pqoyepgi2glem"
64 var fakeAuthToken = "a3ltuwzqcu2u4sc0q7yhpc2w7s00fdcqecg5d6e0u3pfohmbjt"
65
66 type TestDockerClient struct {
67         imageLoaded string
68         logReader   io.ReadCloser
69         logWriter   io.WriteCloser
70         fn          func(t *TestDockerClient)
71         finish      chan dockerclient.WaitResult
72         stop        chan bool
73         cwd         string
74         env         []string
75         api         *ArvTestClient
76 }
77
78 func NewTestDockerClient() *TestDockerClient {
79         t := &TestDockerClient{}
80         t.logReader, t.logWriter = io.Pipe()
81         t.finish = make(chan dockerclient.WaitResult)
82         t.stop = make(chan bool)
83         t.cwd = "/"
84         return t
85 }
86
87 func (t *TestDockerClient) StopContainer(id string, timeout int) error {
88         t.stop <- true
89         return nil
90 }
91
92 func (t *TestDockerClient) InspectImage(id string) (*dockerclient.ImageInfo, error) {
93         if t.imageLoaded == id {
94                 return &dockerclient.ImageInfo{}, nil
95         } else {
96                 return nil, errors.New("")
97         }
98 }
99
100 func (t *TestDockerClient) LoadImage(reader io.Reader) error {
101         _, err := io.Copy(ioutil.Discard, reader)
102         if err != nil {
103                 return err
104         } else {
105                 t.imageLoaded = hwImageId
106                 return nil
107         }
108 }
109
110 func (t *TestDockerClient) CreateContainer(config *dockerclient.ContainerConfig, name string, authConfig *dockerclient.AuthConfig) (string, error) {
111         if config.WorkingDir != "" {
112                 t.cwd = config.WorkingDir
113         }
114         t.env = config.Env
115         return "abcde", nil
116 }
117
118 func (t *TestDockerClient) StartContainer(id string, config *dockerclient.HostConfig) error {
119         if id == "abcde" {
120                 go t.fn(t)
121                 return nil
122         } else {
123                 return errors.New("Invalid container id")
124         }
125 }
126
127 func (t *TestDockerClient) AttachContainer(id string, options *dockerclient.AttachOptions) (io.ReadCloser, error) {
128         return t.logReader, nil
129 }
130
131 func (t *TestDockerClient) Wait(id string) <-chan dockerclient.WaitResult {
132         return t.finish
133 }
134
135 func (*TestDockerClient) RemoveImage(name string, force bool) ([]*dockerclient.ImageDelete, error) {
136         return nil, nil
137 }
138
139 func (client *ArvTestClient) Create(resourceType string,
140         parameters arvadosclient.Dict,
141         output interface{}) error {
142
143         client.Mutex.Lock()
144         defer client.Mutex.Unlock()
145
146         client.Calls++
147         client.Content = append(client.Content, parameters)
148
149         if resourceType == "logs" {
150                 et := parameters["log"].(arvadosclient.Dict)["event_type"].(string)
151                 if client.Logs == nil {
152                         client.Logs = make(map[string]*bytes.Buffer)
153                 }
154                 if client.Logs[et] == nil {
155                         client.Logs[et] = &bytes.Buffer{}
156                 }
157                 client.Logs[et].Write([]byte(parameters["log"].(arvadosclient.Dict)["properties"].(map[string]string)["text"]))
158         }
159
160         if resourceType == "collections" && output != nil {
161                 mt := parameters["collection"].(arvadosclient.Dict)["manifest_text"].(string)
162                 outmap := output.(*arvados.Collection)
163                 outmap.PortableDataHash = fmt.Sprintf("%x+%d", md5.Sum([]byte(mt)), len(mt))
164         }
165
166         return nil
167 }
168
169 func (client *ArvTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
170         switch {
171         case method == "GET" && resourceType == "containers" && action == "auth":
172                 return json.Unmarshal([]byte(`{
173                         "kind": "arvados#api_client_authorization",
174                         "uuid": "`+fakeAuthUUID+`",
175                         "api_token": "`+fakeAuthToken+`"
176                         }`), output)
177         default:
178                 return fmt.Errorf("Not found")
179         }
180 }
181
182 func (client *ArvTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
183         if resourceType == "collections" {
184                 if uuid == hwPDH {
185                         output.(*arvados.Collection).ManifestText = hwManifest
186                 } else if uuid == otherPDH {
187                         output.(*arvados.Collection).ManifestText = otherManifest
188                 } else if uuid == subdirPDH {
189                         output.(*arvados.Collection).ManifestText = subdirManifest
190                 }
191         }
192         if resourceType == "containers" {
193                 (*output.(*arvados.Container)) = client.Container
194         }
195         return nil
196 }
197
198 func (client *ArvTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
199         client.Mutex.Lock()
200         defer client.Mutex.Unlock()
201         client.Calls++
202         client.Content = append(client.Content, parameters)
203         if resourceType == "containers" {
204                 if parameters["container"].(arvadosclient.Dict)["state"] == "Running" {
205                         client.WasSetRunning = true
206                 }
207         }
208         return nil
209 }
210
211 var discoveryMap = map[string]interface{}{"defaultTrashLifetime": float64(1209600)}
212
213 func (client *ArvTestClient) Discovery(key string) (interface{}, error) {
214         return discoveryMap[key], nil
215 }
216
217 // CalledWith returns the parameters from the first API call whose
218 // parameters match jpath/string. E.g., CalledWith(c, "foo.bar",
219 // "baz") returns parameters with parameters["foo"]["bar"]=="baz". If
220 // no call matches, it returns nil.
221 func (client *ArvTestClient) CalledWith(jpath string, expect interface{}) arvadosclient.Dict {
222 call:
223         for _, content := range client.Content {
224                 var v interface{} = content
225                 for _, k := range strings.Split(jpath, ".") {
226                         if dict, ok := v.(arvadosclient.Dict); !ok {
227                                 continue call
228                         } else {
229                                 v = dict[k]
230                         }
231                 }
232                 if v == expect {
233                         return content
234                 }
235         }
236         return nil
237 }
238
239 func (client *KeepTestClient) PutHB(hash string, buf []byte) (string, int, error) {
240         client.Content = buf
241         return fmt.Sprintf("%s+%d", hash, len(buf)), len(buf), nil
242 }
243
244 type FileWrapper struct {
245         io.ReadCloser
246         len uint64
247 }
248
249 func (fw FileWrapper) Len() uint64 {
250         return fw.len
251 }
252
253 func (client *KeepTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
254         if filename == hwImageId+".tar" {
255                 rdr := ioutil.NopCloser(&bytes.Buffer{})
256                 client.Called = true
257                 return FileWrapper{rdr, 1321984}, nil
258         }
259         return nil, nil
260 }
261
262 func (s *TestSuite) TestLoadImage(c *C) {
263         kc := &KeepTestClient{}
264         docker := NewTestDockerClient()
265         cr := NewContainerRunner(&ArvTestClient{}, kc, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
266
267         _, err := cr.Docker.RemoveImage(hwImageId, true)
268
269         _, err = cr.Docker.InspectImage(hwImageId)
270         c.Check(err, NotNil)
271
272         cr.Container.ContainerImage = hwPDH
273
274         // (1) Test loading image from keep
275         c.Check(kc.Called, Equals, false)
276         c.Check(cr.ContainerConfig.Image, Equals, "")
277
278         err = cr.LoadImage()
279
280         c.Check(err, IsNil)
281         defer func() {
282                 cr.Docker.RemoveImage(hwImageId, true)
283         }()
284
285         c.Check(kc.Called, Equals, true)
286         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
287
288         _, err = cr.Docker.InspectImage(hwImageId)
289         c.Check(err, IsNil)
290
291         // (2) Test using image that's already loaded
292         kc.Called = false
293         cr.ContainerConfig.Image = ""
294
295         err = cr.LoadImage()
296         c.Check(err, IsNil)
297         c.Check(kc.Called, Equals, false)
298         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
299
300 }
301
302 type ArvErrorTestClient struct{}
303
304 func (ArvErrorTestClient) Create(resourceType string,
305         parameters arvadosclient.Dict,
306         output interface{}) error {
307         return nil
308 }
309
310 func (ArvErrorTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
311         return errors.New("ArvError")
312 }
313
314 func (ArvErrorTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
315         return errors.New("ArvError")
316 }
317
318 func (ArvErrorTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
319         return nil
320 }
321
322 func (ArvErrorTestClient) Discovery(key string) (interface{}, error) {
323         return discoveryMap[key], nil
324 }
325
326 type KeepErrorTestClient struct{}
327
328 func (KeepErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
329         return "", 0, errors.New("KeepError")
330 }
331
332 func (KeepErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
333         return nil, errors.New("KeepError")
334 }
335
336 type KeepReadErrorTestClient struct{}
337
338 func (KeepReadErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
339         return "", 0, nil
340 }
341
342 type ErrorReader struct{}
343
344 func (ErrorReader) Read(p []byte) (n int, err error) {
345         return 0, errors.New("ErrorReader")
346 }
347
348 func (ErrorReader) Close() error {
349         return nil
350 }
351
352 func (ErrorReader) Len() uint64 {
353         return 0
354 }
355
356 func (KeepReadErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (keepclient.ReadCloserWithLen, error) {
357         return ErrorReader{}, nil
358 }
359
360 func (s *TestSuite) TestLoadImageArvError(c *C) {
361         // (1) Arvados error
362         cr := NewContainerRunner(ArvErrorTestClient{}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
363         cr.Container.ContainerImage = hwPDH
364
365         err := cr.LoadImage()
366         c.Check(err.Error(), Equals, "While getting container image collection: ArvError")
367 }
368
369 func (s *TestSuite) TestLoadImageKeepError(c *C) {
370         // (2) Keep error
371         docker := NewTestDockerClient()
372         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
373         cr.Container.ContainerImage = hwPDH
374
375         err := cr.LoadImage()
376         c.Check(err.Error(), Equals, "While creating ManifestFileReader for container image: KeepError")
377 }
378
379 func (s *TestSuite) TestLoadImageCollectionError(c *C) {
380         // (3) Collection doesn't contain image
381         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
382         cr.Container.ContainerImage = otherPDH
383
384         err := cr.LoadImage()
385         c.Check(err.Error(), Equals, "First file in the container image collection does not end in .tar")
386 }
387
388 func (s *TestSuite) TestLoadImageKeepReadError(c *C) {
389         // (4) Collection doesn't contain image
390         docker := NewTestDockerClient()
391         cr := NewContainerRunner(&ArvTestClient{}, KeepReadErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
392         cr.Container.ContainerImage = hwPDH
393
394         err := cr.LoadImage()
395         c.Check(err, NotNil)
396 }
397
398 type ClosableBuffer struct {
399         bytes.Buffer
400 }
401
402 func (*ClosableBuffer) Close() error {
403         return nil
404 }
405
406 type TestLogs struct {
407         Stdout ClosableBuffer
408         Stderr ClosableBuffer
409 }
410
411 func (tl *TestLogs) NewTestLoggingWriter(logstr string) io.WriteCloser {
412         if logstr == "stdout" {
413                 return &tl.Stdout
414         }
415         if logstr == "stderr" {
416                 return &tl.Stderr
417         }
418         return nil
419 }
420
421 func dockerLog(fd byte, msg string) []byte {
422         by := []byte(msg)
423         header := make([]byte, 8+len(by))
424         header[0] = fd
425         header[7] = byte(len(by))
426         copy(header[8:], by)
427         return header
428 }
429
430 func (s *TestSuite) TestRunContainer(c *C) {
431         docker := NewTestDockerClient()
432         docker.fn = func(t *TestDockerClient) {
433                 t.logWriter.Write(dockerLog(1, "Hello world\n"))
434                 t.logWriter.Close()
435                 t.finish <- dockerclient.WaitResult{}
436         }
437         cr := NewContainerRunner(&ArvTestClient{}, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
438
439         var logs TestLogs
440         cr.NewLogWriter = logs.NewTestLoggingWriter
441         cr.Container.ContainerImage = hwPDH
442         cr.Container.Command = []string{"./hw"}
443         err := cr.LoadImage()
444         c.Check(err, IsNil)
445
446         err = cr.CreateContainer()
447         c.Check(err, IsNil)
448
449         err = cr.StartContainer()
450         c.Check(err, IsNil)
451
452         err = cr.WaitFinish()
453         c.Check(err, IsNil)
454
455         c.Check(strings.HasSuffix(logs.Stdout.String(), "Hello world\n"), Equals, true)
456         c.Check(logs.Stderr.String(), Equals, "")
457 }
458
459 func (s *TestSuite) TestCommitLogs(c *C) {
460         api := &ArvTestClient{}
461         kc := &KeepTestClient{}
462         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
463         cr.CrunchLog.Timestamper = (&TestTimestamper{}).Timestamp
464
465         cr.CrunchLog.Print("Hello world!")
466         cr.CrunchLog.Print("Goodbye")
467         cr.finalState = "Complete"
468
469         err := cr.CommitLogs()
470         c.Check(err, IsNil)
471
472         c.Check(api.Calls, Equals, 2)
473         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["name"], Equals, "logs for zzzzz-zzzzz-zzzzzzzzzzzzzzz")
474         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["manifest_text"], Equals, ". 744b2e4553123b02fa7b452ec5c18993+123 0:123:crunch-run.txt\n")
475         c.Check(*cr.LogsPDH, Equals, "63da7bdacf08c40f604daad80c261e9a+60")
476 }
477
478 func (s *TestSuite) TestUpdateContainerRunning(c *C) {
479         api := &ArvTestClient{}
480         kc := &KeepTestClient{}
481         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
482
483         err := cr.UpdateContainerRunning()
484         c.Check(err, IsNil)
485
486         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Running")
487 }
488
489 func (s *TestSuite) TestUpdateContainerComplete(c *C) {
490         api := &ArvTestClient{}
491         kc := &KeepTestClient{}
492         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
493
494         cr.LogsPDH = new(string)
495         *cr.LogsPDH = "d3a229d2fe3690c2c3e75a71a153c6a3+60"
496
497         cr.ExitCode = new(int)
498         *cr.ExitCode = 42
499         cr.finalState = "Complete"
500
501         err := cr.UpdateContainerFinal()
502         c.Check(err, IsNil)
503
504         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], Equals, *cr.LogsPDH)
505         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], Equals, *cr.ExitCode)
506         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
507 }
508
509 func (s *TestSuite) TestUpdateContainerCancelled(c *C) {
510         api := &ArvTestClient{}
511         kc := &KeepTestClient{}
512         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
513         cr.Cancelled = true
514         cr.finalState = "Cancelled"
515
516         err := cr.UpdateContainerFinal()
517         c.Check(err, IsNil)
518
519         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], IsNil)
520         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], IsNil)
521         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Cancelled")
522 }
523
524 // Used by the TestFullRun*() test below to DRY up boilerplate setup to do full
525 // dress rehearsal of the Run() function, starting from a JSON container record.
526 func FullRunHelper(c *C, record string, extraMounts []string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner) {
527         rec := arvados.Container{}
528         err := json.Unmarshal([]byte(record), &rec)
529         c.Check(err, IsNil)
530
531         docker := NewTestDockerClient()
532         docker.fn = fn
533         docker.RemoveImage(hwImageId, true)
534
535         api = &ArvTestClient{Container: rec}
536         docker.api = api
537         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
538         cr.statInterval = 100 * time.Millisecond
539         am := &ArvMountCmdLine{}
540         cr.RunArvMount = am.ArvMountTest
541
542         if extraMounts != nil && len(extraMounts) > 0 {
543                 err := cr.SetupArvMountPoint("keep")
544                 c.Check(err, IsNil)
545
546                 for _, m := range extraMounts {
547                         os.MkdirAll(cr.ArvMountPoint+"/by_id/"+m, os.ModePerm)
548                 }
549         }
550
551         err = cr.Run()
552         c.Check(err, IsNil)
553         c.Check(api.WasSetRunning, Equals, true)
554
555         c.Check(api.Content[api.Calls-1]["container"].(arvadosclient.Dict)["log"], NotNil)
556
557         if err != nil {
558                 for k, v := range api.Logs {
559                         c.Log(k)
560                         c.Log(v.String())
561                 }
562         }
563
564         return
565 }
566
567 func (s *TestSuite) TestFullRunHello(c *C) {
568         api, _ := FullRunHelper(c, `{
569     "command": ["echo", "hello world"],
570     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
571     "cwd": ".",
572     "environment": {},
573     "mounts": {"/tmp": {"kind": "tmp"} },
574     "output_path": "/tmp",
575     "priority": 1,
576     "runtime_constraints": {}
577 }`, nil, func(t *TestDockerClient) {
578                 t.logWriter.Write(dockerLog(1, "hello world\n"))
579                 t.logWriter.Close()
580                 t.finish <- dockerclient.WaitResult{}
581         })
582
583         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
584         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
585         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello world\n"), Equals, true)
586
587 }
588
589 func (s *TestSuite) TestCrunchstat(c *C) {
590         api, _ := FullRunHelper(c, `{
591                 "command": ["sleep", "1"],
592                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
593                 "cwd": ".",
594                 "environment": {},
595                 "mounts": {"/tmp": {"kind": "tmp"} },
596                 "output_path": "/tmp",
597                 "priority": 1,
598                 "runtime_constraints": {}
599         }`, nil, func(t *TestDockerClient) {
600                 time.Sleep(time.Second)
601                 t.logWriter.Close()
602                 t.finish <- dockerclient.WaitResult{}
603         })
604
605         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
606         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
607
608         // We didn't actually start a container, so crunchstat didn't
609         // find accounting files and therefore didn't log any stats.
610         // It should have logged a "can't find accounting files"
611         // message after one poll interval, though, so we can confirm
612         // it's alive:
613         c.Assert(api.Logs["crunchstat"], NotNil)
614         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files have not appeared after 100ms.*`)
615
616         // The "files never appeared" log assures us that we called
617         // (*crunchstat.Reporter)Stop(), and that we set it up with
618         // the correct container ID "abcde":
619         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files never appeared for abcde\n`)
620 }
621
622 func (s *TestSuite) TestFullRunStderr(c *C) {
623         api, _ := FullRunHelper(c, `{
624     "command": ["/bin/sh", "-c", "echo hello ; echo world 1>&2 ; exit 1"],
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 }`, nil, func(t *TestDockerClient) {
633                 t.logWriter.Write(dockerLog(1, "hello\n"))
634                 t.logWriter.Write(dockerLog(2, "world\n"))
635                 t.logWriter.Close()
636                 t.finish <- dockerclient.WaitResult{ExitCode: 1}
637         })
638
639         final := api.CalledWith("container.state", "Complete")
640         c.Assert(final, NotNil)
641         c.Check(final["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
642         c.Check(final["container"].(arvadosclient.Dict)["log"], NotNil)
643
644         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello\n"), Equals, true)
645         c.Check(strings.HasSuffix(api.Logs["stderr"].String(), "world\n"), Equals, true)
646 }
647
648 func (s *TestSuite) TestFullRunDefaultCwd(c *C) {
649         api, _ := FullRunHelper(c, `{
650     "command": ["pwd"],
651     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
652     "cwd": ".",
653     "environment": {},
654     "mounts": {"/tmp": {"kind": "tmp"} },
655     "output_path": "/tmp",
656     "priority": 1,
657     "runtime_constraints": {}
658 }`, nil, func(t *TestDockerClient) {
659                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
660                 t.logWriter.Close()
661                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
662         })
663
664         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
665         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
666         c.Log(api.Logs["stdout"])
667         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/\n"), Equals, true)
668 }
669
670 func (s *TestSuite) TestFullRunSetCwd(c *C) {
671         api, _ := FullRunHelper(c, `{
672     "command": ["pwd"],
673     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
674     "cwd": "/bin",
675     "environment": {},
676     "mounts": {"/tmp": {"kind": "tmp"} },
677     "output_path": "/tmp",
678     "priority": 1,
679     "runtime_constraints": {}
680 }`, nil, func(t *TestDockerClient) {
681                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
682                 t.logWriter.Close()
683                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
684         })
685
686         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
687         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
688         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/bin\n"), Equals, true)
689 }
690
691 func (s *TestSuite) TestCancel(c *C) {
692         record := `{
693     "command": ["/bin/sh", "-c", "echo foo && sleep 30 && echo bar"],
694     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
695     "cwd": ".",
696     "environment": {},
697     "mounts": {"/tmp": {"kind": "tmp"} },
698     "output_path": "/tmp",
699     "priority": 1,
700     "runtime_constraints": {}
701 }`
702
703         rec := arvados.Container{}
704         err := json.Unmarshal([]byte(record), &rec)
705         c.Check(err, IsNil)
706
707         docker := NewTestDockerClient()
708         docker.fn = func(t *TestDockerClient) {
709                 <-t.stop
710                 t.logWriter.Write(dockerLog(1, "foo\n"))
711                 t.logWriter.Close()
712                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
713         }
714         docker.RemoveImage(hwImageId, true)
715
716         api := &ArvTestClient{Container: rec}
717         cr := NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
718         am := &ArvMountCmdLine{}
719         cr.RunArvMount = am.ArvMountTest
720
721         go func() {
722                 for cr.ContainerID == "" {
723                         time.Sleep(time.Millisecond)
724                 }
725                 cr.SigChan <- syscall.SIGINT
726         }()
727
728         err = cr.Run()
729
730         c.Check(err, IsNil)
731         if err != nil {
732                 for k, v := range api.Logs {
733                         c.Log(k)
734                         c.Log(v.String())
735                 }
736         }
737
738         c.Check(api.CalledWith("container.log", nil), NotNil)
739         c.Check(api.CalledWith("container.state", "Cancelled"), NotNil)
740         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "foo\n"), Equals, true)
741
742 }
743
744 func (s *TestSuite) TestFullRunSetEnv(c *C) {
745         api, _ := FullRunHelper(c, `{
746     "command": ["/bin/sh", "-c", "echo $FROBIZ"],
747     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
748     "cwd": "/bin",
749     "environment": {"FROBIZ": "bilbo"},
750     "mounts": {"/tmp": {"kind": "tmp"} },
751     "output_path": "/tmp",
752     "priority": 1,
753     "runtime_constraints": {}
754 }`, nil, func(t *TestDockerClient) {
755                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
756                 t.logWriter.Close()
757                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
758         })
759
760         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
761         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
762         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "bilbo\n"), Equals, true)
763 }
764
765 type ArvMountCmdLine struct {
766         Cmd   []string
767         token string
768 }
769
770 func (am *ArvMountCmdLine) ArvMountTest(c []string, token string) (*exec.Cmd, error) {
771         am.Cmd = c
772         am.token = token
773         return nil, nil
774 }
775
776 func stubCert(temp string) string {
777         path := temp + "/ca-certificates.crt"
778         crt, _ := os.Create(path)
779         crt.Close()
780         arvadosclient.CertFiles = []string{path}
781         return path
782 }
783
784 func (s *TestSuite) TestSetupMounts(c *C) {
785         api := &ArvTestClient{}
786         kc := &KeepTestClient{}
787         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
788         am := &ArvMountCmdLine{}
789         cr.RunArvMount = am.ArvMountTest
790
791         realTemp, err := ioutil.TempDir("", "crunchrun_test1-")
792         c.Assert(err, IsNil)
793         certTemp, err := ioutil.TempDir("", "crunchrun_test2-")
794         c.Assert(err, IsNil)
795         stubCertPath := stubCert(certTemp)
796
797         defer os.RemoveAll(realTemp)
798         defer os.RemoveAll(certTemp)
799
800         i := 0
801         cr.MkTempDir = func(_ string, prefix string) (string, error) {
802                 i++
803                 d := fmt.Sprintf("%s/%s%d", realTemp, prefix, i)
804                 err := os.Mkdir(d, os.ModePerm)
805                 if err != nil && strings.Contains(err.Error(), ": file exists") {
806                         // Test case must have pre-populated the tempdir
807                         err = nil
808                 }
809                 return d, err
810         }
811
812         checkEmpty := func() {
813                 filepath.Walk(realTemp, func(path string, _ os.FileInfo, err error) error {
814                         c.Check(path, Equals, realTemp)
815                         c.Check(err, IsNil)
816                         return nil
817                 })
818         }
819
820         {
821                 i = 0
822                 cr.ArvMountPoint = ""
823                 cr.Container.Mounts = make(map[string]arvados.Mount)
824                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
825                 cr.OutputPath = "/tmp"
826
827                 err := cr.SetupMounts()
828                 c.Check(err, IsNil)
829                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
830                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp"})
831                 cr.CleanupDirs()
832                 checkEmpty()
833         }
834
835         {
836                 i = 0
837                 cr.ArvMountPoint = ""
838                 cr.Container.Mounts = make(map[string]arvados.Mount)
839                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
840                 cr.OutputPath = "/tmp"
841
842                 apiflag := true
843                 cr.Container.RuntimeConstraints.API = &apiflag
844
845                 err := cr.SetupMounts()
846                 c.Check(err, IsNil)
847                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
848                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", stubCertPath + ":/etc/arvados/ca-certificates.crt:ro"})
849                 cr.CleanupDirs()
850                 checkEmpty()
851
852                 apiflag = false
853         }
854
855         {
856                 i = 0
857                 cr.ArvMountPoint = ""
858                 cr.Container.Mounts = map[string]arvados.Mount{
859                         "/keeptmp": {Kind: "collection", Writable: true},
860                 }
861                 cr.OutputPath = "/keeptmp"
862
863                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
864
865                 err := cr.SetupMounts()
866                 c.Check(err, IsNil)
867                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
868                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/tmp0:/keeptmp"})
869                 cr.CleanupDirs()
870                 checkEmpty()
871         }
872
873         {
874                 i = 0
875                 cr.ArvMountPoint = ""
876                 cr.Container.Mounts = map[string]arvados.Mount{
877                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
878                         "/keepout": {Kind: "collection", Writable: true},
879                 }
880                 cr.OutputPath = "/keepout"
881
882                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
883                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
884
885                 err := cr.SetupMounts()
886                 c.Check(err, IsNil)
887                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
888                 sort.StringSlice(cr.Binds).Sort()
889                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
890                         realTemp + "/keep1/tmp0:/keepout"})
891                 cr.CleanupDirs()
892                 checkEmpty()
893         }
894
895         {
896                 i = 0
897                 cr.ArvMountPoint = ""
898                 cr.Container.RuntimeConstraints.KeepCacheRAM = 512
899                 cr.Container.Mounts = map[string]arvados.Mount{
900                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
901                         "/keepout": {Kind: "collection", Writable: true},
902                 }
903                 cr.OutputPath = "/keepout"
904
905                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
906                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
907
908                 err := cr.SetupMounts()
909                 c.Check(err, IsNil)
910                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
911                 sort.StringSlice(cr.Binds).Sort()
912                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
913                         realTemp + "/keep1/tmp0:/keepout"})
914                 cr.CleanupDirs()
915                 checkEmpty()
916         }
917
918         for _, test := range []struct {
919                 in  interface{}
920                 out string
921         }{
922                 {in: "foo", out: `"foo"`},
923                 {in: nil, out: `null`},
924                 {in: map[string]int{"foo": 123}, out: `{"foo":123}`},
925         } {
926                 i = 0
927                 cr.ArvMountPoint = ""
928                 cr.Container.Mounts = map[string]arvados.Mount{
929                         "/mnt/test.json": {Kind: "json", Content: test.in},
930                 }
931                 err := cr.SetupMounts()
932                 c.Check(err, IsNil)
933                 sort.StringSlice(cr.Binds).Sort()
934                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2/mountdata.json:/mnt/test.json:ro"})
935                 content, err := ioutil.ReadFile(realTemp + "/2/mountdata.json")
936                 c.Check(err, IsNil)
937                 c.Check(content, DeepEquals, []byte(test.out))
938                 cr.CleanupDirs()
939                 checkEmpty()
940         }
941
942         // Read-only mount points are allowed underneath output_dir mount point
943         {
944                 i = 0
945                 cr.ArvMountPoint = ""
946                 cr.Container.Mounts = make(map[string]arvados.Mount)
947                 cr.Container.Mounts = map[string]arvados.Mount{
948                         "/tmp":     {Kind: "tmp"},
949                         "/tmp/foo": {Kind: "collection"},
950                 }
951                 cr.OutputPath = "/tmp"
952
953                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
954
955                 err := cr.SetupMounts()
956                 c.Check(err, IsNil)
957                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
958                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", realTemp + "/keep1/tmp0:/tmp/foo:ro"})
959                 cr.CleanupDirs()
960                 checkEmpty()
961         }
962
963         // Writable mount points are not allowed underneath output_dir mount point
964         {
965                 i = 0
966                 cr.ArvMountPoint = ""
967                 cr.Container.Mounts = make(map[string]arvados.Mount)
968                 cr.Container.Mounts = map[string]arvados.Mount{
969                         "/tmp":     {Kind: "tmp"},
970                         "/tmp/foo": {Kind: "collection", Writable: true},
971                 }
972                 cr.OutputPath = "/tmp"
973
974                 err := cr.SetupMounts()
975                 c.Check(err, NotNil)
976                 c.Check(err, ErrorMatches, `Writable mount points are not permitted underneath the output_path.*`)
977                 cr.CleanupDirs()
978                 checkEmpty()
979         }
980
981         // Only mount points of kind 'collection' are allowed underneath output_dir mount point
982         {
983                 i = 0
984                 cr.ArvMountPoint = ""
985                 cr.Container.Mounts = make(map[string]arvados.Mount)
986                 cr.Container.Mounts = map[string]arvados.Mount{
987                         "/tmp":     {Kind: "tmp"},
988                         "/tmp/foo": {Kind: "json"},
989                 }
990                 cr.OutputPath = "/tmp"
991
992                 err := cr.SetupMounts()
993                 c.Check(err, NotNil)
994                 c.Check(err, ErrorMatches, `Only mount points of kind 'collection' are supported underneath the output_path.*`)
995                 cr.CleanupDirs()
996                 checkEmpty()
997         }
998 }
999
1000 func (s *TestSuite) TestStdout(c *C) {
1001         helperRecord := `{
1002                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1003                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1004                 "cwd": "/bin",
1005                 "environment": {"FROBIZ": "bilbo"},
1006                 "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"} },
1007                 "output_path": "/tmp",
1008                 "priority": 1,
1009                 "runtime_constraints": {}
1010         }`
1011
1012         api, _ := FullRunHelper(c, helperRecord, nil, func(t *TestDockerClient) {
1013                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1014                 t.logWriter.Close()
1015                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
1016         })
1017
1018         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1019         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1020         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
1021 }
1022
1023 // Used by the TestStdoutWithWrongPath*()
1024 func StdoutErrorRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, err error) {
1025         rec := arvados.Container{}
1026         err = json.Unmarshal([]byte(record), &rec)
1027         c.Check(err, IsNil)
1028
1029         docker := NewTestDockerClient()
1030         docker.fn = fn
1031         docker.RemoveImage(hwImageId, true)
1032
1033         api = &ArvTestClient{Container: rec}
1034         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1035         am := &ArvMountCmdLine{}
1036         cr.RunArvMount = am.ArvMountTest
1037
1038         err = cr.Run()
1039         return
1040 }
1041
1042 func (s *TestSuite) TestStdoutWithWrongPath(c *C) {
1043         _, _, err := StdoutErrorRunHelper(c, `{
1044     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path":"/tmpa.out"} },
1045     "output_path": "/tmp"
1046 }`, func(t *TestDockerClient) {})
1047
1048         c.Check(err, NotNil)
1049         c.Check(strings.Contains(err.Error(), "Stdout path does not start with OutputPath"), Equals, true)
1050 }
1051
1052 func (s *TestSuite) TestStdoutWithWrongKindTmp(c *C) {
1053         _, _, err := StdoutErrorRunHelper(c, `{
1054     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "tmp", "path":"/tmp/a.out"} },
1055     "output_path": "/tmp"
1056 }`, func(t *TestDockerClient) {})
1057
1058         c.Check(err, NotNil)
1059         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'tmp' for stdout"), Equals, true)
1060 }
1061
1062 func (s *TestSuite) TestStdoutWithWrongKindCollection(c *C) {
1063         _, _, err := StdoutErrorRunHelper(c, `{
1064     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "collection", "path":"/tmp/a.out"} },
1065     "output_path": "/tmp"
1066 }`, func(t *TestDockerClient) {})
1067
1068         c.Check(err, NotNil)
1069         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'collection' for stdout"), Equals, true)
1070 }
1071
1072 func (s *TestSuite) TestFullRunWithAPI(c *C) {
1073         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1074         defer os.Unsetenv("ARVADOS_API_HOST")
1075         api, _ := FullRunHelper(c, `{
1076     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1077     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1078     "cwd": "/bin",
1079     "environment": {},
1080     "mounts": {"/tmp": {"kind": "tmp"} },
1081     "output_path": "/tmp",
1082     "priority": 1,
1083     "runtime_constraints": {"API": true}
1084 }`, nil, func(t *TestDockerClient) {
1085                 t.logWriter.Write(dockerLog(1, t.env[1][17:]+"\n"))
1086                 t.logWriter.Close()
1087                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
1088         })
1089
1090         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1091         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1092         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "test.arvados.org\n"), Equals, true)
1093         c.Check(api.CalledWith("container.output", "d41d8cd98f00b204e9800998ecf8427e+0"), NotNil)
1094 }
1095
1096 func (s *TestSuite) TestFullRunSetOutput(c *C) {
1097         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1098         defer os.Unsetenv("ARVADOS_API_HOST")
1099         api, _ := FullRunHelper(c, `{
1100     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1101     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1102     "cwd": "/bin",
1103     "environment": {},
1104     "mounts": {"/tmp": {"kind": "tmp"} },
1105     "output_path": "/tmp",
1106     "priority": 1,
1107     "runtime_constraints": {"API": true}
1108 }`, nil, func(t *TestDockerClient) {
1109                 t.api.Container.Output = "d4ab34d3d4f8a72f5c4973051ae69fab+122"
1110                 t.logWriter.Close()
1111                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
1112         })
1113
1114         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1115         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1116         c.Check(api.CalledWith("container.output", "d4ab34d3d4f8a72f5c4973051ae69fab+122"), NotNil)
1117 }
1118
1119 func (s *TestSuite) TestStdoutWithExcludeFromOutputMountPointUnderOutputDir(c *C) {
1120         helperRecord := `{
1121                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1122                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1123                 "cwd": "/bin",
1124                 "environment": {"FROBIZ": "bilbo"},
1125                 "mounts": {
1126         "/tmp": {"kind": "tmp"},
1127         "/tmp/foo": {"kind": "collection",
1128                      "portable_data_hash": "a3e8f74c6f101eae01fa08bfb4e49b3a+54",
1129                      "content": {"exclude_from_output": true}
1130         },
1131         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1132     },
1133                 "output_path": "/tmp",
1134                 "priority": 1,
1135                 "runtime_constraints": {}
1136         }`
1137
1138         extraMounts := []string{"a3e8f74c6f101eae01fa08bfb4e49b3a+54"}
1139
1140         api, _ := FullRunHelper(c, helperRecord, extraMounts, func(t *TestDockerClient) {
1141                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1142                 t.logWriter.Close()
1143                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
1144         })
1145
1146         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1147         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1148         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
1149 }
1150
1151 func (s *TestSuite) TestStdoutWithMountPointForFileUnderOutputDir(c *C) {
1152         helperRecord := `{
1153                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1154                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1155                 "cwd": "/bin",
1156                 "environment": {"FROBIZ": "bilbo"},
1157                 "mounts": {
1158         "/tmp": {"kind": "tmp"},
1159         "/tmp/foo": {"kind": "collection", "portable_data_hash": "a3e8f74c6f101eae01fa08bfb4e49b3a+54/md5sum.txt"},
1160         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1161     },
1162                 "output_path": "/tmp",
1163                 "priority": 1,
1164                 "runtime_constraints": {}
1165         }`
1166
1167         extraMounts := []string{"a3e8f74c6f101eae01fa08bfb4e49b3a+54/md5sum.txt"}
1168
1169         api, _ := FullRunHelper(c, helperRecord, extraMounts, func(t *TestDockerClient) {
1170                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1171                 t.logWriter.Close()
1172                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
1173         })
1174
1175         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1176         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1177         for _, v := range api.Content {
1178                 if v["collection"] != nil {
1179                         collection := v["collection"].(arvadosclient.Dict)
1180                         if strings.Index(collection["name"].(string), "output") == 0 {
1181                                 streams := strings.Split(collection["manifest_text"].(string), "\n")
1182                                 c.Check(streams[0], Equals, "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out")
1183                                 c.Check(streams[1], Equals, ". 68a84f561b1d1708c6baff5e019a9ab3+46+Ae5d0af96944a3690becb1decdf60cc1c937f556d@5693216f 0:46:foo")
1184                         }
1185                 }
1186         }
1187 }
1188
1189 func (s *TestSuite) TestStdoutWithMultipleMountPointsUnderOutputDir(c *C) {
1190         helperRecord := `{
1191                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1192                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1193                 "cwd": "/bin",
1194                 "environment": {"FROBIZ": "bilbo"},
1195                 "mounts": {
1196         "/tmp": {"kind": "tmp"},
1197         "/tmp/foo": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367"},
1198         "/tmp/foo/sub1": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1"},
1199         "/tmp/foo/sub1file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1/file2_in_subdir1.txt"},
1200         "/tmp/foo/sub2file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1/subdir2/file2_in_subdir2.txt"},
1201         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1202     },
1203                 "output_path": "/tmp",
1204                 "priority": 1,
1205                 "runtime_constraints": {}
1206         }`
1207
1208         extraMounts := []string{"a0def87f80dd594d4675809e83bd4f15+367"}
1209
1210         api, _ := FullRunHelper(c, helperRecord, extraMounts, func(t *TestDockerClient) {
1211                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1212                 t.logWriter.Close()
1213                 t.finish <- dockerclient.WaitResult{ExitCode: 0}
1214         })
1215
1216         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1217         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1218         for _, v := range api.Content {
1219                 if v["collection"] != nil {
1220                         collection := v["collection"].(arvadosclient.Dict)
1221                         if strings.Index(collection["name"].(string), "output") == 0 {
1222                                 manifest := collection["manifest_text"].(string)
1223
1224                                 c.Check(-1, Not(Equals), strings.Index(manifest, "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out"))
1225
1226                                 origManifestWithDotReplacedAsFoo := strings.Replace(subdirManifest, "./", "./foo/", -1)
1227                                 c.Check(-1, Not(Equals), strings.Index(manifest, "./foo"+origManifestWithDotReplacedAsFoo[1:]))
1228
1229                                 c.Check(-1, Not(Equals), strings.Index(manifest, "./foo/sub1 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0a18eda6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt"))
1230
1231                                 c.Check(-1, Not(Equals), strings.Index(manifest, "./foo 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0a18eda6419876543234@569fa8c4 9:18:sub1file2"))
1232
1233                                 c.Check(-1, Not(Equals), strings.Index(manifest, "./foo 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0a18eda6415544332211@569fa8c5 9:19:sub2file2"))
1234                         }
1235                 }
1236         }
1237 }