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