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