toil 6.1.0__py3-none-any.whl → 7.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. toil/__init__.py +1 -232
  2. toil/batchSystems/abstractBatchSystem.py +22 -13
  3. toil/batchSystems/abstractGridEngineBatchSystem.py +59 -45
  4. toil/batchSystems/awsBatch.py +8 -8
  5. toil/batchSystems/contained_executor.py +4 -5
  6. toil/batchSystems/gridengine.py +1 -1
  7. toil/batchSystems/htcondor.py +5 -5
  8. toil/batchSystems/kubernetes.py +25 -11
  9. toil/batchSystems/local_support.py +3 -3
  10. toil/batchSystems/lsf.py +2 -2
  11. toil/batchSystems/mesos/batchSystem.py +4 -4
  12. toil/batchSystems/mesos/executor.py +3 -2
  13. toil/batchSystems/options.py +9 -0
  14. toil/batchSystems/singleMachine.py +11 -10
  15. toil/batchSystems/slurm.py +64 -22
  16. toil/batchSystems/torque.py +1 -1
  17. toil/bus.py +7 -3
  18. toil/common.py +36 -13
  19. toil/cwl/cwltoil.py +365 -312
  20. toil/deferred.py +1 -1
  21. toil/fileStores/abstractFileStore.py +17 -17
  22. toil/fileStores/cachingFileStore.py +2 -2
  23. toil/fileStores/nonCachingFileStore.py +1 -1
  24. toil/job.py +228 -60
  25. toil/jobStores/abstractJobStore.py +18 -10
  26. toil/jobStores/aws/jobStore.py +280 -218
  27. toil/jobStores/aws/utils.py +57 -29
  28. toil/jobStores/conftest.py +2 -2
  29. toil/jobStores/fileJobStore.py +2 -2
  30. toil/jobStores/googleJobStore.py +3 -4
  31. toil/leader.py +72 -24
  32. toil/lib/aws/__init__.py +26 -10
  33. toil/lib/aws/iam.py +2 -2
  34. toil/lib/aws/session.py +62 -22
  35. toil/lib/aws/utils.py +73 -37
  36. toil/lib/conversions.py +5 -1
  37. toil/lib/ec2.py +118 -69
  38. toil/lib/expando.py +1 -1
  39. toil/lib/io.py +14 -2
  40. toil/lib/misc.py +1 -3
  41. toil/lib/resources.py +55 -21
  42. toil/lib/retry.py +12 -5
  43. toil/lib/threading.py +2 -2
  44. toil/lib/throttle.py +1 -1
  45. toil/options/common.py +27 -24
  46. toil/provisioners/__init__.py +9 -3
  47. toil/provisioners/abstractProvisioner.py +9 -7
  48. toil/provisioners/aws/__init__.py +20 -15
  49. toil/provisioners/aws/awsProvisioner.py +406 -329
  50. toil/provisioners/gceProvisioner.py +2 -2
  51. toil/provisioners/node.py +13 -5
  52. toil/server/app.py +1 -1
  53. toil/statsAndLogging.py +58 -16
  54. toil/test/__init__.py +27 -12
  55. toil/test/batchSystems/batchSystemTest.py +40 -33
  56. toil/test/batchSystems/batch_system_plugin_test.py +79 -0
  57. toil/test/batchSystems/test_slurm.py +1 -1
  58. toil/test/cwl/cwlTest.py +8 -91
  59. toil/test/cwl/seqtk_seq.cwl +1 -1
  60. toil/test/docs/scriptsTest.py +10 -13
  61. toil/test/jobStores/jobStoreTest.py +33 -49
  62. toil/test/lib/aws/test_iam.py +2 -2
  63. toil/test/provisioners/aws/awsProvisionerTest.py +51 -34
  64. toil/test/provisioners/clusterTest.py +90 -8
  65. toil/test/server/serverTest.py +2 -2
  66. toil/test/src/autoDeploymentTest.py +1 -1
  67. toil/test/src/dockerCheckTest.py +2 -1
  68. toil/test/src/environmentTest.py +125 -0
  69. toil/test/src/fileStoreTest.py +1 -1
  70. toil/test/src/jobDescriptionTest.py +18 -8
  71. toil/test/src/jobTest.py +1 -1
  72. toil/test/src/realtimeLoggerTest.py +4 -0
  73. toil/test/src/workerTest.py +52 -19
  74. toil/test/utils/toilDebugTest.py +61 -3
  75. toil/test/utils/utilsTest.py +20 -18
  76. toil/test/wdl/wdltoil_test.py +24 -71
  77. toil/test/wdl/wdltoil_test_kubernetes.py +77 -0
  78. toil/toilState.py +68 -9
  79. toil/utils/toilDebugJob.py +153 -26
  80. toil/utils/toilLaunchCluster.py +12 -2
  81. toil/utils/toilRsyncCluster.py +7 -2
  82. toil/utils/toilSshCluster.py +7 -3
  83. toil/utils/toilStats.py +2 -1
  84. toil/utils/toilStatus.py +97 -51
  85. toil/version.py +10 -10
  86. toil/wdl/wdltoil.py +318 -51
  87. toil/worker.py +96 -69
  88. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/LICENSE +25 -0
  89. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/METADATA +55 -21
  90. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/RECORD +93 -90
  91. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/WHEEL +1 -1
  92. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/entry_points.txt +0 -0
  93. {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/top_level.txt +0 -0
@@ -11,36 +11,40 @@
11
11
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
12
  # See the License for the specific language governing permissions and
13
13
  # limitations under the License.
14
-
15
14
  import logging
16
15
  import os
17
16
  import subprocess
18
17
  import time
18
+
19
19
  from uuid import uuid4
20
20
  from typing import Optional, List
21
21
 
22
- from toil.lib.aws import zone_to_region
23
22
  from toil.lib.retry import retry
23
+ from toil.test import ToilTest, needs_aws_ec2, needs_fetchable_appliance, slow, needs_env_var
24
+ from toil.lib.aws import zone_to_region
25
+ from toil.lib.aws.session import AWSConnectionManager
26
+ from toil.provisioners import cluster_factory
24
27
  from toil.provisioners.aws import get_best_aws_zone
25
- from toil.test import ToilTest, needs_aws_ec2, needs_fetchable_appliance
26
28
 
27
29
  log = logging.getLogger(__name__)
28
30
 
31
+
29
32
  @needs_aws_ec2
30
33
  @needs_fetchable_appliance
31
34
  class AbstractClusterTest(ToilTest):
32
35
  def __init__(self, methodName: str) -> None:
33
36
  super().__init__(methodName=methodName)
34
37
  self.keyName = os.getenv('TOIL_AWS_KEYNAME').strip() or 'id_rsa'
35
- self.clusterName = 'aws-provisioner-test-' + str(uuid4())
38
+ self.clusterName = f'aws-provisioner-test-{uuid4()}'
36
39
  self.leaderNodeType = 't2.medium'
37
40
  self.clusterType = 'mesos'
38
41
  self.zone = get_best_aws_zone()
39
42
  assert self.zone is not None, "Could not determine AWS availability zone to test in; is TOIL_AWS_ZONE set?"
40
- # We need a boto2 connection to EC2 to check on the cluster.
41
- # Since we are protected by needs_aws_ec2 we can import from boto.
42
- import boto.ec2
43
- self.boto2_ec2 = boto.ec2.connect_to_region(zone_to_region(self.zone))
43
+ self.region = zone_to_region(self.zone)
44
+
45
+ # Get connection to AWS
46
+ self.aws = AWSConnectionManager()
47
+
44
48
  # Where should we put our virtualenv?
45
49
  self.venvDir = '/tmp/venv'
46
50
 
@@ -170,3 +174,81 @@ class AbstractClusterTest(ToilTest):
170
174
 
171
175
  def launchCluster(self) -> None:
172
176
  self.createClusterUtil()
177
+
178
+
179
+ @needs_aws_ec2
180
+ @needs_fetchable_appliance
181
+ @slow
182
+ class CWLOnARMTest(AbstractClusterTest):
183
+ """Run the CWL 1.2 conformance tests on ARM specifically."""
184
+ def __init__(self, methodName: str) -> None:
185
+ super().__init__(methodName=methodName)
186
+ self.clusterName = f'cwl-test-{uuid4()}'
187
+ self.leaderNodeType = "t4g.2xlarge"
188
+ self.clusterType = "kubernetes"
189
+ # We need to be running in a directory which Flatcar and the Toil Appliance both have
190
+ self.cwl_test_dir = "/tmp/toil/cwlTests"
191
+
192
+ def setUp(self) -> None:
193
+ super().setUp()
194
+ self.jobStore = f"aws:{self.awsRegion()}:cluster-{uuid4()}"
195
+
196
+ @needs_env_var("CI_COMMIT_SHA", "a git commit sha")
197
+ def test_cwl_on_arm(self) -> None:
198
+ # Make a cluster
199
+ self.launchCluster()
200
+ # get the leader so we know the IP address - we don't need to wait since create cluster
201
+ # already ensures the leader is running
202
+ self.cluster = cluster_factory(
203
+ provisioner="aws", zone=self.zone, clusterName=self.clusterName
204
+ )
205
+ self.leader = self.cluster.getLeader()
206
+
207
+ commit = os.environ["CI_COMMIT_SHA"]
208
+ self.sshUtil(
209
+ [
210
+ "bash",
211
+ "-c",
212
+ f"mkdir -p {self.cwl_test_dir} && cd {self.cwl_test_dir} && git clone https://github.com/DataBiosphere/toil.git",
213
+ ]
214
+ )
215
+
216
+ # We use CI_COMMIT_SHA to retrieve the Toil version needed to run the CWL tests
217
+ self.sshUtil(
218
+ ["bash", "-c", f"cd {self.cwl_test_dir}/toil && git checkout {commit}"]
219
+ )
220
+
221
+ # --never-download prevents silent upgrades to pip, wheel and setuptools
222
+ self.sshUtil(
223
+ [
224
+ "bash",
225
+ "-c",
226
+ f"virtualenv --system-site-packages --never-download {self.venvDir}",
227
+ ]
228
+ )
229
+ self.sshUtil(
230
+ [
231
+ "bash",
232
+ "-c",
233
+ f". .{self.venvDir}/bin/activate && cd {self.cwl_test_dir}/toil && make prepare && make develop extras=[all]",
234
+ ]
235
+ )
236
+
237
+ # Runs the CWLv12Test on an ARM instance
238
+ self.sshUtil(
239
+ [
240
+ "bash",
241
+ "-c",
242
+ f". .{self.venvDir}/bin/activate && cd {self.cwl_test_dir}/toil && pytest --log-cli-level DEBUG -r s src/toil/test/cwl/cwlTest.py::CWLv12Test::test_run_conformance",
243
+ ]
244
+ )
245
+
246
+ # We know if it succeeds it should save a junit XML for us to read.
247
+ # Bring it back to be an artifact.
248
+ self.rsync_util(
249
+ f":{self.cwl_test_dir}/toil/conformance-1.2.junit.xml",
250
+ os.path.join(
251
+ self._projectRootPath(),
252
+ "arm-conformance-1.2.junit.xml"
253
+ )
254
+ )
@@ -575,9 +575,9 @@ class ToilWESServerWorkflowTest(AbstractToilWESServerTest):
575
575
  with self.app.test_client() as client:
576
576
  rv = client.post("/ga4gh/wes/v1/runs", data={
577
577
  "workflow_url": "https://raw.githubusercontent.com/DataBiosphere/toil/releases/5.4.x/src/toil"
578
- "/test/docs/scripts/cwlExampleFiles/hello.cwl",
578
+ "/test/cwl/echo.cwl",
579
579
  "workflow_type": "CWL",
580
- "workflow_type_version": "v1.0",
580
+ "workflow_type_version": "v1.2",
581
581
  "workflow_params": json.dumps({"message": "Hello, world!"}),
582
582
  })
583
583
  # workflow is submitted successfully
@@ -243,7 +243,7 @@ class AutoDeploymentTest(ApplianceTestSupport):
243
243
 
244
244
  3) it is an instance of Job (and so does not introduce the user script to sys.path itself),
