1 # Copyright (C) The Arvados Authors. All rights reserved.
3 # SPDX-License-Identifier: Apache-2.0
6 import arvados_cwl.context
7 from arvados_cwl.arvdocker import arv_docker_clear_cache
15 import cwltool.process
16 import cwltool.secrets
17 from schema_salad.ref_resolver import Loader
18 from schema_salad.sourceline import cmap
20 from .matcher import JsonDiffMatcher
21 from .mock_discovery import get_rootDesc
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)
27 class CollectionMock(object):
28 def __init__(self, vwdmock, *args, **kwargs):
29 self.vwdmock = vwdmock
32 def open(self, *args, **kwargs):
34 return self.vwdmock.open(*args, **kwargs)
36 def copy(self, *args, **kwargs):
38 self.vwdmock.copy(*args, **kwargs)
40 def save_new(self, *args, **kwargs):
46 def portable_data_hash(self):
48 return arvados.config.EMPTY_BLOCK_LOCATOR
50 return "99999999999999999999999999999996+99"
53 class TestContainer(unittest.TestCase):
55 def helper(self, runner, enable_reuse=True):
56 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
58 make_fs_access=functools.partial(arvados_cwl.CollectionFsAccess,
59 collection_cache=arvados_cwl.CollectionCache(runner.api, None, 0))
60 loadingContext = arvados_cwl.context.ArvLoadingContext(
61 {"avsc_names": avsc_names,
63 "make_fs_access": make_fs_access,
65 "metadata": {"cwlVersion": "v1.0"}})
66 runtimeContext = arvados_cwl.context.ArvRuntimeContext(
67 {"work_api": "containers",
69 "name": "test_run_"+str(enable_reuse),
70 "make_fs_access": make_fs_access,
72 "enable_reuse": enable_reuse,
75 return loadingContext, runtimeContext
77 # The test passes no builder.resources
78 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
79 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
80 def test_run(self, keepdocker):
81 for enable_reuse in (True, False):
82 arv_docker_clear_cache()
84 runner = mock.MagicMock()
85 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
86 runner.ignore_docker_for_reuse = False
87 runner.intermediate_output_ttl = 0
88 runner.secret_store = cwltool.secrets.SecretStore()
90 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
91 runner.api.collections().get().execute.return_value = {
92 "portable_data_hash": "99999999999999999999999999999993+99"}
98 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
100 "class": "CommandLineTool"
103 loadingContext, runtimeContext = self.helper(runner, enable_reuse)
105 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
106 arvtool.formatgraph = None
108 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
109 j.run(runtimeContext)
110 runner.api.container_requests().create.assert_called_with(
111 body=JsonDiffMatcher({
113 'HOME': '/var/spool/cwl',
116 'name': 'test_run_'+str(enable_reuse),
117 'runtime_constraints': {
121 'use_existing': enable_reuse,
124 '/tmp': {'kind': 'tmp',
125 "capacity": 1073741824
127 '/var/spool/cwl': {'kind': 'tmp',
128 "capacity": 1073741824 }
130 'state': 'Committed',
131 'output_name': 'Output for step test_run_'+str(enable_reuse),
132 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
133 'output_path': '/var/spool/cwl',
135 'container_image': 'arvados/jobs',
136 'command': ['ls', '/var/spool/cwl'],
137 'cwd': '/var/spool/cwl',
138 'scheduling_parameters': {},
143 # The test passes some fields in builder.resources
144 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
145 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
146 def test_resource_requirements(self, keepdocker):
147 arv_docker_clear_cache()
148 runner = mock.MagicMock()
149 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
150 runner.ignore_docker_for_reuse = False
151 runner.intermediate_output_ttl = 3600
152 runner.secret_store = cwltool.secrets.SecretStore()
154 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
155 runner.api.collections().get().execute.return_value = {
156 "portable_data_hash": "99999999999999999999999999999993+99"}
162 "class": "ResourceRequirement",
168 "class": "http://arvados.org/cwl#RuntimeConstraints",
171 "class": "http://arvados.org/cwl#APIRequirement",
173 "class": "http://arvados.org/cwl#PartitionRequirement",
176 "class": "http://arvados.org/cwl#IntermediateOutput",
179 "class": "http://arvados.org/cwl#ReuseRequirement",
184 "class": "CommandLineTool"
187 loadingContext, runtimeContext = self.helper(runner)
188 runtimeContext.name = "test_resource_requirements"
190 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
191 arvtool.formatgraph = None
192 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
193 j.run(runtimeContext)
195 call_args, call_kwargs = runner.api.container_requests().create.call_args
197 call_body_expected = {
199 'HOME': '/var/spool/cwl',
202 'name': 'test_resource_requirements',
203 'runtime_constraints': {
206 'keep_cache_ram': 536870912,
209 'use_existing': False,
212 '/tmp': {'kind': 'tmp',
213 "capacity": 4194304000 },
214 '/var/spool/cwl': {'kind': 'tmp',
215 "capacity": 5242880000 }
217 'state': 'Committed',
218 'output_name': 'Output for step test_resource_requirements',
219 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
220 'output_path': '/var/spool/cwl',
222 'container_image': 'arvados/jobs',
224 'cwd': '/var/spool/cwl',
225 'scheduling_parameters': {
226 'partitions': ['blurb']
232 call_body = call_kwargs.get('body', None)
233 self.assertNotEqual(None, call_body)
234 for key in call_body:
235 self.assertEqual(call_body_expected.get(key), call_body.get(key))
238 # The test passes some fields in builder.resources
239 # For the remaining fields, the defaults will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
240 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
241 @mock.patch("arvados.collection.Collection")
242 def test_initial_work_dir(self, collection_mock, keepdocker):
243 arv_docker_clear_cache()
244 runner = mock.MagicMock()
245 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
246 runner.ignore_docker_for_reuse = False
247 runner.intermediate_output_ttl = 0
248 runner.secret_store = cwltool.secrets.SecretStore()
250 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
251 runner.api.collections().get().execute.return_value = {
252 "portable_data_hash": "99999999999999999999999999999993+99"}
254 sourcemock = mock.MagicMock()
255 def get_collection_mock(p):
257 return (sourcemock, p.split("/", 1)[1])
259 return (sourcemock, "")
260 runner.fs_access.get_collection.side_effect = get_collection_mock
262 vwdmock = mock.MagicMock()
263 collection_mock.side_effect = lambda *args, **kwargs: CollectionMock(vwdmock, *args, **kwargs)
269 "class": "InitialWorkDirRequirement",
273 "location": "keep:99999999999999999999999999999995+99/bar"
276 "class": "Directory",
278 "location": "keep:99999999999999999999999999999995+99"
282 "basename": "filename",
283 "location": "keep:99999999999999999999999999999995+99/baz/filename"
286 "class": "Directory",
287 "basename": "subdir",
288 "location": "keep:99999999999999999999999999999995+99/subdir"
293 "class": "CommandLineTool"
296 loadingContext, runtimeContext = self.helper(runner)
297 runtimeContext.name = "test_initial_work_dir"
299 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
300 arvtool.formatgraph = None
301 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
302 j.run(runtimeContext)
304 call_args, call_kwargs = runner.api.container_requests().create.call_args
306 vwdmock.copy.assert_has_calls([mock.call('bar', 'foo', source_collection=sourcemock)])
307 vwdmock.copy.assert_has_calls([mock.call('', 'foo2', source_collection=sourcemock)])
308 vwdmock.copy.assert_has_calls([mock.call('baz/filename', 'filename', source_collection=sourcemock)])
309 vwdmock.copy.assert_has_calls([mock.call('subdir', 'subdir', source_collection=sourcemock)])
311 call_body_expected = {
313 'HOME': '/var/spool/cwl',
316 'name': 'test_initial_work_dir',
317 'runtime_constraints': {
321 'use_existing': True,
324 '/tmp': {'kind': 'tmp',
325 "capacity": 1073741824 },
326 '/var/spool/cwl': {'kind': 'tmp',
327 "capacity": 1073741824 },
328 '/var/spool/cwl/foo': {
329 'kind': 'collection',
331 'portable_data_hash': '99999999999999999999999999999996+99'
333 '/var/spool/cwl/foo2': {
334 'kind': 'collection',
336 'portable_data_hash': '99999999999999999999999999999996+99'
338 '/var/spool/cwl/filename': {
339 'kind': 'collection',
341 'portable_data_hash': '99999999999999999999999999999996+99'
343 '/var/spool/cwl/subdir': {
344 'kind': 'collection',
346 'portable_data_hash': '99999999999999999999999999999996+99'
349 'state': 'Committed',
350 'output_name': 'Output for step test_initial_work_dir',
351 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
352 'output_path': '/var/spool/cwl',
354 'container_image': 'arvados/jobs',
356 'cwd': '/var/spool/cwl',
357 'scheduling_parameters': {
363 call_body = call_kwargs.get('body', None)
364 self.assertNotEqual(None, call_body)
365 for key in call_body:
366 self.assertEqual(call_body_expected.get(key), call_body.get(key))
369 # Test redirecting stdin/stdout/stderr
370 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
371 def test_redirects(self, keepdocker):
372 arv_docker_clear_cache()
374 runner = mock.MagicMock()
375 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
376 runner.ignore_docker_for_reuse = False
377 runner.intermediate_output_ttl = 0
378 runner.secret_store = cwltool.secrets.SecretStore()
380 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
381 runner.api.collections().get().execute.return_value = {
382 "portable_data_hash": "99999999999999999999999999999993+99"}
384 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
390 "stdout": "stdout.txt",
391 "stderr": "stderr.txt",
392 "stdin": "/keep/99999999999999999999999999999996+99/file.txt",
393 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
395 "class": "CommandLineTool"
398 loadingContext, runtimeContext = self.helper(runner)
399 runtimeContext.name = "test_run_redirect"
401 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
402 arvtool.formatgraph = None
403 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
404 j.run(runtimeContext)
405 runner.api.container_requests().create.assert_called_with(
406 body=JsonDiffMatcher({
408 'HOME': '/var/spool/cwl',
411 'name': 'test_run_redirect',
412 'runtime_constraints': {
416 'use_existing': True,
419 '/tmp': {'kind': 'tmp',
420 "capacity": 1073741824 },
421 '/var/spool/cwl': {'kind': 'tmp',
422 "capacity": 1073741824 },
425 "path": "/var/spool/cwl/stderr.txt"
428 "kind": "collection",
430 "portable_data_hash": "99999999999999999999999999999996+99"
434 "path": "/var/spool/cwl/stdout.txt"
437 'state': 'Committed',
438 "output_name": "Output for step test_run_redirect",
439 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
440 'output_path': '/var/spool/cwl',
442 'container_image': 'arvados/jobs',
443 'command': ['ls', '/var/spool/cwl'],
444 'cwd': '/var/spool/cwl',
445 'scheduling_parameters': {},
450 @mock.patch("arvados.collection.Collection")
451 def test_done(self, col):
452 api = mock.MagicMock()
454 runner = mock.MagicMock()
456 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
457 runner.num_retries = 0
458 runner.ignore_docker_for_reuse = False
459 runner.intermediate_output_ttl = 0
460 runner.secret_store = cwltool.secrets.SecretStore()
462 runner.api.containers().get().execute.return_value = {"state":"Complete",
466 col().open.return_value = []
468 arvjob = arvados_cwl.ArvadosContainer(runner,
475 arvjob.output_callback = mock.MagicMock()
476 arvjob.collect_outputs = mock.MagicMock()
477 arvjob.successCodes = [0]
478 arvjob.outdir = "/var/spool/cwl"
479 arvjob.output_ttl = 3600
481 arvjob.collect_outputs.return_value = {"out": "stuff"}
485 "log_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
486 "output_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
487 "uuid": "zzzzz-xvhdp-zzzzzzzzzzzzzzz",
488 "container_uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
489 "modified_at": "2017-05-26T12:01:22Z"
492 self.assertFalse(api.collections().create.called)
493 self.assertFalse(runner.runtime_status_error.called)
495 arvjob.collect_outputs.assert_called_with("keep:abc+123")
496 arvjob.output_callback.assert_called_with({"out": "stuff"}, "success")
497 runner.add_intermediate_output.assert_called_with("zzzzz-4zz18-zzzzzzzzzzzzzz2")
499 @mock.patch("arvados_cwl.get_current_container")
500 @mock.patch("arvados.collection.CollectionReader")
501 @mock.patch("arvados.collection.Collection")
502 def test_child_failure(self, col, reader, gcc_mock):
503 api = mock.MagicMock()
504 api._rootDesc = copy.deepcopy(get_rootDesc())
505 del api._rootDesc.get('resources')['jobs']['methods']['create']
507 # Set up runner with mocked runtime_status_update()
508 self.assertFalse(gcc_mock.called)
509 runtime_status_update = mock.MagicMock()
510 arvados_cwl.ArvCwlRunner.runtime_status_update = runtime_status_update
511 runner = arvados_cwl.ArvCwlRunner(api)
512 self.assertEqual(runner.work_api, 'containers')
514 # Make sure ArvCwlRunner thinks it's running inside a container so it
515 # adds the logging handler that will call runtime_status_update() mock
516 gcc_mock.return_value = {"uuid" : "zzzzz-dz642-zzzzzzzzzzzzzzz"}
517 self.assertTrue(gcc_mock.called)
518 root_logger = logging.getLogger('')
519 self.assertEqual(1, len(root_logger.handlers))
520 handler = root_logger.handlers[0]
521 self.assertEqual(arvados_cwl.RuntimeStatusLoggingHandler, handler.__class__)
523 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
524 runner.num_retries = 0
525 runner.ignore_docker_for_reuse = False
526 runner.intermediate_output_ttl = 0
527 runner.secret_store = cwltool.secrets.SecretStore()
528 runner.label = mock.MagicMock()
529 runner.label.return_value = '[container testjob]'
531 runner.api.containers().get().execute.return_value = {
538 col().open.return_value = []
540 arvjob = arvados_cwl.ArvadosContainer(runner,
547 arvjob.output_callback = mock.MagicMock()
548 arvjob.collect_outputs = mock.MagicMock()
549 arvjob.successCodes = [0]
550 arvjob.outdir = "/var/spool/cwl"
551 arvjob.output_ttl = 3600
552 arvjob.collect_outputs.return_value = {"out": "stuff"}
556 "log_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz1",
557 "output_uuid": "zzzzz-4zz18-zzzzzzzzzzzzzz2",
558 "uuid": "zzzzz-xvhdp-zzzzzzzzzzzzzzz",
559 "container_uuid": "zzzzz-8i9sb-zzzzzzzzzzzzzzz",
560 "modified_at": "2017-05-26T12:01:22Z"
563 runtime_status_update.assert_called_with(
565 'arvados.cwl-runner: [container testjob] (zzzzz-xvhdp-zzzzzzzzzzzzzzz) error log:',
566 ' ** log is empty **'
568 arvjob.output_callback.assert_called_with({"out": "stuff"}, "permanentFail")
570 # The test passes no builder.resources
571 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
572 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
573 def test_mounts(self, keepdocker):
574 arv_docker_clear_cache()
576 runner = mock.MagicMock()
577 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
578 runner.ignore_docker_for_reuse = False
579 runner.intermediate_output_ttl = 0
580 runner.secret_store = cwltool.secrets.SecretStore()
582 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
583 runner.api.collections().get().execute.return_value = {
584 "portable_data_hash": "99999999999999999999999999999994+99",
585 "manifest_text": ". 99999999999999999999999999999994+99 0:0:file1 0:0:file2"}
587 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
596 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
598 "class": "CommandLineTool"
601 loadingContext, runtimeContext = self.helper(runner)
602 runtimeContext.name = "test_run_mounts"
604 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
605 arvtool.formatgraph = None
608 "class": "Directory",
609 "location": "keep:99999999999999999999999999999994+44",
613 "location": "keep:99999999999999999999999999999994+44/file1",
617 "location": "keep:99999999999999999999999999999994+44/file2",
622 for j in arvtool.job(job_order, mock.MagicMock(), runtimeContext):
623 j.run(runtimeContext)
624 runner.api.container_requests().create.assert_called_with(
625 body=JsonDiffMatcher({
627 'HOME': '/var/spool/cwl',
630 'name': 'test_run_mounts',
631 'runtime_constraints': {
635 'use_existing': True,
638 "/keep/99999999999999999999999999999994+44": {
639 "kind": "collection",
640 "portable_data_hash": "99999999999999999999999999999994+44"
642 '/tmp': {'kind': 'tmp',
643 "capacity": 1073741824 },
644 '/var/spool/cwl': {'kind': 'tmp',
645 "capacity": 1073741824 }
647 'state': 'Committed',
648 'output_name': 'Output for step test_run_mounts',
649 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
650 'output_path': '/var/spool/cwl',
652 'container_image': 'arvados/jobs',
653 'command': ['ls', '/var/spool/cwl'],
654 'cwd': '/var/spool/cwl',
655 'scheduling_parameters': {},
660 # The test passes no builder.resources
661 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
662 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
663 def test_secrets(self, keepdocker):
664 arv_docker_clear_cache()
666 runner = mock.MagicMock()
667 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
668 runner.ignore_docker_for_reuse = False
669 runner.intermediate_output_ttl = 0
670 runner.secret_store = cwltool.secrets.SecretStore()
672 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
673 runner.api.collections().get().execute.return_value = {
674 "portable_data_hash": "99999999999999999999999999999993+99"}
676 document_loader, avsc_names, schema_metadata, metaschema_loader = cwltool.process.get_schema("v1.0")
678 tool = cmap({"arguments": ["md5sum", "example.conf"],
679 "class": "CommandLineTool",
682 "class": "http://commonwl.org/cwltool#Secrets",
688 "id": "#secret_job.cwl",
691 "id": "#secret_job.cwl/pw",
699 "class": "InitialWorkDirRequirement",
702 "entry": "username: user\npassword: $(inputs.pw)\n",
703 "entryname": "example.conf"
709 loadingContext, runtimeContext = self.helper(runner)
710 runtimeContext.name = "test_secrets"
712 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
713 arvtool.formatgraph = None
715 job_order = {"pw": "blorp"}
716 runner.secret_store.store(["pw"], job_order)
718 for j in arvtool.job(job_order, mock.MagicMock(), runtimeContext):
719 j.run(runtimeContext)
720 runner.api.container_requests().create.assert_called_with(
721 body=JsonDiffMatcher({
723 'HOME': '/var/spool/cwl',
726 'name': 'test_secrets',
727 'runtime_constraints': {
731 'use_existing': True,
734 '/tmp': {'kind': 'tmp',
735 "capacity": 1073741824
737 '/var/spool/cwl': {'kind': 'tmp',
738 "capacity": 1073741824 }
740 'state': 'Committed',
741 'output_name': 'Output for step test_secrets',
742 'owner_uuid': 'zzzzz-8i9sb-zzzzzzzzzzzzzzz',
743 'output_path': '/var/spool/cwl',
745 'container_image': 'arvados/jobs',
746 'command': ['md5sum', 'example.conf'],
747 'cwd': '/var/spool/cwl',
748 'scheduling_parameters': {},
751 "/var/spool/cwl/example.conf": {
752 "content": "username: user\npassword: blorp\n",
758 # The test passes no builder.resources
759 # Hence the default resources will apply: {'cores': 1, 'ram': 1024, 'outdirSize': 1024, 'tmpdirSize': 1024}
760 @mock.patch("arvados.commands.keepdocker.list_images_in_arv")
761 def test_timelimit(self, keepdocker):
762 arv_docker_clear_cache()
764 runner = mock.MagicMock()
765 runner.project_uuid = "zzzzz-8i9sb-zzzzzzzzzzzzzzz"
766 runner.ignore_docker_for_reuse = False
767 runner.intermediate_output_ttl = 0
768 runner.secret_store = cwltool.secrets.SecretStore()
770 keepdocker.return_value = [("zzzzz-4zz18-zzzzzzzzzzzzzz3", "")]
771 runner.api.collections().get().execute.return_value = {
772 "portable_data_hash": "99999999999999999999999999999993+99"}
778 "arguments": [{"valueFrom": "$(runtime.outdir)"}],
780 "class": "CommandLineTool",
783 "class": "http://commonwl.org/cwltool#TimeLimit",
789 loadingContext, runtimeContext = self.helper(runner)
790 runtimeContext.name = "test_timelimit"
792 arvtool = arvados_cwl.ArvadosCommandTool(runner, tool, loadingContext)
793 arvtool.formatgraph = None
795 for j in arvtool.job({}, mock.MagicMock(), runtimeContext):
796 j.run(runtimeContext)
798 _, kwargs = runner.api.container_requests().create.call_args
799 self.assertEqual(42, kwargs['body']['scheduling_parameters'].get('max_run_time'))