11469: All {kind: tmp} mounts set up under host tempdir, don't try to use
[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["/tmp"] = arvados.Mount{Kind: "tmp"}
976                 cr.OutputPath = "/tmp"
977
978                 apiflag := true
979                 cr.Container.RuntimeConstraints.API = &apiflag
980
981                 err := cr.SetupMounts()
982                 c.Check(err, IsNil)
983                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
984                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", stubCertPath + ":/etc/arvados/ca-certificates.crt:ro"})
985                 cr.CleanupDirs()
986                 checkEmpty()
987
988                 apiflag = false
989         }
990
991         {
992                 i = 0
993                 cr.ArvMountPoint = ""
994                 cr.Container.Mounts = map[string]arvados.Mount{
995                         "/keeptmp": {Kind: "collection", Writable: true},
996                 }
997                 cr.OutputPath = "/keeptmp"
998
999                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1000
1001                 err := cr.SetupMounts()
1002                 c.Check(err, IsNil)
1003                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1004                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/tmp0:/keeptmp"})
1005                 cr.CleanupDirs()
1006                 checkEmpty()
1007         }
1008
1009         {
1010                 i = 0
1011                 cr.ArvMountPoint = ""
1012                 cr.Container.Mounts = map[string]arvados.Mount{
1013                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
1014                         "/keepout": {Kind: "collection", Writable: true},
1015                 }
1016                 cr.OutputPath = "/keepout"
1017
1018                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
1019                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1020
1021                 err := cr.SetupMounts()
1022                 c.Check(err, IsNil)
1023                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1024                 sort.StringSlice(cr.Binds).Sort()
1025                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
1026                         realTemp + "/keep1/tmp0:/keepout"})
1027                 cr.CleanupDirs()
1028                 checkEmpty()
1029         }
1030
1031         {
1032                 i = 0
1033                 cr.ArvMountPoint = ""
1034                 cr.Container.RuntimeConstraints.KeepCacheRAM = 512
1035                 cr.Container.Mounts = map[string]arvados.Mount{
1036                         "/keepinp": {Kind: "collection", PortableDataHash: "59389a8f9ee9d399be35462a0f92541c+53"},
1037                         "/keepout": {Kind: "collection", Writable: true},
1038                 }
1039                 cr.OutputPath = "/keepout"
1040
1041                 os.MkdirAll(realTemp+"/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53", os.ModePerm)
1042                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1043
1044                 err := cr.SetupMounts()
1045                 c.Check(err, IsNil)
1046                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1047                 sort.StringSlice(cr.Binds).Sort()
1048                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/keep1/by_id/59389a8f9ee9d399be35462a0f92541c+53:/keepinp:ro",
1049                         realTemp + "/keep1/tmp0:/keepout"})
1050                 cr.CleanupDirs()
1051                 checkEmpty()
1052         }
1053
1054         for _, test := range []struct {
1055                 in  interface{}
1056                 out string
1057         }{
1058                 {in: "foo", out: `"foo"`},
1059                 {in: nil, out: `null`},
1060                 {in: map[string]int{"foo": 123}, out: `{"foo":123}`},
1061         } {
1062                 i = 0
1063                 cr.ArvMountPoint = ""
1064                 cr.Container.Mounts = map[string]arvados.Mount{
1065                         "/mnt/test.json": {Kind: "json", Content: test.in},
1066                 }
1067                 err := cr.SetupMounts()
1068                 c.Check(err, IsNil)
1069                 sort.StringSlice(cr.Binds).Sort()
1070                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2/mountdata.json:/mnt/test.json:ro"})
1071                 content, err := ioutil.ReadFile(realTemp + "/2/mountdata.json")
1072                 c.Check(err, IsNil)
1073                 c.Check(content, DeepEquals, []byte(test.out))
1074                 cr.CleanupDirs()
1075                 checkEmpty()
1076         }
1077
1078         // Read-only mount points are allowed underneath output_dir mount point
1079         {
1080                 i = 0
1081                 cr.ArvMountPoint = ""
1082                 cr.Container.Mounts = make(map[string]arvados.Mount)
1083                 cr.Container.Mounts = map[string]arvados.Mount{
1084                         "/tmp":     {Kind: "tmp"},
1085                         "/tmp/foo": {Kind: "collection"},
1086                 }
1087                 cr.OutputPath = "/tmp"
1088
1089                 os.MkdirAll(realTemp+"/keep1/tmp0", os.ModePerm)
1090
1091                 err := cr.SetupMounts()
1092                 c.Check(err, IsNil)
1093                 c.Check(am.Cmd, DeepEquals, []string{"--foreground", "--allow-other", "--read-write", "--file-cache", "512", "--mount-tmp", "tmp0", "--mount-by-pdh", "by_id", realTemp + "/keep1"})
1094                 c.Check(cr.Binds, DeepEquals, []string{realTemp + "/2:/tmp", realTemp + "/keep1/tmp0:/tmp/foo:ro"})
1095                 cr.CleanupDirs()
1096                 checkEmpty()
1097         }
1098
1099         // Writable mount points are not allowed underneath output_dir mount point
1100         {
1101                 i = 0
1102                 cr.ArvMountPoint = ""
1103                 cr.Container.Mounts = make(map[string]arvados.Mount)
1104                 cr.Container.Mounts = map[string]arvados.Mount{
1105                         "/tmp":     {Kind: "tmp"},
1106                         "/tmp/foo": {Kind: "collection", Writable: true},
1107                 }
1108                 cr.OutputPath = "/tmp"
1109
1110                 err := cr.SetupMounts()
1111                 c.Check(err, NotNil)
1112                 c.Check(err, ErrorMatches, `Writable mount points are not permitted underneath the output_path.*`)
1113                 cr.CleanupDirs()
1114                 checkEmpty()
1115         }
1116
1117         // Only mount points of kind 'collection' are allowed underneath output_dir mount point
1118         {
1119                 i = 0
1120                 cr.ArvMountPoint = ""
1121                 cr.Container.Mounts = make(map[string]arvados.Mount)
1122                 cr.Container.Mounts = map[string]arvados.Mount{
1123                         "/tmp":     {Kind: "tmp"},
1124                         "/tmp/foo": {Kind: "json"},
1125                 }
1126                 cr.OutputPath = "/tmp"
1127
1128                 err := cr.SetupMounts()
1129                 c.Check(err, NotNil)
1130                 c.Check(err, ErrorMatches, `Only mount points of kind 'collection' are supported underneath the output_path.*`)
1131                 cr.CleanupDirs()
1132                 checkEmpty()
1133         }
1134
1135         // Only mount point of kind 'collection' is allowed for stdin
1136         {
1137                 i = 0
1138                 cr.ArvMountPoint = ""
1139                 cr.Container.Mounts = make(map[string]arvados.Mount)
1140                 cr.Container.Mounts = map[string]arvados.Mount{
1141                         "stdin": {Kind: "tmp"},
1142                 }
1143
1144                 err := cr.SetupMounts()
1145                 c.Check(err, NotNil)
1146                 c.Check(err, ErrorMatches, `Unsupported mount kind 'tmp' for stdin.*`)
1147                 cr.CleanupDirs()
1148                 checkEmpty()
1149         }
1150 }
1151
1152 func (s *TestSuite) TestStdout(c *C) {
1153         helperRecord := `{
1154                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1155                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1156                 "cwd": "/bin",
1157                 "environment": {"FROBIZ": "bilbo"},
1158                 "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"} },
1159                 "output_path": "/tmp",
1160                 "priority": 1,
1161                 "runtime_constraints": {}
1162         }`
1163
1164         api, _, _ := FullRunHelper(c, helperRecord, nil, 0, func(t *TestDockerClient) {
1165                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1166                 t.logWriter.Close()
1167         })
1168
1169         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1170         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1171         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
1172 }
1173
1174 // Used by the TestStdoutWithWrongPath*()
1175 func StdoutErrorRunHelper(c *C, record string, fn func(t *TestDockerClient)) (api *ArvTestClient, cr *ContainerRunner, err error) {
1176         rec := arvados.Container{}
1177         err = json.Unmarshal([]byte(record), &rec)
1178         c.Check(err, IsNil)
1179
1180         docker := NewTestDockerClient(0)
1181         docker.fn = fn
1182         docker.ImageRemove(nil, hwImageId, dockertypes.ImageRemoveOptions{})
1183
1184         api = &ArvTestClient{Container: rec}
1185         cr = NewContainerRunner(api, &KeepTestClient{}, docker, "zzzzz-zzzzz-zzzzzzzzzzzzzzz")
1186         am := &ArvMountCmdLine{}
1187         cr.RunArvMount = am.ArvMountTest
1188
1189         err = cr.Run()
1190         return
1191 }
1192
1193 func (s *TestSuite) TestStdoutWithWrongPath(c *C) {
1194         _, _, err := StdoutErrorRunHelper(c, `{
1195     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "file", "path":"/tmpa.out"} },
1196     "output_path": "/tmp"
1197 }`, func(t *TestDockerClient) {})
1198
1199         c.Check(err, NotNil)
1200         c.Check(strings.Contains(err.Error(), "Stdout path does not start with OutputPath"), Equals, true)
1201 }
1202
1203 func (s *TestSuite) TestStdoutWithWrongKindTmp(c *C) {
1204         _, _, err := StdoutErrorRunHelper(c, `{
1205     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "tmp", "path":"/tmp/a.out"} },
1206     "output_path": "/tmp"
1207 }`, func(t *TestDockerClient) {})
1208
1209         c.Check(err, NotNil)
1210         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'tmp' for stdout"), Equals, true)
1211 }
1212
1213 func (s *TestSuite) TestStdoutWithWrongKindCollection(c *C) {
1214         _, _, err := StdoutErrorRunHelper(c, `{
1215     "mounts": {"/tmp": {"kind": "tmp"}, "stdout": {"kind": "collection", "path":"/tmp/a.out"} },
1216     "output_path": "/tmp"
1217 }`, func(t *TestDockerClient) {})
1218
1219         c.Check(err, NotNil)
1220         c.Check(strings.Contains(err.Error(), "Unsupported mount kind 'collection' for stdout"), Equals, true)
1221 }
1222
1223 func (s *TestSuite) TestFullRunWithAPI(c *C) {
1224         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1225         defer os.Unsetenv("ARVADOS_API_HOST")
1226         api, _, _ := FullRunHelper(c, `{
1227     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1228     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1229     "cwd": "/bin",
1230     "environment": {},
1231     "mounts": {"/tmp": {"kind": "tmp"} },
1232     "output_path": "/tmp",
1233     "priority": 1,
1234     "runtime_constraints": {"API": true}
1235 }`, nil, 0, func(t *TestDockerClient) {
1236                 t.logWriter.Write(dockerLog(1, t.env[1][17:]+"\n"))
1237                 t.logWriter.Close()
1238         })
1239
1240         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1241         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1242         c.Check(strings.HasSuffix(api.Logs["stdout"].String(), "test.arvados.org\n"), Equals, true)
1243         c.Check(api.CalledWith("container.output", "d41d8cd98f00b204e9800998ecf8427e+0"), NotNil)
1244 }
1245
1246 func (s *TestSuite) TestFullRunSetOutput(c *C) {
1247         os.Setenv("ARVADOS_API_HOST", "test.arvados.org")
1248         defer os.Unsetenv("ARVADOS_API_HOST")
1249         api, _, _ := FullRunHelper(c, `{
1250     "command": ["/bin/sh", "-c", "echo $ARVADOS_API_HOST"],
1251     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1252     "cwd": "/bin",
1253     "environment": {},
1254     "mounts": {"/tmp": {"kind": "tmp"} },
1255     "output_path": "/tmp",
1256     "priority": 1,
1257     "runtime_constraints": {"API": true}
1258 }`, nil, 0, func(t *TestDockerClient) {
1259                 t.api.Container.Output = "d4ab34d3d4f8a72f5c4973051ae69fab+122"
1260                 t.logWriter.Close()
1261         })
1262
1263         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1264         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1265         c.Check(api.CalledWith("container.output", "d4ab34d3d4f8a72f5c4973051ae69fab+122"), NotNil)
1266 }
1267
1268 func (s *TestSuite) TestStdoutWithExcludeFromOutputMountPointUnderOutputDir(c *C) {
1269         helperRecord := `{
1270                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1271                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1272                 "cwd": "/bin",
1273                 "environment": {"FROBIZ": "bilbo"},
1274                 "mounts": {
1275         "/tmp": {"kind": "tmp"},
1276         "/tmp/foo": {"kind": "collection",
1277                      "portable_data_hash": "a3e8f74c6f101eae01fa08bfb4e49b3a+54",
1278                      "exclude_from_output": true
1279         },
1280         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1281     },
1282                 "output_path": "/tmp",
1283                 "priority": 1,
1284                 "runtime_constraints": {}
1285         }`
1286
1287         extraMounts := []string{"a3e8f74c6f101eae01fa08bfb4e49b3a+54"}
1288
1289         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1290                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1291                 t.logWriter.Close()
1292         })
1293
1294         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1295         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1296         c.Check(api.CalledWith("collection.manifest_text", "./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out\n"), NotNil)
1297 }
1298
1299 func (s *TestSuite) TestStdoutWithMultipleMountPointsUnderOutputDir(c *C) {
1300         helperRecord := `{
1301                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1302                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1303                 "cwd": "/bin",
1304                 "environment": {"FROBIZ": "bilbo"},
1305                 "mounts": {
1306         "/tmp": {"kind": "tmp"},
1307         "/tmp/foo/bar": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/file2_in_main.txt"},
1308         "/tmp/foo/sub1": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1"},
1309         "/tmp/foo/sub1file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1/file2_in_subdir1.txt"},
1310         "/tmp/foo/baz/sub2file2": {"kind": "collection", "portable_data_hash": "a0def87f80dd594d4675809e83bd4f15+367", "path":"/subdir1/subdir2/file2_in_subdir2.txt"},
1311         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1312     },
1313                 "output_path": "/tmp",
1314                 "priority": 1,
1315                 "runtime_constraints": {}
1316         }`
1317
1318         extraMounts := []string{
1319                 "a0def87f80dd594d4675809e83bd4f15+367/file2_in_main.txt",
1320                 "a0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt",
1321                 "a0def87f80dd594d4675809e83bd4f15+367/subdir1/subdir2/file2_in_subdir2.txt",
1322         }
1323
1324         api, runner, realtemp := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1325                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1326                 t.logWriter.Close()
1327         })
1328
1329         c.Check(runner.Binds, DeepEquals, []string{realtemp + "/2:/tmp",
1330                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/file2_in_main.txt:/tmp/foo/bar:ro",
1331                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1/subdir2/file2_in_subdir2.txt:/tmp/foo/baz/sub2file2:ro",
1332                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1:/tmp/foo/sub1:ro",
1333                 realtemp + "/keep1/by_id/a0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt:/tmp/foo/sub1file2:ro",
1334         })
1335
1336         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1337         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1338         for _, v := range api.Content {
1339                 if v["collection"] != nil {
1340                         c.Check(v["ensure_unique_name"], Equals, true)
1341                         collection := v["collection"].(arvadosclient.Dict)
1342                         if strings.Index(collection["name"].(string), "output") == 0 {
1343                                 manifest := collection["manifest_text"].(string)
1344
1345                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1346 ./foo 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 9:18:bar 9:18:sub1file2
1347 ./foo/baz 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 9:18:sub2file2
1348 ./foo/sub1 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396cabcdefghij6419876543234@569fa8c4 0:9:file1_in_subdir1.txt 9:18:file2_in_subdir1.txt
1349 ./foo/sub1/subdir2 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0bcdefghijk544332211@569fa8c5 0:9:file1_in_subdir2.txt 9:18:file2_in_subdir2.txt
1350 `)
1351                         }
1352                 }
1353         }
1354 }
1355
1356 func (s *TestSuite) TestStdoutWithMountPointsUnderOutputDirDenormalizedManifest(c *C) {
1357         helperRecord := `{
1358                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1359                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1360                 "cwd": "/bin",
1361                 "environment": {"FROBIZ": "bilbo"},
1362                 "mounts": {
1363         "/tmp": {"kind": "tmp"},
1364         "/tmp/foo/bar": {"kind": "collection", "portable_data_hash": "b0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt"},
1365         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1366     },
1367                 "output_path": "/tmp",
1368                 "priority": 1,
1369                 "runtime_constraints": {}
1370         }`
1371
1372         extraMounts := []string{
1373                 "b0def87f80dd594d4675809e83bd4f15+367/subdir1/file2_in_subdir1.txt",
1374         }
1375
1376         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1377                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1378                 t.logWriter.Close()
1379         })
1380
1381         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1382         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1383         for _, v := range api.Content {
1384                 if v["collection"] != nil {
1385                         collection := v["collection"].(arvadosclient.Dict)
1386                         if strings.Index(collection["name"].(string), "output") == 0 {
1387                                 manifest := collection["manifest_text"].(string)
1388
1389                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1390 ./foo 3e426d509afffb85e06c4c96a7c15e91+27+Aa124ac75e5168396c73c0abcdefgh11234567890@569fa8c3 10:17:bar
1391 `)
1392                         }
1393                 }
1394         }
1395 }
1396
1397 func (s *TestSuite) TestStdinCollectionMountPoint(c *C) {
1398         helperRecord := `{
1399                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1400                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1401                 "cwd": "/bin",
1402                 "environment": {"FROBIZ": "bilbo"},
1403                 "mounts": {
1404         "/tmp": {"kind": "tmp"},
1405         "stdin": {"kind": "collection", "portable_data_hash": "b0def87f80dd594d4675809e83bd4f15+367", "path": "/file1_in_main.txt"},
1406         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1407     },
1408                 "output_path": "/tmp",
1409                 "priority": 1,
1410                 "runtime_constraints": {}
1411         }`
1412
1413         extraMounts := []string{
1414                 "b0def87f80dd594d4675809e83bd4f15+367/file1_in_main.txt",
1415         }
1416
1417         api, _, _ := FullRunHelper(c, helperRecord, extraMounts, 0, func(t *TestDockerClient) {
1418                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1419                 t.logWriter.Close()
1420         })
1421
1422         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1423         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1424         for _, v := range api.Content {
1425                 if v["collection"] != nil {
1426                         collection := v["collection"].(arvadosclient.Dict)
1427                         if strings.Index(collection["name"].(string), "output") == 0 {
1428                                 manifest := collection["manifest_text"].(string)
1429                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1430 `)
1431                         }
1432                 }
1433         }
1434 }
1435
1436 func (s *TestSuite) TestStdinJsonMountPoint(c *C) {
1437         helperRecord := `{
1438                 "command": ["/bin/sh", "-c", "echo $FROBIZ"],
1439                 "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1440                 "cwd": "/bin",
1441                 "environment": {"FROBIZ": "bilbo"},
1442                 "mounts": {
1443         "/tmp": {"kind": "tmp"},
1444         "stdin": {"kind": "json", "content": "foo"},
1445         "stdout": {"kind": "file", "path": "/tmp/a/b/c.out"}
1446     },
1447                 "output_path": "/tmp",
1448                 "priority": 1,
1449                 "runtime_constraints": {}
1450         }`
1451
1452         api, _, _ := FullRunHelper(c, helperRecord, nil, 0, func(t *TestDockerClient) {
1453                 t.logWriter.Write(dockerLog(1, t.env[0][7:]+"\n"))
1454                 t.logWriter.Close()
1455         })
1456
1457         c.Check(api.CalledWith("container.exit_code", 0), NotNil)
1458         c.Check(api.CalledWith("container.state", "Complete"), NotNil)
1459         for _, v := range api.Content {
1460                 if v["collection"] != nil {
1461                         collection := v["collection"].(arvadosclient.Dict)
1462                         if strings.Index(collection["name"].(string), "output") == 0 {
1463                                 manifest := collection["manifest_text"].(string)
1464                                 c.Check(manifest, Equals, `./a/b 307372fa8fd5c146b22ae7a45b49bc31+6 0:6:c.out
1465 `)
1466                         }
1467                 }
1468         }
1469 }
1470
1471 func (s *TestSuite) TestStderrMount(c *C) {
1472         api, _, _ := FullRunHelper(c, `{
1473     "command": ["/bin/sh", "-c", "echo hello;exit 1"],
1474     "container_image": "d4ab34d3d4f8a72f5c4973051ae69fab+122",
1475     "cwd": ".",
1476     "environment": {},
1477     "mounts": {"/tmp": {"kind": "tmp"},
1478                "stdout": {"kind": "file", "path": "/tmp/a/out.txt"},
1479                "stderr": {"kind": "file", "path": "/tmp/b/err.txt"}},
1480     "output_path": "/tmp",
1481     "priority": 1,
1482     "runtime_constraints": {}
1483 }`, nil, 1, func(t *TestDockerClient) {
1484                 t.logWriter.Write(dockerLog(1, "hello\n"))
1485                 t.logWriter.Write(dockerLog(2, "oops\n"))
1486                 t.logWriter.Close()
1487         })
1488
1489         final := api.CalledWith("container.state", "Complete")
1490         c.Assert(final, NotNil)
1491         c.Check(final["container"].(arvadosclient.Dict)["exit_code"], Equals, 1)
1492         c.Check(final["container"].(arvadosclient.Dict)["log"], NotNil)
1493
1494         c.Check(api.CalledWith("collection.manifest_text", "./a b1946ac92492d2347c6235b4d2611184+6 0:6:out.txt\n./b 38af5c54926b620264ab1501150cf189+5 0:5:err.txt\n"), NotNil)
1495 }