10793: Ensure on_error is set if not present in job_order_object.
[arvados.git] / sdk / cwl / tests / test_submit.py
1 import copy
2 import cStringIO
3 import functools
4 import hashlib
5 import json
6 import logging
7 import mock
8 import sys
9 import unittest
10
11 import arvados
12 import arvados.collection
13 import arvados_cwl
14 import arvados_cwl.runner
15 import arvados.keep
16
17 from .matcher import JsonDiffMatcher
18 from .mock_discovery import get_rootDesc
19
20 _rootDesc = None
21
22 def stubs(func):
23     @functools.wraps(func)
24     @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
25     @mock.patch("arvados.collection.KeepClient")
26     @mock.patch("arvados.keep.KeepClient")
27     @mock.patch("arvados.events.subscribe")
28     def wrapped(self, events, keep_client1, keep_client2, keepdocker, *args, **kwargs):
29         class Stubs:
30             pass
31         stubs = Stubs()
32         stubs.events = events
33         stubs.keepdocker = keepdocker
34
35
36         def putstub(p, **kwargs):
37             return "%s+%i" % (hashlib.md5(p).hexdigest(), len(p))
38         keep_client1().put.side_effect = putstub
39         keep_client1.put.side_effect = putstub
40         keep_client2().put.side_effect = putstub
41         keep_client2.put.side_effect = putstub
42
43         stubs.keep_client = keep_client2
44         stubs.keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
45         stubs.fake_user_uuid = "zzzzz-tpzed-zzzzzzzzzzzzzzz"
46
47         stubs.api = mock.MagicMock()
48         stubs.api._rootDesc = get_rootDesc()
49
50         stubs.api.users().current().execute.return_value = {
51             "uuid": stubs.fake_user_uuid,
52         }
53         stubs.api.collections().list().execute.return_value = {"items": []}
54         stubs.api.collections().create().execute.side_effect = ({
55             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
56             "portable_data_hash": "99999999999999999999999999999991+99",
57             "manifest_text": ""
58         }, {
59             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
60             "portable_data_hash": "99999999999999999999999999999992+99",
61             "manifest_text": "./tool 00000000000000000000000000000000+0 0:0:submit_tool.cwl 0:0:blub.txt"
62         },
63         {
64             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz4",
65             "portable_data_hash": "99999999999999999999999999999994+99",
66             "manifest_text": ""
67         },
68         {
69             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz5",
70             "portable_data_hash": "99999999999999999999999999999995+99",
71             "manifest_text": ""
72         },
73         {
74             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz6",
75             "portable_data_hash": "99999999999999999999999999999996+99",
76             "manifest_text": ""
77         }
78         )
79         stubs.api.collections().get().execute.return_value = {
80             "portable_data_hash": "99999999999999999999999999999993+99", "manifest_text": "./tool 00000000000000000000000000000000+0 0:0:submit_tool.cwl 0:0:blub.txt"}
81
82         stubs.expect_job_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
83         stubs.api.jobs().create().execute.return_value = {
84             "uuid": stubs.expect_job_uuid,
85             "state": "Queued",
86         }
87
88         stubs.expect_container_request_uuid = "zzzzz-xvhdp-zzzzzzzzzzzzzzz"
89         stubs.api.container_requests().create().execute.return_value = {
90             "uuid": stubs.expect_container_request_uuid,
91             "container_uuid": "zzzzz-dz642-zzzzzzzzzzzzzzz",
92             "state": "Queued"
93         }
94
95         stubs.expect_pipeline_template_uuid = "zzzzz-d1hrv-zzzzzzzzzzzzzzz"
96         stubs.api.pipeline_templates().create().execute.return_value = {
97             "uuid": stubs.expect_pipeline_template_uuid,
98         }
99         stubs.expect_job_spec = {
100             'runtime_constraints': {
101                 'docker_image': 'arvados/jobs:'+arvados_cwl.__version__,
102                 'min_ram_mb_per_node': 1024
103             },
104             'script_parameters': {
105                 'x': {
106                     'basename': 'blorp.txt',
107                     'location': 'keep:99999999999999999999999999999994+99/blorp.txt',
108                     'class': 'File'
109                 },
110                 'y': {
111                     'basename': '99999999999999999999999999999998+99',
112                     'location': 'keep:99999999999999999999999999999998+99',
113                     'class': 'Directory'
114                 },
115                 'z': {
116                     'basename': 'anonymous',
117                     "listing": [{
118                         "basename": "renamed.txt",
119                         "class": "File",
120                         "location": "keep:99999999999999999999999999999998+99/file1.txt"
121                     }],
122                     'class': 'Directory'
123                 },
124                 'cwl:tool':
125                 '99999999999999999999999999999991+99/wf/submit_wf.cwl'
126             },
127             'repository': 'arvados',
128             'script_version': 'master',
129             'minimum_script_version': '570509ab4d2ef93d870fd2b1f2eab178afb1bad9',
130             'script': 'cwl-runner'
131         }
132         stubs.pipeline_component = stubs.expect_job_spec.copy()
133         stubs.expect_pipeline_instance = {
134             'name': 'submit_wf.cwl',
135             'state': 'RunningOnServer',
136             'owner_uuid': None,
137             "components": {
138                 "cwl-runner": {
139                     'runtime_constraints': {'docker_image': 'arvados/jobs:'+arvados_cwl.__version__, 'min_ram_mb_per_node': 1024},
140                     'script_parameters': {
141                         'y': {"value": {'basename': '99999999999999999999999999999998+99', 'location': 'keep:99999999999999999999999999999998+99', 'class': 'Directory'}},
142                         'x': {"value": {'basename': 'blorp.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999994+99/blorp.txt'}},
143                         'z': {"value": {'basename': 'anonymous', 'class': 'Directory',
144                               'listing': [
145                                   {'basename': 'renamed.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999998+99/file1.txt'}
146                               ]}},
147                         'cwl:tool': '99999999999999999999999999999991+99/wf/submit_wf.cwl',
148                         'arv:enable_reuse': True,
149                         'arv:on_error': 'continue'
150                     },
151                     'repository': 'arvados',
152                     'script_version': 'master',
153                     'minimum_script_version': '570509ab4d2ef93d870fd2b1f2eab178afb1bad9',
154                     'script': 'cwl-runner',
155                     'job': {'state': 'Queued', 'uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz'}
156                 }
157             }
158         }
159         stubs.pipeline_create = copy.deepcopy(stubs.expect_pipeline_instance)
160         stubs.expect_pipeline_uuid = "zzzzz-d1hrv-zzzzzzzzzzzzzzz"
161         stubs.pipeline_create["uuid"] = stubs.expect_pipeline_uuid
162         stubs.pipeline_with_job = copy.deepcopy(stubs.pipeline_create)
163         stubs.pipeline_with_job["components"]["cwl-runner"]["job"] = {
164             "uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
165             "state": "Queued"
166         }
167         stubs.api.pipeline_instances().create().execute.return_value = stubs.pipeline_create
168         stubs.api.pipeline_instances().get().execute.return_value = stubs.pipeline_with_job
169
170         stubs.expect_container_spec = {
171             'priority': 1,
172             'mounts': {
173                 '/var/spool/cwl': {
174                     'writable': True,
175                     'kind': 'collection'
176                 },
177                 '/var/lib/cwl/workflow': {
178                     'portable_data_hash': '99999999999999999999999999999991+99',
179                     'kind': 'collection'
180                 },
181                 'stdout': {
182                     'path': '/var/spool/cwl/cwl.output.json',
183                     'kind': 'file'
184                 },
185                 '/var/lib/cwl/cwl.input.json': {
186                     'kind': 'json',
187                     'content': {
188                         'y': {'basename': '99999999999999999999999999999998+99', 'location': 'keep:99999999999999999999999999999998+99', 'class': 'Directory'},
189                         'x': {'basename': u'blorp.txt', 'class': 'File', 'location': u'keep:99999999999999999999999999999994+99/blorp.txt'},
190                         'z': {'basename': 'anonymous', 'class': 'Directory', 'listing': [
191                             {'basename': 'renamed.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999998+99/file1.txt'}
192                         ]}
193                     },
194                     'kind': 'json'
195                 }
196             },
197             'state': 'Committed',
198             'owner_uuid': None,
199             'command': ['arvados-cwl-runner', '--local', '--api=containers', '--no-log-timestamps',
200                         '--enable-reuse', '--on-error=continue',
201                         '/var/lib/cwl/workflow/submit_wf.cwl', '/var/lib/cwl/cwl.input.json'],
202             'name': 'submit_wf.cwl',
203             'container_image': 'arvados/jobs:'+arvados_cwl.__version__,
204             'output_path': '/var/spool/cwl',
205             'cwd': '/var/spool/cwl',
206             'runtime_constraints': {
207                 'API': True,
208                 'vcpus': 1,
209                 'ram': 1024*1024*1024
210             },
211             "properties": {}
212         }
213
214         stubs.expect_workflow_uuid = "zzzzz-7fd4e-zzzzzzzzzzzzzzz"
215         stubs.api.workflows().create().execute.return_value = {
216             "uuid": stubs.expect_workflow_uuid,
217         }
218         def update_mock(**kwargs):
219             stubs.updated_uuid = kwargs.get('uuid')
220             return mock.DEFAULT
221         stubs.api.workflows().update.side_effect = update_mock
222         stubs.api.workflows().update().execute.side_effect = lambda **kwargs: {
223             "uuid": stubs.updated_uuid,
224         }
225
226         return func(self, stubs, *args, **kwargs)
227     return wrapped
228
229
230 class TestSubmit(unittest.TestCase):
231     @mock.patch("time.sleep")
232     @stubs
233     def test_submit(self, stubs, tm):
234         capture_stdout = cStringIO.StringIO()
235         exited = arvados_cwl.main(
236             ["--submit", "--no-wait", "--debug",
237              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
238             capture_stdout, sys.stderr, api_client=stubs.api)
239         self.assertEqual(exited, 0)
240
241         stubs.api.collections().create.assert_has_calls([
242             mock.call(),
243             mock.call(body={
244                 'manifest_text':
245                 './tool d51232d96b6116d964a69bfb7e0c73bf+450 '
246                 '0:16:blub.txt 16:434:submit_tool.cwl\n./wf '
247                 'cc2ffb940e60adf1b2b282c67587e43d+413 0:413:submit_wf.cwl\n',
248                 'owner_uuid': None,
249                 'name': 'submit_wf.cwl',
250             }, ensure_unique_name=True),
251             mock.call().execute(),
252             mock.call(body={'manifest_text': '. d41d8cd98f00b204e9800998ecf8427e+0 '
253                             '0:0:blub.txt 0:0:submit_tool.cwl\n',
254                             'replication_desired': None,
255                             'name': 'New collection'
256             }, ensure_unique_name=True),
257             mock.call().execute(num_retries=4),
258             mock.call(body={
259                 'manifest_text':
260                 '. 979af1245a12a1fed634d4222473bfdc+16 0:16:blorp.txt\n',
261                 'owner_uuid': None,
262                 'name': '#',
263             }, ensure_unique_name=True),
264             mock.call().execute()])
265
266         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
267         stubs.api.pipeline_instances().create.assert_called_with(
268             body=expect_pipeline)
269         self.assertEqual(capture_stdout.getvalue(),
270                          stubs.expect_pipeline_uuid + '\n')
271
272
273     @mock.patch("time.sleep")
274     @stubs
275     def test_submit_no_reuse(self, stubs, tm):
276         capture_stdout = cStringIO.StringIO()
277         exited = arvados_cwl.main(
278             ["--submit", "--no-wait", "--debug", "--disable-reuse",
279              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
280             capture_stdout, sys.stderr, api_client=stubs.api)
281         self.assertEqual(exited, 0)
282
283         stubs.expect_pipeline_instance["components"]["cwl-runner"]["script_parameters"]["arv:enable_reuse"] = {"value": False}
284
285         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
286         stubs.api.pipeline_instances().create.assert_called_with(
287             body=JsonDiffMatcher(expect_pipeline))
288         self.assertEqual(capture_stdout.getvalue(),
289                          stubs.expect_pipeline_uuid + '\n')
290
291     @mock.patch("time.sleep")
292     @stubs
293     def test_submit_on_error(self, stubs, tm):
294         capture_stdout = cStringIO.StringIO()
295         exited = arvados_cwl.main(
296             ["--submit", "--no-wait", "--debug", "--on-error=stop",
297              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
298             capture_stdout, sys.stderr, api_client=stubs.api)
299         self.assertEqual(exited, 0)
300
301         stubs.expect_pipeline_instance["components"]["cwl-runner"]["script_parameters"]["arv:on_error"] = "stop"
302
303         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
304         stubs.api.pipeline_instances().create.assert_called_with(
305             body=JsonDiffMatcher(expect_pipeline))
306         self.assertEqual(capture_stdout.getvalue(),
307                          stubs.expect_pipeline_uuid + '\n')
308
309
310     @mock.patch("time.sleep")
311     @stubs
312     def test_submit_runner_ram(self, stubs, tm):
313         capture_stdout = cStringIO.StringIO()
314         exited = arvados_cwl.main(
315             ["--submit", "--no-wait", "--debug", "--submit-runner-ram=2048",
316              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
317             capture_stdout, sys.stderr, api_client=stubs.api)
318         self.assertEqual(exited, 0)
319
320         stubs.expect_pipeline_instance["components"]["cwl-runner"]["runtime_constraints"]["min_ram_mb_per_node"] = 2048
321
322         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
323         stubs.api.pipeline_instances().create.assert_called_with(
324             body=JsonDiffMatcher(expect_pipeline))
325         self.assertEqual(capture_stdout.getvalue(),
326                          stubs.expect_pipeline_uuid + '\n')
327
328
329     @mock.patch("time.sleep")
330     @stubs
331     def test_submit_invalid_runner_ram(self, stubs, tm):
332         capture_stdout = cStringIO.StringIO()
333         exited = arvados_cwl.main(
334             ["--submit", "--no-wait", "--debug", "--submit-runner-ram=-2048",
335              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
336             capture_stdout, sys.stderr, api_client=stubs.api)
337         self.assertEqual(exited, 1)
338
339     @mock.patch("time.sleep")
340     @stubs
341     def test_submit_output_name(self, stubs, tm):
342         output_name = "test_output_name"
343
344         capture_stdout = cStringIO.StringIO()
345         exited = arvados_cwl.main(
346             ["--submit", "--no-wait", "--debug", "--output-name", output_name,
347              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
348             capture_stdout, sys.stderr, api_client=stubs.api)
349         self.assertEqual(exited, 0)
350
351         stubs.expect_pipeline_instance["components"]["cwl-runner"]["script_parameters"]["arv:output_name"] = output_name
352
353         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
354         stubs.api.pipeline_instances().create.assert_called_with(
355             body=JsonDiffMatcher(expect_pipeline))
356         self.assertEqual(capture_stdout.getvalue(),
357                          stubs.expect_pipeline_uuid + '\n')
358
359
360     @mock.patch("time.sleep")
361     @stubs
362     def test_submit_pipeline_name(self, stubs, tm):
363         capture_stdout = cStringIO.StringIO()
364         exited = arvados_cwl.main(
365             ["--submit", "--no-wait", "--debug", "--name=hello job 123",
366              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
367             capture_stdout, sys.stderr, api_client=stubs.api)
368         self.assertEqual(exited, 0)
369
370         stubs.expect_pipeline_instance["name"] = "hello job 123"
371
372         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
373         stubs.api.pipeline_instances().create.assert_called_with(
374             body=expect_pipeline)
375         self.assertEqual(capture_stdout.getvalue(),
376                          stubs.expect_pipeline_uuid + '\n')
377
378     @mock.patch("time.sleep")
379     @stubs
380     def test_submit_output_tags(self, stubs, tm):
381         output_tags = "tag0,tag1,tag2"
382
383         capture_stdout = cStringIO.StringIO()
384         exited = arvados_cwl.main(
385             ["--submit", "--no-wait", "--debug", "--output-tags", output_tags,
386              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
387             capture_stdout, sys.stderr, api_client=stubs.api)
388         self.assertEqual(exited, 0)
389
390         stubs.expect_pipeline_instance["components"]["cwl-runner"]["script_parameters"]["arv:output_tags"] = output_tags
391
392         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
393         stubs.api.pipeline_instances().create.assert_called_with(
394             body=JsonDiffMatcher(expect_pipeline))
395         self.assertEqual(capture_stdout.getvalue(),
396                          stubs.expect_pipeline_uuid + '\n')
397
398     @mock.patch("time.sleep")
399     @stubs
400     def test_submit_with_project_uuid(self, stubs, tm):
401         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
402
403         exited = arvados_cwl.main(
404             ["--submit", "--no-wait",
405              "--project-uuid", project_uuid,
406              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
407             sys.stdout, sys.stderr, api_client=stubs.api)
408         self.assertEqual(exited, 0)
409
410         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
411         expect_pipeline["owner_uuid"] = project_uuid
412         stubs.api.pipeline_instances().create.assert_called_with(
413             body=JsonDiffMatcher(expect_pipeline))
414
415     @stubs
416     def test_submit_container(self, stubs):
417         capture_stdout = cStringIO.StringIO()
418         try:
419             exited = arvados_cwl.main(
420                 ["--submit", "--no-wait", "--api=containers", "--debug",
421                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
422                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
423             self.assertEqual(exited, 0)
424         except:
425             logging.exception("")
426
427         stubs.api.collections().create.assert_has_calls([
428             mock.call(),
429             mock.call(body={
430                 'manifest_text':
431                 './tool d51232d96b6116d964a69bfb7e0c73bf+450 '
432                 '0:16:blub.txt 16:434:submit_tool.cwl\n./wf '
433                 'cc2ffb940e60adf1b2b282c67587e43d+413 0:413:submit_wf.cwl\n',
434                 'owner_uuid': None,
435                 'name': 'submit_wf.cwl',
436             }, ensure_unique_name=True),
437             mock.call().execute(),
438             mock.call(body={'manifest_text': '. d41d8cd98f00b204e9800998ecf8427e+0 '
439                             '0:0:blub.txt 0:0:submit_tool.cwl\n',
440                             'name': 'New collection',
441                             'replication_desired': None,
442             }, ensure_unique_name=True),
443             mock.call().execute(num_retries=4),
444             mock.call(body={
445                 'manifest_text':
446                 '. 979af1245a12a1fed634d4222473bfdc+16 0:16:blorp.txt\n',
447                 'owner_uuid': None,
448                 'name': '#',
449             }, ensure_unique_name=True),
450             mock.call().execute()])
451
452         expect_container = copy.deepcopy(stubs.expect_container_spec)
453         stubs.api.container_requests().create.assert_called_with(
454             body=JsonDiffMatcher(expect_container))
455         self.assertEqual(capture_stdout.getvalue(),
456                          stubs.expect_container_request_uuid + '\n')
457
458     @stubs
459     def test_submit_container_no_reuse(self, stubs):
460         capture_stdout = cStringIO.StringIO()
461         try:
462             exited = arvados_cwl.main(
463                 ["--submit", "--no-wait", "--api=containers", "--debug", "--disable-reuse",
464                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
465                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
466             self.assertEqual(exited, 0)
467         except:
468             logging.exception("")
469
470         stubs.expect_container_spec["command"] = ['arvados-cwl-runner', '--local', '--api=containers', '--no-log-timestamps',
471                                                   '--disable-reuse', '--on-error=continue',
472                                                   '/var/lib/cwl/workflow/submit_wf.cwl', '/var/lib/cwl/cwl.input.json']
473
474         expect_container = copy.deepcopy(stubs.expect_container_spec)
475         stubs.api.container_requests().create.assert_called_with(
476             body=JsonDiffMatcher(expect_container))
477         self.assertEqual(capture_stdout.getvalue(),
478                          stubs.expect_container_request_uuid + '\n')
479
480
481     @stubs
482     def test_submit_container_on_error(self, stubs):
483         capture_stdout = cStringIO.StringIO()
484         try:
485             exited = arvados_cwl.main(
486                 ["--submit", "--no-wait", "--api=containers", "--debug", "--on-error=stop",
487                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
488                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
489             self.assertEqual(exited, 0)
490         except:
491             logging.exception("")
492
493         stubs.expect_container_spec["command"] = ['arvados-cwl-runner', '--local', '--api=containers', '--no-log-timestamps',
494                                                   '--enable-reuse', '--on-error=stop',
495                                                   '/var/lib/cwl/workflow/submit_wf.cwl', '/var/lib/cwl/cwl.input.json']
496
497         expect_container = copy.deepcopy(stubs.expect_container_spec)
498         stubs.api.container_requests().create.assert_called_with(
499             body=JsonDiffMatcher(expect_container))
500         self.assertEqual(capture_stdout.getvalue(),
501                          stubs.expect_container_request_uuid + '\n')
502
503     @stubs
504     def test_submit_container_output_name(self, stubs):
505         output_name = "test_output_name"
506
507         capture_stdout = cStringIO.StringIO()
508         try:
509             exited = arvados_cwl.main(
510                 ["--submit", "--no-wait", "--api=containers", "--debug", "--output-name", output_name,
511                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
512                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
513             self.assertEqual(exited, 0)
514         except:
515             logging.exception("")
516
517         stubs.expect_container_spec["command"] = ['arvados-cwl-runner', '--local', '--api=containers', '--no-log-timestamps',
518                                                   "--output-name="+output_name, '--enable-reuse', '--on-error=continue',
519                                                   '/var/lib/cwl/workflow/submit_wf.cwl', '/var/lib/cwl/cwl.input.json']
520
521         expect_container = copy.deepcopy(stubs.expect_container_spec)
522         stubs.api.container_requests().create.assert_called_with(
523             body=JsonDiffMatcher(expect_container))
524         self.assertEqual(capture_stdout.getvalue(),
525                          stubs.expect_container_request_uuid + '\n')
526
527     @stubs
528     def test_submit_container_output_tags(self, stubs):
529         output_tags = "tag0,tag1,tag2"
530
531         capture_stdout = cStringIO.StringIO()
532         try:
533             exited = arvados_cwl.main(
534                 ["--submit", "--no-wait", "--api=containers", "--debug", "--output-tags", output_tags,
535                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
536                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
537             self.assertEqual(exited, 0)
538         except:
539             logging.exception("")
540
541         stubs.expect_container_spec["command"] = ['arvados-cwl-runner', '--local', '--api=containers', '--no-log-timestamps',
542                                                   "--output-tags="+output_tags, '--enable-reuse', '--on-error=continue',
543                                                   '/var/lib/cwl/workflow/submit_wf.cwl', '/var/lib/cwl/cwl.input.json']
544
545         expect_container = copy.deepcopy(stubs.expect_container_spec)
546         stubs.api.container_requests().create.assert_called_with(
547             body=JsonDiffMatcher(expect_container))
548         self.assertEqual(capture_stdout.getvalue(),
549                          stubs.expect_container_request_uuid + '\n')
550
551     @stubs
552     def test_submit_container_runner_ram(self, stubs):
553         capture_stdout = cStringIO.StringIO()
554         try:
555             exited = arvados_cwl.main(
556                 ["--submit", "--no-wait", "--api=containers", "--debug", "--submit-runner-ram=2048",
557                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
558                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
559             self.assertEqual(exited, 0)
560         except:
561             logging.exception("")
562
563         stubs.expect_container_spec["runtime_constraints"]["ram"] = 2048*1024*1024
564
565         expect_container = copy.deepcopy(stubs.expect_container_spec)
566         stubs.api.container_requests().create.assert_called_with(
567             body=JsonDiffMatcher(expect_container))
568         self.assertEqual(capture_stdout.getvalue(),
569                          stubs.expect_container_request_uuid + '\n')
570
571     @mock.patch("arvados.collection.CollectionReader")
572     @mock.patch("time.sleep")
573     @stubs
574     def test_submit_file_keepref(self, stubs, tm, collectionReader):
575         capture_stdout = cStringIO.StringIO()
576         exited = arvados_cwl.main(
577             ["--submit", "--no-wait", "--api=containers", "--debug",
578              "tests/wf/submit_keepref_wf.cwl"],
579             capture_stdout, sys.stderr, api_client=stubs.api)
580         self.assertEqual(exited, 0)
581
582
583     @mock.patch("arvados.collection.CollectionReader")
584     @mock.patch("time.sleep")
585     @stubs
586     def test_submit_keepref(self, stubs, tm, reader):
587         capture_stdout = cStringIO.StringIO()
588
589         with open("tests/wf/expect_arvworkflow.cwl") as f:
590             reader().open().__enter__().read.return_value = f.read()
591
592         exited = arvados_cwl.main(
593             ["--submit", "--no-wait", "--api=containers", "--debug",
594              "keep:99999999999999999999999999999994+99/expect_arvworkflow.cwl#main", "-x", "XxX"],
595             capture_stdout, sys.stderr, api_client=stubs.api)
596         self.assertEqual(exited, 0)
597
598         expect_container = {
599             'priority': 1,
600             'mounts': {
601                 '/var/spool/cwl': {
602                     'writable': True,
603                     'kind': 'collection'
604                 },
605                 'stdout': {
606                     'path': '/var/spool/cwl/cwl.output.json',
607                     'kind': 'file'
608                 },
609                 '/var/lib/cwl/workflow': {
610                     'portable_data_hash': '99999999999999999999999999999994+99',
611                     'kind': 'collection'
612                 },
613                 '/var/lib/cwl/cwl.input.json': {
614                     'content': {
615                         'x': 'XxX'
616                     },
617                     'kind': 'json'
618                 }
619             }, 'state': 'Committed',
620             'owner_uuid': None,
621             'output_path': '/var/spool/cwl',
622             'name': 'expect_arvworkflow.cwl#main',
623             'container_image': 'arvados/jobs:'+arvados_cwl.__version__,
624             'command': ['arvados-cwl-runner', '--local', '--api=containers', '--no-log-timestamps',
625                         '--enable-reuse', '--on-error=continue',
626                         '/var/lib/cwl/workflow/expect_arvworkflow.cwl#main', '/var/lib/cwl/cwl.input.json'],
627             'cwd': '/var/spool/cwl',
628             'runtime_constraints': {
629                 'API': True,
630                 'vcpus': 1,
631                 'ram': 1073741824
632             },
633             "properties": {}
634         }
635
636         stubs.api.container_requests().create.assert_called_with(
637             body=JsonDiffMatcher(expect_container))
638         self.assertEqual(capture_stdout.getvalue(),
639                          stubs.expect_container_request_uuid + '\n')
640
641     @mock.patch("time.sleep")
642     @stubs
643     def test_submit_arvworkflow(self, stubs, tm):
644         capture_stdout = cStringIO.StringIO()
645
646         with open("tests/wf/expect_arvworkflow.cwl") as f:
647             stubs.api.workflows().get().execute.return_value = {"definition": f.read(), "name": "a test workflow"}
648
649         exited = arvados_cwl.main(
650             ["--submit", "--no-wait", "--api=containers", "--debug",
651              "962eh-7fd4e-gkbzl62qqtfig37", "-x", "XxX"],
652             capture_stdout, sys.stderr, api_client=stubs.api)
653         self.assertEqual(exited, 0)
654
655         expect_container = {
656             'priority': 1,
657             'mounts': {
658                 '/var/spool/cwl': {
659                     'writable': True,
660                     'kind': 'collection'
661                 },
662                 'stdout': {
663                     'path': '/var/spool/cwl/cwl.output.json',
664                     'kind': 'file'
665                 },
666                 '/var/lib/cwl/workflow.json': {
667                     'kind': 'json',
668                     'json': {
669                         'cwlVersion': 'v1.0',
670                         '$graph': [
671                             {
672                                 'inputs': [
673                                     {
674                                         'inputBinding': {'position': 1},
675                                         'type': 'string',
676                                         'id': '#submit_tool.cwl/x'}
677                                 ],
678                                 'requirements': [
679                                     {'dockerPull': 'debian:8', 'class': 'DockerRequirement'}
680                                 ],
681                                 'id': '#submit_tool.cwl',
682                                 'outputs': [],
683                                 'baseCommand': 'cat',
684                                 'class': 'CommandLineTool'
685                             }, {
686                                 'id': '#main',
687                                 'inputs': [
688                                     {'type': 'string', 'id': '#main/x'}
689                                 ],
690                                 'steps': [
691                                     {'in': [{'source': '#main/x', 'id': '#main/step1/x'}],
692                                      'run': '#submit_tool.cwl',
693                                      'id': '#main/step1',
694                                      'out': []}
695                                 ],
696                                 'class': 'Workflow',
697                                 'outputs': []
698                             }
699                         ]
700                     }
701                 },
702                 '/var/lib/cwl/cwl.input.json': {
703                     'content': {
704                         'x': 'XxX'
705                     },
706                     'kind': 'json'
707                 }
708             }, 'state': 'Committed',
709             'owner_uuid': None,
710             'output_path': '/var/spool/cwl',
711             'name': 'a test workflow',
712             'container_image': 'arvados/jobs:'+arvados_cwl.__version__,
713             'command': ['arvados-cwl-runner', '--local', '--api=containers', '--no-log-timestamps',
714                         '--enable-reuse', '--on-error=continue',
715                         '/var/lib/cwl/workflow.json#main', '/var/lib/cwl/cwl.input.json'],
716             'cwd': '/var/spool/cwl',
717             'runtime_constraints': {
718                 'API': True,
719                 'vcpus': 1,
720                 'ram': 1073741824
721             },
722             "properties": {
723                 "template_uuid": "962eh-7fd4e-gkbzl62qqtfig37"
724             }
725         }
726
727         stubs.api.container_requests().create.assert_called_with(
728             body=JsonDiffMatcher(expect_container))
729         self.assertEqual(capture_stdout.getvalue(),
730                          stubs.expect_container_request_uuid + '\n')
731
732
733     @stubs
734     def test_submit_container_name(self, stubs):
735         capture_stdout = cStringIO.StringIO()
736         try:
737             exited = arvados_cwl.main(
738                 ["--submit", "--no-wait", "--api=containers", "--debug", "--name=hello container 123",
739                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
740                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
741             self.assertEqual(exited, 0)
742         except:
743             logging.exception("")
744
745         stubs.expect_container_spec["name"] = "hello container 123"
746
747         expect_container = copy.deepcopy(stubs.expect_container_spec)
748         stubs.api.container_requests().create.assert_called_with(
749             body=expect_container)
750         self.assertEqual(capture_stdout.getvalue(),
751                          stubs.expect_container_request_uuid + '\n')
752
753
754     @mock.patch("arvados.commands.keepdocker.find_one_image_hash")
755     @mock.patch("cwltool.docker.get_image")
756     @mock.patch("arvados.api")
757     def test_arvados_jobs_image(self, api, get_image, find_one_image_hash):
758         arvrunner = mock.MagicMock()
759         arvrunner.project_uuid = ""
760         api.return_value = mock.MagicMock()
761         arvrunner.api = api.return_value
762         arvrunner.api.links().list().execute.side_effect = ({"items": [], "items_available": 0, "offset": 0},
763                                                             {"items": [], "items_available": 0, "offset": 0},
764                                                             {"items": [], "items_available": 0, "offset": 0},
765                                                             {"items": [{"created_at": "",
766                                                                         "head_uuid": "",
767                                                                         "link_class": "docker_image_hash",
768                                                                         "name": "123456",
769                                                                         "owner_uuid": "",
770                                                                         "properties": {"image_timestamp": ""}}], "items_available": 1, "offset": 0},
771                                                             {"items": [], "items_available": 0, "offset": 0},
772                                                             {"items": [{"created_at": "",
773                                                                         "head_uuid": "",
774                                                                         "link_class": "docker_image_repo+tag",
775                                                                         "name": "arvados/jobs:"+arvados_cwl.__version__,
776                                                                         "owner_uuid": "",
777                                                                         "properties": {"image_timestamp": ""}}], "items_available": 1, "offset": 0},
778                                                             {"items": [{"created_at": "",
779                                                                         "head_uuid": "",
780                                                                         "link_class": "docker_image_hash",
781                                                                         "name": "123456",
782                                                                         "owner_uuid": "",
783                                                                         "properties": {"image_timestamp": ""}}], "items_available": 1, "offset": 0}                                                            ,
784         )
785         find_one_image_hash.return_value = "123456"
786
787         arvrunner.api.collections().list().execute.side_effect = ({"items": [], "items_available": 0, "offset": 0},
788                                                                   {"items": [{"uuid": "",
789                                                                               "owner_uuid": "",
790                                                                               "manifest_text": "",
791                                                                               "properties": ""
792                                                                           }], "items_available": 1, "offset": 0},
793                                                                   {"items": [{"uuid": ""}], "items_available": 1, "offset": 0})
794         arvrunner.api.collections().create().execute.return_value = {"uuid": ""}
795         self.assertEqual("arvados/jobs:"+arvados_cwl.__version__, arvados_cwl.runner.arvados_jobs_image(arvrunner))
796
797 class TestCreateTemplate(unittest.TestCase):
798     existing_template_uuid = "zzzzz-d1hrv-validworkfloyml"
799
800     def _adjust_script_params(self, expect_component):
801         expect_component['script_parameters']['x'] = {
802             'dataclass': 'File',
803             'required': True,
804             'type': 'File',
805             'value': '99999999999999999999999999999994+99/blorp.txt',
806         }
807         expect_component['script_parameters']['y'] = {
808             'dataclass': 'Collection',
809             'required': True,
810             'type': 'Directory',
811             'value': '99999999999999999999999999999998+99',
812         }
813         expect_component['script_parameters']['z'] = {
814             'dataclass': 'Collection',
815             'required': True,
816             'type': 'Directory',
817         }
818
819     @stubs
820     def test_create(self, stubs):
821         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
822
823         capture_stdout = cStringIO.StringIO()
824
825         exited = arvados_cwl.main(
826             ["--create-workflow", "--debug",
827              "--api=jobs",
828              "--project-uuid", project_uuid,
829              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
830             capture_stdout, sys.stderr, api_client=stubs.api)
831         self.assertEqual(exited, 0)
832
833         stubs.api.pipeline_instances().create.refute_called()
834         stubs.api.jobs().create.refute_called()
835
836         expect_component = copy.deepcopy(stubs.expect_job_spec)
837         self._adjust_script_params(expect_component)
838         expect_template = {
839             "components": {
840                 "submit_wf.cwl": expect_component,
841             },
842             "name": "submit_wf.cwl",
843             "owner_uuid": project_uuid,
844         }
845         stubs.api.pipeline_templates().create.assert_called_with(
846             body=JsonDiffMatcher(expect_template), ensure_unique_name=True)
847
848         self.assertEqual(capture_stdout.getvalue(),
849                          stubs.expect_pipeline_template_uuid + '\n')
850
851
852     @stubs
853     def test_create_name(self, stubs):
854         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
855
856         capture_stdout = cStringIO.StringIO()
857
858         exited = arvados_cwl.main(
859             ["--create-workflow", "--debug",
860              "--project-uuid", project_uuid,
861              "--api=jobs",
862              "--name", "testing 123",
863              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
864             capture_stdout, sys.stderr, api_client=stubs.api)
865         self.assertEqual(exited, 0)
866
867         stubs.api.pipeline_instances().create.refute_called()
868         stubs.api.jobs().create.refute_called()
869
870         expect_component = copy.deepcopy(stubs.expect_job_spec)
871         self._adjust_script_params(expect_component)
872         expect_template = {
873             "components": {
874                 "testing 123": expect_component,
875             },
876             "name": "testing 123",
877             "owner_uuid": project_uuid,
878         }
879         stubs.api.pipeline_templates().create.assert_called_with(
880             body=JsonDiffMatcher(expect_template), ensure_unique_name=True)
881
882         self.assertEqual(capture_stdout.getvalue(),
883                          stubs.expect_pipeline_template_uuid + '\n')
884
885
886     @stubs
887     def test_update_name(self, stubs):
888         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
889
890         capture_stdout = cStringIO.StringIO()
891
892         exited = arvados_cwl.main(
893             ["--update-workflow", self.existing_template_uuid,
894              "--debug",
895              "--project-uuid", project_uuid,
896              "--api=jobs",
897              "--name", "testing 123",
898              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
899             capture_stdout, sys.stderr, api_client=stubs.api)
900         self.assertEqual(exited, 0)
901
902         stubs.api.pipeline_instances().create.refute_called()
903         stubs.api.jobs().create.refute_called()
904
905         expect_component = copy.deepcopy(stubs.expect_job_spec)
906         self._adjust_script_params(expect_component)
907         expect_template = {
908             "components": {
909                 "testing 123": expect_component,
910             },
911             "name": "testing 123",
912             "owner_uuid": project_uuid,
913         }
914         stubs.api.pipeline_templates().create.refute_called()
915         stubs.api.pipeline_templates().update.assert_called_with(
916             body=JsonDiffMatcher(expect_template), uuid=self.existing_template_uuid)
917
918         self.assertEqual(capture_stdout.getvalue(),
919                          self.existing_template_uuid + '\n')
920
921
922 class TestCreateWorkflow(unittest.TestCase):
923     existing_workflow_uuid = "zzzzz-7fd4e-validworkfloyml"
924     expect_workflow = open("tests/wf/expect_packed.cwl").read()
925
926     @stubs
927     def test_create(self, stubs):
928         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
929
930         capture_stdout = cStringIO.StringIO()
931
932         exited = arvados_cwl.main(
933             ["--create-workflow", "--debug",
934              "--api=containers",
935              "--project-uuid", project_uuid,
936              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
937             capture_stdout, sys.stderr, api_client=stubs.api)
938         self.assertEqual(exited, 0)
939
940         stubs.api.pipeline_templates().create.refute_called()
941         stubs.api.container_requests().create.refute_called()
942
943         body = {
944             "workflow": {
945                 "owner_uuid": project_uuid,
946                 "name": "submit_wf.cwl",
947                 "description": "",
948                 "definition": self.expect_workflow,
949             }
950         }
951         stubs.api.workflows().create.assert_called_with(
952             body=JsonDiffMatcher(body))
953
954         self.assertEqual(capture_stdout.getvalue(),
955                          stubs.expect_workflow_uuid + '\n')
956
957
958     @stubs
959     def test_create_name(self, stubs):
960         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
961
962         capture_stdout = cStringIO.StringIO()
963
964         exited = arvados_cwl.main(
965             ["--create-workflow", "--debug",
966              "--api=containers",
967              "--project-uuid", project_uuid,
968              "--name", "testing 123",
969              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
970             capture_stdout, sys.stderr, api_client=stubs.api)
971         self.assertEqual(exited, 0)
972
973         stubs.api.pipeline_templates().create.refute_called()
974         stubs.api.container_requests().create.refute_called()
975
976         body = {
977             "workflow": {
978                 "owner_uuid": project_uuid,
979                 "name": "testing 123",
980                 "description": "",
981                 "definition": self.expect_workflow,
982             }
983         }
984         stubs.api.workflows().create.assert_called_with(
985             body=JsonDiffMatcher(body))
986
987         self.assertEqual(capture_stdout.getvalue(),
988                          stubs.expect_workflow_uuid + '\n')
989
990     @stubs
991     def test_incompatible_api(self, stubs):
992         capture_stderr = cStringIO.StringIO()
993         logging.getLogger('arvados.cwl-runner').addHandler(
994             logging.StreamHandler(capture_stderr))
995
996         exited = arvados_cwl.main(
997             ["--update-workflow", self.existing_workflow_uuid,
998              "--api=jobs",
999              "--debug",
1000              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
1001             sys.stderr, sys.stderr, api_client=stubs.api)
1002         self.assertEqual(exited, 1)
1003         self.assertRegexpMatches(
1004             capture_stderr.getvalue(),
1005             "--update-workflow arg '{}' uses 'containers' API, but --api='jobs' specified".format(self.existing_workflow_uuid))
1006
1007     @stubs
1008     def test_update(self, stubs):
1009         capture_stdout = cStringIO.StringIO()
1010
1011         exited = arvados_cwl.main(
1012             ["--update-workflow", self.existing_workflow_uuid,
1013              "--debug",
1014              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
1015             capture_stdout, sys.stderr, api_client=stubs.api)
1016         self.assertEqual(exited, 0)
1017
1018         body = {
1019             "workflow": {
1020                 "name": "submit_wf.cwl",
1021                 "description": "",
1022                 "definition": self.expect_workflow,
1023             }
1024         }
1025         stubs.api.workflows().update.assert_called_with(
1026             uuid=self.existing_workflow_uuid,
1027             body=JsonDiffMatcher(body))
1028         self.assertEqual(capture_stdout.getvalue(),
1029                          self.existing_workflow_uuid + '\n')
1030
1031
1032     @stubs
1033     def test_update_name(self, stubs):
1034         capture_stdout = cStringIO.StringIO()
1035
1036         exited = arvados_cwl.main(
1037             ["--update-workflow", self.existing_workflow_uuid,
1038              "--debug", "--name", "testing 123",
1039              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
1040             capture_stdout, sys.stderr, api_client=stubs.api)
1041         self.assertEqual(exited, 0)
1042
1043         body = {
1044             "workflow": {
1045                 "name": "testing 123",
1046                 "description": "",
1047                 "definition": self.expect_workflow,
1048             }
1049         }
1050         stubs.api.workflows().update.assert_called_with(
1051             uuid=self.existing_workflow_uuid,
1052             body=JsonDiffMatcher(body))
1053         self.assertEqual(capture_stdout.getvalue(),
1054                          self.existing_workflow_uuid + '\n')
1055
1056
1057 class TestTemplateInputs(unittest.TestCase):
1058     expect_template = {
1059         "components": {
1060             "inputs_test.cwl": {
1061                 'runtime_constraints': {
1062                     'docker_image': 'arvados/jobs:'+arvados_cwl.__version__,
1063                     'min_ram_mb_per_node': 1024
1064                 },
1065                 'script_parameters': {
1066                     'cwl:tool':
1067                     '99999999999999999999999999999991+99/'
1068                     'wf/inputs_test.cwl',
1069                     'optionalFloatInput': None,
1070                     'fileInput': {
1071                         'type': 'File',
1072                         'dataclass': 'File',
1073                         'required': True,
1074                         'title': "It's a file; we expect to find some characters in it.",
1075                         'description': 'If there were anything further to say, it would be said here,\nor here.'
1076                     },
1077                     'floatInput': {
1078                         'type': 'float',
1079                         'dataclass': 'number',
1080                         'required': True,
1081                         'title': 'Floats like a duck',
1082                         'default': 0.1,
1083                         'value': 0.1,
1084                     },
1085                     'optionalFloatInput': {
1086                         'type': ['null', 'float'],
1087                         'dataclass': 'number',
1088                         'required': False,
1089                     },
1090                     'boolInput': {
1091                         'type': 'boolean',
1092                         'dataclass': 'boolean',
1093                         'required': True,
1094                         'title': 'True or false?',
1095                     },
1096                 },
1097                 'repository': 'arvados',
1098                 'script_version': 'master',
1099                 'minimum_script_version': '570509ab4d2ef93d870fd2b1f2eab178afb1bad9',
1100                 'script': 'cwl-runner',
1101             },
1102         },
1103         "name": "inputs_test.cwl",
1104     }
1105
1106     @stubs
1107     def test_inputs_empty(self, stubs):
1108         exited = arvados_cwl.main(
1109             ["--create-template",
1110              "tests/wf/inputs_test.cwl", "tests/order/empty_order.json"],
1111             cStringIO.StringIO(), sys.stderr, api_client=stubs.api)
1112         self.assertEqual(exited, 0)
1113
1114         stubs.api.pipeline_templates().create.assert_called_with(
1115             body=JsonDiffMatcher(self.expect_template), ensure_unique_name=True)
1116
1117     @stubs
1118     def test_inputs(self, stubs):
1119         exited = arvados_cwl.main(
1120             ["--create-template",
1121              "tests/wf/inputs_test.cwl", "tests/order/inputs_test_order.json"],
1122             cStringIO.StringIO(), sys.stderr, api_client=stubs.api)
1123         self.assertEqual(exited, 0)
1124
1125         expect_template = copy.deepcopy(self.expect_template)
1126         params = expect_template[
1127             "components"]["inputs_test.cwl"]["script_parameters"]
1128         params["fileInput"]["value"] = '99999999999999999999999999999994+99/blorp.txt'
1129         params["floatInput"]["value"] = 1.234
1130         params["boolInput"]["value"] = True
1131
1132         stubs.api.pipeline_templates().create.assert_called_with(
1133             body=JsonDiffMatcher(expect_template), ensure_unique_name=True)