12519: Simplified code by nesting an additional MergingLoader
[arvados.git] / services / crunch-run / crunchrun_test.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package main
6
7 import (
8         "bufio"
9         "bytes"
10         "context"
11         "crypto/md5"
12         "encoding/json"
13         "errors"
14         "fmt"
15         "io"
16         "io/ioutil"
17         "net"
18         "os"
19         "os/exec"
20         "path/filepath"
21         "runtime/pprof"
22         "sort"
23         "strings"
24         "sync"
25         "syscall"
26         "testing"
27         "time"
28
29         "git.curoverse.com/arvados.git/sdk/go/arvados"
30         "git.curoverse.com/arvados.git/sdk/go/arvadosclient"
31         "git.curoverse.com/arvados.git/sdk/go/manifest"
32
33         dockertypes "github.com/docker/docker/api/types"
34         dockercontainer "github.com/docker/docker/api/types/container"
35         dockernetwork "github.com/docker/docker/api/types/network"
36         . "gopkg.in/check.v1"
37 )
38
39 // Gocheck boilerplate
40 func TestCrunchExec(t *testing.T) {
41         TestingT(t)
42 }
43
44 type TestSuite struct{}
45
46 // Gocheck boilerplate
47 var _ = Suite(&TestSuite{})
48
49 type ArvTestClient struct {
50         Total   int64
51         Calls   int
52         Content []arvadosclient.Dict
53         arvados.Container
54         Logs map[string]*bytes.Buffer
55         sync.Mutex
56         WasSetRunning bool
57         callraw       bool
58 }
59
60 type KeepTestClient struct {
61         Called  bool
62         Content []byte
63 }
64
65 var hwManifest = ". 82ab40c24fc8df01798e57ba66795bb1+841216+Aa124ac75e5168396c73c0a18eda641a4f41791c0@569fa8c3 0:841216:9c31ee32b3d15268a0754e8edc74d4f815ee014b693bc5109058e431dd5caea7.tar\n"
66 var hwPDH = "a45557269dcb65a6b78f9ac061c0850b+120"
67 var hwImageId = "9c31ee32b3d15268a0754e8edc74d4f815ee014b693bc5109058e431dd5caea7"
68
69 var otherManifest = ". 68a84f561b1d1708c6baff5e019a9ab3+46+Ae5d0af96944a3690becb1decdf60cc1c937f556d@5693216f 0:46:md5sum.txt\n"
70 var otherPDH = "a3e8f74c6f101eae01fa08bfb4e49b3a+54"
71
72 var normalizedManifestWithSubdirs = `. 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 0:9:file1_in_main.txt 9:18:file2_in_main.txt 0:27:zzzzz-8i9sb-bcdefghijkdhvnk.log.txt
73 ./subdir1 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt
74 ./subdir1/subdir2 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 0:9:file1_in_subdir2.txt 9:18:file2_in_subdir2.txt
75 `
76
77 var normalizedWithSubdirsPDH = "a0def87f80dd594d4675809e83bd4f15+367"
78
79 var denormalizedManifestWithSubdirs = ". 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 0:9:file1_in_main.txt 9:18:file2_in_main.txt 0:27:zzzzz-8i9sb-bcdefghijkdhvnk.log.txt 0:10:subdir1/file1_in_subdir1.txt 10:17:subdir1/file2_in_subdir1.txt\n"
80 var denormalizedWithSubdirsPDH = "b0def87f80dd594d4675809e83bd4f15+367"
81
82 var fakeAuthUUID = "zzzzz-gj3su-55pqoyepgi2glem"
83 var fakeAuthToken = "a3ltuwzqcu2u4sc0q7yhpc2w7s00fdcqecg5d6e0u3pfohmbjt"
84
85 type TestDockerClient struct {
86         imageLoaded string
87         logReader   io.ReadCloser
88         logWriter   io.WriteCloser
89         fn          func(t *TestDockerClient)
90         finish      int
91         stop        chan bool
92         cwd         string
93         env         []string
94         api         *ArvTestClient
95         realTemp    string
96 }
97
98 func NewTestDockerClient(exitCode int) *TestDockerClient {
99         t := &TestDockerClient{}
100         t.logReader, t.logWriter = io.Pipe()
101         t.finish = exitCode
102         t.stop = make(chan bool, 1)
103         t.cwd = "/"
104         return t
105 }
106
107 type MockConn struct {
108         net.Conn
109 }
110
111 func (m *MockConn) Write(b []byte) (int, error) {
112         return len(b), nil
113 }
114
115 func NewMockConn() *MockConn {
116         c := &MockConn{}
117         return c
118 }
119
120 func (t *TestDockerClient) ContainerAttach(ctx context.Context, container string, options dockertypes.ContainerAttachOptions) (dockertypes.HijackedResponse, error) {
121         return dockertypes.HijackedResponse{Conn: NewMockConn(), Reader: bufio.NewReader(t.logReader)}, nil
122 }
123
124 func (t *TestDockerClient) ContainerCreate(ctx context.Context, config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig, networkingConfig *dockernetwork.NetworkingConfig, containerName string) (dockercontainer.ContainerCreateCreatedBody, error) {
125         if config.WorkingDir != "" {
126                 t.cwd = config.WorkingDir
127         }
128         t.env = config.Env
129         return dockercontainer.ContainerCreateCreatedBody{ID: "abcde"}, nil
130 }
131
132 func (t *TestDockerClient) ContainerStart(ctx context.Context, container string, options dockertypes.ContainerStartOptions) error {
133         if container == "abcde" {
134                 // t.fn gets executed in ContainerWait
135                 return nil
136         } else {
137                 return errors.New("Invalid container id")
138         }
139 }
140
141 func (t *TestDockerClient) ContainerStop(ctx context.Context, container string, timeout *time.Duration) error {
142         t.stop <- true
143         return nil
144 }
145
146 func (t *TestDockerClient) ContainerWait(ctx context.Context, container string, condition dockercontainer.WaitCondition) (<-chan dockercontainer.ContainerWaitOKBody, <-chan error) {
147         body := make(chan dockercontainer.ContainerWaitOKBody)
148         err := make(chan error)
149         go func() {
150                 t.fn(t)
151                 body <- dockercontainer.ContainerWaitOKBody{StatusCode: int64(t.finish)}
152                 close(body)
153                 close(err)
154         }()
155         return body, err
156 }
157
158 func (t *TestDockerClient) ImageInspectWithRaw(ctx context.Context, image string) (dockertypes.ImageInspect, []byte, error) {
159         if t.imageLoaded == image {
160                 return dockertypes.ImageInspect{}, nil, nil
161         } else {
162                 return dockertypes.ImageInspect{}, nil, errors.New("")
163         }
164 }
165
166 func (t *TestDockerClient) ImageLoad(ctx context.Context, input io.Reader, quiet bool) (dockertypes.ImageLoadResponse, error) {
167         _, err := io.Copy(ioutil.Discard, input)
168         if err != nil {
169                 return dockertypes.ImageLoadResponse{}, err
170         } else {
171                 t.imageLoaded = hwImageId
172                 return dockertypes.ImageLoadResponse{Body: ioutil.NopCloser(input)}, nil
173         }
174 }
175
176 func (*TestDockerClient) ImageRemove(ctx context.Context, image string, options dockertypes.ImageRemoveOptions) ([]dockertypes.ImageDeleteResponseItem, error) {
177         return nil, nil
178 }
179
180 func (client *ArvTestClient) Create(resourceType string,
181         parameters arvadosclient.Dict,
182         output interface{}) error {
183
184         client.Mutex.Lock()
185         defer client.Mutex.Unlock()
186
187         client.Calls++
188         client.Content = append(client.Content, parameters)
189
190         if resourceType == "logs" {
191                 et := parameters["log"].(arvadosclient.Dict)["event_type"].(string)
192                 if client.Logs == nil {
193                         client.Logs = make(map[string]*bytes.Buffer)
194                 }
195                 if client.Logs[et] == nil {
196                         client.Logs[et] = &bytes.Buffer{}
197                 }
198                 client.Logs[et].Write([]byte(parameters["log"].(arvadosclient.Dict)["properties"].(map[string]string)["text"]))
199         }
200
201         if resourceType == "collections" && output != nil {
202                 mt := parameters["collection"].(arvadosclient.Dict)["manifest_text"].(string)
203                 outmap := output.(*arvados.Collection)
204                 outmap.PortableDataHash = fmt.Sprintf("%x+%d", md5.Sum([]byte(mt)), len(mt))
205         }
206
207         return nil
208 }
209
210 func (client *ArvTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
211         switch {
212         case method == "GET" && resourceType == "containers" && action == "auth":
213                 return json.Unmarshal([]byte(`{
214                         "kind": "arvados#api_client_authorization",
215                         "uuid": "`+fakeAuthUUID+`",
216                         "api_token": "`+fakeAuthToken+`"
217                         }`), output)
218         default:
219                 return fmt.Errorf("Not found")
220         }
221 }
222
223 func (client *ArvTestClient) CallRaw(method, resourceType, uuid, action string,
224         parameters arvadosclient.Dict) (reader io.ReadCloser, err error) {
225         var j []byte
226         if method == "GET" && resourceType == "containers" && action == "" && !client.callraw {
227                 j, err = json.Marshal(client.Container)
228         } else {
229                 j = []byte(`{
230                         "command": ["sleep", "1"],
231                         "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
232                         "cwd": ".",
233                         "environment": {},
234                         "mounts": {"/tmp": {"kind": "tmp"}, "/json": {"kind": "json", "content": {"number": 123456789123456789}}},
235                         "output_path": "/tmp",
236                         "priority": 1,
237                         "runtime_constraints": {}
238                 }`)
239         }
240         return ioutil.NopCloser(bytes.NewReader(j)), err
241 }
242
243 func (client *ArvTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
244         if resourceType == "collections" {
245                 if uuid == hwPDH {
246                         output.(*arvados.Collection).ManifestText = hwManifest
247                 } else if uuid == otherPDH {
248                         output.(*arvados.Collection).ManifestText = otherManifest
249                 } else if uuid == normalizedWithSubdirsPDH {
250                         output.(*arvados.Collection).ManifestText = normalizedManifestWithSubdirs
251                 } else if uuid == denormalizedWithSubdirsPDH {
252                         output.(*arvados.Collection).ManifestText = denormalizedManifestWithSubdirs
253                 }
254         }
255         if resourceType == "containers" {
256                 (*output.(*arvados.Container)) = client.Container
257         }
258         return nil
259 }
260
261 func (client *ArvTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
262         client.Mutex.Lock()
263         defer client.Mutex.Unlock()
264         client.Calls++
265         client.Content = append(client.Content, parameters)
266         if resourceType == "containers" {
267                 if parameters["container"].(arvadosclient.Dict)["state"] == "Running" {
268                         client.WasSetRunning = true
269                 }
270         }
271         return nil
272 }
273
274 var discoveryMap = map[string]interface{}{
275         "defaultTrashLifetime":               float64(1209600),
276         "crunchLimitLogBytesPerJob":          float64(67108864),
277         "crunchLogThrottleBytes":             float64(65536),
278         "crunchLogThrottlePeriod":            float64(60),
279         "crunchLogThrottleLines":             float64(1024),
280         "crunchLogPartialLineThrottlePeriod": float64(5),
281         "crunchLogBytesPerEvent":             float64(4096),
282         "crunchLogSecondsBetweenEvents":      float64(1),
283 }
284
285 func (client *ArvTestClient) Discovery(key string) (interface{}, error) {
286         return discoveryMap[key], nil
287 }
288
289 // CalledWith returns the parameters from the first API call whose
290 // parameters match jpath/string. E.g., CalledWith(c, "foo.bar",
291 // "baz") returns parameters with parameters["foo"]["bar"]=="baz". If
292 // no call matches, it returns nil.
293 func (client *ArvTestClient) CalledWith(jpath string, expect interface{}) arvadosclient.Dict {
294 call:
295         for _, content := range client.Content {
296                 var v interface{} = content
297                 for _, k := range strings.Split(jpath, ".") {
298                         if dict, ok := v.(arvadosclient.Dict); !ok {
299                                 continue call
300                         } else {
301                                 v = dict[k]
302                         }
303                 }
304                 if v == expect {
305                         return content
306                 }
307         }
308         return nil
309 }
310
311 func (client *KeepTestClient) PutHB(hash string, buf []byte) (string, int, error) {
312         client.Content = buf
313         return fmt.Sprintf("%s+%d", hash, len(buf)), len(buf), nil
314 }
315
316 func (*KeepTestClient) ClearBlockCache() {
317 }
318
319 type FileWrapper struct {
320         io.ReadCloser
321         len int64
322 }
323
324 func (fw FileWrapper) Size() int64 {
325         return fw.len
326 }
327
328 func (fw FileWrapper) Seek(int64, int) (int64, error) {
329         return 0, errors.New("not implemented")
330 }
331
332 func (client *KeepTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
333         if filename == hwImageId+".tar" {
334                 rdr := ioutil.NopCloser(&bytes.Buffer{})
335                 client.Called = true
336                 return FileWrapper{rdr, 1321984}, nil
337         } else if filename == "/file1_in_main.txt" {
338                 rdr := ioutil.NopCloser(strings.NewReader("foo"))
339                 client.Called = true
340                 return FileWrapper{rdr, 3}, nil
341         }
342         return nil, nil
343 }
344
345 func (s *TestSuite) TestLoadImage(c *C) {
346         kc := &KeepTestClient{}
347         docker := NewTestDockerClient(0)
348         cr := NewContainerRunner(&ArvTestClient{}, kc, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
349
350         _, err := cr.Docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
351
352         _, _, err = cr.Docker.ImageInspectWithRaw(nil, hwImageId)
353         c.Check(err, NotNil)
354
355         cr.Container.ContainerImage = hwPDH
356
357         // (1) Test loading image from keep
358         c.Check(kc.Called, Equals, false)
359         c.Check(cr.ContainerConfig.Image, Equals, "")
360
361         err = cr.LoadImage()
362
363         c.Check(err, IsNil)
364         defer func() {
365                 cr.Docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
366         }()
367
368         c.Check(kc.Called, Equals, true)
369         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
370
371         _, _, err = cr.Docker.ImageInspectWithRaw(nil, hwImageId)
372         c.Check(err, IsNil)
373
374         // (2) Test using image that's already loaded
375         kc.Called = false
376         cr.ContainerConfig.Image = ""
377
378         err = cr.LoadImage()
379         c.Check(err, IsNil)
380         c.Check(kc.Called, Equals, false)
381         c.Check(cr.ContainerConfig.Image, Equals, hwImageId)
382
383 }
384
385 type ArvErrorTestClient struct{}
386
387 func (ArvErrorTestClient) Create(resourceType string,
388         parameters arvadosclient.Dict,
389         output interface{}) error {
390         return nil
391 }
392
393 func (ArvErrorTestClient) Call(method, resourceType, uuid, action string, parameters arvadosclient.Dict, output interface{}) error {
394         return errors.New("ArvError")
395 }
396
397 func (ArvErrorTestClient) CallRaw(method, resourceType, uuid, action string,
398         parameters arvadosclient.Dict) (reader io.ReadCloser, err error) {
399         return nil, errors.New("ArvError")
400 }
401
402 func (ArvErrorTestClient) Get(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) error {
403         return errors.New("ArvError")
404 }
405
406 func (ArvErrorTestClient) Update(resourceType string, uuid string, parameters arvadosclient.Dict, output interface{}) (err error) {
407         return nil
408 }
409
410 func (ArvErrorTestClient) Discovery(key string) (interface{}, error) {
411         return discoveryMap[key], nil
412 }
413
414 type KeepErrorTestClient struct{}
415
416 func (KeepErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
417         return "", 0, errors.New("KeepError")
418 }
419
420 func (KeepErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
421         return nil, errors.New("KeepError")
422 }
423
424 func (KeepErrorTestClient) ClearBlockCache() {
425 }
426
427 type KeepReadErrorTestClient struct{}
428
429 func (KeepReadErrorTestClient) PutHB(hash string, buf []byte) (string, int, error) {
430         return "", 0, nil
431 }
432
433 func (KeepReadErrorTestClient) ClearBlockCache() {
434 }
435
436 type ErrorReader struct{}
437
438 func (ErrorReader) Read(p []byte) (n int, err error) {
439         return 0, errors.New("ErrorReader")
440 }
441
442 func (ErrorReader) Close() error {
443         return nil
444 }
445
446 func (ErrorReader) Size() int64 {
447         return 0
448 }
449
450 func (ErrorReader) Seek(int64, int) (int64, error) {
451         return 0, errors.New("ErrorReader")
452 }
453
454 func (KeepReadErrorTestClient) ManifestFileReader(m manifest.Manifest, filename string) (arvados.File, error) {
455         return ErrorReader{}, nil
456 }
457
458 func (s *TestSuite) TestLoadImageArvError(c *C) {
459         // (1) Arvados error
460         cr := NewContainerRunner(ArvErrorTestClient{}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
461         cr.Container.ContainerImage = hwPDH
462
463         err := cr.LoadImage()
464         c.Check(err.Error(), Equals, "While getting container image collection: ArvError")
465 }
466
467 func (s *TestSuite) TestLoadImageKeepError(c *C) {
468         // (2) Keep error
469         docker := NewTestDockerClient(0)
470         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
471         cr.Container.ContainerImage = hwPDH
472
473         err := cr.LoadImage()
474         c.Check(err.Error(), Equals, "While creating ManifestFileReader for container image: KeepError")
475 }
476
477 func (s *TestSuite) TestLoadImageCollectionError(c *C) {
478         // (3) Collection doesn't contain image
479         cr := NewContainerRunner(&ArvTestClient{}, KeepErrorTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
480         cr.Container.ContainerImage = otherPDH
481
482         err := cr.LoadImage()
483         c.Check(err.Error(), Equals, "First file in the container image collection does not end in .tar")
484 }
485
486 func (s *TestSuite) TestLoadImageKeepReadError(c *C) {
487         // (4) Collection doesn't contain image
488         docker := NewTestDockerClient(0)
489         cr := NewContainerRunner(&ArvTestClient{}, KeepReadErrorTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
490         cr.Container.ContainerImage = hwPDH
491
492         err := cr.LoadImage()
493         c.Check(err, NotNil)
494 }
495
496 type ClosableBuffer struct {
497         bytes.Buffer
498 }
499
500 func (*ClosableBuffer) Close() error {
501         return nil
502 }
503
504 type TestLogs struct {
505         Stdout ClosableBuffer
506         Stderr ClosableBuffer
507 }
508
509 func (tl *TestLogs) NewTestLoggingWriter(logstr string) io.WriteCloser {
510         if logstr == "stdout" {
511                 return &tl.Stdout
512         }
513         if logstr == "stderr" {
514                 return &tl.Stderr
515         }
516         return nil
517 }
518
519 func dockerLog(fd byte, msg string) []byte {
520         by := []byte(msg)
521         header := make([]byte, 8+len(by))
522         header[0] = fd
523         header[7] = byte(len(by))
524         copy(header[8:], by)
525         return header
526 }
527
528 func (s *TestSuite) TestRunContainer(c *C) {
529         docker := NewTestDockerClient(0)
530         docker.fn = func(t *TestDockerClient) {
531                 t.logWriter.Write(dockerLog(1, "Hello world\n"))
532                 t.logWriter.Close()
533         }
534         cr := NewContainerRunner(&ArvTestClient{}, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
535
536         var logs TestLogs
537         cr.NewLogWriter = logs.NewTestLoggingWriter
538         cr.Container.ContainerImage = hwPDH
539         cr.Container.Command = []string{"./hw"}
540         err := cr.LoadImage()
541         c.Check(err, IsNil)
542
543         err = cr.CreateContainer()
544         c.Check(err, IsNil)
545
546         err = cr.StartContainer()
547         c.Check(err, IsNil)
548
549         err = cr.WaitFinish()
550         c.Check(err, IsNil)
551
552         c.Check(strings.HasSuffix(logs.Stdout.String(), "Hello world\n"), Equals, true)
553         c.Check(logs.Stderr.String(), Equals, "")
554 }
555
556 func (s *TestSuite) TestCommitLogs(c *C) {
557         api := &ArvTestClient{}
558         kc := &KeepTestClient{}
559         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
560         cr.CrunchLog.Timestamper = (&TestTimestamper{}).Timestamp
561
562         cr.CrunchLog.Print("Hello world!")
563         cr.CrunchLog.Print("Goodbye")
564         cr.finalState = "Complete"
565
566         err := cr.CommitLogs()
567         c.Check(err, IsNil)
568
569         c.Check(api.Calls, Equals, 2)
570         c.Check(api.Content[1]["ensure_unique_name"], Equals, true)
571         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["name"], Equals, "logs for zzzzz-zzzzz-zzzzzzzzzzzzzzz")
572         c.Check(api.Content[1]["collection"].(arvadosclient.Dict)["manifest_text"], Equals, ". 744b2e4553123b02fa7b452ec5c18993+123 0:123:crunch-run.txt\n")
573         c.Check(*cr.LogsPDH, Equals, "63da7bdacf08c40f604daad80c261e9a+60")
574 }
575
576 func (s *TestSuite) TestUpdateContainerRunning(c *C) {
577         api := &ArvTestClient{}
578         kc := &KeepTestClient{}
579         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
580
581         err := cr.UpdateContainerRunning()
582         c.Check(err, IsNil)
583
584         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Running")
585 }
586
587 func (s *TestSuite) TestUpdateContainerComplete(c *C) {
588         api := &ArvTestClient{}
589         kc := &KeepTestClient{}
590         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
591
592         cr.LogsPDH = new(string)
593         *cr.LogsPDH = "d3a229d2fe3690c2c3e75a71a153c6a3+60"
594
595         cr.ExitCode = new(int)
596         *cr.ExitCode = 42
597         cr.finalState = "Complete"
598
599         err := cr.UpdateContainerFinal()
600         c.Check(err, IsNil)
601
602         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], Equals, *cr.LogsPDH)
603         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], Equals, *cr.ExitCode)
604         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Complete")
605 }
606
607 func (s *TestSuite) TestUpdateContainerCancelled(c *C) {
608         api := &ArvTestClient{}
609         kc := &KeepTestClient{}
610         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
611         cr.cCancelled = true
612         cr.finalState = "Cancelled"
613
614         err := cr.UpdateContainerFinal()
615         c.Check(err, IsNil)
616
617         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["log"], IsNil)
618         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["exit_code"], IsNil)
619         c.Check(api.Content[0]["container"].(arvadosclient.Dict)["state"], Equals, "Cancelled")
620 }
621
622 // Used by the TestFullRun*() test below to DRY up boilerplate setup to do full
623 // dress rehearsal of the Run() function, starting from a JSON container record.
624 func FullRunHelper(c *C, record string, extraMounts []string, exitCode int, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, realTemp string) {
625         rec := arvados.Container{}
626         err := json.Unmarshal([]byte(record), &rec)
627         c.Check(err, IsNil)
628
629         docker := NewTestDockerClient(exitCode)
630         docker.fn = fn
631         docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
632
633         api = &ArvTestClient{Container: rec}
634         docker.api = api
635         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
636         cr.statInterval = 100 * time.Millisecond
637         am := &ArvMountCmdLine{}
638         cr.RunArvMount = am.ArvMountTest
639
640         realTemp, err = ioutil.TempDir("", "crunchrun_test1-")
641         c.Assert(err, IsNil)
642         defer os.RemoveAll(realTemp)
643
644         docker.realTemp = realTemp
645
646         tempcount := 0
647         cr.MkTempDir = func(_ string, prefix string) (string, error) {
648                 tempcount++
649                 d := fmt.Sprintf("%s/%s%d", realTemp, prefix, tempcount)
650                 err := os.Mkdir(d, os.ModePerm)
651                 if err != nil && strings.Contains(err.Error(), ": file exists") {
652                         // Test case must have pre-populated the tempdir
653                         err = nil
654                 }
655                 return d, err
656         }
657
658         if extraMounts != nil && len(extraMounts) > 0 {
659                 err := cr.SetupArvMountPoint("keep")
660                 c.Check(err, IsNil)
661
662                 for _, m := range extraMounts {
663                         os.MkdirAll(cr.ArvMountPoint+"/by_id/"+m, os.ModePerm)
664                 }
665         }
666
667         err = cr.Run()
668         if api.CalledWith("container.state", "Complete") != nil {
669                 c.Check(err, IsNil)
670         }
671         c.Check(api.WasSetRunning, Equals, true)
672
673         c.Check(api.Content[api.Calls-2]["container"].(arvadosclient.Dict)["log"], NotNil)
674
675         if err != nil {
676                 for k, v := range api.Logs {
677                         c.Log(k)
678                         c.Log(v.String())
679                 }
680         }
681
682         return
683 }
684
685 func (s *TestSuite) TestFullRunHello(c *C) {
686         api, _, _ := FullRunHelper(c, `{
687     "command": ["echo", "hello world"],
688     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
689     "cwd": ".",
690     "environment": {},
691     "mounts": {"/tmp": {"kind": "tmp"} },
692     "output_path": "/tmp",
693     "priority": 1,
694     "runtime_constraints": {}
695 }`, nil, 0, func(t *TestDockerClient) {
696                 t.logWriter.Write(dockerLog(1, "hello world\n"))
697                 t.logWriter.Close()
698         })
699
700         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
701         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
702         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello world\n"), Equals, true)
703
704 }
705
706 func (s *TestSuite) TestCrunchstat(c *C) {
707         api, _, _ := FullRunHelper(c, `{
708                 "command": ["sleep", "1"],
709                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
710                 "cwd": ".",
711                 "environment": {},
712                 "mounts": {"/tmp": {"kind": "tmp"} },
713                 "output_path": "/tmp",
714                 "priority": 1,
715                 "runtime_constraints": {}
716         }`, nil, 0, func(t *TestDockerClient) {
717                 time.Sleep(time.Second)
718                 t.logWriter.Close()
719         })
720
721         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
722         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
723
724         // We didn't actually start a container, so crunchstat didn't
725         // find accounting files and therefore didn't log any stats.
726         // It should have logged a "can't find accounting files"
727         // message after one poll interval, though, so we can confirm
728         // it's alive:
729         c.Assert(api.Logs["crunchstat"], NotNil)
730         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files have not appeared after 100ms.*`)
731
732         // The "files never appeared" log assures us that we called
733         // (*crunchstat.Reporter)Stop(), and that we set it up with
734         // the correct container ID "abcde":
735         c.Check(api.Logs["crunchstat"].String(), Matches, `(?ms).*cgroup stats files never appeared for abcde\n`)
736 }
737
738 func (s *TestSuite) TestNodeInfoLog(c *C) {
739         api, _, _ := FullRunHelper(c, `{
740                 "command": ["sleep", "1"],
741                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
742                 "cwd": ".",
743                 "environment": {},
744                 "mounts": {"/tmp": {"kind": "tmp"} },
745                 "output_path": "/tmp",
746                 "priority": 1,
747                 "runtime_constraints": {}
748         }`, nil, 0,
749                 func(t *TestDockerClient) {
750                         time.Sleep(time.Second)
751                         t.logWriter.Close()
752                 })
753
754         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
755         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
756
757         c.Assert(api.Logs["node-info"], NotNil)
758         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*Host Information.*`)
759         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*CPU Information.*`)
760         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*Memory Information.*`)
761         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*Disk Space.*`)
762         c.Check(api.Logs["node-info"].String(), Matches, `(?ms).*Disk INodes.*`)
763 }
764
765 func (s *TestSuite) TestContainerRecordLog(c *C) {
766         api, _, _ := FullRunHelper(c, `{
767                 "command": ["sleep", "1"],
768                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
769                 "cwd": ".",
770                 "environment": {},
771                 "mounts": {"/tmp": {"kind": "tmp"} },
772                 "output_path": "/tmp",
773                 "priority": 1,
774                 "runtime_constraints": {}
775         }`, nil, 0,
776                 func(t *TestDockerClient) {
777                         time.Sleep(time.Second)
778                         t.logWriter.Close()
779                 })
780
781         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
782         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
783
784         c.Assert(api.Logs["container"], NotNil)
785         c.Check(api.Logs["container"].String(), Matches, `(?ms).*container_image.*`)
786 }
787
788 func (s *TestSuite) TestFullRunStderr(c *C) {
789         api, _, _ := FullRunHelper(c, `{
790     "command": ["/bin/sh", "-c", "echo hello ; echo world 1>&2 ; exit 1"],
791     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
792     "cwd": ".",
793     "environment": {},
794     "mounts": {"/tmp": {"kind": "tmp"} },
795     "output_path": "/tmp",
796     "priority": 1,
797     "runtime_constraints": {}
798 }`, nil, 1, func(t *TestDockerClient) {
799                 t.logWriter.Write(dockerLog(1, "hello\n"))
800                 t.logWriter.Write(dockerLog(2, "world\n"))
801                 t.logWriter.Close()
802         })
803
804         final := api.CalledWith("container.state", "Complete")
805         c.Assert(final, NotNil)
806         c.Check(final["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
807         c.Check(final["container"].(arvadosclient.Dict)["log"], NotNil)
808
809         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "hello\n"), Equals, true)
810         c.Check(strings.HasSuffix(api.Logs["stderr"].String(), "world\n"), Equals, true)
811 }
812
813 func (s *TestSuite) TestFullRunDefaultCwd(c *C) {
814         api, _, _ := FullRunHelper(c, `{
815     "command": ["pwd"],
816     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
817     "cwd": ".",
818     "environment": {},
819     "mounts": {"/tmp": {"kind": "tmp"} },
820     "output_path": "/tmp",
821     "priority": 1,
822     "runtime_constraints": {}
823 }`, nil, 0, func(t *TestDockerClient) {
824                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
825                 t.logWriter.Close()
826         })
827
828         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
829         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
830         c.Log(api.Logs["stdout"])
831         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/\n"), Equals, true)
832 }
833
834 func (s *TestSuite) TestFullRunSetCwd(c *C) {
835         api, _, _ := FullRunHelper(c, `{
836     "command": ["pwd"],
837     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
838     "cwd": "/bin",
839     "environment": {},
840     "mounts": {"/tmp": {"kind": "tmp"} },
841     "output_path": "/tmp",
842     "priority": 1,
843     "runtime_constraints": {}
844 }`, nil, 0, func(t *TestDockerClient) {
845                 t.logWriter.Write(dockerLog(1, t.cwd+"\n"))
846                 t.logWriter.Close()
847         })
848
849         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
850         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
851         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "/bin\n"), Equals, true)
852 }
853
854 func (s *TestSuite) TestStopOnSignal(c *C) {
855         s.testStopContainer(c, func(cr *ContainerRunner) {
856                 go func() {
857                         for !cr.cStarted {
858                                 time.Sleep(time.Millisecond)
859                         }
860                         cr.SigChan <- syscall.SIGINT
861                 }()
862         })
863 }
864
865 func (s *TestSuite) TestStopOnArvMountDeath(c *C) {
866         s.testStopContainer(c, func(cr *ContainerRunner) {
867                 cr.ArvMountExit = make(chan error)
868                 go func() {
869                         cr.ArvMountExit <- exec.Command("true").Run()
870                         close(cr.ArvMountExit)
871                 }()
872         })
873 }
874
875 func (s *TestSuite) testStopContainer(c *C, setup func(cr *ContainerRunner)) {
876         record := `{
877     "command": ["/bin/sh", "-c", "echo foo && sleep 30 && echo bar"],
878     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
879     "cwd": ".",
880     "environment": {},
881     "mounts": {"/tmp": {"kind": "tmp"} },
882     "output_path": "/tmp",
883     "priority": 1,
884     "runtime_constraints": {}
885 }`
886
887         rec := arvados.Container{}
888         err := json.Unmarshal([]byte(record), &rec)
889         c.Check(err, IsNil)
890
891         docker := NewTestDockerClient(0)
892         docker.fn = func(t *TestDockerClient) {
893                 <-t.stop
894                 t.logWriter.Write(dockerLog(1, "foo\n"))
895                 t.logWriter.Close()
896         }
897         docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
898
899         api := &ArvTestClient{Container: rec}
900         cr := NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
901         cr.RunArvMount = func([]string, string) (*exec.Cmd, error) { return nil, nil }
902         setup(cr)
903
904         done := make(chan error)
905         go func() {
906                 done <- cr.Run()
907         }()
908         select {
909         case <-time.After(20 * time.Second):
910                 pprof.Lookup("goroutine").WriteTo(os.Stderr, 1)
911                 c.Fatal("timed out")
912         case err = <-done:
913                 c.Check(err, IsNil)
914         }
915         for k, v := range api.Logs {
916                 c.Log(k)
917                 c.Log(v.String())
918         }
919
920         c.Check(api.CalledWith("container.log", nil), NotNil)
921         c.Check(api.CalledWith("container.state", "Cancelled"), NotNil)
922         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "foo\n"), Equals, true)
923 }
924
925 func (s *TestSuite) TestFullRunSetEnv(c *C) {
926         api, _, _ := FullRunHelper(c, `{
927     "command": ["/bin/sh", "-c", "echo $FROBIZ"],
928     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
929     "cwd": "/bin",
930     "environment": {"FROBIZ": "bilbo"},
931     "mounts": {"/tmp": {"kind": "tmp"} },
932     "output_path": "/tmp",
933     "priority": 1,
934     "runtime_constraints": {}
935 }`, nil, 0, func(t *TestDockerClient) {
936                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
937                 t.logWriter.Close()
938         })
939
940         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
941         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
942         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "bilbo\n"), Equals, true)
943 }
944
945 type ArvMountCmdLine struct {
946         Cmd   []string
947         token string
948 }
949
950 func (am *ArvMountCmdLine) ArvMountTest(c []string, token string) (*exec.Cmd, error) {
951         am.Cmd = c
952         am.token = token
953         return nil, nil
954 }
955
956 func stubCert(temp string) string {
957         path := temp + "/ca-certificates.crt"
958         crt, _ := os.Create(path)
959         crt.Close()
960         arvadosclient.CertFiles = []string{path}
961         return path
962 }
963
964 func (s *TestSuite) TestSetupMounts(c *C) {
965         api := &ArvTestClient{}
966         kc := &KeepTestClient{}
967         cr := NewContainerRunner(api, kc, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
968         am := &ArvMountCmdLine{}
969         cr.RunArvMount = am.ArvMountTest
970
971         realTemp, err := ioutil.TempDir("", "crunchrun_test1-")
972         c.Assert(err, IsNil)
973         certTemp, err := ioutil.TempDir("", "crunchrun_test2-")
974         c.Assert(err, IsNil)
975         stubCertPath := stubCert(certTemp)
976
977         defer os.RemoveAll(realTemp)
978         defer os.RemoveAll(certTemp)
979
980         i := 0
981         cr.MkTempDir = func(_ string, prefix string) (string, error) {
982                 i++
983                 d := fmt.Sprintf("%s/%s%d", realTemp, prefix, i)
984                 err := os.Mkdir(d, os.ModePerm)
985                 if err != nil && strings.Contains(err.Error(), ": file exists") {
986                         // Test case must have pre-populated the tempdir
987                         err = nil
988                 }
989                 return d, err
990         }
991
992         checkEmpty := func() {
993                 filepath.Walk(realTemp, func(path string, _ os.FileInfo, err error) error {
994                         c.Check(path, Equals, realTemp)
995                         c.Check(err, IsNil)
996                         return nil
997                 })
998         }
999
1000         {
1001                 i = 0
1002                 cr.ArvMountPoint = ""
1003                 cr.Container.Mounts = make(map[string]arvados.Mount)
1004                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
1005                 cr.OutputPath = "/tmp"
1006                 cr.statInterval = 5 * time.Second
1007                 err := cr.SetupMounts()
1008                 c.Check(err, IsNil)
1009                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1010                         "--read-write", "--crunchstat-interval=5",
1011                         "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1012                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp"})
1013                 os.RemoveAll(cr.ArvMountPoint)
1014                 cr.CleanupDirs()
1015                 checkEmpty()
1016         }
1017
1018         {
1019                 i = 0
1020                 cr.ArvMountPoint = ""
1021                 cr.Container.Mounts = make(map[string]arvados.Mount)
1022                 cr.Container.Mounts["/out"] = arvados.Mount{Kind: "tmp"}
1023                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
1024                 cr.OutputPath = "/out"
1025
1026                 err := cr.SetupMounts()
1027                 c.Check(err, IsNil)
1028                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1029                         "--read-write", "--crunchstat-interval=5",
1030                         "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1031                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/out", realTemp + "/3:/tmp"})
1032                 os.RemoveAll(cr.ArvMountPoint)
1033                 cr.CleanupDirs()
1034                 checkEmpty()
1035         }
1036
1037         {
1038                 i = 0
1039                 cr.ArvMountPoint = ""
1040                 cr.Container.Mounts = make(map[string]arvados.Mount)
1041                 cr.Container.Mounts["/tmp"] = arvados.Mount{Kind: "tmp"}
1042                 cr.OutputPath = "/tmp"
1043
1044                 apiflag := true
1045                 cr.Container.RuntimeConstraints.API = &apiflag
1046
1047                 err := cr.SetupMounts()
1048                 c.Check(err, IsNil)
1049                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1050                         "--read-write", "--crunchstat-interval=5",
1051                         "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1052                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", stubCertPath + ":/etc/arvados/ca-certificates.crt:ro"})
1053                 os.RemoveAll(cr.ArvMountPoint)
1054                 cr.CleanupDirs()
1055                 checkEmpty()
1056
1057                 apiflag = false
1058         }
1059
1060         {
1061                 i = 0
1062                 cr.ArvMountPoint = ""
1063                 cr.Container.Mounts = map[string]arvados.Mount{
1064                         "/keeptmp": {Kind: "collection", Writable: true},
1065                 }
1066                 cr.OutputPath = "/keeptmp"
1067
1068                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1069
1070                 err := cr.SetupMounts()
1071                 c.Check(err, IsNil)
1072                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1073                         "--read-write", "--crunchstat-interval=5",
1074                         "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1075                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/tmp0:/keeptmp"})
1076                 os.RemoveAll(cr.ArvMountPoint)
1077                 cr.CleanupDirs()
1078                 checkEmpty()
1079         }
1080
1081         {
1082                 i = 0
1083                 cr.ArvMountPoint = ""
1084                 cr.Container.Mounts = map[string]arvados.Mount{
1085                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
1086                         "/keepout": {Kind: "collection", Writable: true},
1087                 }
1088                 cr.OutputPath = "/keepout"
1089
1090                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
1091                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1092
1093                 err := cr.SetupMounts()
1094                 c.Check(err, IsNil)
1095                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1096                         "--read-write", "--crunchstat-interval=5",
1097                         "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1098                 sort.StringSlice(cr.Binds).Sort()
1099                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
1100                         realTemp + "/keep1/tmp0:/keepout"})
1101                 os.RemoveAll(cr.ArvMountPoint)
1102                 cr.CleanupDirs()
1103                 checkEmpty()
1104         }
1105
1106         {
1107                 i = 0
1108                 cr.ArvMountPoint = ""
1109                 cr.Container.RuntimeConstraints.KeepCacheRAM = 512
1110                 cr.Container.Mounts = map[string]arvados.Mount{
1111                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
1112                         "/keepout": {Kind: "collection", Writable: true},
1113                 }
1114                 cr.OutputPath = "/keepout"
1115
1116                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
1117                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1118
1119                 err := cr.SetupMounts()
1120                 c.Check(err, IsNil)
1121                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1122                         "--read-write", "--crunchstat-interval=5",
1123                         "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1124                 sort.StringSlice(cr.Binds).Sort()
1125                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
1126                         realTemp + "/keep1/tmp0:/keepout"})
1127                 os.RemoveAll(cr.ArvMountPoint)
1128                 cr.CleanupDirs()
1129                 checkEmpty()
1130         }
1131
1132         for _, test := range []struct {
1133                 in  interface{}
1134                 out string
1135         }{
1136                 {in: "foo", out: `"foo"`},
1137                 {in: nil, out: `null`},
1138                 {in: map[string]int64{"foo": 123456789123456789}, out: `{"foo":123456789123456789}`},
1139         } {
1140                 i = 0
1141                 cr.ArvMountPoint = ""
1142                 cr.Container.Mounts = map[string]arvados.Mount{
1143                         "/mnt/test.json": {Kind: "json", Content: test.in},
1144                 }
1145                 err := cr.SetupMounts()
1146                 c.Check(err, IsNil)
1147                 sort.StringSlice(cr.Binds).Sort()
1148                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2/mountdata.json:/mnt/test.json:ro"})
1149                 content, err := ioutil.ReadFile(realTemp + "/2/mountdata.json")
1150                 c.Check(err, IsNil)
1151                 c.Check(content, DeepEquals, []byte(test.out))
1152                 os.RemoveAll(cr.ArvMountPoint)
1153                 cr.CleanupDirs()
1154                 checkEmpty()
1155         }
1156
1157         // Read-only mount points are allowed underneath output_dir mount point
1158         {
1159                 i = 0
1160                 cr.ArvMountPoint = ""
1161                 cr.Container.Mounts = make(map[string]arvados.Mount)
1162                 cr.Container.Mounts = map[string]arvados.Mount{
1163                         "/tmp":     {Kind: "tmp"},
1164                         "/tmp/foo": {Kind: "collection"},
1165                 }
1166                 cr.OutputPath = "/tmp"
1167
1168                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1169
1170                 err := cr.SetupMounts()
1171                 c.Check(err, IsNil)
1172                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other",
1173                         "--read-write", "--crunchstat-interval=5",
1174                         "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1175                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", realTemp + "/keep1/tmp0:/tmp/foo:ro"})
1176                 os.RemoveAll(cr.ArvMountPoint)
1177                 cr.CleanupDirs()
1178                 checkEmpty()
1179         }
1180
1181         // Writable mount points are not allowed underneath output_dir mount point
1182         {
1183                 i = 0
1184                 cr.ArvMountPoint = ""
1185                 cr.Container.Mounts = make(map[string]arvados.Mount)
1186                 cr.Container.Mounts = map[string]arvados.Mount{
1187                         "/tmp":     {Kind: "tmp"},
1188                         "/tmp/foo": {Kind: "collection", Writable: true},
1189                 }
1190                 cr.OutputPath = "/tmp"
1191
1192                 err := cr.SetupMounts()
1193                 c.Check(err, NotNil)
1194                 c.Check(err, ErrorMatches, `Writable mount points are not permitted underneath the output_path.*`)
1195                 os.RemoveAll(cr.ArvMountPoint)
1196                 cr.CleanupDirs()
1197                 checkEmpty()
1198         }
1199
1200         // Only mount points of kind 'collection' are allowed underneath output_dir mount point
1201         {
1202                 i = 0
1203                 cr.ArvMountPoint = ""
1204                 cr.Container.Mounts = make(map[string]arvados.Mount)
1205                 cr.Container.Mounts = map[string]arvados.Mount{
1206                         "/tmp":     {Kind: "tmp"},
1207                         "/tmp/foo": {Kind: "json"},
1208                 }
1209                 cr.OutputPath = "/tmp"
1210
1211                 err := cr.SetupMounts()
1212                 c.Check(err, NotNil)
1213                 c.Check(err, ErrorMatches, `Only mount points of kind 'collection' are supported underneath the output_path.*`)
1214                 os.RemoveAll(cr.ArvMountPoint)
1215                 cr.CleanupDirs()
1216                 checkEmpty()
1217         }
1218
1219         // Only mount point of kind 'collection' is allowed for stdin
1220         {
1221                 i = 0
1222                 cr.ArvMountPoint = ""
1223                 cr.Container.Mounts = make(map[string]arvados.Mount)
1224                 cr.Container.Mounts = map[string]arvados.Mount{
1225                         "stdin": {Kind: "tmp"},
1226                 }
1227
1228                 err := cr.SetupMounts()
1229                 c.Check(err, NotNil)
1230                 c.Check(err, ErrorMatches, `Unsupported mount kind 'tmp' for stdin.*`)
1231                 os.RemoveAll(cr.ArvMountPoint)
1232                 cr.CleanupDirs()
1233                 checkEmpty()
1234         }
1235 }
1236
1237 func (s *TestSuite) TestStdout(c *C) {
1238         helperRecord := `{
1239                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1240                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1241                 "cwd": "/bin",
1242                 "environment": {"FROBIZ": "bilbo"},
1243                 "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"} },
1244                 "output_path": "/tmp",
1245                 "priority": 1,
1246                 "runtime_constraints": {}
1247         }`
1248
1249         api, _, _ := FullRunHelper(c, helperRecord, nil, 0, func(t *TestDockerClient) {
1250                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1251                 t.logWriter.Close()
1252         })
1253
1254         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1255         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1256         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
1257 }
1258
1259 // Used by the TestStdoutWithWrongPath*()
1260 func StdoutErrorRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, err error) {
1261         rec := arvados.Container{}
1262         err = json.Unmarshal([]byte(record), &rec)
1263         c.Check(err, IsNil)
1264
1265         docker := NewTestDockerClient(0)
1266         docker.fn = fn
1267         docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
1268
1269         api = &ArvTestClient{Container: rec}
1270         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1271         am := &ArvMountCmdLine{}
1272         cr.RunArvMount = am.ArvMountTest
1273
1274         err = cr.Run()
1275         return
1276 }
1277
1278 func (s *TestSuite) TestStdoutWithWrongPath(c *C) {
1279         _, _, err := StdoutErrorRunHelper(c, `{
1280     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path":"/tmpa.out"} },
1281     "output_path": "/tmp"
1282 }`, func(t *TestDockerClient) {})
1283
1284         c.Check(err, NotNil)
1285         c.Check(strings.Contains(err.Error(), "Stdout path does not start with OutputPath"), Equals, true)
1286 }
1287
1288 func (s *TestSuite) TestStdoutWithWrongKindTmp(c *C) {
1289         _, _, err := StdoutErrorRunHelper(c, `{
1290     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "tmp", "path":"/tmp/a.out"} },
1291     "output_path": "/tmp"
1292 }`, func(t *TestDockerClient) {})
1293
1294         c.Check(err, NotNil)
1295         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'tmp' for stdout"), Equals, true)
1296 }
1297
1298 func (s *TestSuite) TestStdoutWithWrongKindCollection(c *C) {
1299         _, _, err := StdoutErrorRunHelper(c, `{
1300     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "collection", "path":"/tmp/a.out"} },
1301     "output_path": "/tmp"
1302 }`, func(t *TestDockerClient) {})
1303
1304         c.Check(err, NotNil)
1305         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'collection' for stdout"), Equals, true)
1306 }
1307
1308 func (s *TestSuite) TestFullRunWithAPI(c *C) {
1309         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1310         defer os.Unsetenv("ARVADOS_API_HOST")
1311         api, _, _ := FullRunHelper(c, `{
1312     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1313     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1314     "cwd": "/bin",
1315     "environment": {},
1316     "mounts": {"/tmp": {"kind": "tmp"} },
1317     "output_path": "/tmp",
1318     "priority": 1,
1319     "runtime_constraints": {"API": true}
1320 }`, nil, 0, func(t *TestDockerClient) {
1321                 t.logWriter.Write(dockerLog(1, t.env[1][17:]+"\n"))
1322                 t.logWriter.Close()
1323         })
1324
1325         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1326         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1327         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "test.arvados.org\n"), Equals, true)
1328         c.Check(api.CalledWith("container.output", "d41d8cd98f00b204e9800998ecf8427e+0"), NotNil)
1329 }
1330
1331 func (s *TestSuite) TestFullRunSetOutput(c *C) {
1332         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1333         defer os.Unsetenv("ARVADOS_API_HOST")
1334         api, _, _ := FullRunHelper(c, `{
1335     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1336     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1337     "cwd": "/bin",
1338     "environment": {},
1339     "mounts": {"/tmp": {"kind": "tmp"} },
1340     "output_path": "/tmp",
1341     "priority": 1,
1342     "runtime_constraints": {"API": true}
1343 }`, nil, 0, func(t *TestDockerClient) {
1344                 t.api.Container.Output = "d4ab34d3d4f8a72f5c4973051ae69fab+122"
1345                 t.logWriter.Close()
1346         })
1347
1348         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1349         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1350         c.Check(api.CalledWith("container.output", "d4ab34d3d4f8a72f5c4973051ae69fab+122"), NotNil)
1351 }
1352
1353 func (s *TestSuite) TestStdoutWithExcludeFromOutputMountPointUnderOutputDir(c *C) {
1354         helperRecord := `{
1355                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1356                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1357                 "cwd": "/bin",
1358                 "environment": {"FROBIZ": "bilbo"},
1359                 "mounts": {
1360         "/tmp": {"kind": "tmp"},
1361         "/tmp/foo": {"kind": "collection",
1362                      "portable_data_hash": "a3e8f74c6f101eae01fa08bfb4e49b3a+54",
1363                      "exclude_from_output": true
1364         },
1365         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1366     },
1367                 "output_path": "/tmp",
1368                 "priority": 1,
1369                 "runtime_constraints": {}
1370         }`
1371
1372         extraMounts := []string{"a3e8f74c6f101eae01fa08bfb4e49b3a+54"}
1373
1374         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1375                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1376                 t.logWriter.Close()
1377         })
1378
1379         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1380         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1381         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
1382 }
1383
1384 func (s *TestSuite) TestStdoutWithMultipleMountPointsUnderOutputDir(c *C) {
1385         helperRecord := `{
1386                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1387                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1388                 "cwd": "/bin",
1389                 "environment": {"FROBIZ": "bilbo"},
1390                 "mounts": {
1391         "/tmp": {"kind": "tmp"},
1392         "/tmp/foo/bar": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/file2_in_main.txt"},
1393         "/tmp/foo/sub1": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1"},
1394         "/tmp/foo/sub1file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1/file2_in_subdir1.txt"},
1395         "/tmp/foo/baz/sub2file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1/subdir2/file2_in_subdir2.txt"},
1396         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1397     },
1398                 "output_path": "/tmp",
1399                 "priority": 1,
1400                 "runtime_constraints": {}
1401         }`
1402
1403         extraMounts := []string{
1404                 "a0def87f80dd594d4675809e83bd4f15+367/file2_in_main.txt",
1405                 "a0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt",
1406                 "a0def87f80dd594d4675809e83bd4f15+367/subdir1/subdir2/file2_in_subdir2.txt",
1407         }
1408
1409         api, runner, realtemp := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1410                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1411                 t.logWriter.Close()
1412         })
1413
1414         c.Check(runner.Binds, DeepEquals, []string{realtemp + "/2:/tmp",
1415                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/file2_in_main.txt:/tmp/foo/bar:ro",
1416                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1/subdir2/file2_in_subdir2.txt:/tmp/foo/baz/sub2file2:ro",
1417                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1:/tmp/foo/sub1:ro",
1418                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt:/tmp/foo/sub1file2:ro",
1419         })
1420
1421         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1422         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1423         for _, v := range api.Content {
1424                 if v["collection"] != nil {
1425                         c.Check(v["ensure_unique_name"], Equals, true)
1426                         collection := v["collection"].(arvadosclient.Dict)
1427                         if strings.Index(collection["name"].(string), "output") == 0 {
1428                                 manifest := collection["manifest_text"].(string)
1429
1430                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1431 ./foo 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 9:18:bar 9:18:sub1file2
1432 ./foo/baz 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 9:18:sub2file2
1433 ./foo/sub1 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt
1434 ./foo/sub1/subdir2 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 0:9:file1_in_subdir2.txt 9:18:file2_in_subdir2.txt
1435 `)
1436                         }
1437                 }
1438         }
1439 }
1440
1441 func (s *TestSuite) TestStdoutWithMountPointsUnderOutputDirDenormalizedManifest(c *C) {
1442         helperRecord := `{
1443                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1444                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1445                 "cwd": "/bin",
1446                 "environment": {"FROBIZ": "bilbo"},
1447                 "mounts": {
1448         "/tmp": {"kind": "tmp"},
1449         "/tmp/foo/bar": {"kind": "collection", "portable_data_hash": "b0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt"},
1450         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1451     },
1452                 "output_path": "/tmp",
1453                 "priority": 1,
1454                 "runtime_constraints": {}
1455         }`
1456
1457         extraMounts := []string{
1458                 "b0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt",
1459         }
1460
1461         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1462                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1463                 t.logWriter.Close()
1464         })
1465
1466         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1467         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1468         for _, v := range api.Content {
1469                 if v["collection"] != nil {
1470                         collection := v["collection"].(arvadosclient.Dict)
1471                         if strings.Index(collection["name"].(string), "output") == 0 {
1472                                 manifest := collection["manifest_text"].(string)
1473
1474                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1475 ./foo 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 10:17:bar
1476 `)
1477                         }
1478                 }
1479         }
1480 }
1481
1482 func (s *TestSuite) TestOutputSymlinkToInput(c *C) {
1483         helperRecord := `{
1484                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1485                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1486                 "cwd": "/bin",
1487                 "environment": {"FROBIZ": "bilbo"},
1488                 "mounts": {
1489         "/tmp": {"kind": "tmp"},
1490         "/keep/foo/sub1file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path": "/subdir1/file2_in_subdir1.txt"},
1491         "/keep/foo2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367"}
1492     },
1493                 "output_path": "/tmp",
1494                 "priority": 1,
1495                 "runtime_constraints": {}
1496         }`
1497
1498         extraMounts := []string{
1499                 "a0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt",
1500         }
1501
1502         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1503                 os.Symlink("/keep/foo/sub1file2", t.realTemp+"/2/baz")
1504                 os.Symlink("/keep/foo2/subdir1/file2_in_subdir1.txt", t.realTemp+"/2/baz2")
1505                 os.Symlink("/keep/foo2/subdir1", t.realTemp+"/2/baz3")
1506                 os.Mkdir(t.realTemp+"/2/baz4", 0700)
1507                 os.Symlink("/keep/foo2/subdir1/file2_in_subdir1.txt", t.realTemp+"/2/baz4/baz5")
1508                 t.logWriter.Close()
1509         })
1510
1511         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1512         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1513         for _, v := range api.Content {
1514                 if v["collection"] != nil {
1515                         collection := v["collection"].(arvadosclient.Dict)
1516                         if strings.Index(collection["name"].(string), "output") == 0 {
1517                                 manifest := collection["manifest_text"].(string)
1518                                 c.Check(manifest, Equals, `. 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 9:18:baz 9:18:baz2
1519 ./baz3 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt
1520 ./baz3/subdir2 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 0:9:file1_in_subdir2.txt 9:18:file2_in_subdir2.txt
1521 ./baz4 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 9:18:baz5
1522 `)
1523                         }
1524                 }
1525         }
1526 }
1527
1528 func (s *TestSuite) TestOutputError(c *C) {
1529         helperRecord := `{
1530                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1531                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1532                 "cwd": "/bin",
1533                 "environment": {"FROBIZ": "bilbo"},
1534                 "mounts": {
1535         "/tmp": {"kind": "tmp"}
1536     },
1537                 "output_path": "/tmp",
1538                 "priority": 1,
1539                 "runtime_constraints": {}
1540         }`
1541
1542         extraMounts := []string{}
1543
1544         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1545                 os.Symlink("/etc/hosts", t.realTemp+"/2/baz")
1546                 t.logWriter.Close()
1547         })
1548
1549         c.Check(api.CalledWith("container.state", "Cancelled"), NotNil)
1550 }
1551
1552 func (s *TestSuite) TestOutputSymlinkToOutput(c *C) {
1553         helperRecord := `{
1554                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1555                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1556                 "cwd": "/bin",
1557                 "environment": {"FROBIZ": "bilbo"},
1558                 "mounts": {
1559         "/tmp": {"kind": "tmp"}
1560     },
1561                 "output_path": "/tmp",
1562                 "priority": 1,
1563                 "runtime_constraints": {}
1564         }`
1565
1566         extraMounts := []string{}
1567
1568         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1569                 rf, _ := os.Create(t.realTemp + "/2/realfile")
1570                 rf.Write([]byte("foo"))
1571                 rf.Close()
1572
1573                 os.Mkdir(t.realTemp+"/2/realdir", 0700)
1574                 rf, _ = os.Create(t.realTemp + "/2/realdir/subfile")
1575                 rf.Write([]byte("bar"))
1576                 rf.Close()
1577
1578                 os.Symlink("/tmp/realfile", t.realTemp+"/2/file1")
1579                 os.Symlink("realfile", t.realTemp+"/2/file2")
1580                 os.Symlink("/tmp/file1", t.realTemp+"/2/file3")
1581                 os.Symlink("file2", t.realTemp+"/2/file4")
1582                 os.Symlink("realdir", t.realTemp+"/2/dir1")
1583                 os.Symlink("/tmp/realdir", t.realTemp+"/2/dir2")
1584                 t.logWriter.Close()
1585         })
1586
1587         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1588         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1589         for _, v := range api.Content {
1590                 if v["collection"] != nil {
1591                         collection := v["collection"].(arvadosclient.Dict)
1592                         if strings.Index(collection["name"].(string), "output") == 0 {
1593                                 manifest := collection["manifest_text"].(string)
1594                                 c.Check(manifest, Equals,
1595                                         `. 7a2c86e102dcc231bd232aad99686dfa+15 0:3:file1 3:3:file2 6:3:file3 9:3:file4 12:3:realfile
1596 ./dir1 37b51d194a7513e45b56f6524f2d51f2+3 0:3:subfile
1597 ./dir2 37b51d194a7513e45b56f6524f2d51f2+3 0:3:subfile
1598 ./realdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:subfile
1599 `)
1600                         }
1601                 }
1602         }
1603 }
1604
1605 func (s *TestSuite) TestStdinCollectionMountPoint(c *C) {
1606         helperRecord := `{
1607                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1608                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1609                 "cwd": "/bin",
1610                 "environment": {"FROBIZ": "bilbo"},
1611                 "mounts": {
1612         "/tmp": {"kind": "tmp"},
1613         "stdin": {"kind": "collection", "portable_data_hash": "b0def87f80dd594d4675809e83bd4f15+367", "path": "/file1_in_main.txt"},
1614         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1615     },
1616                 "output_path": "/tmp",
1617                 "priority": 1,
1618                 "runtime_constraints": {}
1619         }`
1620
1621         extraMounts := []string{
1622                 "b0def87f80dd594d4675809e83bd4f15+367/file1_in_main.txt",
1623         }
1624
1625         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1626                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1627                 t.logWriter.Close()
1628         })
1629
1630         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1631         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1632         for _, v := range api.Content {
1633                 if v["collection"] != nil {
1634                         collection := v["collection"].(arvadosclient.Dict)
1635                         if strings.Index(collection["name"].(string), "output") == 0 {
1636                                 manifest := collection["manifest_text"].(string)
1637                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1638 `)
1639                         }
1640                 }
1641         }
1642 }
1643
1644 func (s *TestSuite) TestStdinJsonMountPoint(c *C) {
1645         helperRecord := `{
1646                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1647                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1648                 "cwd": "/bin",
1649                 "environment": {"FROBIZ": "bilbo"},
1650                 "mounts": {
1651         "/tmp": {"kind": "tmp"},
1652         "stdin": {"kind": "json", "content": "foo"},
1653         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1654     },
1655                 "output_path": "/tmp",
1656                 "priority": 1,
1657                 "runtime_constraints": {}
1658         }`
1659
1660         api, _, _ := FullRunHelper(c, helperRecord, nil, 0, func(t *TestDockerClient) {
1661                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1662                 t.logWriter.Close()
1663         })
1664
1665         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1666         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1667         for _, v := range api.Content {
1668                 if v["collection"] != nil {
1669                         collection := v["collection"].(arvadosclient.Dict)
1670                         if strings.Index(collection["name"].(string), "output") == 0 {
1671                                 manifest := collection["manifest_text"].(string)
1672                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1673 `)
1674                         }
1675                 }
1676         }
1677 }
1678
1679 func (s *TestSuite) TestStderrMount(c *C) {
1680         api, _, _ := FullRunHelper(c, `{
1681     "command": ["/bin/sh", "-c", "echo hello;exit 1"],
1682     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1683     "cwd": ".",
1684     "environment": {},
1685     "mounts": {"/tmp": {"kind": "tmp"},
1686                "stdout": {"kind": "file", "path": "/tmp/a/out.txt"},
1687                "stderr": {"kind": "file", "path": "/tmp/b/err.txt"}},
1688     "output_path": "/tmp",
1689     "priority": 1,
1690     "runtime_constraints": {}
1691 }`, nil, 1, func(t *TestDockerClient) {
1692                 t.logWriter.Write(dockerLog(1, "hello\n"))
1693                 t.logWriter.Write(dockerLog(2, "oops\n"))
1694                 t.logWriter.Close()
1695         })
1696
1697         final := api.CalledWith("container.state", "Complete")
1698         c.Assert(final, NotNil)
1699         c.Check(final["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
1700         c.Check(final["container"].(arvadosclient.Dict)["log"], NotNil)
1701
1702         c.Check(api.CalledWith("collection.manifest_text", "./a b1946ac92492d2347c6235b4d2611184+6 0:6:out.txt\n./b 38af5c54926b620264ab1501150cf189+5 0:5:err.txt\n"), NotNil)
1703 }
1704
1705 func (s *TestSuite) TestNumberRoundTrip(c *C) {
1706         cr := NewContainerRunner(&ArvTestClient{callraw: true}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1707         cr.fetchContainerRecord()
1708
1709         jsondata, err := json.Marshal(cr.Container.Mounts["/json"].Content)
1710
1711         c.Check(err, IsNil)
1712         c.Check(string(jsondata), Equals, `{"number":123456789123456789}`)
1713 }
1714
1715 func (s *TestSuite) TestEvalSymlinks(c *C) {
1716         cr := NewContainerRunner(&ArvTestClient{callraw: true}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1717
1718         realTemp, err := ioutil.TempDir("", "crunchrun_test-")
1719         c.Assert(err, IsNil)
1720         defer os.RemoveAll(realTemp)
1721
1722         cr.HostOutputDir = realTemp
1723
1724         // Absolute path outside output dir
1725         os.Symlink("/etc/passwd", realTemp+"/p1")
1726
1727         // Relative outside output dir
1728         os.Symlink("../zip", realTemp+"/p2")
1729
1730         // Circular references
1731         os.Symlink("p4", realTemp+"/p3")
1732         os.Symlink("p5", realTemp+"/p4")
1733         os.Symlink("p3", realTemp+"/p5")
1734
1735         // Target doesn't exist
1736         os.Symlink("p99", realTemp+"/p6")
1737
1738         for _, v := range []string{"p1", "p2", "p3", "p4", "p5"} {
1739                 info, err := os.Lstat(realTemp + "/" + v)
1740                 _, _, _, err = cr.derefOutputSymlink(realTemp+"/"+v, info)
1741                 c.Assert(err, NotNil)
1742         }
1743 }
1744
1745 func (s *TestSuite) TestEvalSymlinkDir(c *C) {
1746         cr := NewContainerRunner(&ArvTestClient{callraw: true}, &KeepTestClient{}, nil, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1747
1748         realTemp, err := ioutil.TempDir("", "crunchrun_test-")
1749         c.Assert(err, IsNil)
1750         defer os.RemoveAll(realTemp)
1751
1752         cr.HostOutputDir = realTemp
1753
1754         // Absolute path outside output dir
1755         os.Symlink(".", realTemp+"/loop")
1756
1757         v := "loop"
1758         info, err := os.Lstat(realTemp + "/" + v)
1759         _, err = cr.UploadOutputFile(realTemp+"/"+v, info, err, []string{}, nil, "", "", 0)
1760         c.Assert(err, NotNil)
1761 }