toil 6.1.0a1__py3-none-any.whl → 8.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 (193) hide show
  1. toil/__init__.py +122 -315
  2. toil/batchSystems/__init__.py +1 -0
  3. toil/batchSystems/abstractBatchSystem.py +173 -89
  4. toil/batchSystems/abstractGridEngineBatchSystem.py +272 -148
  5. toil/batchSystems/awsBatch.py +244 -135
  6. toil/batchSystems/cleanup_support.py +26 -16
  7. toil/batchSystems/contained_executor.py +31 -28
  8. toil/batchSystems/gridengine.py +86 -50
  9. toil/batchSystems/htcondor.py +166 -89
  10. toil/batchSystems/kubernetes.py +632 -382
  11. toil/batchSystems/local_support.py +20 -15
  12. toil/batchSystems/lsf.py +134 -81
  13. toil/batchSystems/lsfHelper.py +13 -11
  14. toil/batchSystems/mesos/__init__.py +41 -29
  15. toil/batchSystems/mesos/batchSystem.py +290 -151
  16. toil/batchSystems/mesos/executor.py +79 -50
  17. toil/batchSystems/mesos/test/__init__.py +31 -23
  18. toil/batchSystems/options.py +46 -28
  19. toil/batchSystems/registry.py +53 -19
  20. toil/batchSystems/singleMachine.py +296 -125
  21. toil/batchSystems/slurm.py +603 -138
  22. toil/batchSystems/torque.py +47 -33
  23. toil/bus.py +186 -76
  24. toil/common.py +664 -368
  25. toil/cwl/__init__.py +1 -1
  26. toil/cwl/cwltoil.py +1136 -483
  27. toil/cwl/utils.py +17 -22
  28. toil/deferred.py +63 -42
  29. toil/exceptions.py +5 -3
  30. toil/fileStores/__init__.py +5 -5
  31. toil/fileStores/abstractFileStore.py +140 -60
  32. toil/fileStores/cachingFileStore.py +717 -269
  33. toil/fileStores/nonCachingFileStore.py +116 -87
  34. toil/job.py +1225 -368
  35. toil/jobStores/abstractJobStore.py +416 -266
  36. toil/jobStores/aws/jobStore.py +863 -477
  37. toil/jobStores/aws/utils.py +201 -120
  38. toil/jobStores/conftest.py +3 -2
  39. toil/jobStores/fileJobStore.py +292 -154
  40. toil/jobStores/googleJobStore.py +140 -74
  41. toil/jobStores/utils.py +36 -15
  42. toil/leader.py +668 -272
  43. toil/lib/accelerators.py +115 -18
  44. toil/lib/aws/__init__.py +74 -31
  45. toil/lib/aws/ami.py +122 -87
  46. toil/lib/aws/iam.py +284 -108
  47. toil/lib/aws/s3.py +31 -0
  48. toil/lib/aws/session.py +214 -39
  49. toil/lib/aws/utils.py +287 -231
  50. toil/lib/bioio.py +13 -5
  51. toil/lib/compatibility.py +11 -6
  52. toil/lib/conversions.py +104 -47
  53. toil/lib/docker.py +131 -103
  54. toil/lib/ec2.py +361 -199
  55. toil/lib/ec2nodes.py +174 -106
  56. toil/lib/encryption/_dummy.py +5 -3
  57. toil/lib/encryption/_nacl.py +10 -6
  58. toil/lib/encryption/conftest.py +1 -0
  59. toil/lib/exceptions.py +26 -7
  60. toil/lib/expando.py +5 -3
  61. toil/lib/ftp_utils.py +217 -0
  62. toil/lib/generatedEC2Lists.py +127 -19
  63. toil/lib/humanize.py +6 -2
  64. toil/lib/integration.py +341 -0
  65. toil/lib/io.py +141 -15
  66. toil/lib/iterables.py +4 -2
  67. toil/lib/memoize.py +12 -8
  68. toil/lib/misc.py +66 -21
  69. toil/lib/objects.py +2 -2
  70. toil/lib/resources.py +68 -15
  71. toil/lib/retry.py +126 -81
  72. toil/lib/threading.py +299 -82
  73. toil/lib/throttle.py +16 -15
  74. toil/options/common.py +843 -409
  75. toil/options/cwl.py +175 -90
  76. toil/options/runner.py +50 -0
  77. toil/options/wdl.py +73 -17
  78. toil/provisioners/__init__.py +117 -46
  79. toil/provisioners/abstractProvisioner.py +332 -157
  80. toil/provisioners/aws/__init__.py +70 -33
  81. toil/provisioners/aws/awsProvisioner.py +1145 -715
  82. toil/provisioners/clusterScaler.py +541 -279
  83. toil/provisioners/gceProvisioner.py +282 -179
  84. toil/provisioners/node.py +155 -79
  85. toil/realtimeLogger.py +34 -22
  86. toil/resource.py +137 -75
  87. toil/server/app.py +128 -62
  88. toil/server/celery_app.py +3 -1
  89. toil/server/cli/wes_cwl_runner.py +82 -53
  90. toil/server/utils.py +54 -28
  91. toil/server/wes/abstract_backend.py +64 -26
  92. toil/server/wes/amazon_wes_utils.py +21 -15
  93. toil/server/wes/tasks.py +121 -63
  94. toil/server/wes/toil_backend.py +142 -107
  95. toil/server/wsgi_app.py +4 -3
  96. toil/serviceManager.py +58 -22
  97. toil/statsAndLogging.py +224 -70
  98. toil/test/__init__.py +282 -183
  99. toil/test/batchSystems/batchSystemTest.py +460 -210
  100. toil/test/batchSystems/batch_system_plugin_test.py +90 -0
  101. toil/test/batchSystems/test_gridengine.py +173 -0
  102. toil/test/batchSystems/test_lsf_helper.py +67 -58
  103. toil/test/batchSystems/test_slurm.py +110 -49
  104. toil/test/cactus/__init__.py +0 -0
  105. toil/test/cactus/test_cactus_integration.py +56 -0
  106. toil/test/cwl/cwlTest.py +496 -287
  107. toil/test/cwl/measure_default_memory.cwl +12 -0
  108. toil/test/cwl/not_run_required_input.cwl +29 -0
  109. toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
  110. toil/test/cwl/seqtk_seq.cwl +1 -1
  111. toil/test/docs/scriptsTest.py +69 -46
  112. toil/test/jobStores/jobStoreTest.py +427 -264
  113. toil/test/lib/aws/test_iam.py +118 -50
  114. toil/test/lib/aws/test_s3.py +16 -9
  115. toil/test/lib/aws/test_utils.py +5 -6
  116. toil/test/lib/dockerTest.py +118 -141
  117. toil/test/lib/test_conversions.py +113 -115
  118. toil/test/lib/test_ec2.py +58 -50
  119. toil/test/lib/test_integration.py +104 -0
  120. toil/test/lib/test_misc.py +12 -5
  121. toil/test/mesos/MesosDataStructuresTest.py +23 -10
  122. toil/test/mesos/helloWorld.py +7 -6
  123. toil/test/mesos/stress.py +25 -20
  124. toil/test/options/__init__.py +13 -0
  125. toil/test/options/options.py +42 -0
  126. toil/test/provisioners/aws/awsProvisionerTest.py +320 -150
  127. toil/test/provisioners/clusterScalerTest.py +440 -250
  128. toil/test/provisioners/clusterTest.py +166 -44
  129. toil/test/provisioners/gceProvisionerTest.py +174 -100
  130. toil/test/provisioners/provisionerTest.py +25 -13
  131. toil/test/provisioners/restartScript.py +5 -4
  132. toil/test/server/serverTest.py +188 -141
  133. toil/test/sort/restart_sort.py +137 -68
  134. toil/test/sort/sort.py +134 -66
  135. toil/test/sort/sortTest.py +91 -49
  136. toil/test/src/autoDeploymentTest.py +141 -101
  137. toil/test/src/busTest.py +20 -18
  138. toil/test/src/checkpointTest.py +8 -2
  139. toil/test/src/deferredFunctionTest.py +49 -35
  140. toil/test/src/dockerCheckTest.py +32 -24
  141. toil/test/src/environmentTest.py +135 -0
  142. toil/test/src/fileStoreTest.py +539 -272
  143. toil/test/src/helloWorldTest.py +7 -4
  144. toil/test/src/importExportFileTest.py +61 -31
  145. toil/test/src/jobDescriptionTest.py +46 -21
  146. toil/test/src/jobEncapsulationTest.py +2 -0
  147. toil/test/src/jobFileStoreTest.py +74 -50
  148. toil/test/src/jobServiceTest.py +187 -73
  149. toil/test/src/jobTest.py +121 -71
  150. toil/test/src/miscTests.py +19 -18
  151. toil/test/src/promisedRequirementTest.py +82 -36
  152. toil/test/src/promisesTest.py +7 -6
  153. toil/test/src/realtimeLoggerTest.py +10 -6
  154. toil/test/src/regularLogTest.py +71 -37
  155. toil/test/src/resourceTest.py +80 -49
  156. toil/test/src/restartDAGTest.py +36 -22
  157. toil/test/src/resumabilityTest.py +9 -2
  158. toil/test/src/retainTempDirTest.py +45 -14
  159. toil/test/src/systemTest.py +12 -8
  160. toil/test/src/threadingTest.py +44 -25
  161. toil/test/src/toilContextManagerTest.py +10 -7
  162. toil/test/src/userDefinedJobArgTypeTest.py +8 -5
  163. toil/test/src/workerTest.py +73 -23
  164. toil/test/utils/toilDebugTest.py +103 -33
  165. toil/test/utils/toilKillTest.py +4 -5
  166. toil/test/utils/utilsTest.py +245 -106
  167. toil/test/wdl/wdltoil_test.py +818 -149
  168. toil/test/wdl/wdltoil_test_kubernetes.py +91 -0
  169. toil/toilState.py +120 -35
  170. toil/utils/toilConfig.py +13 -4
  171. toil/utils/toilDebugFile.py +44 -27
  172. toil/utils/toilDebugJob.py +214 -27
  173. toil/utils/toilDestroyCluster.py +11 -6
  174. toil/utils/toilKill.py +8 -3
  175. toil/utils/toilLaunchCluster.py +256 -140
  176. toil/utils/toilMain.py +37 -16
  177. toil/utils/toilRsyncCluster.py +32 -14
  178. toil/utils/toilSshCluster.py +49 -22
  179. toil/utils/toilStats.py +356 -273
  180. toil/utils/toilStatus.py +292 -139
  181. toil/utils/toilUpdateEC2Instances.py +3 -1
  182. toil/version.py +12 -12
  183. toil/wdl/utils.py +5 -5
  184. toil/wdl/wdltoil.py +3913 -1033
  185. toil/worker.py +367 -184
  186. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/LICENSE +25 -0
  187. toil-8.0.0.dist-info/METADATA +173 -0
  188. toil-8.0.0.dist-info/RECORD +253 -0
  189. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/WHEEL +1 -1
  190. toil-6.1.0a1.dist-info/METADATA +0 -125
  191. toil-6.1.0a1.dist-info/RECORD +0 -237
  192. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/entry_points.txt +0 -0
  193. {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/top_level.txt +0 -0
toil/test/cwl/cwlTest.py CHANGED
@@ -26,10 +26,13 @@ import zipfile
26
26
  from functools import partial
27
27
  from io import StringIO
28
28
  from pathlib import Path
29
- from typing import Dict, List, MutableMapping, Optional
29
+ from typing import TYPE_CHECKING, Callable, Optional, cast
30
30
  from unittest.mock import Mock, call
31
31
  from urllib.request import urlretrieve
32
32
 
33
+ if TYPE_CHECKING:
34
+ from cwltool.utils import CWLObjectType
35
+
33
36
  import pytest
34
37
 
35
38
  pkg_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) # noqa
