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