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