@@ -37,33 +40,33 @@ sys.path.insert(0, pkg_root) # noqa
37
40
 
38
41
  from schema_salad.exceptions import ValidationException
39
42
 
40
- from toil.cwl.utils import (download_structure,
41
- visit_cwl_class_and_reduce,
42
- visit_top_cwl_class)
43
- from toil.exceptions import FailedJobsException
43
+ from toil.cwl.utils import (
44
+ DirectoryStructure,
45
+ download_structure,
46
+ visit_cwl_class_and_reduce,
47
+ visit_top_cwl_class,
48
+ )
44
49
  from toil.fileStores import FileID
45
50
  from toil.fileStores.abstractFileStore import AbstractFileStore
46
51
  from toil.lib.threading import cpu_count
47
- from toil.provisioners import cluster_factory
48
- from toil.test import (ToilTest,
49
- needs_aws_ec2,
50
- needs_aws_s3,
51
- needs_cwl,
52
- needs_docker,
53
- needs_docker_cuda,
54
- needs_env_var,
55
- needs_fetchable_appliance,
56
- needs_gridengine,
57
- needs_kubernetes,
58
- needs_local_cuda,
59
- needs_lsf,
60
- needs_mesos,
61
- needs_online,
62
- needs_slurm,
63
- needs_torque,
64
- needs_wes_server,
65
- slow)
66
- from toil.test.provisioners.clusterTest import AbstractClusterTest
52
+ from toil.test import (
53
+ ToilTest,
54
+ needs_aws_s3,
55
+ needs_cwl,
56
+ needs_docker,
57
+ needs_docker_cuda,
58
+ needs_gridengine,
59
+ needs_kubernetes,
60
+ needs_local_cuda,
61
+ needs_lsf,
62
+ needs_mesos,
63
+ needs_online,
64
+ needs_singularity_or_docker,
65
+ needs_slurm,
66
+ needs_torque,
67
+ needs_wes_server,
68
+ slow,
69
+ )
67
70
 
68
71
  log = logging.getLogger(__name__)
69
72
  CONFORMANCE_TEST_TIMEOUT = 10000
@@ -74,14 +77,14 @@ def run_conformance_tests(
74
77
  yml: str,
75
78
  runner: Optional[str] = None,
76
79
  caching: bool = False,
77
- batchSystem: str = None,
78
- selected_tests: str = None,
79
- selected_tags: str = None,
80
- skipped_tests: str = None,
81
- extra_args: Optional[List[str]] = None,
80
+ batchSystem: Optional[str] = None,
81
+ selected_tests: Optional[str] = None,
82
+ selected_tags: Optional[str] = None,
83
+ skipped_tests: Optional[str] = None,
84
+ extra_args: Optional[list[str]] = None,
82
85
  must_support_all_features: bool = False,
83
86
  junit_file: Optional[str] = None,
84
- ):
87
+ ) -> None:
85
88
  """
86
89
  Run the CWL conformance tests.
87
90
 
@@ -139,7 +142,7 @@ def run_conformance_tests(
139
142
  "--relax-path-checks",
140
143
  # Defaults to 20s but we can't start hundreds of nodejs processes that fast on our CI potatoes
141
144
  "--eval-timeout=600",
142
- f"--caching={caching}"
145
+ f"--caching={caching}",
143
146
  ]
144
147
 
145
148
  if extra_args:
@@ -173,30 +176,53 @@ def run_conformance_tests(
173
176
  cmd.extend(["--"] + args_passed_directly_to_runner)
174
177
 
175
178
  log.info("Running: '%s'", "' '".join(cmd))
179
+ output_lines: list[str] = []
176
180
  try:
177
- output = subprocess.check_output(cmd, cwd=workDir, stderr=subprocess.STDOUT)
181
+ child = subprocess.Popen(
182
+ cmd, cwd=workDir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
183
+ )
184
+
185
+ if child.stdout is not None:
186
+ for line_bytes in child.stdout:
187
+ # Pass through all the logs
188
+ line_text = line_bytes.decode("utf-8", errors="replace").rstrip()
189
+ output_lines.append(line_text)
190
+ log.info(line_text)
191
+
192
+ # Once it's done writing, amke sure it succeeded.
193
+ child.wait()
194
+ log.info("CWL tests finished with exit code %s", child.returncode)
195
+ if child.returncode != 0:
196
+ # Act like check_output and raise an error.
197
+ raise subprocess.CalledProcessError(child.returncode, " ".join(cmd))
178
198
  finally:
179
199
  if job_store_override:
180
200
  # Clean up the job store we used for all the tests, if it is still there.
181
201
  subprocess.run(["toil", "clean", job_store_override])
182
202
 
183
203
  except subprocess.CalledProcessError as e:
204
+ log.info("CWL test runner return code was unsuccessful")
184
205
  only_unsupported = False
185
206
  # check output -- if we failed but only have unsupported features, we're okay
186
207
  p = re.compile(
187
208
  r"(?P<failures>\d+) failures, (?P<unsupported>\d+) unsupported features"
188
209
  )
189
210
 
190
- error_log = e.output.decode("utf-8")
191
- for line in error_log.split("\n"):
192
- m = p.search(line)
211
+ for line_text in output_lines:
212
+ m = p.search(line_text)
193
213
  if m:
194
214
  if int(m.group("failures")) == 0 and int(m.group("unsupported")) > 0:
195
215
  only_unsupported = True
196
216
  break
197
217
  if (not only_unsupported) or must_support_all_features:
198
- print(error_log)
218
+ log.error(
219
+ "CWL tests gave unacceptable output:\n%s", "\n".join(output_lines)
220
+ )
199
221
  raise e
222
+ log.info("Unsuccessful return code is OK")
223
+
224
+
225
+ TesterFuncType = Callable[[str, str, "CWLObjectType"], None]
200
226
 
201
227
 
202
228
  @needs_cwl
@@ -207,43 +233,49 @@ class CWLWorkflowTest(ToilTest):
207
233
  inputs.
208
234
  """
209
235
 
210
- def setUp(self):
236
+ def setUp(self) -> None:
211
237
  """Runs anew before each test to create farm fresh temp dirs."""
212
238
  self.outDir = f"/tmp/toil-cwl-test-{str(uuid.uuid4())}"
213
239
  os.makedirs(self.outDir)
214
240
  self.rootDir = self._projectRootPath()
241
+ self.jobStoreDir = f"./jobstore-{str(uuid.uuid4())}"
215
242
 
216
- def tearDown(self):
243
+ def tearDown(self) -> None:
217
244
  """Clean up outputs."""
218
245
  if os.path.exists(self.outDir):
219
246
  shutil.rmtree(self.outDir)
247
+ if os.path.exists(self.jobStoreDir):
248
+ shutil.rmtree(self.jobStoreDir)
220
249
  unittest.TestCase.tearDown(self)
221
250
 
222
- def test_cwl_cmdline_input(self):
251
+ def test_cwl_cmdline_input(self) -> None:
223
252
  """
224
253
  Test that running a CWL workflow with inputs specified on the command line passes.
225
254
  """
226
255
  from toil.cwl import cwltoil
256
+
227
257
  cwlfile = "src/toil/test/cwl/conditional_wf.cwl"
228
258
  args = [cwlfile, "--message", "str", "--sleep", "2"]
229
259
  st = StringIO()
230
260
  # If the workflow runs, it must have had options
231
261
  cwltoil.main(args, stdout=st)
232
262
 
233
- def _tester(self, cwlfile, jobfile, expect, main_args=[], out_name="output", output_here=False):
263
+ def _tester(
264
+ self,
265
+ cwlfile: str,
266
+ jobfile: str,
267
+ expect: "CWLObjectType",
268
+ main_args: list[str] = [],
269
+ out_name: str = "output",
270
+ output_here: bool = False,
271
+ ) -> None:
234
272
  from toil.cwl import cwltoil
235
273
 