245
245
 
246
- … it might cause problems with deserializing a defered function defined in the user script.
246
+ … it might cause problems with deserializing a deferred function defined in the user script.
247
247
 
248
248
  `Encapsulated` has two children to ensure that `Follow-on` is run in a separate worker.
249
249
  """
@@ -14,7 +14,7 @@
14
14
  import unittest
15
15
 
16
16
  from docker.errors import ImageNotFound
17
- from toil import checkDockerImageExists, parseDockerAppliance
17
+ from toil import checkDockerImageExists, parseDockerAppliance, retry
18
18
  from toil.test import ToilTest, needs_docker
19
19
 
20
20
 
@@ -76,6 +76,7 @@ class DockerCheckTest(ToilTest):
76
76
  google_repo = 'gcr.io/google-containers/busybox:latest'
77
77
  assert checkDockerImageExists(google_repo)
78
78
 
79
+ @retry(errors=[TimeoutError]) # see: https://github.com/DataBiosphere/toil/issues/4902
79
80
  def testBadGoogleRepo(self):
80
81
  """Bad repo and tag. This should raise."""
81
82
  nonexistent_google_repo = 'gcr.io/google-containers/--------:---'
@@ -0,0 +1,125 @@
1
+ # Copyright (C) 2015-2024 Regents of the University of California
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ import logging
15
+ import os
16
+ import sys
17
+ import time
18
+
19
+ from argparse import Namespace
20
+ from threading import Thread
21
+ from typing import Optional
22
+
23
+ from toil.common import Toil
24
+ from toil.job import Job
25
+ from toil.test import ToilTest, slow
26
+ from toil.jobStores.abstractJobStore import NoSuchFileException
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+ class EnvironmentTest(ToilTest):
31
+ """
32
+ Test to make sure that Toil's environment variable save and restore system
33
+ (environment.pickle) works.
34
+
35
+ The environment should be captured once at the start of the workflow and
36
+ should be sent through based on that, not base don the leader's current
37
+ environment when the job is launched.
38
+ """
39
+
40
+ def test_environment(self):
41
+ options = Job.Runner.getDefaultOptions(self._getTestJobStorePath())
42
+ options.logLevel = "DEBUG"
43
+ options.retryCount = 0
44
+
45
+ main(options)
46
+
47
+ def signal_leader(job):
48
+ """
49
+ Make a file in the file store that the leader can see.
50
+ """
51
+ with job.fileStore.jobStore.write_shared_file_stream("jobstarted.txt", encoding="utf-8") as stream:
52
+ stream.write("Job has run")
53
+
54
+ def check_environment(job, try_name: str):
55
+ """
56
+ Fail if the test environment is wrong.
57
+ """
58
+
59
+ job.fileStore.log_to_leader(f"Try {try_name} checking environment")
60
+ value = os.environ["MAGIC_ENV_VAR_123"]
61
+ job.fileStore.log_to_leader(f"Try {try_name} got: {value}")
62
+ if value != "Value1":
63
+ raise RuntimeError("Environment variable is wrong!")
64
+
65
+ def wait_a_bit(job):
66
+ """
67
+ Toil job that waits.
68
+ """
69
+ time.sleep(10)
70
+
71
+ def check_environment_repeatedly(job):
72
+ """
73
+ Toil job that checks the environment, waits, and checks it again, as
74
+ separate invocations.
75
+ """
76
+
77
+ signal = job.addChildJobFn(signal_leader)
78
+ check1 = signal.addFollowOnJobFn(check_environment, "try1")
79
+ waiter = check1.addFollowOnJobFn(wait_a_bit)
80
+ check2 = waiter.addFollowOnJobFn(check_environment, "try2")
81
+ # Add another one to make sure we don't chain
82
+ check3 = waiter.addFollowOnJobFn(check_environment, "try3")
83
+
84
+ def main(options: Optional[Namespace] = None):
85
+ """
86
+ Run the actual workflow with the given options.
87
+ """
88
+ if not options:
89
+ # deal with command line arguments
90
+ parser = Job.Runner.getDefaultArgumentParser()
91
+ options = parser.parse_args()
92
+ logging.basicConfig()
93
+
94
+ # Set something that should be seen by Toil jobs
95
+ os.environ["MAGIC_ENV_VAR_123"] = "Value1"
96
+
97
+ with Toil(options) as toil:
98
+
99
+ # Get a tthe job store so we can use shared files.
100
+ jobStore = toil._jobStore
101
+
102
+ # Once the workflow has started, change the environment
103
+ def change_environment_later():
104
+ """
105
+ After waiting, modify the environment.
106
+ """
107
+ while True:
108
+ # Wait for the workflow to say it ran something
109
+ time.sleep(5)
110
+ try:
111
+ with jobStore.read_shared_file_stream("jobstarted.txt", encoding="utf-8") as stream:
112
+ logger.info("Got signal from job: %s", stream.read().strip())
113
+ break
114
+ except NoSuchFileException:
115
+ pass
116
+ # Change the environment variable
117
+ logger.info("Changing environment variable")
118
+ os.environ["MAGIC_ENV_VAR_123"] = "Value2"
119
+ changer_thread = Thread(target=change_environment_later)
120
+ changer_thread.start()
121
+
122
+ toil.start(Job.wrapJobFn(check_environment_repeatedly))
123
+
124
+ if __name__ == "__main__":
125
+ main()
@@ -641,7 +641,7 @@ class hidden:
641
641
  file into cache then rewrites it to the job store triggering an async write since the
642
642
  two unique jobstore IDs point to the same local file. Also, the second write is not
643
643
  cached since the first was written to cache, and there "isn't enough space" to cache the
644
- second. Imediately assert that the second write isn't cached, and is being
644
+ second. Immediately assert that the second write isn't cached, and is being
645
645
  asynchronously written to the job store.
646
646
 
647
647
  Attempting to get the file from the jobstore should not fail.
@@ -18,6 +18,7 @@ from configargparse import ArgumentParser
18
18
 
19
19
  from toil.common import Toil
20
20
  from toil.job import Job, JobDescription, TemporaryID
21
+ from toil.resource import ModuleDescriptor
21
22
  from toil.test import ToilTest
22
23
 
23
24
 
@@ -43,17 +44,24 @@ class JobDescriptionTest(ToilTest):
43
44
  Tests the public interface of a JobDescription.
44
45
  """
