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