236
274
  st = StringIO()
237
275
  main_args = main_args[:]
238
276
  if not output_here:
239
277
  # Don't just dump output in the working directory.
240
- main_args.extend(
241
- [
242
- "--logDebug",
243
- "--outdir",
244
- self.outDir
245
- ]
246
- )
278
+ main_args.extend(["--logDebug", "--outdir", self.outDir])
247
279
  main_args.extend(
248
280
  [
249
281
  os.path.join(self.rootDir, cwlfile),
@@ -258,13 +290,20 @@ class CWLWorkflowTest(ToilTest):
258
290
  self.assertEqual(out, expect)
259
291
 
260
292
  for k, v in expect.items():
261
- if isinstance(v, dict) and "class" in v and v["class"] == "File" and "path" in v:
293
+ if (
294
+ isinstance(v, dict)
295
+ and "class" in v
296
+ and v["class"] == "File"
297
+ and "path" in v
298
+ ):
262
299
  # This is a top-level output file.
263
300
  # None of our output files should be executable.
264
301
  self.assertTrue(os.path.exists(v["path"]))
265
302
  self.assertFalse(os.stat(v["path"]).st_mode & stat.S_IXUSR)
266
303
 
267
- def _debug_worker_tester(self, cwlfile, jobfile, expect):
304
+ def _debug_worker_tester(
305
+ self, cwlfile: str, jobfile: str, expect: "CWLObjectType"
306
+ ) -> None:
268
307
  from toil.cwl import cwltoil
269
308
 
270
309
  st = StringIO()
@@ -284,21 +323,21 @@ class CWLWorkflowTest(ToilTest):
284
323
  out["output"].pop("nameroot", None)
285
324
  self.assertEqual(out, expect)
286
325
 
287
- def revsort(self, cwl_filename, tester_fn):
326
+ def revsort(self, cwl_filename: str, tester_fn: TesterFuncType) -> None:
288
327
  tester_fn(
289
328
  "src/toil/test/cwl/" + cwl_filename,
290
329
  "src/toil/test/cwl/revsort-job.json",
291
330
  self._expected_revsort_output(self.outDir),
292
331
  )
293
332
 
294
- def revsort_no_checksum(self, cwl_filename, tester_fn):
333
+ def revsort_no_checksum(self, cwl_filename: str, tester_fn: TesterFuncType) -> None:
295
334
  tester_fn(
296
335
  "src/toil/test/cwl/" + cwl_filename,
297
336
  "src/toil/test/cwl/revsort-job.json",
298
337
  self._expected_revsort_nochecksum_output(self.outDir),
299
338
  )
300
339
 
301
- def download(self, inputs, tester_fn):
340
+ def download(self, inputs: str, tester_fn: TesterFuncType) -> None:
302
341
  input_location = os.path.join("src/toil/test/cwl", inputs)
303
342
  tester_fn(
304
343
  "src/toil/test/cwl/download.cwl",
@@ -306,7 +345,7 @@ class CWLWorkflowTest(ToilTest):
306
345
  self._expected_download_output(self.outDir),
307
346
  )
308
347
 
309
- def load_contents(self, inputs, tester_fn):
348
+ def load_contents(self, inputs: str, tester_fn: TesterFuncType) -> None:
310
349
  input_location = os.path.join("src/toil/test/cwl", inputs)
311
350
  tester_fn(
312
351
  "src/toil/test/cwl/load_contents.cwl",
@@ -314,7 +353,7 @@ class CWLWorkflowTest(ToilTest):
314
353
  self._expected_load_contents_output(self.outDir),
315
354
  )
316
355
 
317
- def download_directory(self, inputs, tester_fn):
356
+ def download_directory(self, inputs: str, tester_fn: TesterFuncType) -> None:
318
357
  input_location = os.path.join("src/toil/test/cwl", inputs)
319
358
  tester_fn(
320
359
  "src/toil/test/cwl/download_directory.cwl",
@@ -322,7 +361,7 @@ class CWLWorkflowTest(ToilTest):
322
361
  self._expected_download_output(self.outDir),
323
362
  )
324
363
 
325
- def download_subdirectory(self, inputs, tester_fn):
364
+ def download_subdirectory(self, inputs: str, tester_fn: TesterFuncType) -> None:
326
365
  input_location = os.path.join("src/toil/test/cwl", inputs)
327
366
  tester_fn(
328
367
  "src/toil/test/cwl/download_subdirectory.cwl",
@@ -330,7 +369,7 @@ class CWLWorkflowTest(ToilTest):
330
369
  self._expected_download_output(self.outDir),
331
370
  )
332
371
 
333
- def test_mpi(self):
372
+ def test_mpi(self) -> None:
334
373
  from toil.cwl import cwltoil
335
374
 
336
375
  stdout = StringIO()
@@ -355,7 +394,7 @@ class CWLWorkflowTest(ToilTest):
355
394
  self.assertTrue(isinstance(two_pids[1], int))
356
395
 
357
396
  @needs_aws_s3
358
- def test_s3_as_secondary_file(self):
397
+ def test_s3_as_secondary_file(self) -> None:
359
398
  from toil.cwl import cwltoil
360
399
 
361
400
  stdout = StringIO()
@@ -374,29 +413,52 @@ class CWLWorkflowTest(ToilTest):
374
413
  with open(out["output"]["location"][len("file://") :]) as f:
375
414
  self.assertEqual(f.read().strip(), "When is s4 coming out?")
376
415
 
377
- def test_run_revsort(self):
416
+ def test_run_revsort(self) -> None:
378
417
  self.revsort("revsort.cwl", self._tester)
379
418
 
380
- def test_run_revsort_nochecksum(self):
419
+ def test_run_revsort_nochecksum(self) -> None:
381
420
  self.revsort_no_checksum(
382
421
  "revsort.cwl", partial(self._tester, main_args=["--no-compute-checksum"])
383
422
  )
384
423
 
385
- def test_run_revsort2(self):
424
+ def test_run_revsort_no_container(self) -> None:
425
+ self.revsort(
426
+ "revsort.cwl", partial(self._tester, main_args=["--no-container"])
427
+ )
428
+
429
+ def test_run_revsort2(self) -> None:
386
430
  self.revsort("revsort2.cwl", self._tester)
387
431
 
388
- def test_run_revsort_debug_worker(self):
432
+ def test_run_revsort_debug_worker(self) -> None:
389
433
  self.revsort("revsort.cwl", self._debug_worker_tester)
390
434
 
391
- def test_run_colon_output(self):
435
+ def test_run_colon_output(self) -> None:
392
436
  self._tester(
393
437
  "src/toil/test/cwl/colon_test_output.cwl",
394
438
  "src/toil/test/cwl/colon_test_output_job.yaml",
395
439
  self._expected_colon_output(self.outDir),
396
440
  out_name="result",
397
441
  )
442
+
443
+ @pytest.mark.integrative
444
+ @needs_singularity_or_docker
445
+ def test_run_dockstore_trs(self) -> None:
446
+ from toil.cwl import cwltoil
447
+
448
+ stdout = StringIO()
449
+ main_args = [
450
+ "--outdir",
451
+ self.outDir,
452
+ "#workflow/github.com/dockstore-testing/md5sum-checker",
453
+ "https://raw.githubusercontent.com/dockstore-testing/md5sum-checker/refs/heads/master/md5sum/md5sum-input-cwl.json"
454
+ ]
455
+ cwltoil.main(main_args, stdout=stdout)
456
+ out = json.loads(stdout.getvalue())
457
+ with open(out.get("output_file", {}).get("location")[len("file://") :]) as f:
458
+ computed_hash = f.read().strip()
459
+ self.assertEqual(computed_hash, "00579a00e3e7fa0674428ac7049423e2")
398
460
 
399
- def test_glob_dir_bypass_file_store(self):
461
+ def test_glob_dir_bypass_file_store(self) -> None:
400
462
  self.maxDiff = 1000
401
463
  try:
402
464
  # We need to output to the current directory to make sure that
@@ -406,7 +468,7 @@ class CWLWorkflowTest(ToilTest):
406
468
  "src/toil/test/cwl/empty.json",
407
469
  self._expected_glob_dir_output(os.getcwd()),
408
470
  main_args=["--bypass-file-store"],
409
- output_here=True
471
+ output_here=True,
410
472
  )
411
473
  finally:
412
474
  # Clean up anything we made in the current directory.
@@ -415,59 +477,128 @@ class CWLWorkflowTest(ToilTest):
415
477
  except FileNotFoundError:
416
478
  pass
417
479
 
480
+ def test_required_input_condition_protection(self) -> None:
481
+ # This doesn't run containerized
482
+ self._tester(
483
+ "src/toil/test/cwl/not_run_required_input.cwl",
484
+ "src/toil/test/cwl/empty.json",
485
+ {},
486
+ )
487
+
488
+ @needs_slurm
489
+ def test_slurm_node_memory(self) -> None:
490
+ pass
491
+
492
+ # Run the workflow. This will either finish quickly and tell us the
493
+ # memory we got, or take a long time because it requested a whole
494
+ # node's worth of memory and no nodes are free right now. We need to
495
+ # support both.
496
+
497
+ # And if we run out of time we need to stop the workflow gracefully and
498
+ # cancel the Slurm jobs.
499
+
500
+ main_args = [
501
+ f"--jobStore={self.jobStoreDir}",
502
+ # Avoid racing to toil kill before the jobstore is removed
503
+ "--clean=never",
504
+ "--batchSystem=slurm",
505
+ "--no-cwl-default-ram",
506
+ "--slurmDefaultAllMem=True",
507
+ "--outdir",
508
+ self.outDir,
509
+ os.path.join(self.rootDir, "src/toil/test/cwl/measure_default_memory.cwl"),
510
+ ]
511
+ try:
512
+ log.debug("Start test workflow")
513
+ child = subprocess.Popen(
514
+ ["toil-cwl-runner"] + main_args, stdout=subprocess.PIPE
515
+ )
516
+ output, _ = child.communicate(timeout=60)
517
+ except subprocess.TimeoutExpired:
518
+ # The job didn't finish quickly; presumably waiting for a full node.
519
+ # Stop the workflow
520
+ log.debug("Workflow might be waiting for a full node. Stop it.")
521
+ subprocess.check_call(["toil", "kill", self.jobStoreDir])
522
+ # Wait another little bit for it to clean up, making sure to collect output in case it is blocked on writing
523
+ child.communicate(timeout=20)
524
+ # Kill it off in case it is still running
525
+ child.kill()
526
+ # Reap it
527
+ child.wait()
528
+ # The test passes
529
+ else:
530
+ out = json.loads(output)
531
+ log.debug("Workflow output: %s", out)
532
+ memory_string = out["memory"]
533
+ log.debug("Observed memory: %s", memory_string)
534
+ # If there's no memory limit enforced, Slurm will return "unlimited".
535
+ # Set result to something sensible.
536
+ if memory_string.strip() == "unlimited":
537
+ result = 4 * 1024 * 1024
538
+ else:
539
+ result = int(memory_string)
540
+ # We should see more than the CWL default or the Toil default, assuming Slurm nodes of reasonable size (3 GiB).
541
+ self.assertGreater(result, 3 * 1024 * 1024)
542
+
418
543
  @needs_aws_s3
419
- def test_download_s3(self):
544
+ def test_download_s3(self) -> None:
420
545
  self.download("download_s3.json", self._tester)
421
546
 
422
- def test_download_http(self):
547
+ def test_download_http(self) -> None:
423
548
  self.download("download_http.json", self._tester)
424
549
 
425
- def test_download_https(self):
550
+ def test_download_https(self) -> None:
426
551
  self.download("download_https.json", self._tester)
427
552
 
428
- def test_download_https_reference(self):
429
- self.download("download_https.json", partial(self._tester, main_args=["--reference-inputs"]))
553
+ def test_download_https_reference(self) -> None:
554
+ self.download(
555
+ "download_https.json",
556
+ partial(self._tester, main_args=["--reference-inputs"]),
557
+ )
430
558
 
431
- def test_download_file(self):
559
+ def test_download_file(self) -> None:
432
560
  self.download("download_file.json", self._tester)
433
561
 
434
562
  @needs_aws_s3
435
- def test_download_directory_s3(self):
563
+ def test_download_directory_s3(self) -> None:
436
564
  self.download_directory("download_directory_s3.json", self._tester)
437
565
 
438
566
  @needs_aws_s3
439
- def test_download_directory_s3_reference(self):
440
- self.download_directory("download_directory_s3.json", partial(self._tester, main_args=["--reference-inputs"]))
567
+ def test_download_directory_s3_reference(self) -> None:
568
+ self.download_directory(
569
+ "download_directory_s3.json",
570
+ partial(self._tester, main_args=["--reference-inputs"]),
571
+ )
441
572
 
442
- def test_download_directory_file(self):
573
+ def test_download_directory_file(self) -> None:
443
574
  self.download_directory("download_directory_file.json", self._tester)
444
575
 
445
576
  @needs_aws_s3
446
- def test_download_subdirectory_s3(self):
577
+ def test_download_subdirectory_s3(self) -> None:
447
578
  self.download_subdirectory("download_subdirectory_s3.json", self._tester)
448
579
 
449
- def test_download_subdirectory_file(self):
580
+ def test_download_subdirectory_file(self) -> None:
450
581
  self.download_subdirectory("download_subdirectory_file.json", self._tester)
451
582
 
452
583
  # We also want to make sure we can run a bare tool with loadContents on the inputs, which requires accessing the input data early in the leader.
453
584
 
454
585
  @needs_aws_s3
455
- def test_load_contents_s3(self):
586
+ def test_load_contents_s3(self) -> None:
456
587
  self.load_contents("download_s3.json", self._tester)
457
588
 
458
- def test_load_contents_http(self):
589
+ def test_load_contents_http(self) -> None:
459
590
  self.load_contents("download_http.json", self._tester)
460
591
 
461
- def test_load_contents_https(self):
592
+ def test_load_contents_https(self) -> None:
462
593
  self.load_contents("download_https.json", self._tester)
463
594
 
464
- def test_load_contents_file(self):
595
+ def test_load_contents_file(self) -> None:
465
596
  self.load_contents("download_file.json", self._tester)
466
597
 
467
598
  @slow
468
599
  @pytest.mark.integrative
469
- @unittest.skip
470
- def test_bioconda(self):
600
+ @unittest.skip("Fails too often due to remote service")
601
+ def test_bioconda(self) -> None:
471
602
  self._tester(
472
603
  "src/toil/test/cwl/seqtk_seq.cwl",
473
604
  "src/toil/test/cwl/seqtk_seq_job.json",
@@ -476,10 +607,23 @@ class CWLWorkflowTest(ToilTest):
476
607
  out_name="output1",
477
608
  )
478
609
 
610
+ @needs_docker
611
+ def test_default_args(self) -> None:
612
+ self._tester(
613
+ "src/toil/test/cwl/seqtk_seq.cwl",
614
+ "src/toil/test/cwl/seqtk_seq_job.json",
615
+ self._expected_seqtk_output(self.outDir),
616
+ main_args=[
617
+ "--default-container",
618
+ "quay.io/biocontainers/seqtk:1.4--he4a0461_1",
619
+ ],
620
+ out_name="output1",
621
+ )
622
+
479
623
  @needs_docker
480
624
  @pytest.mark.integrative
481
- @unittest.skip
482
- def test_biocontainers(self):
625
+ @unittest.skip("Fails too often due to remote service")
626
+ def test_biocontainers(self) -> None:
483
627
  self._tester(
484
628
  "src/toil/test/cwl/seqtk_seq.cwl",
485
629
  "src/toil/test/cwl/seqtk_seq_job.json",
@@ -491,7 +635,7 @@ class CWLWorkflowTest(ToilTest):
491
635
  @needs_docker
492
636
  @needs_docker_cuda
493
637
  @needs_local_cuda
494
- def test_cuda(self):
638
+ def test_cuda(self) -> None:
495
639
  self._tester(
496
640
  "src/toil/test/cwl/nvidia_smi.cwl",
497
641
  "src/toil/test/cwl/empty.json",
@@ -500,14 +644,13 @@ class CWLWorkflowTest(ToilTest):
500
644
  )
501
645
 
502
646
  @slow
503
- def test_restart(self):
647
+ def test_restart(self) -> None:
504
648
  """
505
649
  Enable restarts with toil-cwl-runner -- run failing test, re-run correct test.
506
650
  Only implemented for single machine.
507
651
  """
508
652
  log.info("Running CWL Test Restart. Expecting failure, then success.")
509
653
  from toil.cwl import cwltoil
510
- from toil.jobStores.abstractJobStore import NoSuchJobStoreException
511
654
 
512
655
  outDir = self._createTempDir()
513
656
  cwlDir = os.path.join(self._projectRootPath(), "src", "toil", "test", "cwl")
@@ -529,7 +672,7 @@ class CWLWorkflowTest(ToilTest):
529
672
  ][-1]
530
673
  os.symlink(os.path.join(cal_path, "date"), f'{os.path.join(outDir, "rev")}')
531
674
 
532
- def path_with_bogus_rev():
675
+ def path_with_bogus_rev() -> str:
533
676
  # append to the front of the PATH so that we check there first
534
677
  return f"{outDir}:" + os.environ["PATH"]
535
678
 
@@ -537,22 +680,31 @@ class CWLWorkflowTest(ToilTest):
537
680
  # Force a failure by trying to use an incorrect version of `rev` from the PATH
538
681
  os.environ["PATH"] = path_with_bogus_rev()
539
682
  try:
540
- cwltoil.main(cmd)
683
+ subprocess.check_output(
684
+ ["toil-cwl-runner"] + cmd,
685
+ env=os.environ.copy(),
686
+ stderr=subprocess.STDOUT,
687
+ )
541
688
  self.fail("Expected problem job with incorrect PATH did not fail")
542
- except FailedJobsException:
689
+ except subprocess.CalledProcessError:
543
690
  pass
544
691
  # Finish the job with a correct PATH
545
692
  os.environ["PATH"] = orig_path
546
- cwltoil.main(["--restart"] + cmd)
693
+ cmd.insert(0, "--restart")
694
+ cwltoil.main(cmd)
547
695
  # Should fail because previous job completed successfully
548
696
  try:
549
- cwltoil.main(["--restart"] + cmd)
697
+ subprocess.check_output(
698
+ ["toil-cwl-runner"] + cmd,
699
+ env=os.environ.copy(),
700
+ stderr=subprocess.STDOUT,
701
+ )
550
702
  self.fail("Restart with missing directory did not fail")
551
- except NoSuchJobStoreException:
703
+ except subprocess.CalledProcessError:
552
704
  pass
553
705
 
554
706
  @needs_aws_s3
555
- def test_streamable(self, extra_args: List[str] = None):
707
+ def test_streamable(self, extra_args: Optional[list[str]] = None) -> None:
556
708
  """
557
709
  Test that a file with 'streamable'=True is a named pipe.
558
710
  This is a CWL1.2 feature.
@@ -585,13 +737,13 @@ class CWLWorkflowTest(ToilTest):
585
737
  self.assertEqual(f.read().strip(), "When is s4 coming out?")
586
738
 
587
739
  @needs_aws_s3
588
- def test_streamable_reference(self):
740
+ def test_streamable_reference(self) -> None:
589
741
  """
590
742
  Test that a streamable file is a stream even when passed around by URI.
591
743
  """
592
744
  self.test_streamable(extra_args=["--reference-inputs"])
593
745
 
594
- def test_preemptible(self):
746
+ def test_preemptible(self) -> None:
595
747
  """
596
748
  Tests that the http://arvados.org/cwl#UsePreemptible extension is supported.
597
749
  """
@@ -615,7 +767,7 @@ class CWLWorkflowTest(ToilTest):
615
767
  with open(out[out_name]["location"][len("file://") :]) as f:
616
768
  self.assertEqual(f.read().strip(), "hello")
617
769
 
618
- def test_preemptible_expression(self):
770
+ def test_preemptible_expression(self) -> None:
619
771
  """
620
772
  Tests that the http://arvados.org/cwl#UsePreemptible extension is validated.
621
773
  """
@@ -636,10 +788,9 @@ class CWLWorkflowTest(ToilTest):
636
788
  except ValidationException as e:
637
789
  # Make sure we chastise the user appropriately.
638
790
  assert "expressions are not allowed" in str(e)
639
-
640
791
 
641
792
  @staticmethod
642
- def _expected_seqtk_output(outDir):
793
+ def _expected_seqtk_output(outDir: str) -> "CWLObjectType":
643
794
  path = os.path.join(outDir, "out")
644
795
  loc = "file://" + path
645
796
  return {
@@ -654,7 +805,7 @@ class CWLWorkflowTest(ToilTest):
654
805
  }
655
806
 
656
807
  @staticmethod
657
- def _expected_revsort_output(outDir):
808
+ def _expected_revsort_output(outDir: str) -> "CWLObjectType":
658
809
  path = os.path.join(outDir, "output.txt")
659
810
  loc = "file://" + path
660
811
  return {
@@ -669,7 +820,7 @@ class CWLWorkflowTest(ToilTest):
669
820
  }
670
821
 
671
822
  @staticmethod
672
- def _expected_revsort_nochecksum_output(outDir):
823
+ def _expected_revsort_nochecksum_output(outDir: str) -> "CWLObjectType":
673
824
  path = os.path.join(outDir, "output.txt")
674
825
  loc = "file://" + path
675
826
  return {
@@ -683,7 +834,7 @@ class CWLWorkflowTest(ToilTest):
683
834
  }
684
835
 
685
836
  @staticmethod
686
- def _expected_download_output(outDir):
837
+ def _expected_download_output(outDir: str) -> "CWLObjectType":
687
838
  path = os.path.join(outDir, "output.txt")
688
839
  loc = "file://" + path
689
840
  return {
@@ -693,12 +844,12 @@ class CWLWorkflowTest(ToilTest):
693
844
  "size": 0,
694
845
  "class": "File",
695
846
  "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709",
696
- "path": path
847
+ "path": path,
697
848
  }
698
849
  }
699
850
 
700
851
  @staticmethod
701
- def _expected_glob_dir_output(out_dir):
852
+ def _expected_glob_dir_output(out_dir: str) -> "CWLObjectType":
702
853
  dir_path = os.path.join(out_dir, "shouldmake")
703
854
  dir_loc = "file://" + dir_path
704
855
  file_path = os.path.join(dir_path, "test.txt")
@@ -720,14 +871,14 @@ class CWLWorkflowTest(ToilTest):
720
871
  "checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709",
721
872
  "size": 0,
722
873
  "nameroot": "test",
723
- "nameext": ".txt"
874
+ "nameext": ".txt",
724
875
  }
725
- ]
876
+ ],
726
877
  }
727
878
  }
728
879
 
729
880
  @classmethod
730
- def _expected_load_contents_output(cls, out_dir):
881
+ def _expected_load_contents_output(cls, out_dir: str) -> "CWLObjectType":
731
882
  """
732
883
  Generate the putput we expect from load_contents.cwl, when sending
733
884
  output files to the given directory.
@@ -737,7 +888,7 @@ class CWLWorkflowTest(ToilTest):
737
888
  return expected
738
889
 
739
890
  @staticmethod
740
- def _expected_colon_output(outDir):
891
+ def _expected_colon_output(outDir: str) -> "CWLObjectType":
741
892
  path = os.path.join(outDir, "A:Gln2Cys_result")
742
893
  loc = "file://" + os.path.join(outDir, "A%3AGln2Cys_result")
743
894
  return {
@@ -755,13 +906,13 @@ class CWLWorkflowTest(ToilTest):
755
906
  "size": 1111,
756
907
  "nameroot": "whale",
757
908
  "nameext": ".txt",
758
- "path": f"{path}/whale.txt"
909
+ "path": f"{path}/whale.txt",
759
910
  }
760
911
  ],