45
46
 
46
- command = "by your command"
47
47
  memory = 2^32
48
48
  disk = 2^32
49
49
  cores = "1"
50
50
  preemptible = 1
51
51
 
52
- j = JobDescription(command=command, requirements={"memory": memory, "cores": cores, "disk": disk, "preemptible": preemptible},
52
+ j = JobDescription(requirements={"memory": memory, "cores": cores, "disk": disk, "preemptible": preemptible},
53
53
  jobName='testJobGraph', unitName='noName')
54
+
55
+
56
+ # Without a body, and with nothing to run, nextSuccessors will be None
57
+ self.assertEqual(j.has_body(), False)
58
+ self.assertEqual(j.nextSuccessors(), None)
59
+
60
+ # Attach a body so the job has something to do itself.
61
+ j.attach_body("fake", ModuleDescriptor.forModule("toil"))
62
+ self.assertEqual(j.has_body(), True)
54
63
 
55
64
  #Check attributes
56
- self.assertEqual(j.command, command)
57
65
  self.assertEqual(j.memory, memory)
58
66
  self.assertEqual(j.disk, disk)
59
67
  self.assertEqual(j.cores, int(cores))
@@ -68,22 +76,24 @@ class JobDescriptionTest(ToilTest):
68
76
  self.assertEqual(j.logJobStoreFileID, None)
69
77
 
70
78
  #Check equals function (should be based on object identity and not contents)
71
- j2 = JobDescription(command=command, requirements={"memory": memory, "cores": cores, "disk": disk, "preemptible": preemptible},
79
+ j2 = JobDescription(requirements={"memory": memory, "cores": cores, "disk": disk, "preemptible": preemptible},
72
80
  jobName='testJobGraph', unitName='noName')
81
+ j2.attach_body("fake", ModuleDescriptor.forModule("toil"))
73
82
  self.assertNotEqual(j, j2)
74
83
  ###TODO test other functionality
75
84
 
76
85
  def testJobDescriptionSequencing(self):
77
- j = JobDescription(command='command', requirements={}, jobName='unimportant')
86
+ j = JobDescription(requirements={}, jobName='unimportant')
78
87
 
79
88
  j.addChild('child')
80
89
  j.addFollowOn('followOn')
81
90
 
82
- # With a command, nothing should be ready to run
91
+ # With a body, nothing should be ready to run
92
+ j.attach_body("fake", ModuleDescriptor.forModule("toil"))
83
93
  self.assertEqual(list(j.nextSuccessors()), [])
84
94
 
85
- # With command cleared, child should be ready to run
86
- j.command = None
95
+ # With body cleared, child should be ready to run
96
+ j.detach_body()
87
97
  self.assertEqual(list(j.nextSuccessors()), ['child'])
88
98
 
89
99
  # Without the child, the follow-on should be ready to run
toil/test/src/jobTest.py CHANGED
@@ -268,7 +268,7 @@ class JobTest(ToilTest):
268
268
  Test for issue #1465: Detection of checkpoint jobs that are not leaf vertices
269
269
  identifies leaf vertices incorrectly
270
270
 
271
- Test verification of new checkpoint jobs being leaf verticies,
271
+ Test verification of new checkpoint jobs being leaf vertices,
272
272
  starting with the following baseline workflow::
273
273
 
274
274
  Parent
@@ -30,6 +30,9 @@ class RealtimeLoggerTest(ToilTest):
30
30
  # Set up a log message detector to the root logger
31
31
  logging.getLogger().addHandler(detector)
32
32
 
33
+ # I believe coloredlogs replaces handlers with its own when doing handler formatting, preserving only filters
34
+ # https://github.com/xolox/python-coloredlogs/blob/65bdfe976ac0bf81e8c0bd9a98242b9d666b2859/coloredlogs/__init__.py#L453-L459
35
+ options.colored_logs = False
33
36
  Job.Runner.startToil(LogTest(), options)
34
37
 
35
38
  # We need the message we're supposed to see
@@ -46,6 +49,7 @@ class MessageDetector(logging.StreamHandler):
46
49
  def __init__(self):
47
50
  self.detected = False # Have we seen the message we want?
48
51
  self.overLogged = False # Have we seen the message we don't want?
52
+
49
53
  super().__init__()
50
54
 
51
55
  def emit(self, record):
@@ -18,6 +18,8 @@ from toil.jobStores.fileJobStore import FileJobStore
18
18
  from toil.test import ToilTest
19
19
  from toil.worker import nextChainable
20
20
 
21
+ from typing import Optional
22
+
21
23
 
22
24
  class WorkerTests(ToilTest):
23
25
  """Test miscellaneous units of the worker."""
@@ -32,7 +34,7 @@ class WorkerTests(ToilTest):
32
34
 
33
35
  def testNextChainable(self):
34
36
  """Make sure chainable/non-chainable jobs are identified correctly."""
35
- def createTestJobDesc(memory, cores, disk, preemptible, checkpoint):
37
+ def createTestJobDesc(memory, cores, disk, preemptible: bool = True, checkpoint: bool = False, local: Optional[bool] = None):
36
38
  """
37
39
  Create a JobDescription with no command (representing a Job that
38
40
  has already run) and return the JobDescription.
@@ -41,7 +43,16 @@ class WorkerTests(ToilTest):
41
43
  self.jobNumber += 1
42
44
 
43
45
  descClass = CheckpointJobDescription if checkpoint else JobDescription
44
- jobDesc = descClass(requirements={'memory': memory, 'cores': cores, 'disk': disk, 'preemptible': preemptible}, jobName=name)
46
+ jobDesc = descClass(
47
+ requirements={
48
+ 'memory': memory,
49
+ 'cores': cores,
50
+ 'disk': disk,
51
+ 'preemptible': preemptible
52
+ },
53
+ jobName=name,
54
+ local=local
55
+ )
45
56
 
46
57
  # Assign an ID
47
58
  self.jobStore.assign_job_id(jobDesc)
@@ -53,42 +64,64 @@ class WorkerTests(ToilTest):
53
64
  # Try with the branch point at both child and follow-on stages
54
65
 
55
66
  # Identical non-checkpoint jobs should be chainable.
56
- jobDesc1 = createTestJobDesc(1, 2, 3, True, False)
57
- jobDesc2 = createTestJobDesc(1, 2, 3, True, False)
67
+ jobDesc1 = createTestJobDesc(1, 2, 3)
68
+ jobDesc2 = createTestJobDesc(1, 2, 3)
58
69
  getattr(jobDesc1, successorType)(jobDesc2.jobStoreID)
59
70
  chainable = nextChainable(jobDesc1, self.jobStore, self.config)
60
71
  self.assertNotEqual(chainable, None)
61
- self.assertEqual(jobDesc2.jobStoreID, chainable.jobStoreID)
72
+ self.assertEqual(chainable.jobStoreID, jobDesc2.jobStoreID)
62
73
 
63
74
  # Identical checkpoint jobs should not be chainable.
64
- jobDesc1 = createTestJobDesc(1, 2, 3, True, False)
65
- jobDesc2 = createTestJobDesc(1, 2, 3, True, True)
75
+ jobDesc1 = createTestJobDesc(1, 2, 3, checkpoint=True)
76
+ jobDesc2 = createTestJobDesc(1, 2, 3, checkpoint=True)
77
+ getattr(jobDesc1, successorType)(jobDesc2.jobStoreID)
78
+ self.assertEqual(nextChainable(jobDesc1, self.jobStore, self.config), None)
79
+
80
+ # Changing checkpoint from false to true should make it not chainable.
81
+ jobDesc1 = createTestJobDesc(1, 2, 3, checkpoint=False)
82
+ jobDesc2 = createTestJobDesc(1, 2, 3, checkpoint=True)
66
83
  getattr(jobDesc1, successorType)(jobDesc2.jobStoreID)
67
- self.assertEqual(None, nextChainable(jobDesc1, self.jobStore, self.config))
84
+ self.assertEqual(nextChainable(jobDesc1, self.jobStore, self.config), None)
68
85
 
69
86
  # If there is no child we should get nothing to chain.
70
- jobDesc1 = createTestJobDesc(1, 2, 3, True, False)
71
- self.assertEqual(None, nextChainable(jobDesc1, self.jobStore, self.config))
87
+ jobDesc1 = createTestJobDesc(1, 2, 3)
88
+ self.assertEqual(nextChainable(jobDesc1, self.jobStore, self.config), None)
72
89
 
73
90
  # If there are 2 or more children we should get nothing to chain.
74
- jobDesc1 = createTestJobDesc(1, 2, 3, True, False)
75
- jobDesc2 = createTestJobDesc(1, 2, 3, True, False)
76
- jobDesc3 = createTestJobDesc(1, 2, 3, True, False)
91
+ jobDesc1 = createTestJobDesc(1, 2, 3)
92
+ jobDesc2 = createTestJobDesc(1, 2, 3)
93
+ jobDesc3 = createTestJobDesc(1, 2, 3)
77
94
  getattr(jobDesc1, successorType)(jobDesc2.jobStoreID)
78
95
  getattr(jobDesc1, successorType)(jobDesc3.jobStoreID)
79
- self.assertEqual(None, nextChainable(jobDesc1, self.jobStore, self.config))
96
+ self.assertEqual(nextChainable(jobDesc1, self.jobStore, self.config), None)
80
97
 
81
98
  # If there is an increase in resource requirements we should get nothing to chain.
82
- reqs = {'memory': 1, 'cores': 2, 'disk': 3, 'preemptible': True, 'checkpoint': False}
99
+ base_reqs = {'memory': 1, 'cores': 2, 'disk': 3, 'preemptible': True, 'checkpoint': False}
83
100
  for increased_attribute in ('memory', 'cores', 'disk'):
101
+ reqs = dict(base_reqs)
84
102
  jobDesc1 = createTestJobDesc(**reqs)
85
103
  reqs[increased_attribute] += 1
86
104
  jobDesc2 = createTestJobDesc(**reqs)
87
105
  getattr(jobDesc1, successorType)(jobDesc2.jobStoreID)
88
- self.assertEqual(None, nextChainable(jobDesc1, self.jobStore, self.config))
106
+ self.assertEqual(nextChainable(jobDesc1, self.jobStore, self.config), None)
89
107
 
90
108
  # A change in preemptability from True to False should be disallowed.
91
- jobDesc1 = createTestJobDesc(1, 2, 3, True, False)
92
- jobDesc2 = createTestJobDesc(1, 2, 3, False, True)
109
+ jobDesc1 = createTestJobDesc(1, 2, 3, preemptible=True)
110
+ jobDesc2 = createTestJobDesc(1, 2, 3, preemptible=False)
111
+ getattr(jobDesc1, successorType)(jobDesc2.jobStoreID)
112
+ self.assertEqual(nextChainable(jobDesc1, self.jobStore, self.config), None)
113
+
114
+ # A change in local-ness from True to False should be disallowed.
115
+ jobDesc1 = createTestJobDesc(1, 2, 3, local=True)
116
+ jobDesc2 = createTestJobDesc(1, 2, 3, local=False)
93
117
  getattr(jobDesc1, successorType)(jobDesc2.jobStoreID)
94
- self.assertEqual(None, nextChainable(jobDesc1, self.jobStore, self.config))
118
+ self.assertEqual(nextChainable(jobDesc1, self.jobStore, self.config), None)
119
+
120
+ # A change in local-ness from False to True should be allowed,
121
+ # since running locally is an optional optimization.
122
+ jobDesc1 = createTestJobDesc(1, 2, 3, local=False)
123
+ jobDesc2 = createTestJobDesc(1, 2, 3, local=True)
124
+ getattr(jobDesc1, successorType)(jobDesc2.jobStoreID)
125
+ chainable = nextChainable(jobDesc1, self.jobStore, self.config)
126
+ self.assertNotEqual(chainable, None)
127
+ self.assertEqual(chainable.jobStoreID, jobDesc2.jobStoreID)
@@ -21,7 +21,7 @@ import pytest
21
21
  from toil.test import ToilTest
22
22
 
23
23
  from toil.lib.resources import glob
24
- from toil.test import slow
24
+ from toil.test import slow, needs_wdl
25
25
  from toil.version import python
26
26
 
27
27
  logger = logging.getLogger(__name__)
@@ -136,7 +136,7 @@ class DebugJobTest(ToilTest):
136
136
 
137
137
  logger.info("Running workflow that always fails")
138
138
  try:
139
- # Run an always-failign workflow
139
+ # Run an always-failing workflow
140
140
  subprocess.check_call([
141
141
  python,
142
142
  os.path.abspath("src/toil/test/docs/scripts/example_alwaysfail.py"),
@@ -150,13 +150,44 @@ class DebugJobTest(ToilTest):
150
150
  # Should fail to run
151
151
  logger.info("Task failed successfully")
152
152
  pass
153
-
153
+
154
154
  # Get the job ID.
155
155
  # TODO: This assumes a lot about the FileJobStore. Use the MessageBus instead?
156
156
  job_id = "kind-explode/" + os.listdir(os.path.join(job_store, "jobs/kind-explode"))[0]
157
157
 
158
158
  return job_store, job_id
159
159
 
160
+ def _get_wdl_job_store_and_job_name(self):
161
+ """
162
+ Get a job store and the name of a failed job in it that actually wanted to use some files.
163
+ """
164
+
165
+ # First make a job store.
166
+ job_store = os.path.join(self._createTempDir(), "tree")
167
+
168
+ logger.info("Running workflow that always fails")
169
+ try:
170
+ # Run an always-failing workflow
171
+ subprocess.check_call([
172
+ "toil-wdl-runner",
173
+ os.path.abspath("src/toil/test/docs/scripts/example_alwaysfail_with_files.wdl"),
174
+ "--retryCount=0",
175
+ "--logCritical",
176
+ "--disableProgress",
177
+ "--jobStore",
178
+ job_store
179
+ ], stderr=subprocess.DEVNULL)
180
+ raise RuntimeError("Failing workflow succeeded!")
181
+ except subprocess.CalledProcessError:
182
+ # Should fail to run
183
+ logger.info("Task failed successfully")
184
+ pass
185
+
186
+ # Get a job name for a job that fails
187
+ job_name = "WDLTaskJob"
188
+
189
+ return job_store, job_name
190
+
160
191
  def test_run_job(self):
161
192
  """
162
193
  Make sure that we can use toil debug-job to try and run a job in-process.
@@ -198,4 +229,31 @@ class DebugJobTest(ToilTest):
198
229
  job_id
199
230
  ])
200
231
 
232
+ @needs_wdl
233
+ def test_retrieve_task_directory(self):
234
+ """
235
+ Make sure that we can use --retrieveTaskDirectory to get the input files for a job.
236
+ """
237
+
238
+ job_store, job_name = self._get_wdl_job_store_and_job_name()
239
+
240
+ logger.info("Trying to retrieve task dorectory for job %s", job_name)
241
+
242
+ dest_dir = os.path.join(self._createTempDir(), "dump")
243
+
244
+ # Print the job info and make sure that doesn't crash.
245
+ subprocess.check_call([
246
+ "toil",
247
+ "debug-job",
248
+ "--logDebug",
249
+ job_store,
250
+ job_name,
251
+ "--retrieveTaskDirectory",
252
+ dest_dir
253
+ ])
254
+
255
+ first_file = os.path.join(dest_dir, "inside/mnt/miniwdl_task_container/work/_miniwdl_inputs/0/test.txt")
256
+ assert os.path.exists(first_file), "Input file not found in fake container environment"
257
+ self.assertEqual(open(first_file).read(), "These are the contents\n")
258
+
201
259