Merge branch '13330-collection-save'
[arvados.git] / sdk / cwl / tests / test_job.py
1 # Copyright (C) The Arvados Authors. All rights reserved.
2 #
3 # SPDX-License-Identifier: Apache-2.0
4
5 import functools
6 import json
7 import logging
8 import mock
9 import os
10 import unittest
11 import copy
12 import StringIO
13
14 import arvados
15 import arvados_cwl
16 import cwltool.process
17 from arvados.errors import ApiError
18 from schema_salad.ref_resolver import Loader
19 from schema_salad.sourceline import cmap
20 from .mock_discovery import get_rootDesc
21 from .matcher import JsonDiffMatcher, StripYAMLComments
22
23 if not os.getenv('ARVADOS_DEBUG'):
24     logging.getLogger('arvados.cwl-runner').setLevel(logging.WARN)
25     logging.getLogger('arvados.arv-run').setLevel(logging.WARN)
26
27 class TestJob(unittest.TestCase):
28
29     # The test passes no builder.resources
30     # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
31     @mock.patch('arvados.commands.keepdocker.list_images_in_arv')
32     def test_run(self, list_images_in_arv):
33         for enable_reuse in (True, False):
34             runner = mock.MagicMock()
35             runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
36             runner.ignore_docker_for_reuse = False
37             runner.num_retries = 0
38             document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
39
40             list_images_in_arv.return_value = [["zzzzz-4zz18-zzzzzzzzzzzzzzz"]]
41             runner.api.collections().get().execute.return_value = {"portable_data_hash": "99999999999999999999999999999993+99"}
42             # Simulate reused job from another project so that we can check is a can_read
43             # link is added.
44             runner.api.jobs().create().execute.return_value = {
45                 'state': 'Complete' if enable_reuse else 'Queued',
46                 'owner_uuid': 'zzzzz-tpzed-yyyyyyyyyyyyyyy' if enable_reuse else 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
47                 'uuid': 'zzzzz-819sb-yyyyyyyyyyyyyyy',
48                 'output': None,
49             }
50
51             tool = cmap({
52                 "inputs": [],
53                 "outputs": [],
54                 "baseCommand": "ls",
55                 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
56                 "id": "#",
57                 "class": "CommandLineTool"
58             })
59             make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
60                                          collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
61             arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="jobs", avsc_names=avsc_names,
62                                                      basedir="", make_fs_access=make_fs_access, loader=Loader({}),
63                                                      metadata={"cwlVersion": "v1.0"})
64             arvtool.formatgraph = None
65             for j in arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access):
66                 j.run(enable_reuse=enable_reuse)
67                 runner.api.jobs().create.assert_called_with(
68                     body=JsonDiffMatcher({
69                         'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
70                         'runtime_constraints': {},
71                         'script_parameters': {
72                             'tasks': [{
73                                 'task.env': {'HOME': '$(task.outdir)', 'TMPDIR': '$(task.tmpdir)'},
74                                 'command': ['ls', '$(task.outdir)']
75                             }],
76                         },
77                         'script_version': 'master',
78                         'minimum_script_version': 'a3f2cb186e437bfce0031b024b2157b73ed2717d',
79                         'repository': 'arvados',
80                         'script': 'crunchrunner',
81                         'runtime_constraints': {
82                             'docker_image': 'arvados/jobs',
83                             'min_cores_per_node': 1,
84                             'min_ram_mb_per_node': 1024,
85                             'min_scratch_mb_per_node': 2048 # tmpdirSize + outdirSize
86                         }
87                     }),
88                     find_or_create=enable_reuse,
89                     filters=[['repository', '=', 'arvados'],
90                              ['script', '=', 'crunchrunner'],
91                              ['script_version', 'in git', 'a3f2cb186e437bfce0031b024b2157b73ed2717d'],
92                              ['docker_image_locator', 'in docker', 'arvados/jobs']]
93                 )
94                 if enable_reuse:
95                     runner.api.links().create.assert_called_with(
96                         body=JsonDiffMatcher({
97                             'link_class': 'permission',
98                             'name': 'can_read',
99                             "tail_uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
100                             "head_uuid": "zzzzz-819sb-yyyyyyyyyyyyyyy",
101                         })
102                     )
103                     # Simulate an API excepction when trying to create a
104                     # sharing link on the job
105                     runner.api.links().create.side_effect = ApiError(
106                         mock.MagicMock(return_value={'status': 403}),
107                         'Permission denied')
108                     j.run(enable_reuse=enable_reuse)
109                 else:
110                     assert not runner.api.links().create.called
111
112     # The test passes some fields in builder.resources
113     # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
114     @mock.patch('arvados.commands.keepdocker.list_images_in_arv')
115     def test_resource_requirements(self, list_images_in_arv):
116         runner = mock.MagicMock()
117         runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
118         runner.ignore_docker_for_reuse = False
119         runner.num_retries = 0
120         arvados_cwl.add_arv_hints()
121
122         list_images_in_arv.return_value = [["zzzzz-4zz18-zzzzzzzzzzzzzzz"]]
123         runner.api.collections().get().execute.return_vaulue = {"portable_data_hash": "99999999999999999999999999999993+99"}
124
125         document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
126
127
128         tool = {
129             "inputs": [],
130             "outputs": [],
131             "hints": [{
132                 "class": "ResourceRequirement",
133                 "coresMin": 3,
134                 "ramMin": 3000,
135                 "tmpdirMin": 4000
136             }, {
137                 "class": "http://arvados.org/cwl#RuntimeConstraints",
138                 "keep_cache": 512,
139                 "outputDirType": "keep_output_dir"
140             }, {
141                 "class": "http://arvados.org/cwl#APIRequirement",
142             },
143             {
144                 "class": "http://arvados.org/cwl#ReuseRequirement",
145                 "enableReuse": False
146             }],
147             "baseCommand": "ls",
148             "id": "#",
149             "class": "CommandLineTool"
150         }
151         make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
152                                          collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
153         arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, work_api="jobs", avsc_names=avsc_names,
154                                                  make_fs_access=make_fs_access, loader=Loader({}),
155                                                  metadata={"cwlVersion": "v1.0"})
156         arvtool.formatgraph = None
157         for j in arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access):
158             j.run(enable_reuse=True)
159         runner.api.jobs().create.assert_called_with(
160             body=JsonDiffMatcher({
161                 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
162                 'runtime_constraints': {},
163                 'script_parameters': {
164                     'tasks': [{
165                         'task.env': {'HOME': '$(task.outdir)', 'TMPDIR': '$(task.tmpdir)'},
166                         'task.keepTmpOutput': True,
167                         'command': ['ls']
168                     }]
169             },
170             'script_version': 'master',
171                 'minimum_script_version': 'a3f2cb186e437bfce0031b024b2157b73ed2717d',
172                 'repository': 'arvados',
173                 'script': 'crunchrunner',
174                 'runtime_constraints': {
175                     'docker_image': 'arvados/jobs',
176                     'min_cores_per_node': 3,
177                     'min_ram_mb_per_node': 3512,     # ramMin + keep_cache
178                     'min_scratch_mb_per_node': 5024, # tmpdirSize + outdirSize
179                     'keep_cache_mb_per_task': 512
180                 }
181             }),
182             find_or_create=False,
183             filters=[['repository', '=', 'arvados'],
184                      ['script', '=', 'crunchrunner'],
185                      ['script_version', 'in git', 'a3f2cb186e437bfce0031b024b2157b73ed2717d'],
186                      ['docker_image_locator', 'in docker', 'arvados/jobs']])
187
188     @mock.patch("arvados.collection.CollectionReader")
189     def test_done(self, reader):
190         api = mock.MagicMock()
191
192         runner = mock.MagicMock()
193         runner.api = api
194         runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
195         runner.num_retries = 0
196         runner.ignore_docker_for_reuse = False
197
198         reader().open.return_value = StringIO.StringIO(
199             """2016-11-02_23:12:18 c97qk-8i9sb-cryqw2blvzy4yaj 13358 0 stderr 2016/11/02 23:12:18 crunchrunner: $(task.tmpdir)=/tmp/crunch-job-task-work/compute3.1/tmpdir
200 2016-11-02_23:12:18 c97qk-8i9sb-cryqw2blvzy4yaj 13358 0 stderr 2016/11/02 23:12:18 crunchrunner: $(task.outdir)=/tmp/crunch-job-task-work/compute3.1/outdir
201 2016-11-02_23:12:18 c97qk-8i9sb-cryqw2blvzy4yaj 13358 0 stderr 2016/11/02 23:12:18 crunchrunner: $(task.keep)=/keep
202         """)
203         api.collections().list().execute.side_effect = ({"items": []},
204                                                         {"items": [{"manifest_text": "XYZ"}]},
205                                                         {"items": []},
206                                                         {"items": [{"manifest_text": "ABC"}]})
207
208         arvjob = arvados_cwl.ArvadosJob(runner)
209         arvjob.name = "testjob"
210         arvjob.builder = mock.MagicMock()
211         arvjob.output_callback = mock.MagicMock()
212         arvjob.collect_outputs = mock.MagicMock()
213         arvjob.collect_outputs.return_value = {"out": "stuff"}
214
215         arvjob.done({
216             "state": "Complete",
217             "output": "99999999999999999999999999999993+99",
218             "log": "99999999999999999999999999999994+99",
219             "uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
220         })
221
222         api.collections().list.assert_has_calls([
223             mock.call(),
224             # Output collection check
225             mock.call(filters=[['owner_uuid', '=', 'zzzzz-8i9sb-zzzzzzzzzzzzzzz'],
226                           ['portable_data_hash', '=', '99999999999999999999999999999993+99'],
227                           ['name', '=', 'Output 9999999 of testjob']]),
228             mock.call().execute(num_retries=0),
229             mock.call(limit=1, filters=[['portable_data_hash', '=', '99999999999999999999999999999993+99']],
230                  select=['manifest_text']),
231             mock.call().execute(num_retries=0),
232             # Log collection's turn
233             mock.call(filters=[['owner_uuid', '=', 'zzzzz-8i9sb-zzzzzzzzzzzzzzz'],
234                           ['portable_data_hash', '=', '99999999999999999999999999999994+99'],
235                           ['name', '=', 'Log of zzzzz-8i9sb-zzzzzzzzzzzzzzz']]),
236             mock.call().execute(num_retries=0),
237             mock.call(limit=1, filters=[['portable_data_hash', '=', '99999999999999999999999999999994+99']],
238                  select=['manifest_text']),
239             mock.call().execute(num_retries=0)])
240
241         api.collections().create.assert_has_calls([
242             mock.call(ensure_unique_name=True,
243                       body={'portable_data_hash': '99999999999999999999999999999993+99',
244                             'manifest_text': 'XYZ',
245                             'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
246                             'name': 'Output 9999999 of testjob'}),
247             mock.call().execute(num_retries=0),
248             mock.call(ensure_unique_name=True,
249                       body={'portable_data_hash': '99999999999999999999999999999994+99',
250                             'manifest_text': 'ABC',
251                             'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
252                             'name': 'Log of zzzzz-8i9sb-zzzzzzzzzzzzzzz'}),
253             mock.call().execute(num_retries=0),
254         ])
255
256         arvjob.output_callback.assert_called_with({"out": "stuff"}, "success")
257
258     @mock.patch("arvados.collection.CollectionReader")
259     def test_done_use_existing_collection(self, reader):
260         api = mock.MagicMock()
261
262         runner = mock.MagicMock()
263         runner.api = api
264         runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
265         runner.num_retries = 0
266
267         reader().open.return_value = StringIO.StringIO(
268             """2016-11-02_23:12:18 c97qk-8i9sb-cryqw2blvzy4yaj 13358 0 stderr 2016/11/02 23:12:18 crunchrunner: $(task.tmpdir)=/tmp/crunch-job-task-work/compute3.1/tmpdir
269 2016-11-02_23:12:18 c97qk-8i9sb-cryqw2blvzy4yaj 13358 0 stderr 2016/11/02 23:12:18 crunchrunner: $(task.outdir)=/tmp/crunch-job-task-work/compute3.1/outdir
270 2016-11-02_23:12:18 c97qk-8i9sb-cryqw2blvzy4yaj 13358 0 stderr 2016/11/02 23:12:18 crunchrunner: $(task.keep)=/keep
271         """)
272
273         api.collections().list().execute.side_effect = (
274             {"items": [{"uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2"}]},
275             {"items": [{"uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2"}]},
276         )
277
278         arvjob = arvados_cwl.ArvadosJob(runner)
279         arvjob.name = "testjob"
280         arvjob.builder = mock.MagicMock()
281         arvjob.output_callback = mock.MagicMock()
282         arvjob.collect_outputs = mock.MagicMock()
283         arvjob.collect_outputs.return_value = {"out": "stuff"}
284
285         arvjob.done({
286             "state": "Complete",
287             "output": "99999999999999999999999999999993+99",
288             "log": "99999999999999999999999999999994+99",
289             "uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
290         })
291
292         api.collections().list.assert_has_calls([
293             mock.call(),
294             # Output collection
295             mock.call(filters=[['owner_uuid', '=', 'zzzzz-8i9sb-zzzzzzzzzzzzzzz'],
296                                ['portable_data_hash', '=', '99999999999999999999999999999993+99'],
297                                ['name', '=', 'Output 9999999 of testjob']]),
298             mock.call().execute(num_retries=0),
299             # Log collection
300             mock.call(filters=[['owner_uuid', '=', 'zzzzz-8i9sb-zzzzzzzzzzzzzzz'],
301                                ['portable_data_hash', '=', '99999999999999999999999999999994+99'],
302                                ['name', '=', 'Log of zzzzz-8i9sb-zzzzzzzzzzzzzzz']]),
303             mock.call().execute(num_retries=0)
304         ])
305
306         self.assertFalse(api.collections().create.called)
307
308         arvjob.output_callback.assert_called_with({"out": "stuff"}, "success")
309
310
311 class TestWorkflow(unittest.TestCase):
312     # The test passes no builder.resources
313     # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
314     @mock.patch("arvados.collection.CollectionReader")
315     @mock.patch("arvados.collection.Collection")
316     @mock.patch('arvados.commands.keepdocker.list_images_in_arv')
317     def test_run(self, list_images_in_arv, mockcollection, mockcollectionreader):
318         arvados_cwl.add_arv_hints()
319
320         api = mock.MagicMock()
321         api._rootDesc = get_rootDesc()
322
323         runner = arvados_cwl.ArvCwlRunner(api)
324         self.assertEqual(runner.work_api, 'jobs')
325
326         list_images_in_arv.return_value = [["zzzzz-4zz18-zzzzzzzzzzzzzzz"]]
327         runner.api.collections().get().execute.return_vaulue = {"portable_data_hash": "99999999999999999999999999999993+99"}
328         runner.api.collections().list().execute.return_vaulue = {"items": [{"portable_data_hash": "99999999999999999999999999999993+99"}]}
329
330         runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
331         runner.ignore_docker_for_reuse = False
332         runner.num_retries = 0
333         document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
334
335         make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
336                                          collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
337         document_loader.fetcher_constructor = functools.partial(arvados_cwl.CollectionFetcher, api_client=api, fs_access=make_fs_access(""))
338         document_loader.fetcher = document_loader.fetcher_constructor(document_loader.cache, document_loader.session)
339         document_loader.fetch_text = document_loader.fetcher.fetch_text
340         document_loader.check_exists = document_loader.fetcher.check_exists
341
342         tool, metadata = document_loader.resolve_ref("tests/wf/scatter2.cwl")
343         metadata["cwlVersion"] = tool["cwlVersion"]
344
345         mockcollection().portable_data_hash.return_value = "99999999999999999999999999999999+118"
346
347         arvtool = arvados_cwl.ArvadosWorkflow(runner, tool, work_api="jobs", avsc_names=avsc_names,
348                                               basedir="", make_fs_access=make_fs_access, loader=document_loader,
349                                               makeTool=runner.arv_make_tool, metadata=metadata)
350         arvtool.formatgraph = None
351         it = arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access, tmp_outdir_prefix="")
352         it.next().run()
353         it.next().run()
354
355         with open("tests/wf/scatter2_subwf.cwl") as f:
356             subwf = StripYAMLComments(f.read())
357
358         runner.api.jobs().create.assert_called_with(
359             body=JsonDiffMatcher({
360                 'minimum_script_version': 'a3f2cb186e437bfce0031b024b2157b73ed2717d',
361                 'repository': 'arvados',
362                 'script_version': 'master',
363                 'script': 'crunchrunner',
364                 'script_parameters': {
365                     'tasks': [{'task.env': {
366                         'HOME': '$(task.outdir)',
367                         'TMPDIR': '$(task.tmpdir)'},
368                                'task.vwd': {
369                                    'workflow.cwl': '$(task.keep)/99999999999999999999999999999999+118/workflow.cwl',
370                                    'cwl.input.yml': '$(task.keep)/99999999999999999999999999999999+118/cwl.input.yml'
371                                },
372                     'command': [u'cwltool', u'--no-container', u'--move-outputs', u'--preserve-entire-environment', u'workflow.cwl#main', u'cwl.input.yml'],
373                     'task.stdout': 'cwl.output.json'}]},
374                 'runtime_constraints': {
375                     'min_scratch_mb_per_node': 2048,
376                     'min_cores_per_node': 1,
377                     'docker_image': 'arvados/jobs',
378                     'min_ram_mb_per_node': 1024
379                 },
380                 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz'}),
381             filters=[['repository', '=', 'arvados'],
382                      ['script', '=', 'crunchrunner'],
383                      ['script_version', 'in git', 'a3f2cb186e437bfce0031b024b2157b73ed2717d'],
384                      ['docker_image_locator', 'in docker', 'arvados/jobs']],
385             find_or_create=True)
386
387         mockcollection().open().__enter__().write.assert_has_calls([mock.call(subwf)])
388         mockcollection().open().__enter__().write.assert_has_calls([mock.call(
389 '''{
390   "fileblub": {
391     "basename": "token.txt",
392     "class": "File",
393     "location": "/keep/99999999999999999999999999999999+118/token.txt"
394   },
395   "sleeptime": 5
396 }''')])
397
398     # The test passes no builder.resources
399     # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
400     @mock.patch("arvados.collection.CollectionReader")
401     @mock.patch("arvados.collection.Collection")
402     @mock.patch('arvados.commands.keepdocker.list_images_in_arv')
403     def test_overall_resource_singlecontainer(self, list_images_in_arv, mockcollection, mockcollectionreader):
404         arvados_cwl.add_arv_hints()
405
406         api = mock.MagicMock()
407         api._rootDesc = get_rootDesc()
408
409         runner = arvados_cwl.ArvCwlRunner(api)
410         self.assertEqual(runner.work_api, 'jobs')
411
412         list_images_in_arv.return_value = [["zzzzz-4zz18-zzzzzzzzzzzzzzz"]]
413         runner.api.collections().get().execute.return_vaulue = {"portable_data_hash": "99999999999999999999999999999993+99"}
414         runner.api.collections().list().execute.return_vaulue = {"items": [{"portable_data_hash": "99999999999999999999999999999993+99"}]}
415
416         runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
417         runner.ignore_docker_for_reuse = False
418         runner.num_retries = 0
419         document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
420
421         make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
422                                          collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
423         document_loader.fetcher_constructor = functools.partial(arvados_cwl.CollectionFetcher, api_client=api, fs_access=make_fs_access(""))
424         document_loader.fetcher = document_loader.fetcher_constructor(document_loader.cache, document_loader.session)
425         document_loader.fetch_text = document_loader.fetcher.fetch_text
426         document_loader.check_exists = document_loader.fetcher.check_exists
427
428         tool, metadata = document_loader.resolve_ref("tests/wf/echo-wf.cwl")
429         metadata["cwlVersion"] = tool["cwlVersion"]
430
431         mockcollection().portable_data_hash.return_value = "99999999999999999999999999999999+118"
432
433         arvtool = arvados_cwl.ArvadosWorkflow(runner, tool, work_api="jobs", avsc_names=avsc_names,
434                                               basedir="", make_fs_access=make_fs_access, loader=document_loader,
435                                               makeTool=runner.arv_make_tool, metadata=metadata)
436         arvtool.formatgraph = None
437         it = arvtool.job({}, mock.MagicMock(), basedir="", make_fs_access=make_fs_access, tmp_outdir_prefix="")
438         it.next().run()
439         it.next().run()
440
441         with open("tests/wf/echo-subwf.cwl") as f:
442             subwf = StripYAMLComments(f.read())
443
444         runner.api.jobs().create.assert_called_with(
445             body=JsonDiffMatcher({
446                 'minimum_script_version': 'a3f2cb186e437bfce0031b024b2157b73ed2717d',
447                 'repository': 'arvados',
448                 'script_version': 'master',
449                 'script': 'crunchrunner',
450                 'script_parameters': {
451                     'tasks': [{'task.env': {
452                         'HOME': '$(task.outdir)',
453                         'TMPDIR': '$(task.tmpdir)'},
454                                'task.vwd': {
455                                    'workflow.cwl': '$(task.keep)/99999999999999999999999999999999+118/workflow.cwl',
456                                    'cwl.input.yml': '$(task.keep)/99999999999999999999999999999999+118/cwl.input.yml'
457                                },
458                     'command': [u'cwltool', u'--no-container', u'--move-outputs', u'--preserve-entire-environment', u'workflow.cwl#main', u'cwl.input.yml'],
459                     'task.stdout': 'cwl.output.json'}]},
460                 'runtime_constraints': {
461                     'min_scratch_mb_per_node': 4096,
462                     'min_cores_per_node': 3,
463                     'docker_image': 'arvados/jobs',
464                     'min_ram_mb_per_node': 1024
465                 },
466                 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz'}),
467             filters=[['repository', '=', 'arvados'],
468                      ['script', '=', 'crunchrunner'],
469                      ['script_version', 'in git', 'a3f2cb186e437bfce0031b024b2157b73ed2717d'],
470                      ['docker_image_locator', 'in docker', 'arvados/jobs']],
471             find_or_create=True)
472
473     def test_default_work_api(self):
474         arvados_cwl.add_arv_hints()
475
476         api = mock.MagicMock()
477         api._rootDesc = copy.deepcopy(get_rootDesc())
478         del api._rootDesc.get('resources')['jobs']['methods']['create']
479         runner = arvados_cwl.ArvCwlRunner(api)
480         self.assertEqual(runner.work_api, 'containers')