761
912
  }
762
913
  }
763
914
 
764
- def _expected_streaming_output(self, outDir):
915
+ def _expected_streaming_output(self, outDir: str) -> "CWLObjectType":
765
916
  path = os.path.join(outDir, "output.txt")
766
917
  loc = "file://" + path
767
918
  return {
@@ -783,7 +934,7 @@ class CWLv10Test(ToilTest):
783
934
  Run the CWL 1.0 conformance tests in various environments.
784
935
  """
785
936
 
786
- def setUp(self):
937
+ def setUp(self) -> None:
787
938
  """Runs anew before each test to create farm fresh temp dirs."""
788
939
  self.outDir = f"/tmp/toil-cwl-test-{str(uuid.uuid4())}"
789
940
  os.makedirs(self.outDir)
@@ -804,7 +955,7 @@ class CWLv10Test(ToilTest):
804
955
  shutil.move("common-workflow-language-%s" % testhash, self.cwlSpec)
805
956
  os.remove("spec.zip")
806
957
 
807
- def tearDown(self):
958
+ def tearDown(self) -> None:
808
959
  """Clean up outputs."""
809
960
  if os.path.exists(self.outDir):
810
961
  shutil.rmtree(self.outDir)
@@ -812,99 +963,106 @@ class CWLv10Test(ToilTest):
812
963
 
813
964
  @slow
814
965
  @pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
815
- def test_run_conformance_with_caching(self):
966
+ def test_run_conformance_with_caching(self) -> None:
816
967
  self.test_run_conformance(caching=True)
817
968
 
818
969
  @slow
819
970
  @pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
820
971
  def test_run_conformance(
821
- self, batchSystem=None, caching=False, selected_tests=None
822
- ):
972
+ self,
973
+ batchSystem: Optional[str] = None,
974
+ caching: bool = False,
975
+ selected_tests: Optional[str] = None,
976
+ skipped_tests: Optional[str] = None,
977
+ extra_args: Optional[list[str]] = None,
978
+ ) -> None:
823
979
  run_conformance_tests(
824
980
  workDir=self.workDir,
825
981
  yml="conformance_test_v1.0.yaml",
826
982
  caching=caching,
827
983
  batchSystem=batchSystem,
828
984
  selected_tests=selected_tests,
985
+ skipped_tests=skipped_tests,
986
+ extra_args=extra_args,
829
987
  )
830
988
 
831
989
  @slow
832
990
  @needs_lsf
833
- @unittest.skip
834
- def test_lsf_cwl_conformance(self, **kwargs):
835
- return self.test_run_conformance(batchSystem="lsf", **kwargs)
991
+ @unittest.skip("Not run")
992
+ def test_lsf_cwl_conformance(self, caching: bool = False) -> None:
993
+ self.test_run_conformance(batchSystem="lsf", caching=caching)
836
994
 
837
995
  @slow
838
996
  @needs_slurm
839
- @unittest.skip
840
- def test_slurm_cwl_conformance(self, **kwargs):
841
- return self.test_run_conformance(batchSystem="slurm", **kwargs)
997
+ @unittest.skip("Not run")
998
+ def test_slurm_cwl_conformance(self, caching: bool = False) -> None:
999
+ self.test_run_conformance(batchSystem="slurm", caching=caching)
842
1000
 
843
1001
  @slow
844
1002
  @needs_torque
845
- @unittest.skip
846
- def test_torque_cwl_conformance(self, **kwargs):
847
- return self.test_run_conformance(batchSystem="torque", **kwargs)
1003
+ @unittest.skip("Not run")
1004
+ def test_torque_cwl_conformance(self, caching: bool = False) -> None:
1005
+ self.test_run_conformance(batchSystem="torque", caching=caching)
848
1006
 
849
1007
  @slow
850
1008
  @needs_gridengine
851
- @unittest.skip
852
- def test_gridengine_cwl_conformance(self, **kwargs):
853
- return self.test_run_conformance(batchSystem="grid_engine", **kwargs)
1009
+ @unittest.skip("Not run")
1010
+ def test_gridengine_cwl_conformance(self, caching: bool = False) -> None:
1011
+ self.test_run_conformance(batchSystem="grid_engine", caching=caching)
854
1012
 
855
1013
  @slow
856
1014
  @needs_mesos
857
- @unittest.skip
858
- def test_mesos_cwl_conformance(self, **kwargs):
859
- return self.test_run_conformance(batchSystem="mesos", **kwargs)
1015
+ @unittest.skip("Not run")
1016
+ def test_mesos_cwl_conformance(self, caching: bool = False) -> None:
1017
+ self.test_run_conformance(batchSystem="mesos", caching=caching)
860
1018
 
861
1019
  @slow
862
1020
  @needs_kubernetes
863
- def test_kubernetes_cwl_conformance(self, **kwargs):
864
- return self.test_run_conformance(
1021
+ def test_kubernetes_cwl_conformance(self, caching: bool = False) -> None:
1022
+ self.test_run_conformance(
1023
+ caching=caching,
865
1024
  batchSystem="kubernetes",
866
1025
  extra_args=["--retryCount=3"],
867
1026
  # This test doesn't work with
868
1027
  # Singularity; see
869
1028
  # https://github.com/common-workflow-language/cwltool/blob/7094ede917c2d5b16d11f9231fe0c05260b51be6/conformance-test.sh#L99-L117
870
1029
  skipped_tests="docker_entrypoint",
871
- **kwargs,
872
1030
  )
873
1031
 
874
1032
  @slow
875
1033
  @needs_lsf
876
- @unittest.skip
877
- def test_lsf_cwl_conformance_with_caching(self):
878
- return self.test_lsf_cwl_conformance(caching=True)
1034
+ @unittest.skip("Not run")
1035
+ def test_lsf_cwl_conformance_with_caching(self) -> None:
1036
+ self.test_lsf_cwl_conformance(caching=True)
879
1037
 
880
1038
  @slow
881
1039
  @needs_slurm
882
- @unittest.skip
883
- def test_slurm_cwl_conformance_with_caching(self):
884
- return self.test_slurm_cwl_conformance(caching=True)
1040
+ @unittest.skip("Not run")
1041
+ def test_slurm_cwl_conformance_with_caching(self) -> None:
1042
+ self.test_slurm_cwl_conformance(caching=True)
885
1043
 
886
1044
  @slow
887
1045
  @needs_torque
888
- @unittest.skip
889
- def test_torque_cwl_conformance_with_caching(self):
890
- return self.test_torque_cwl_conformance(caching=True)
1046
+ @unittest.skip("Not run")
1047
+ def test_torque_cwl_conformance_with_caching(self) -> None:
1048
+ self.test_torque_cwl_conformance(caching=True)
891
1049
 
892
1050
  @slow
893
1051
  @needs_gridengine
894
- @unittest.skip
895
- def test_gridengine_cwl_conformance_with_caching(self):
896
- return self.test_gridengine_cwl_conformance(caching=True)
1052
+ @unittest.skip("Not run")
1053
+ def test_gridengine_cwl_conformance_with_caching(self) -> None:
1054
+ self.test_gridengine_cwl_conformance(caching=True)
897
1055
 
898
1056
  @slow
899
1057
  @needs_mesos
900
- @unittest.skip
901
- def test_mesos_cwl_conformance_with_caching(self):
902
- return self.test_mesos_cwl_conformance(caching=True)
1058
+ @unittest.skip("Not run")
1059
+ def test_mesos_cwl_conformance_with_caching(self) -> None:
1060
+ self.test_mesos_cwl_conformance(caching=True)
903
1061
 
904
1062
  @slow
905
1063
  @needs_kubernetes
906
- def test_kubernetes_cwl_conformance_with_caching(self):
907
- return self.test_kubernetes_cwl_conformance(caching=True)
1064
+ def test_kubernetes_cwl_conformance_with_caching(self) -> None:
1065
+ self.test_kubernetes_cwl_conformance(caching=True)
908
1066
 
909
1067
 
910
1068
  @needs_cwl
@@ -914,8 +1072,12 @@ class CWLv11Test(ToilTest):
914
1072
  Run the CWL 1.1 conformance tests in various environments.
915
1073
  """
916
1074
 
1075
+ rootDir: str
1076
+ cwlSpec: str
1077
+ test_yaml: str
1078
+
917
1079
  @classmethod
918
- def setUpClass(cls):
1080
+ def setUpClass(cls) -> None:
919
1081
  """Runs anew before each test."""
920
1082
  cls.rootDir = cls._projectRootPath()
921
1083
  cls.cwlSpec = os.path.join(cls.rootDir, "src/toil/test/cwl/spec_v11")
@@ -929,37 +1091,50 @@ class CWLv11Test(ToilTest):
929
1091
  )
