Merge branch '10291-discovery-blacklist' refs #10291
[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.keep
15
16 from .matcher import JsonDiffMatcher
17
18
19 def stubs(func):
20     @functools.wraps(func)
21     @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
22     @mock.patch("arvados.collection.KeepClient")
23     @mock.patch("arvados.events.subscribe")
24     def wrapped(self, events, keep_client, keepdocker, *args, **kwargs):
25         class Stubs:
26             pass
27         stubs = Stubs()
28         stubs.events = events
29         stubs.keepdocker = keepdocker
30         stubs.keep_client = keep_client
31
32         def putstub(p, **kwargs):
33             return "%s+%i" % (hashlib.md5(p).hexdigest(), len(p))
34         stubs.keep_client().put.side_effect = putstub
35         stubs.keep_client.put.side_effect = putstub
36
37         stubs.keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
38         stubs.fake_user_uuid = "zzzzz-tpzed-zzzzzzzzzzzzzzz"
39
40
41         stubs.api = mock.MagicMock()
42         stubs.api._rootDesc = arvados.api('v1')._rootDesc
43         stubs.api.users().current().execute.return_value = {
44             "uuid": stubs.fake_user_uuid,
45         }
46         stubs.api.collections().list().execute.return_value = {"items": []}
47         stubs.api.collections().create().execute.side_effect = ({
48             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
49             "portable_data_hash": "99999999999999999999999999999991+99",
50             "manifest_text": ""
51         }, {
52             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
53             "portable_data_hash": "99999999999999999999999999999992+99",
54             "manifest_text": "./tool 00000000000000000000000000000000+0 0:0:submit_tool.cwl 0:0:blub.txt"
55         },
56         {
57             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz4",
58             "portable_data_hash": "99999999999999999999999999999994+99",
59             "manifest_text": ""
60         },
61         {
62             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz5",
63             "portable_data_hash": "99999999999999999999999999999995+99",
64             "manifest_text": ""
65         },
66         {
67             "uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz6",
68             "portable_data_hash": "99999999999999999999999999999996+99",
69             "manifest_text": ""
70         }
71         )
72         stubs.api.collections().get().execute.return_value = {
73             "portable_data_hash": "99999999999999999999999999999993+99", "manifest_text": "./tool 00000000000000000000000000000000+0 0:0:submit_tool.cwl 0:0:blub.txt"}
74
75         stubs.expect_job_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
76         stubs.api.jobs().create().execute.return_value = {
77             "uuid": stubs.expect_job_uuid,
78             "state": "Queued",
79         }
80
81         stubs.expect_container_request_uuid = "zzzzz-xvhdp-zzzzzzzzzzzzzzz"
82         stubs.api.container_requests().create().execute.return_value = {
83             "uuid": stubs.expect_container_request_uuid,
84             "container_uuid": "zzzzz-dz642-zzzzzzzzzzzzzzz",
85             "state": "Queued"
86         }
87
88         stubs.expect_pipeline_template_uuid = "zzzzz-d1hrv-zzzzzzzzzzzzzzz"
89         stubs.api.pipeline_templates().create().execute.return_value = {
90             "uuid": stubs.expect_pipeline_template_uuid,
91         }
92         stubs.expect_job_spec = {
93             'runtime_constraints': {
94                 'docker_image': 'arvados/jobs'
95             },
96             'script_parameters': {
97                 'x': {
98                     'basename': 'blorp.txt',
99                     'location': 'keep:99999999999999999999999999999994+99/blorp.txt',
100                     'class': 'File'
101                 },
102                 'y': {
103                     'basename': '99999999999999999999999999999998+99',
104                     'location': 'keep:99999999999999999999999999999998+99',
105                     'class': 'Directory'
106                 },
107                 'z': {
108                     'basename': 'anonymous',
109                     "listing": [{
110                         "basename": "renamed.txt",
111                         "class": "File",
112                         "location": "keep:99999999999999999999999999999998+99/file1.txt"
113                     }],
114                     'class': 'Directory'
115                 },
116                 'cwl:tool':
117                 '99999999999999999999999999999991+99/wf/submit_wf.cwl'
118             },
119             'repository': 'arvados',
120             'script_version': 'master',
121             'script': 'cwl-runner'
122         }
123         stubs.pipeline_component = stubs.expect_job_spec.copy()
124         stubs.expect_pipeline_instance = {
125             'name': 'submit_wf.cwl',
126             'state': 'RunningOnServer',
127             "components": {
128                 "cwl-runner": {
129                     'runtime_constraints': {'docker_image': 'arvados/jobs'},
130                     'script_parameters': {
131                         'y': {"value": {'basename': '99999999999999999999999999999998+99', 'location': 'keep:99999999999999999999999999999998+99', 'class': 'Directory'}},
132                         'x': {"value": {'basename': 'blorp.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999994+99/blorp.txt'}},
133                         'z': {"value": {'basename': 'anonymous', 'class': 'Directory',
134                               'listing': [
135                                   {'basename': 'renamed.txt', 'class': 'File', 'location': 'keep:99999999999999999999999999999998+99/file1.txt'}
136                               ]}},
137                         'cwl:tool': '99999999999999999999999999999991+99/wf/submit_wf.cwl'
138                     },
139                     'repository': 'arvados',
140                     'script_version': 'master',
141                     'script': 'cwl-runner'
142                 }
143             }
144         }
145         stubs.pipeline_create = copy.deepcopy(stubs.expect_pipeline_instance)
146         stubs.expect_pipeline_uuid = "zzzzz-d1hrv-zzzzzzzzzzzzzzz"
147         stubs.pipeline_create["uuid"] = stubs.expect_pipeline_uuid
148         stubs.pipeline_with_job = copy.deepcopy(stubs.pipeline_create)
149         stubs.pipeline_with_job["components"]["cwl-runner"]["job"] = {
150             "uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
151             "state": "Queued"
152         }
153         stubs.api.pipeline_instances().create().execute.return_value = stubs.pipeline_create
154         stubs.api.pipeline_instances().get().execute.return_value = stubs.pipeline_with_job
155
156         stubs.expect_container_spec = {
157             'priority': 1,
158             'mounts': {
159                 '/var/spool/cwl': {
160                     'writable': True,
161                     'kind': 'collection'
162                 },
163                 '/var/lib/cwl/workflow': {
164                     'portable_data_hash': '99999999999999999999999999999991+99',
165                     'kind': 'collection'
166                 },
167                 'stdout': {
168                     'path': '/var/spool/cwl/cwl.output.json',
169                     'kind': 'file'
170                 },
171                 '/var/lib/cwl/job/cwl.input.json': {
172                     'portable_data_hash': 'd20d7cddd1984f105dd3702c7f125afb+60/cwl.input.json',
173                     'kind': 'collection'
174                 }
175             },
176             'state': 'Committed',
177             'owner_uuid': 'zzzzz-tpzed-zzzzzzzzzzzzzzz',
178             'command': ['arvados-cwl-runner', '--local', '--api=containers', '/var/lib/cwl/workflow/submit_wf.cwl', '/var/lib/cwl/job/cwl.input.json'],
179             'name': 'submit_wf.cwl',
180             'container_image': '99999999999999999999999999999993+99',
181             'output_path': '/var/spool/cwl',
182             'cwd': '/var/spool/cwl',
183             'runtime_constraints': {
184                 'API': True,
185                 'vcpus': 1,
186                 'ram': 268435456
187             }
188         }
189
190         stubs.expect_workflow_uuid = "zzzzz-7fd4e-zzzzzzzzzzzzzzz"
191         stubs.api.workflows().create().execute.return_value = {
192             "uuid": stubs.expect_workflow_uuid,
193         }
194
195         return func(self, stubs, *args, **kwargs)
196     return wrapped
197
198
199 class TestSubmit(unittest.TestCase):
200     @mock.patch("time.sleep")
201     @stubs
202     def test_submit(self, stubs, tm):
203         capture_stdout = cStringIO.StringIO()
204         exited = arvados_cwl.main(
205             ["--submit", "--no-wait", "--debug",
206              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
207             capture_stdout, sys.stderr, api_client=stubs.api)
208         self.assertEqual(exited, 0)
209
210         stubs.api.collections().create.assert_has_calls([
211             mock.call(),
212             mock.call(body={
213                 'manifest_text':
214                 './tool d51232d96b6116d964a69bfb7e0c73bf+450 '
215                 '0:16:blub.txt 16:434:submit_tool.cwl\n./wf '
216                 'cc2ffb940e60adf1b2b282c67587e43d+413 0:413:submit_wf.cwl\n',
217                 'owner_uuid': 'zzzzz-tpzed-zzzzzzzzzzzzzzz',
218                 'name': 'submit_wf.cwl',
219             }, ensure_unique_name=True),
220             mock.call().execute(),
221             mock.call(body={'manifest_text': '. d41d8cd98f00b204e9800998ecf8427e+0 '
222                             '0:0:blub.txt 0:0:submit_tool.cwl\n',
223                             'owner_uuid': 'zzzzz-tpzed-zzzzzzzzzzzzzzz',
224                             'replication_desired': None,
225                             'name': 'New collection'
226             }, ensure_unique_name=True),
227             mock.call().execute(num_retries=4),
228             mock.call(body={
229                 'manifest_text':
230                 '. 979af1245a12a1fed634d4222473bfdc+16 0:16:blorp.txt\n',
231                 'owner_uuid': 'zzzzz-tpzed-zzzzzzzzzzzzzzz',
232                 'name': '#',
233             }, ensure_unique_name=True),
234             mock.call().execute()])
235
236         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
237         expect_pipeline["owner_uuid"] = stubs.fake_user_uuid
238         stubs.api.pipeline_instances().create.assert_called_with(
239             body=expect_pipeline)
240         self.assertEqual(capture_stdout.getvalue(),
241                          stubs.expect_pipeline_uuid + '\n')
242
243     @mock.patch("time.sleep")
244     @stubs
245     def test_submit_with_project_uuid(self, stubs, tm):
246         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
247
248         exited = arvados_cwl.main(
249             ["--submit", "--no-wait",
250              "--project-uuid", project_uuid,
251              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
252             sys.stdout, sys.stderr, api_client=stubs.api)
253         self.assertEqual(exited, 0)
254
255         expect_pipeline = copy.deepcopy(stubs.expect_pipeline_instance)
256         expect_pipeline["owner_uuid"] = project_uuid
257         stubs.api.pipeline_instances().create.assert_called_with(
258             body=expect_pipeline)
259
260     @stubs
261     def test_submit_container(self, stubs):
262         capture_stdout = cStringIO.StringIO()
263         try:
264             exited = arvados_cwl.main(
265                 ["--submit", "--no-wait", "--api=containers", "--debug",
266                  "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
267                 capture_stdout, sys.stderr, api_client=stubs.api, keep_client=stubs.keep_client)
268             self.assertEqual(exited, 0)
269         except:
270             logging.exception("")
271
272         stubs.api.collections().create.assert_has_calls([
273             mock.call(),
274             mock.call(body={
275                 'manifest_text':
276                 './tool d51232d96b6116d964a69bfb7e0c73bf+450 '
277                 '0:16:blub.txt 16:434:submit_tool.cwl\n./wf '
278                 'cc2ffb940e60adf1b2b282c67587e43d+413 0:413:submit_wf.cwl\n',
279                 'owner_uuid': 'zzzzz-tpzed-zzzzzzzzzzzzzzz',
280                 'name': 'submit_wf.cwl',
281             }, ensure_unique_name=True),
282             mock.call().execute(),
283             mock.call(body={'manifest_text': '. d41d8cd98f00b204e9800998ecf8427e+0 '
284                             '0:0:blub.txt 0:0:submit_tool.cwl\n',
285                             'owner_uuid': 'zzzzz-tpzed-zzzzzzzzzzzzzzz',
286                             'name': 'New collection',
287                             'replication_desired': None,
288             }, ensure_unique_name=True),
289             mock.call().execute(num_retries=4),
290             mock.call(body={
291                 'manifest_text':
292                 '. 979af1245a12a1fed634d4222473bfdc+16 0:16:blorp.txt\n',
293                 'owner_uuid': 'zzzzz-tpzed-zzzzzzzzzzzzzzz',
294                 'name': '#',
295             }, ensure_unique_name=True),
296             mock.call().execute()])
297
298         expect_container = copy.deepcopy(stubs.expect_container_spec)
299         expect_container["owner_uuid"] = stubs.fake_user_uuid
300         stubs.api.container_requests().create.assert_called_with(
301             body=expect_container)
302         self.assertEqual(capture_stdout.getvalue(),
303                          stubs.expect_container_request_uuid + '\n')
304
305
306 class TestCreateTemplate(unittest.TestCase):
307     @stubs
308     def test_create(self, stubs):
309         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
310
311         capture_stdout = cStringIO.StringIO()
312
313         exited = arvados_cwl.main(
314             ["--create-template", "--debug",
315              "--project-uuid", project_uuid,
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.api.pipeline_instances().create.refute_called()
321         stubs.api.jobs().create.refute_called()
322
323         expect_component = copy.deepcopy(stubs.expect_job_spec)
324         expect_component['script_parameters']['x'] = {
325             'dataclass': 'File',
326             'required': True,
327             'type': 'File',
328             'value': '99999999999999999999999999999994+99/blorp.txt',
329         }
330         expect_component['script_parameters']['y'] = {
331             'dataclass': 'Collection',
332             'required': True,
333             'type': 'Directory',
334             'value': '99999999999999999999999999999998+99',
335         }
336         expect_component['script_parameters']['z'] = {
337             'dataclass': 'Collection',
338             'required': True,
339             'type': 'Directory',
340         }
341         expect_template = {
342             "components": {
343                 "submit_wf.cwl": expect_component,
344             },
345             "name": "submit_wf.cwl",
346             "owner_uuid": project_uuid,
347         }
348         stubs.api.pipeline_templates().create.assert_called_with(
349             body=JsonDiffMatcher(expect_template), ensure_unique_name=True)
350
351         self.assertEqual(capture_stdout.getvalue(),
352                          stubs.expect_pipeline_template_uuid + '\n')
353
354
355 class TestCreateWorkflow(unittest.TestCase):
356     @stubs
357     def test_create(self, stubs):
358         project_uuid = 'zzzzz-j7d0g-zzzzzzzzzzzzzzz'
359
360         capture_stdout = cStringIO.StringIO()
361
362         exited = arvados_cwl.main(
363             ["--create-workflow", "--debug",
364              "--project-uuid", project_uuid,
365              "tests/wf/submit_wf.cwl", "tests/submit_test_job.json"],
366             capture_stdout, sys.stderr, api_client=stubs.api)
367         self.assertEqual(exited, 0)
368
369         stubs.api.pipeline_templates().create.refute_called()
370         stubs.api.container_requests().create.refute_called()
371
372         with open("tests/wf/expect_packed.cwl") as f:
373             expect_workflow = f.read()
374
375         body = {
376             "workflow": {
377                 "owner_uuid": project_uuid,
378                 "name": "submit_wf.cwl",
379                 "description": "",
380                 "definition": expect_workflow
381                 }
382         }
383         stubs.api.workflows().create.assert_called_with(
384             body=JsonDiffMatcher(body))
385
386         self.assertEqual(capture_stdout.getvalue(),
387                          stubs.expect_workflow_uuid + '\n')
388
389
390 class TestTemplateInputs(unittest.TestCase):
391     expect_template = {
392         "components": {
393             "inputs_test.cwl": {
394                 'runtime_constraints': {
395                     'docker_image': 'arvados/jobs',
396                 },
397                 'script_parameters': {
398                     'cwl:tool':
399                     '99999999999999999999999999999991+99/'
400                     'wf/inputs_test.cwl',
401                     'optionalFloatInput': None,
402                     'fileInput': {
403                         'type': 'File',
404                         'dataclass': 'File',
405                         'required': True,
406                         'title': "It's a file; we expect to find some characters in it.",
407                         'description': 'If there were anything further to say, it would be said here,\nor here.'
408                     },
409                     'floatInput': {
410                         'type': 'float',
411                         'dataclass': 'number',
412                         'required': True,
413                         'title': 'Floats like a duck',
414                         'default': 0.1,
415                         'value': 0.1,
416                     },
417                     'optionalFloatInput': {
418                         'type': ['null', 'float'],
419                         'dataclass': 'number',
420                         'required': False,
421                     },
422                     'boolInput': {
423                         'type': 'boolean',
424                         'dataclass': 'boolean',
425                         'required': True,
426                         'title': 'True or false?',
427                     },
428                 },
429                 'repository': 'arvados',
430                 'script_version': 'master',
431                 'script': 'cwl-runner',
432             },
433         },
434         "name": "inputs_test.cwl",
435     }
436
437     @stubs
438     def test_inputs_empty(self, stubs):
439         exited = arvados_cwl.main(
440             ["--create-template", "--no-wait",
441              "tests/wf/inputs_test.cwl", "tests/order/empty_order.json"],
442             cStringIO.StringIO(), sys.stderr, api_client=stubs.api)
443         self.assertEqual(exited, 0)
444
445         expect_template = copy.deepcopy(self.expect_template)
446         expect_template["owner_uuid"] = stubs.fake_user_uuid
447
448         stubs.api.pipeline_templates().create.assert_called_with(
449             body=JsonDiffMatcher(expect_template), ensure_unique_name=True)
450
451     @stubs
452     def test_inputs(self, stubs):
453         exited = arvados_cwl.main(
454             ["--create-template", "--no-wait",
455              "tests/wf/inputs_test.cwl", "tests/order/inputs_test_order.json"],
456             cStringIO.StringIO(), sys.stderr, api_client=stubs.api)
457         self.assertEqual(exited, 0)
458
459         self.expect_template["owner_uuid"] = stubs.fake_user_uuid
460
461         expect_template = copy.deepcopy(self.expect_template)
462         expect_template["owner_uuid"] = stubs.fake_user_uuid
463         params = expect_template[
464             "components"]["inputs_test.cwl"]["script_parameters"]
465         params["fileInput"]["value"] = '99999999999999999999999999999994+99/blorp.txt'
466         params["floatInput"]["value"] = 1.234
467         params["boolInput"]["value"] = True
468
469         stubs.api.pipeline_templates().create.assert_called_with(
470             body=JsonDiffMatcher(expect_template), ensure_unique_name=True)