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