930
1092
  p.communicate()
931
1093
 
932
- def tearDown(self):
1094
+ def tearDown(self) -> None:
933
1095
  """Clean up outputs."""
934
1096
  unittest.TestCase.tearDown(self)
935
1097
 
936
1098
  @slow
937
1099
  @pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
938
- def test_run_conformance(self, **kwargs):
939
- run_conformance_tests(workDir=self.cwlSpec, yml=self.test_yaml, **kwargs)
1100
+ def test_run_conformance(
1101
+ self,
1102
+ caching: bool = False,
1103
+ batchSystem: Optional[str] = None,
1104
+ skipped_tests: Optional[str] = None,
1105
+ extra_args: Optional[list[str]] = None,
1106
+ ) -> None:
1107
+ run_conformance_tests(
1108
+ workDir=self.cwlSpec,
1109
+ yml=self.test_yaml,
1110
+ caching=caching,
1111
+ batchSystem=batchSystem,
1112
+ skipped_tests=skipped_tests,
1113
+ extra_args=extra_args,
1114
+ )
940
1115
 
941
1116
  @slow
942
1117
  @pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
943
- def test_run_conformance_with_caching(self):
1118
+ def test_run_conformance_with_caching(self) -> None:
944
1119
  self.test_run_conformance(caching=True)
945
1120
 
946
1121
  @slow
947
1122
  @needs_kubernetes
948
- def test_kubernetes_cwl_conformance(self, **kwargs):
949
- return self.test_run_conformance(
1123
+ def test_kubernetes_cwl_conformance(self, caching: bool = False) -> None:
1124
+ self.test_run_conformance(
950
1125
  batchSystem="kubernetes",
951
1126
  extra_args=["--retryCount=3"],
952
1127
  # These tests don't work with
953
1128
  # Singularity; see
954
1129
  # https://github.com/common-workflow-language/cwltool/blob/7094ede917c2d5b16d11f9231fe0c05260b51be6/conformance-test.sh#L99-L117
955
1130
  skipped_tests="docker_entrypoint,stdin_shorcut",
956
- **kwargs,
1131
+ caching=caching,
957
1132
  )
958
1133
 
959
1134
  @slow
960
1135
  @needs_kubernetes
961
- def test_kubernetes_cwl_conformance_with_caching(self):
962
- return self.test_kubernetes_cwl_conformance(caching=True)
1136
+ def test_kubernetes_cwl_conformance_with_caching(self) -> None:
1137
+ self.test_kubernetes_cwl_conformance(caching=True)
963
1138
 
964
1139
 
965
1140
  @needs_cwl
@@ -969,8 +1144,12 @@ class CWLv12Test(ToilTest):
969
1144
  Run the CWL 1.2 conformance tests in various environments.
970
1145
  """
971
1146
 
1147
+ rootDir: str
1148
+ cwlSpec: str
1149
+ test_yaml: str
1150
+
972
1151
  @classmethod
973
- def setUpClass(cls):
1152
+ def setUpClass(cls) -> None:
974
1153
  """Runs anew before each test."""
975
1154
  cls.rootDir = cls._projectRootPath()
976
1155
  cls.cwlSpec = os.path.join(cls.rootDir, "src/toil/test/cwl/spec_v12")
@@ -984,52 +1163,73 @@ class CWLv12Test(ToilTest):
984
1163
  )
985
1164
  p.communicate()
986
1165
 
987
- def tearDown(self):
1166
+ def tearDown(self) -> None:
988
1167
  """Clean up outputs."""
989
1168
  unittest.TestCase.tearDown(self)
990
1169
 
991
1170
  @slow
992
1171
  @pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
993
- def test_run_conformance(self, **kwargs):
994
- if "junit_file" not in kwargs:
995
- kwargs["junit_file"] = os.path.join(
996
- self.rootDir, "conformance-1.2.junit.xml"
997
- )
998
- run_conformance_tests(workDir=self.cwlSpec, yml=self.test_yaml, **kwargs)
1172
+ def test_run_conformance(
1173
+ self,
1174
+ runner: Optional[str] = None,
1175
+ caching: bool = False,
1176
+ batchSystem: Optional[str] = None,
1177
+ selected_tests: Optional[str] = None,
1178
+ skipped_tests: Optional[str] = None,
1179
+ extra_args: Optional[list[str]] = None,
1180
+ must_support_all_features: bool = False,
1181
+ junit_file: Optional[str] = None,
1182
+ ) -> None:
1183
+ if junit_file is None:
1184
+ junit_file = os.path.join(self.rootDir, "conformance-1.2.junit.xml")
1185
+ run_conformance_tests(
1186
+ workDir=self.cwlSpec,
1187
+ yml=self.test_yaml,
1188
+ runner=runner,
1189
+ caching=caching,
1190
+ batchSystem=batchSystem,
1191
+ selected_tests=selected_tests,
1192
+ skipped_tests=skipped_tests,
1193
+ extra_args=extra_args,
1194
+ must_support_all_features=must_support_all_features,
1195
+ junit_file=junit_file,
1196
+ )
999
1197
 
1000
1198
  @slow
1001
1199
  @pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
1002
- def test_run_conformance_with_caching(self):
1200
+ def test_run_conformance_with_caching(self) -> None:
1003
1201
  self.test_run_conformance(
1004
1202
  caching=True,
1005
- junit_file = os.path.join(
1006
- self.rootDir, "caching-conformance-1.2.junit.xml"
1007
- )
1203
+ junit_file=os.path.join(self.rootDir, "caching-conformance-1.2.junit.xml"),
1008
1204
  )
1009
1205
 
1010
1206
  @slow
1011
1207
  @pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
1012
- def test_run_conformance_with_in_place_update(self):
1208
+ def test_run_conformance_with_in_place_update(self) -> None:
1013
1209
  """
1014
1210
  Make sure that with --bypass-file-store we properly support in place
1015
1211
  update on a single node, and that this doesn't break any other
1016
1212
  features.
1017
1213
  """
1018
1214
  self.test_run_conformance(
1019
- extra_args=["--bypass-file-store"], must_support_all_features=True,
1020
- junit_file = os.path.join(
1215
+ extra_args=["--bypass-file-store"],
1216
+ must_support_all_features=True,
1217
+ junit_file=os.path.join(
1021
1218
  self.rootDir, "in-place-update-conformance-1.2.junit.xml"
1022
- )
1219
+ ),
1023
1220
  )
1024
1221
 
1025
1222
  @slow
1026
1223
  @needs_kubernetes
1027
- def test_kubernetes_cwl_conformance(self, **kwargs):
1028
- if "junit_file" not in kwargs:
1029
- kwargs["junit_file"] = os.path.join(
1224
+ def test_kubernetes_cwl_conformance(
1225
+ self, caching: bool = False, junit_file: Optional[str] = None
1226
+ ) -> None:
1227
+ if junit_file is None:
1228
+ junit_file = os.path.join(
1030
1229
  self.rootDir, "kubernetes-conformance-1.2.junit.xml"
1031
1230
  )
1032
- return self.test_run_conformance(
1231
+ self.test_run_conformance(
1232
+ caching=caching,
1033
1233
  batchSystem="kubernetes",
1034
1234
  extra_args=["--retryCount=3"],
1035
1235
  # This test doesn't work with
@@ -1038,13 +1238,13 @@ class CWLv12Test(ToilTest):
1038
1238
  # and
1039
1239
  # https://github.com/common-workflow-language/cwltool/issues/1441#issuecomment-826747975
1040
1240
  skipped_tests="docker_entrypoint",
1041
- **kwargs,
1241
+ junit_file=junit_file,
1042
1242
  )
1043
1243
 
1044
1244
  @slow
1045
1245
  @needs_kubernetes
1046
- def test_kubernetes_cwl_conformance_with_caching(self):
1047
- return self.test_kubernetes_cwl_conformance(
1246
+ def test_kubernetes_cwl_conformance_with_caching(self) -> None:
1247
+ self.test_kubernetes_cwl_conformance(
1048
1248
  caching=True,
1049
1249
  junit_file=os.path.join(
1050
1250
  self.rootDir, "kubernetes-caching-conformance-1.2.junit.xml"
@@ -1053,7 +1253,7 @@ class CWLv12Test(ToilTest):
1053
1253
 
1054
1254
  @slow
1055
1255
  @needs_wes_server
1056
- def test_wes_server_cwl_conformance(self):
1256
+ def test_wes_server_cwl_conformance(self) -> None:
1057
1257
  """
1058
1258
  Run the CWL conformance tests via WES. TOIL_WES_ENDPOINT must be
1059
1259
  specified. If the WES server requires authentication, set TOIL_WES_USER
@@ -1078,96 +1278,16 @@ class CWLv12Test(ToilTest):
1078
1278
  # 1. `cwltool --print-deps` doesn't seem to include secondary files from the default
1079
1279
  # e.g.: https://github.com/common-workflow-language/cwl-v1.2/blob/1.2.1_proposed/tests/mixed-versions/wf-v10.cwl#L4-L10
1080
1280
 
1081
- return self.test_run_conformance(
1281
+ self.test_run_conformance(
1082
1282
  runner="toil-wes-cwl-runner",
1083
1283
  selected_tests="1-309,313-337",
1084
1284
  extra_args=extra_args,
1085
1285
  )
1086
1286
 
1087
1287
 
1088
- @needs_aws_ec2
1089
- @needs_fetchable_appliance
1090
- @slow
1091
- class CWLOnARMTest(AbstractClusterTest):
1092
- """
1093
- Run the CWL 1.2 conformance tests on ARM specifically.
1094
- """
1095
-
1096
- def __init__(self, methodName):
1097
- super().__init__(methodName=methodName)
1098
- self.clusterName = "cwl-test-" + str(uuid.uuid4())
1099
- self.leaderNodeType = "t4g.2xlarge"
1100
- self.clusterType = "kubernetes"
1101
- # We need to be running in a directory which Flatcar and the Toil Appliance both have
1102
- self.cwl_test_dir = "/tmp/toil/cwlTests"
1103
-
1104
- def setUp(self):
1105
- super().setUp()
1106
- self.jobStore = f"aws:{self.awsRegion()}:cluster-{uuid.uuid4()}"
1107
-
1108
- @needs_env_var("CI_COMMIT_SHA", "a git commit sha")
1109
- def test_cwl_on_arm(self):
1110
- # Make a cluster
1111
- self.launchCluster()
1112
- # get the leader so we know the IP address - we don't need to wait since create cluster
1113
- # already ensures the leader is running
1114
- self.cluster = cluster_factory(
1115
- provisioner="aws", zone=self.zone, clusterName=self.clusterName
1116
- )
1117
- self.leader = self.cluster.getLeader()
1118
-
1119
- commit = os.environ["CI_COMMIT_SHA"]
1120
- self.sshUtil(
1121
- [
1122
- "bash",
1123
- "-c",
1124
- f"mkdir -p {self.cwl_test_dir} && cd {self.cwl_test_dir} && git clone https://github.com/DataBiosphere/toil.git",
1125
- ]
1126
- )
1127
-
1128
- # We use CI_COMMIT_SHA to retrieve the Toil version needed to run the CWL tests
1129
- self.sshUtil(
1130
- ["bash", "-c", f"cd {self.cwl_test_dir}/toil && git checkout {commit}"]
1131
- )
1132
-
1133
- # --never-download prevents silent upgrades to pip, wheel and setuptools
1134
- self.sshUtil(
1135
- [
1136
- "bash",
1137
- "-c",
1138
- f"virtualenv --system-site-packages --never-download {self.venvDir}",
1139
- ]
1140
- )
1141
- self.sshUtil(
1142
- [
1143
- "bash",
1144
- "-c",
1145
- f". .{self.venvDir}/bin/activate && cd {self.cwl_test_dir}/toil && make prepare && make develop extras=[all]",
1146
- ]
1147
- )
1148
-
1149
- # Runs the CWLv12Test on an ARM instance
1150
- self.sshUtil(
1151
- [
1152
- "bash",
1153
- "-c",
1154
- 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",
1155
- ]
1156
- )
1157
-
1158
- # We know if it succeeds it should save a junit XML for us to read.
1159
- # Bring it back to be an artifact.
1160
- self.rsync_util(
1161
- f":{self.cwl_test_dir}/toil/conformance-1.2.junit.xml",
1162
- os.path.join(
1163
- self._projectRootPath(),
1164
- "arm-conformance-1.2.junit.xml"
1165
- )
1166
- )
1167
-
1168
1288
  @needs_cwl
1169
1289
  @pytest.mark.cwl_small_log_dir
1170
- def test_workflow_echo_string_scatter_stderr_log_dir(tmp_path: Path):
1290
+ def test_workflow_echo_string_scatter_stderr_log_dir(tmp_path: Path) -> None:
1171
1291
  log_dir = tmp_path / "cwl-logs"
1172
1292
  job_store = "test_workflow_echo_string_scatter_stderr_log_dir"
1173
1293
  toil = "toil-cwl-runner"
@@ -1272,9 +1392,12 @@ def test_log_dir_echo_stderr(tmp_path: Path) -> None:
1272
1392
  assert output == "hello\n"
1273
1393
 
1274
1394
 
1395
+ # TODO: It's not clear how this test tests filename conflict resolution; it
1396
+ # seems like it runs a python script to copy some files and makes sure the
1397
+ # workflow doesn't fail.
1275
1398
  @needs_cwl
1276
1399
  @pytest.mark.cwl_small_log_dir
1277
- def test_filename_conflict_resolution(tmp_path: Path):
1400
+ def test_filename_conflict_resolution(tmp_path: Path) -> None:
1278
1401
  out_dir = tmp_path / "cwl-out-dir"
1279
1402
  toil = "toil-cwl-runner"
1280
1403
  options = [
@@ -1294,10 +1417,31 @@ def test_filename_conflict_resolution(tmp_path: Path):
1294
1417
  assert b"Finished toil run successfully" in stderr
1295
1418
  assert p.returncode == 0
1296
1419
 
1420
+
1421
+ @needs_cwl
1422
+ @pytest.mark.cwl_small_log_dir
1423
+ def test_filename_conflict_resolution_3_or_more(tmp_path: Path) -> None:
1424
+ out_dir = tmp_path / "cwl-out-dir"
1425
+ toil = "toil-cwl-runner"
1426
+ options = [
1427
+ f"--outdir={out_dir}",
1428
+ "--clean=always",
1429
+ ]
1430
+ cwl = os.path.join(os.path.dirname(__file__), "scatter_duplicate_outputs.cwl")
1431
+ cmd = [toil] + options + [cwl]
1432
+ p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1433
+ stdout, stderr = p.communicate()
1434
+ assert b"Finished toil run successfully" in stderr
1435
+ assert p.returncode == 0
1436
+ assert (
1437
+ len(os.listdir(out_dir)) == 9
1438
+ ), "All 9 files made by the scatter should be in the directory"
1439
+
1440
+
1297
1441
  @needs_cwl
1298
1442
  @needs_docker
1299
1443
  @pytest.mark.cwl_small_log_dir
1300
- def test_filename_conflict_detection(tmp_path: Path):
1444
+ def test_filename_conflict_detection(tmp_path: Path) -> None:
1301
1445
  """
1302
1446
  Make sure we don't just stage files over each other when using a container.
1303
1447
  """
@@ -1316,10 +1460,11 @@ def test_filename_conflict_detection(tmp_path: Path):
1316
1460
  assert b"File staging conflict" in stderr
1317
1461
  assert p.returncode != 0
1318
1462
 
1463
+
1319
1464
  @needs_cwl
1320
1465
  @needs_docker
1321
1466
  @pytest.mark.cwl_small_log_dir
1322
- def test_filename_conflict_detection_at_root(tmp_path: Path):
1467
+ def test_filename_conflict_detection_at_root(tmp_path: Path) -> None:
1323
1468
  """
1324
1469
  Make sure we don't just stage files over each other.
1325
1470
 
@@ -1343,7 +1488,7 @@ def test_filename_conflict_detection_at_root(tmp_path: Path):
1343
1488
 
1344
1489
  @needs_cwl
1345
1490
  @pytest.mark.cwl_small
1346
- def test_pick_value_with_one_null_value(caplog):
1491
+ def test_pick_value_with_one_null_value(caplog: pytest.LogCaptureFixture) -> None:
1347
1492
  """
1348
1493
  Make sure toil-cwl-runner does not false log a warning when pickValue is
1349
1494
  used but outputSource only contains one null value. See: #3991.
@@ -1357,12 +1502,15 @@ def test_pick_value_with_one_null_value(caplog):
1357
1502
  with caplog.at_level(logging.WARNING, logger="toil.cwl.cwltoil"):
1358
1503
  cwltoil.main(args)
1359
1504
  for line in caplog.messages:
1360
- assert "You had a conditional step that did not run, but you did not use pickValue to handle the skipped input." not in line
1505
+ assert (
1506
+ "You had a conditional step that did not run, but you did not use pickValue to handle the skipped input."
1507
+ not in line
1508
+ )
1361
1509
 
1362
1510
 
1363
1511
  @needs_cwl
1364
1512
  @pytest.mark.cwl_small
1365
- def test_workflow_echo_string():
1513
+ def test_workflow_echo_string() -> None:
1366
1514
  toil = "toil-cwl-runner"
1367
1515
  jobstore = f"--jobStore=file:explicit-local-jobstore-{uuid.uuid4()}"
1368
1516
  option_1 = "--strict-memory-limit"
@@ -1372,14 +1520,18 @@ def test_workflow_echo_string():
1372
1520
  cmd = [toil, jobstore, option_1, option_2, option_3, cwl]
1373
1521
  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1374
1522
  stdout, stderr = p.communicate()
1375
- assert stdout.decode("utf-8").strip() == "{}", f"Got wrong output: {stdout}\nWith error: {stderr}"
1376
- assert b"Finished toil run successfully" in stderr
1523
+ stdout2 = stdout.decode("utf-8")
1524
+ stderr2 = stderr.decode("utf-8")
1525
+ assert (
1526
+ stdout2.strip() == "{}"
1527
+ ), f"Got wrong output: {stdout2}\nWith error: {stderr2}"
1528
+ assert "Finished toil run successfully" in stderr2
1377
1529
  assert p.returncode == 0
1378
1530
 
1379
1531
 
1380
1532
  @needs_cwl
1381
1533
  @pytest.mark.cwl_small
1382
- def test_workflow_echo_string_scatter_capture_stdout():
1534
+ def test_workflow_echo_string_scatter_capture_stdout() -> None:
1383
1535
  toil = "toil-cwl-runner"
1384
1536
  jobstore = f"--jobStore=file:explicit-local-jobstore-{uuid.uuid4()}"
1385
1537
  option_1 = "--strict-memory-limit"
@@ -1391,6 +1543,8 @@ def test_workflow_echo_string_scatter_capture_stdout():
1391
1543
  cmd = [toil, jobstore, option_1, option_2, option_3, cwl]
1392
1544
  p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
1393
1545
  stdout, stderr = p.communicate()
1546
+ log.debug("Workflow standard output: %s", stdout)
1547
+ assert len(stdout) > 0
1394
1548
  outputs = json.loads(stdout)
1395
1549
  out_list = outputs["list_out"]
1396
1550
  assert len(out_list) == 2, f"outList shoud have two file elements {out_list}"
@@ -1412,7 +1566,7 @@ def test_workflow_echo_string_scatter_capture_stdout():
1412
1566
 
1413
1567
  @needs_cwl
1414
1568
  @pytest.mark.cwl_small
1415
- def test_visit_top_cwl_class():
1569
+ def test_visit_top_cwl_class() -> None:
1416
1570
  structure = {
1417
1571
  "class": "Directory",
1418
1572
  "listing": [
@@ -1438,7 +1592,7 @@ def test_visit_top_cwl_class():
1438
1592
 
1439
1593
  counter = 0
1440
1594
 
1441
- def increment(thing: Dict) -> None:
1595
+ def increment(thing: "CWLObjectType") -> None:
1442
1596
  """
1443
1597
  Make sure we are at something CWL object like, and count it.
1444
1598
  """
@@ -1463,7 +1617,7 @@ def test_visit_top_cwl_class():
1463
1617
 
1464
1618
  @needs_cwl
1465
1619
  @pytest.mark.cwl_small
1466
- def test_visit_cwl_class_and_reduce():
1620
+ def test_visit_cwl_class_and_reduce() -> None:
1467
1621
  structure = {
1468
1622
  "class": "Directory",
1469
1623
  "listing": [
@@ -1489,7 +1643,7 @@ def test_visit_cwl_class_and_reduce():
1489
1643
 
1490
1644
  down_count = 0
1491
1645
 
1492
- def op_down(thing: MutableMapping) -> int:
1646
+ def op_down(thing: "CWLObjectType") -> int:
1493
1647
  """
1494
1648
  Grab the ID of the thing we are at, and count what we visit going
1495
1649
  down.
@@ -1501,7 +1655,7 @@ def test_visit_cwl_class_and_reduce():
1501
1655
  up_count = 0
1502
1656
  up_child_count = 0
1503
1657
 
1504
- def op_up(thing: MutableMapping, down_value: int, child_results: List[str]) -> str:
1658
+ def op_up(thing: "CWLObjectType", down_value: int, child_results: list[str]) -> str:
1505
1659
  """
1506
1660
  Check the down return value and the up return values, and count
1507
1661
  what we visit going up and what child relationships we have.
@@ -1524,7 +1678,7 @@ def test_visit_cwl_class_and_reduce():
1524
1678
 
1525
1679
  @needs_cwl
1526
1680
  @pytest.mark.cwl_small
1527
- def test_download_structure(tmp_path) -> None:
1681
+ def test_download_structure(tmp_path: Path) -> None:
1528
1682
  """
1529
1683
  Make sure that download_structure makes the right calls to what it thinks is the file store.
1530
1684
  """
@@ -1534,7 +1688,7 @@ def test_download_structure(tmp_path) -> None:
1534
1688
  fid2 = FileID("adifferentfile", 1000, True)
1535
1689
 
1536
1690
  # And what directory structure it would be in
1537
- structure = {
1691
+ structure: DirectoryStructure = {
1538
1692
  "dir1": {
1539
1693
  "dir2": {
1540
1694
  "f1": "toilfile:" + fid1.pack(),
@@ -1555,9 +1709,9 @@ def test_download_structure(tmp_path) -> None:
1555
1709
  # These will be populated.
1556
1710
  # TODO: This cache seems unused. Remove it?
1557
1711
  # This maps filesystem path to CWL URI
1558
- index = {}
1712
+ index: dict[str, str] = {}
1559
1713
  # This maps CWL URI to filesystem path
1560
- existing = {}
1714
+ existing: dict[str, str] = {}
1561
1715
 
1562
1716
  # Do the download
1563
1717
  download_structure(file_store, index, existing, structure, to_dir)
@@ -1573,11 +1727,16 @@ def test_download_structure(tmp_path) -> None:
1573
1727
  assert os.path.join(to_dir, "dir1/dir2/f1again") in index
1574
1728
  assert os.path.join(to_dir, "anotherfile") in index
1575
1729
  assert (
1576
- index[os.path.join(to_dir, "dir1/dir2/f1")] == structure["dir1"]["dir2"]["f1"]
1730
+ index[os.path.join(to_dir, "dir1/dir2/f1")]
1731
+ == cast(
1732
+ DirectoryStructure, cast(DirectoryStructure, structure["dir1"])["dir2"]
1733
+ )["f1"]
1577
1734
  )
1578
1735
  assert (
1579
1736
  index[os.path.join(to_dir, "dir1/dir2/f1again")]
1580
- == structure["dir1"]["dir2"]["f1again"]
1737
+ == cast(
1738
+ DirectoryStructure, cast(DirectoryStructure, structure["dir1"])["dir2"]
1739
+ )["f1again"]
1581
1740
  )
1582
1741
  assert index[os.path.join(to_dir, "anotherfile")] == structure["anotherfile"]
1583
1742
 
@@ -1605,3 +1764,53 @@ def test_download_structure(tmp_path) -> None:
1605
1764
  ],
1606
1765
  any_order=True,
1607
1766
  )
1767
+
1768
+
1769
+ @needs_cwl
1770
+ @pytest.mark.timeout(300)
1771
+ def test_import_on_workers() -> None:
1772
+ args = [
1773
+ "src/toil/test/cwl/download.cwl",
1774
+ "src/toil/test/cwl/download_file.json",
1775
+ "--runImportsOnWorkers",
1776
+ "--importWorkersDisk=10MiB",
1777
+ "--realTimeLogging=True",
1778
+ "--logLevel=INFO",
1779
+ "--logColors=False",
1780
+ ]
1781
+ from toil.cwl import cwltoil
1782
+
1783
+ detector = ImportWorkersMessageHandler()
1784
+
1785
+ # Set up a log message detector to the root logger
1786
+ logging.getLogger().addHandler(detector)
1787
+
1788
+ cwltoil.main(args)
1789
+
1790
+ assert detector.detected is True
1791
+
1792
+
1793
+ # StreamHandler is generic, _typeshed doesn't exist at runtime, do a bit of typing trickery, see https://github.com/python/typeshed/issues/5680
1794
+ if TYPE_CHECKING:
1795
+ from _typeshed import SupportsWrite
1796
+
1797
+ _stream_handler = logging.StreamHandler[SupportsWrite[str]]
1798
+ else:
1799
+ _stream_handler = logging.StreamHandler
1800
+
1801
+
1802
+ class ImportWorkersMessageHandler(_stream_handler):
1803
+ """
1804
+ Detect the import workers log message and set a flag.
1805
+ """
1806
+
1807
+ def __init__(self) -> None:
1808
+ self.detected = False # Have we seen the message we want?
1809
+
1810
+ super().__init__(sys.stderr)
1811
+
1812
+ def emit(self, record: logging.LogRecord) -> None:
1813
+ if (record.msg % record.args).startswith(
1814
+ "Issued job 'CWLImportJob' CWLImportJob"
1815
+ ):
1816
+ self.detected = True