toil 8.1.0b1__py3-none-any.whl → 9.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 (275) hide show
  1. toil/__init__.py +0 -35
  2. toil/batchSystems/abstractBatchSystem.py +1 -1
  3. toil/batchSystems/abstractGridEngineBatchSystem.py +1 -1
  4. toil/batchSystems/awsBatch.py +1 -1
  5. toil/batchSystems/cleanup_support.py +1 -1
  6. toil/batchSystems/kubernetes.py +53 -7
  7. toil/batchSystems/local_support.py +1 -1
  8. toil/batchSystems/mesos/batchSystem.py +13 -8
  9. toil/batchSystems/mesos/test/__init__.py +3 -2
  10. toil/batchSystems/registry.py +15 -118
  11. toil/batchSystems/singleMachine.py +1 -1
  12. toil/batchSystems/slurm.py +27 -26
  13. toil/bus.py +5 -3
  14. toil/common.py +59 -12
  15. toil/cwl/cwltoil.py +81 -38
  16. toil/cwl/utils.py +103 -3
  17. toil/job.py +64 -49
  18. toil/jobStores/abstractJobStore.py +35 -239
  19. toil/jobStores/aws/jobStore.py +2 -1
  20. toil/jobStores/fileJobStore.py +27 -2
  21. toil/jobStores/googleJobStore.py +110 -33
  22. toil/leader.py +9 -0
  23. toil/lib/accelerators.py +4 -2
  24. toil/lib/aws/utils.py.orig +504 -0
  25. toil/lib/bioio.py +1 -1
  26. toil/lib/docker.py +252 -91
  27. toil/lib/dockstore.py +11 -3
  28. toil/lib/exceptions.py +5 -3
  29. toil/lib/generatedEC2Lists.py +81 -19
  30. toil/lib/history.py +87 -13
  31. toil/lib/history_submission.py +23 -9
  32. toil/lib/io.py +34 -22
  33. toil/lib/misc.py +8 -2
  34. toil/lib/plugins.py +106 -0
  35. toil/lib/resources.py +2 -1
  36. toil/lib/threading.py +11 -10
  37. toil/lib/url.py +320 -0
  38. toil/options/common.py +8 -0
  39. toil/options/cwl.py +13 -1
  40. toil/options/runner.py +17 -10
  41. toil/options/wdl.py +22 -0
  42. toil/provisioners/aws/awsProvisioner.py +25 -2
  43. toil/server/api_spec/LICENSE +201 -0
  44. toil/server/api_spec/README.rst +5 -0
  45. toil/server/app.py +12 -6
  46. toil/server/cli/wes_cwl_runner.py +3 -2
  47. toil/server/wes/abstract_backend.py +21 -43
  48. toil/server/wes/toil_backend.py +2 -2
  49. toil/test/__init__.py +275 -115
  50. toil/test/batchSystems/batchSystemTest.py +228 -213
  51. toil/test/batchSystems/batch_system_plugin_test.py +7 -0
  52. toil/test/batchSystems/test_slurm.py +27 -0
  53. toil/test/cactus/pestis.tar.gz +0 -0
  54. toil/test/conftest.py +7 -0
  55. toil/test/cwl/2.fasta +11 -0
  56. toil/test/cwl/2.fastq +12 -0
  57. toil/test/cwl/conftest.py +1 -1
  58. toil/test/cwl/cwlTest.py +1175 -870
  59. toil/test/cwl/directory/directory/file.txt +15 -0
  60. toil/test/cwl/download_directory_file.json +4 -0
  61. toil/test/cwl/download_directory_s3.json +4 -0
  62. toil/test/cwl/download_file.json +6 -0
  63. toil/test/cwl/download_http.json +6 -0
  64. toil/test/cwl/download_https.json +6 -0
  65. toil/test/cwl/download_s3.json +6 -0
  66. toil/test/cwl/download_subdirectory_file.json +5 -0
  67. toil/test/cwl/download_subdirectory_s3.json +5 -0
  68. toil/test/cwl/empty.json +1 -0
  69. toil/test/cwl/mock_mpi/fake_mpi.yml +8 -0
  70. toil/test/cwl/mock_mpi/fake_mpi_run.py +42 -0
  71. toil/test/cwl/optional-file-exists.json +6 -0
  72. toil/test/cwl/optional-file-missing.json +6 -0
  73. toil/test/cwl/preemptible_expression.json +1 -0
  74. toil/test/cwl/revsort-job-missing.json +6 -0
  75. toil/test/cwl/revsort-job.json +6 -0
  76. toil/test/cwl/s3_secondary_file.json +16 -0
  77. toil/test/cwl/seqtk_seq_job.json +6 -0
  78. toil/test/cwl/stream.json +6 -0
  79. toil/test/cwl/test_filename_conflict_resolution.ms/table.dat +0 -0
  80. toil/test/cwl/test_filename_conflict_resolution.ms/table.f0 +0 -0
  81. toil/test/cwl/test_filename_conflict_resolution.ms/table.f1 +0 -0
  82. toil/test/cwl/test_filename_conflict_resolution.ms/table.f1i +0 -0
  83. toil/test/cwl/test_filename_conflict_resolution.ms/table.f2 +0 -0
  84. toil/test/cwl/test_filename_conflict_resolution.ms/table.f2_TSM0 +0 -0
  85. toil/test/cwl/test_filename_conflict_resolution.ms/table.f3 +0 -0
  86. toil/test/cwl/test_filename_conflict_resolution.ms/table.f3_TSM0 +0 -0
  87. toil/test/cwl/test_filename_conflict_resolution.ms/table.f4 +0 -0
  88. toil/test/cwl/test_filename_conflict_resolution.ms/table.f4_TSM0 +0 -0
  89. toil/test/cwl/test_filename_conflict_resolution.ms/table.f5 +0 -0
  90. toil/test/cwl/test_filename_conflict_resolution.ms/table.info +0 -0
  91. toil/test/cwl/test_filename_conflict_resolution.ms/table.lock +0 -0
  92. toil/test/cwl/whale.txt +16 -0
  93. toil/test/docs/scripts/example_alwaysfail.py +38 -0
  94. toil/test/docs/scripts/example_alwaysfail_with_files.wdl +33 -0
  95. toil/test/docs/scripts/example_cachingbenchmark.py +117 -0
  96. toil/test/docs/scripts/stagingExampleFiles/in.txt +1 -0
  97. toil/test/docs/scripts/stagingExampleFiles/out.txt +2 -0
  98. toil/test/docs/scripts/tutorial_arguments.py +23 -0
  99. toil/test/docs/scripts/tutorial_debugging.patch +12 -0
  100. toil/test/docs/scripts/tutorial_debugging_hangs.wdl +126 -0
  101. toil/test/docs/scripts/tutorial_debugging_works.wdl +129 -0
  102. toil/test/docs/scripts/tutorial_docker.py +20 -0
  103. toil/test/docs/scripts/tutorial_dynamic.py +24 -0
  104. toil/test/docs/scripts/tutorial_encapsulation.py +28 -0
  105. toil/test/docs/scripts/tutorial_encapsulation2.py +29 -0
  106. toil/test/docs/scripts/tutorial_helloworld.py +15 -0
  107. toil/test/docs/scripts/tutorial_invokeworkflow.py +27 -0
  108. toil/test/docs/scripts/tutorial_invokeworkflow2.py +30 -0
  109. toil/test/docs/scripts/tutorial_jobfunctions.py +22 -0
  110. toil/test/docs/scripts/tutorial_managing.py +29 -0
  111. toil/test/docs/scripts/tutorial_managing2.py +56 -0
  112. toil/test/docs/scripts/tutorial_multiplejobs.py +25 -0
  113. toil/test/docs/scripts/tutorial_multiplejobs2.py +21 -0
  114. toil/test/docs/scripts/tutorial_multiplejobs3.py +22 -0
  115. toil/test/docs/scripts/tutorial_promises.py +25 -0
  116. toil/test/docs/scripts/tutorial_promises2.py +30 -0
  117. toil/test/docs/scripts/tutorial_quickstart.py +22 -0
  118. toil/test/docs/scripts/tutorial_requirements.py +44 -0
  119. toil/test/docs/scripts/tutorial_services.py +45 -0
  120. toil/test/docs/scripts/tutorial_staging.py +45 -0
  121. toil/test/docs/scripts/tutorial_stats.py +64 -0
  122. toil/test/docs/scriptsTest.py +2 -1
  123. toil/test/lib/aws/test_iam.py +3 -1
  124. toil/test/lib/dockerTest.py +205 -122
  125. toil/test/lib/test_history.py +101 -77
  126. toil/test/lib/test_url.py +69 -0
  127. toil/test/lib/url_plugin_test.py +105 -0
  128. toil/test/provisioners/aws/awsProvisionerTest.py +13 -10
  129. toil/test/provisioners/clusterTest.py +17 -4
  130. toil/test/provisioners/gceProvisionerTest.py +17 -15
  131. toil/test/server/serverTest.py +78 -36
  132. toil/test/sort/sort.py +4 -1
  133. toil/test/src/busTest.py +17 -17
  134. toil/test/src/deferredFunctionTest.py +145 -132
  135. toil/test/src/importExportFileTest.py +71 -63
  136. toil/test/src/jobEncapsulationTest.py +27 -28
  137. toil/test/src/jobServiceTest.py +149 -133
  138. toil/test/src/jobTest.py +219 -211
  139. toil/test/src/miscTests.py +66 -60
  140. toil/test/src/promisedRequirementTest.py +163 -169
  141. toil/test/src/regularLogTest.py +24 -24
  142. toil/test/src/resourceTest.py +82 -76
  143. toil/test/src/restartDAGTest.py +51 -47
  144. toil/test/src/resumabilityTest.py +24 -19
  145. toil/test/src/retainTempDirTest.py +60 -57
  146. toil/test/src/systemTest.py +17 -13
  147. toil/test/src/threadingTest.py +29 -32
  148. toil/test/utils/ABCWorkflowDebug/B_file.txt +1 -0
  149. toil/test/utils/ABCWorkflowDebug/debugWorkflow.py +204 -0
  150. toil/test/utils/ABCWorkflowDebug/mkFile.py +16 -0
  151. toil/test/utils/ABCWorkflowDebug/sleep.cwl +12 -0
  152. toil/test/utils/ABCWorkflowDebug/sleep.yaml +1 -0
  153. toil/test/utils/toilDebugTest.py +117 -102
  154. toil/test/utils/toilKillTest.py +54 -53
  155. toil/test/utils/utilsTest.py +303 -229
  156. toil/test/wdl/lint_error.wdl +9 -0
  157. toil/test/wdl/md5sum/empty_file.json +1 -0
  158. toil/test/wdl/md5sum/md5sum-gs.json +1 -0
  159. toil/test/wdl/md5sum/md5sum.1.0.wdl +32 -0
  160. toil/test/wdl/md5sum/md5sum.input +1 -0
  161. toil/test/wdl/md5sum/md5sum.json +1 -0
  162. toil/test/wdl/md5sum/md5sum.wdl +25 -0
  163. toil/test/wdl/miniwdl_self_test/inputs-namespaced.json +1 -0
  164. toil/test/wdl/miniwdl_self_test/inputs.json +1 -0
  165. toil/test/wdl/miniwdl_self_test/self_test.wdl +40 -0
  166. toil/test/wdl/standard_library/as_map.json +16 -0
  167. toil/test/wdl/standard_library/as_map_as_input.wdl +23 -0
  168. toil/test/wdl/standard_library/as_pairs.json +7 -0
  169. toil/test/wdl/standard_library/as_pairs_as_input.wdl +23 -0
  170. toil/test/wdl/standard_library/ceil.json +3 -0
  171. toil/test/wdl/standard_library/ceil_as_command.wdl +16 -0
  172. toil/test/wdl/standard_library/ceil_as_input.wdl +16 -0
  173. toil/test/wdl/standard_library/collect_by_key.json +1 -0
  174. toil/test/wdl/standard_library/collect_by_key_as_input.wdl +23 -0
  175. toil/test/wdl/standard_library/cross.json +11 -0
  176. toil/test/wdl/standard_library/cross_as_input.wdl +19 -0
  177. toil/test/wdl/standard_library/flatten.json +7 -0
  178. toil/test/wdl/standard_library/flatten_as_input.wdl +18 -0
  179. toil/test/wdl/standard_library/floor.json +3 -0
  180. toil/test/wdl/standard_library/floor_as_command.wdl +16 -0
  181. toil/test/wdl/standard_library/floor_as_input.wdl +16 -0
  182. toil/test/wdl/standard_library/keys.json +8 -0
  183. toil/test/wdl/standard_library/keys_as_input.wdl +24 -0
  184. toil/test/wdl/standard_library/length.json +7 -0
  185. toil/test/wdl/standard_library/length_as_input.wdl +16 -0
  186. toil/test/wdl/standard_library/length_as_input_with_map.json +7 -0
  187. toil/test/wdl/standard_library/length_as_input_with_map.wdl +17 -0
  188. toil/test/wdl/standard_library/length_invalid.json +3 -0
  189. toil/test/wdl/standard_library/range.json +3 -0
  190. toil/test/wdl/standard_library/range_0.json +3 -0
  191. toil/test/wdl/standard_library/range_as_input.wdl +17 -0
  192. toil/test/wdl/standard_library/range_invalid.json +3 -0
  193. toil/test/wdl/standard_library/read_boolean.json +3 -0
  194. toil/test/wdl/standard_library/read_boolean_as_command.wdl +17 -0
  195. toil/test/wdl/standard_library/read_float.json +3 -0
  196. toil/test/wdl/standard_library/read_float_as_command.wdl +17 -0
  197. toil/test/wdl/standard_library/read_int.json +3 -0
  198. toil/test/wdl/standard_library/read_int_as_command.wdl +17 -0
  199. toil/test/wdl/standard_library/read_json.json +3 -0
  200. toil/test/wdl/standard_library/read_json_as_output.wdl +31 -0
  201. toil/test/wdl/standard_library/read_lines.json +3 -0
  202. toil/test/wdl/standard_library/read_lines_as_output.wdl +31 -0
  203. toil/test/wdl/standard_library/read_map.json +3 -0
  204. toil/test/wdl/standard_library/read_map_as_output.wdl +31 -0
  205. toil/test/wdl/standard_library/read_string.json +3 -0
  206. toil/test/wdl/standard_library/read_string_as_command.wdl +17 -0
  207. toil/test/wdl/standard_library/read_tsv.json +3 -0
  208. toil/test/wdl/standard_library/read_tsv_as_output.wdl +31 -0
  209. toil/test/wdl/standard_library/round.json +3 -0
  210. toil/test/wdl/standard_library/round_as_command.wdl +16 -0
  211. toil/test/wdl/standard_library/round_as_input.wdl +16 -0
  212. toil/test/wdl/standard_library/size.json +3 -0
  213. toil/test/wdl/standard_library/size_as_command.wdl +17 -0
  214. toil/test/wdl/standard_library/size_as_output.wdl +36 -0
  215. toil/test/wdl/standard_library/stderr.json +3 -0
  216. toil/test/wdl/standard_library/stderr_as_output.wdl +30 -0
  217. toil/test/wdl/standard_library/stdout.json +3 -0
  218. toil/test/wdl/standard_library/stdout_as_output.wdl +30 -0
  219. toil/test/wdl/standard_library/sub.json +3 -0
  220. toil/test/wdl/standard_library/sub_as_input.wdl +17 -0
  221. toil/test/wdl/standard_library/sub_as_input_with_file.wdl +17 -0
  222. toil/test/wdl/standard_library/transpose.json +6 -0
  223. toil/test/wdl/standard_library/transpose_as_input.wdl +18 -0
  224. toil/test/wdl/standard_library/write_json.json +6 -0
  225. toil/test/wdl/standard_library/write_json_as_command.wdl +17 -0
  226. toil/test/wdl/standard_library/write_lines.json +7 -0
  227. toil/test/wdl/standard_library/write_lines_as_command.wdl +17 -0
  228. toil/test/wdl/standard_library/write_map.json +6 -0
  229. toil/test/wdl/standard_library/write_map_as_command.wdl +17 -0
  230. toil/test/wdl/standard_library/write_tsv.json +6 -0
  231. toil/test/wdl/standard_library/write_tsv_as_command.wdl +17 -0
  232. toil/test/wdl/standard_library/zip.json +12 -0
  233. toil/test/wdl/standard_library/zip_as_input.wdl +19 -0
  234. toil/test/wdl/test.csv +3 -0
  235. toil/test/wdl/test.tsv +3 -0
  236. toil/test/wdl/testfiles/croo.wdl +38 -0
  237. toil/test/wdl/testfiles/drop_files.wdl +62 -0
  238. toil/test/wdl/testfiles/drop_files_subworkflow.wdl +13 -0
  239. toil/test/wdl/testfiles/empty.txt +0 -0
  240. toil/test/wdl/testfiles/not_enough_outputs.wdl +33 -0
  241. toil/test/wdl/testfiles/random.wdl +66 -0
  242. toil/test/wdl/testfiles/read_file.wdl +18 -0
  243. toil/test/wdl/testfiles/string_file_coercion.json +1 -0
  244. toil/test/wdl/testfiles/string_file_coercion.wdl +35 -0
  245. toil/test/wdl/testfiles/test.json +4 -0
  246. toil/test/wdl/testfiles/test_boolean.txt +1 -0
  247. toil/test/wdl/testfiles/test_float.txt +1 -0
  248. toil/test/wdl/testfiles/test_int.txt +1 -0
  249. toil/test/wdl/testfiles/test_lines.txt +5 -0
  250. toil/test/wdl/testfiles/test_map.txt +2 -0
  251. toil/test/wdl/testfiles/test_string.txt +1 -0
  252. toil/test/wdl/testfiles/url_to_file.wdl +13 -0
  253. toil/test/wdl/testfiles/url_to_optional_file.wdl +14 -0
  254. toil/test/wdl/testfiles/vocab.json +1 -0
  255. toil/test/wdl/testfiles/vocab.wdl +66 -0
  256. toil/test/wdl/testfiles/wait.wdl +34 -0
  257. toil/test/wdl/wdl_specification/type_pair.json +23 -0
  258. toil/test/wdl/wdl_specification/type_pair_basic.wdl +36 -0
  259. toil/test/wdl/wdl_specification/type_pair_with_files.wdl +36 -0
  260. toil/test/wdl/wdl_specification/v1_spec.json +1 -0
  261. toil/test/wdl/wdl_specification/v1_spec_declaration.wdl +39 -0
  262. toil/test/wdl/wdltoil_test.py +751 -529
  263. toil/test/wdl/wdltoil_test_kubernetes.py +2 -2
  264. toil/utils/toilSshCluster.py +23 -0
  265. toil/utils/toilUpdateEC2Instances.py +1 -0
  266. toil/version.py +5 -5
  267. toil/wdl/wdltoil.py +518 -437
  268. toil/worker.py +11 -6
  269. {toil-8.1.0b1.dist-info → toil-9.0.0.dist-info}/METADATA +25 -24
  270. toil-9.0.0.dist-info/RECORD +444 -0
  271. {toil-8.1.0b1.dist-info → toil-9.0.0.dist-info}/WHEEL +1 -1
  272. toil-8.1.0b1.dist-info/RECORD +0 -259
  273. {toil-8.1.0b1.dist-info → toil-9.0.0.dist-info}/entry_points.txt +0 -0
  274. {toil-8.1.0b1.dist-info → toil-9.0.0.dist-info/licenses}/LICENSE +0 -0
  275. {toil-8.1.0b1.dist-info → toil-9.0.0.dist-info}/top_level.txt +0 -0
@@ -1,25 +1,31 @@
1
1
  import json
2
2
  import logging
3
3
  import os
4
- import pytest
5
4
  import re
6
5
  import shutil
7
6
  import string
8
7
  import subprocess
9
8
  import unittest
10
- from typing import Any, Optional, Union
9
+ from collections.abc import Generator
10
+ from pathlib import Path
11
+ from typing import Any, Optional, Union, cast
11
12
  from unittest.mock import patch
12
13
  from uuid import uuid4
13
14
 
15
+ import pytest
16
+ from pytest_httpserver import HTTPServer
17
+
14
18
  import WDL.Error
15
19
  import WDL.Expr
16
20
 
17
21
  from toil.fileStores import FileID
18
22
  from toil.test import (
19
23
  ToilTest,
24
+ get_data,
20
25
  needs_docker,
21
26
  needs_docker_cuda,
22
27
  needs_google_storage,
28
+ needs_online,
23
29
  needs_singularity_or_docker,
24
30
  needs_wdl,
25
31
  slow,
@@ -29,26 +35,11 @@ from toil.wdl.wdltoil import (
29
35
  WDLSectionJob,
30
36
  WDLWorkflowGraph,
31
37
  parse_disks,
32
- remove_common_leading_whitespace,
33
38
  )
34
39
 
35
40
  logger = logging.getLogger(__name__)
36
41
 
37
42
 
38
- @needs_wdl
39
- class BaseWDLTest(ToilTest):
40
- """Base test class for WDL tests."""
41
-
42
- def setUp(self) -> None:
43
- """Runs anew before each test to create farm fresh temp dirs."""
44
- self.output_dir = os.path.join("/tmp/", "toil-wdl-test-" + str(uuid4()))
45
- os.makedirs(self.output_dir)
46
-
47
- def tearDown(self) -> None:
48
- if os.path.exists(self.output_dir):
49
- shutil.rmtree(self.output_dir)
50
-
51
-
52
43
  WDL_CONFORMANCE_TEST_REPO = "https://github.com/DataBiosphere/wdl-conformance-tests.git"
53
44
  WDL_CONFORMANCE_TEST_COMMIT = "baf44bcc7e6f6927540adf77d91b26a5558ae4b7"
54
45
  # These tests are known to require things not implemented by
@@ -71,31 +62,23 @@ WDL_UNIT_TESTS_UNSUPPORTED_BY_TOIL = [
71
62
  69, # Same as 68
72
63
  87, # MiniWDL does not handle metacharacters properly when running regex, https://github.com/chanzuckerberg/miniwdl/issues/709
73
64
  97, # miniwdl bug, see https://github.com/chanzuckerberg/miniwdl/issues/701
74
- 105, # miniwdl (and toil) bug, unserializable json is serialized, see https://github.com/chanzuckerberg/miniwdl/issues/702
75
- 107, # object not supported
76
- 108, # object not supported
77
- 109, # object not supported
78
- 110, # object not supported
79
- 120, # miniwdl bug, see https://github.com/chanzuckerberg/miniwdl/issues/699
80
- 131, # miniwdl bug, evalerror, see https://github.com/chanzuckerberg/miniwdl/issues/700
81
- 134, # same as 131
82
- 144 # miniwdl and toil bug
65
+ 105, # miniwdl (and toil) bug, unserializable json is serialized, see https://github.com/chanzuckerberg/miniwdl/issues/702
66
+ 107, # object not supported
67
+ 108, # object not supported
68
+ 109, # object not supported
69
+ 110, # object not supported
70
+ 120, # miniwdl bug, see https://github.com/chanzuckerberg/miniwdl/issues/699
71
+ 131, # miniwdl bug, evalerror, see https://github.com/chanzuckerberg/miniwdl/issues/700
72
+ 134, # same as 131
73
+ 144, # miniwdl and toil bug
83
74
  ]
84
75
 
85
76
 
86
-
87
- class WDLConformanceTests(BaseWDLTest):
88
- """
89
- WDL conformance tests for Toil.
90
- """
91
-
92
- wdl_dir = "wdl-conformance-tests"
93
-
94
- @classmethod
95
- def setUpClass(cls) -> None:
96
-
77
+ @pytest.fixture(scope="function")
78
+ def wdl_conformance_test_repo(tmp_path: Path) -> Generator[Path]:
79
+ try:
97
80
  p = subprocess.Popen(
98
- f"git clone {WDL_CONFORMANCE_TEST_REPO} {cls.wdl_dir} && cd {cls.wdl_dir} && git checkout {WDL_CONFORMANCE_TEST_COMMIT}",
81
+ f"git clone {WDL_CONFORMANCE_TEST_REPO} {str(tmp_path)} && cd {str(tmp_path)} && git checkout {WDL_CONFORMANCE_TEST_COMMIT}",
99
82
  shell=True,
100
83
  )
101
84
 
@@ -103,12 +86,17 @@ class WDLConformanceTests(BaseWDLTest):
103
86
 
104
87
  if p.returncode > 0:
105
88
  raise RuntimeError("Could not clone WDL conformance tests")
89
+ yield tmp_path
90
+ finally:
91
+ pass # no cleanup needed
106
92
 
107
- os.chdir(cls.wdl_dir)
108
93
 
109
- cls.base_command = [exactPython, "run.py", "--runner", "toil-wdl-runner"]
94
+ class TestWDLConformance:
95
+ """
96
+ WDL conformance tests for Toil.
97
+ """
110
98
 
111
- def check(self, p: subprocess.CompletedProcess) -> None:
99
+ def check(self, p: "subprocess.CompletedProcess[bytes]") -> None:
112
100
  """
113
101
  Make sure a call completed or explain why it failed.
114
102
  """
@@ -126,365 +114,715 @@ class WDLConformanceTests(BaseWDLTest):
126
114
  p.check_returncode()
127
115
 
128
116
  @slow
129
- def test_unit_tests_v11(self):
130
- # There are still some bugs with the WDL spec, use a fixed version until
131
- # See comments of https://github.com/openwdl/wdl/pull/669
132
- repo_url = "https://github.com/stxue1/wdl.git"
133
- repo_branch = "wdl-1.1.3-fixes"
134
- command = f"{exactPython} setup_unit_tests.py -v 1.1 --extra-patch-data unit_tests_patch_data.yaml --repo {repo_url} --branch {repo_branch} --force-pull"
135
- p = subprocess.run(command.split(" "), capture_output=True)
136
- self.check(p)
137
- command = f"{exactPython} run_unit.py -r toil-wdl-runner -v 1.1 --progress --exclude-numbers {','.join([str(t) for t in WDL_UNIT_TESTS_UNSUPPORTED_BY_TOIL])}"
138
- p = subprocess.run(command.split(" "), capture_output=True)
139
- self.check(p)
117
+ def test_unit_tests_v11(self, wdl_conformance_test_repo: Path) -> None:
118
+ # TODO: Using a branch lets Toil commits that formerly passed start to
119
+ # fail CI when the branch moves.
120
+ os.chdir(wdl_conformance_test_repo)
121
+ repo_url = "https://github.com/openwdl/wdl.git"
122
+ repo_branch = "wdl-1.1"
123
+ commands1 = [
124
+ exactPython,
125
+ "setup_unit_tests.py",
126
+ "-v",
127
+ "1.1",
128
+ "--extra-patch-data",
129
+ "unit_tests_patch_data.yaml",
130
+ "--repo",
131
+ repo_url,
132
+ "--branch",
133
+ repo_branch,
134
+ "--force-pull",
135
+ ]
136
+ p1 = subprocess.run(commands1, capture_output=True)
137
+ self.check(p1)
138
+ commands2 = [
139
+ exactPython,
140
+ "run_unit.py",
141
+ "-r",
142
+ "toil-wdl-runner",
143
+ "-v",
144
+ "1.1",
145
+ "--progress",
146
+ "--exclude-numbers",
147
+ ",".join([str(t) for t in WDL_UNIT_TESTS_UNSUPPORTED_BY_TOIL]),
148
+ ]
149
+ p2 = subprocess.run(commands2, capture_output=True)
150
+ self.check(p2)
140
151
 
141
152
  # estimated running time: 10 minutes
142
153
  @slow
143
- def test_conformance_tests_v10(self):
144
- command = self.base_command + ["-v", "1.0"]
154
+ def test_conformance_tests_v10(self, wdl_conformance_test_repo: Path) -> None:
155
+ os.chdir(wdl_conformance_test_repo)
156
+ commands = [
157
+ exactPython,
158
+ "run.py",
159
+ "--runner",
160
+ "toil-wdl-runner",
161
+ "--conformance-file",
162
+ "conformance.yaml",
163
+ "-v",
164
+ "1.0",
165
+ ]
145
166
  if WDL_CONFORMANCE_TESTS_UNSUPPORTED_BY_TOIL:
146
- command.append("--exclude-numbers")
147
- command.append(
167
+ commands.append("--exclude-numbers")
168
+ commands.append(
148
169
  ",".join([str(t) for t in WDL_CONFORMANCE_TESTS_UNSUPPORTED_BY_TOIL])
149
170
  )
150
- p = subprocess.run(command, capture_output=True)
171
+ p = subprocess.run(commands, capture_output=True)
151
172
 
152
173
  self.check(p)
153
174
 
154
175
  # estimated running time: 10 minutes
155
176
  @slow
156
- def test_conformance_tests_v11(self):
157
- command = self.base_command + ["-v", "1.1"]
177
+ def test_conformance_tests_v11(self, wdl_conformance_test_repo: Path) -> None:
178
+ os.chdir(wdl_conformance_test_repo)
179
+ commands = [
180
+ exactPython,
181
+ "run.py",
182
+ "--runner",
183
+ "toil-wdl-runner",
184
+ "--conformance-file",
185
+ "conformance.yaml",
186
+ "-v",
187
+ "1.1",
188
+ ]
158
189
  if WDL_CONFORMANCE_TESTS_UNSUPPORTED_BY_TOIL:
159
- command.append("--exclude-numbers")
160
- command.append(
190
+ commands.append("--exclude-numbers")
191
+ commands.append(
161
192
  ",".join([str(t) for t in WDL_CONFORMANCE_TESTS_UNSUPPORTED_BY_TOIL])
162
193
  )
163
- p = subprocess.run(command, capture_output=True)
194
+ p = subprocess.run(commands, capture_output=True)
164
195
 
165
196
  self.check(p)
166
197
 
167
198
  @slow
168
- def test_conformance_tests_integration(self):
169
- ids_to_run = "encode,tut01,tut02,tut03,tut04"
199
+ def test_conformance_tests_integration(
200
+ self, wdl_conformance_test_repo: Path
201
+ ) -> None:
202
+ os.chdir(wdl_conformance_test_repo)
203
+ commands = [
204
+ exactPython,
205
+ "run.py",
206
+ "--runner",
207
+ "toil-wdl-runner",
208
+ "-v",
209
+ "1.0",
210
+ "--conformance-file",
211
+ "integration.yaml",
212
+ "--id",
213
+ "encode,tut01,tut02,tut03,tut04",
214
+ ]
170
215
  p = subprocess.run(
171
- self.base_command
172
- + [
173
- "-v",
174
- "1.0",
175
- "--conformance-file",
176
- "integration.yaml",
177
- "--id",
178
- ids_to_run,
179
- ],
216
+ commands,
180
217
  capture_output=True,
181
218
  )
182
219
 
183
220
  self.check(p)
184
221
 
185
- @classmethod
186
- def tearDownClass(cls) -> None:
187
- upper_dir = os.path.dirname(os.getcwd())
188
- os.chdir(upper_dir)
189
- shutil.rmtree("wdl-conformance-tests")
190
222
 
191
-
192
- class WDLTests(BaseWDLTest):
223
+ class TestWDL:
193
224
  """Tests for Toil's MiniWDL-based implementation."""
194
225
 
195
- @classmethod
196
- def setUpClass(cls) -> None:
197
- """Runs once for all tests."""
198
- cls.base_command = [exactPython, "-m", "toil.wdl.wdltoil"]
226
+ base_command = [exactPython, "-m", "toil.wdl.wdltoil"]
199
227
 
200
228
  # We inherit a testMD5sum but it is going to need Singularity or Docker
201
229
  # now. And also needs to have a WDL 1.0+ WDL file. So we replace it.
202
230
  @needs_singularity_or_docker
203
- def test_MD5sum(self):
231
+ def test_MD5sum(self, tmp_path: Path) -> None:
204
232
  """Test if Toil produces the same outputs as known good outputs for WDL's
205
233
  GATK tutorial #1."""
206
- wdl = os.path.abspath("src/toil/test/wdl/md5sum/md5sum.1.0.wdl")
207
- json_file = os.path.abspath("src/toil/test/wdl/md5sum/md5sum.json")
208
-
209
- result_json = subprocess.check_output(
210
- self.base_command
211
- + [wdl, json_file, "-o", self.output_dir, "--logDebug", "--retryCount=0"]
212
- )
213
- result = json.loads(result_json)
214
-
215
- assert "ga4ghMd5.value" in result
216
- assert isinstance(result["ga4ghMd5.value"], str)
217
- assert os.path.exists(result["ga4ghMd5.value"])
218
- assert os.path.basename(result["ga4ghMd5.value"]) == "md5sum.txt"
219
-
220
- def test_url_to_file(self):
234
+ with get_data("test/wdl/md5sum/md5sum.1.0.wdl") as wdl:
235
+ with get_data("test/wdl/md5sum/md5sum.json") as json_file:
236
+ result_json = subprocess.check_output(
237
+ self.base_command
238
+ + [
239
+ str(wdl),
240
+ str(json_file),
241
+ "-o",
242
+ str(tmp_path),
243
+ "--logDebug",
244
+ "--retryCount=0",
245
+ ]
246
+ )
247
+ result = json.loads(result_json)
248
+
249
+ assert "ga4ghMd5.value" in result
250
+ assert isinstance(result["ga4ghMd5.value"], str)
251
+ assert os.path.exists(result["ga4ghMd5.value"])
252
+ assert os.path.basename(result["ga4ghMd5.value"]) == "md5sum.txt"
253
+
254
+ @needs_online
255
+ def test_url_to_file(self, tmp_path: Path) -> None:
221
256
  """
222
257
  Test if web URL strings can be coerced to usable Files.
223
258
  """
224
- wdl = os.path.abspath("src/toil/test/wdl/testfiles/url_to_file.wdl")
259
+ with get_data("test/wdl/testfiles/url_to_file.wdl") as wdl:
260
+ result_json = subprocess.check_output(
261
+ self.base_command
262
+ + [str(wdl), "-o", str(tmp_path), "--logInfo", "--retryCount=0"]
263
+ )
264
+ result = json.loads(result_json)
225
265
 
226
- result_json = subprocess.check_output(
227
- self.base_command
228
- + [wdl, "-o", self.output_dir, "--logInfo", "--retryCount=0"]
229
- )
230
- result = json.loads(result_json)
266
+ assert "url_to_file.first_line" in result
267
+ assert isinstance(result["url_to_file.first_line"], str)
268
+ assert result["url_to_file.first_line"] == "chr1\t248387328"
231
269
 
232
- assert "url_to_file.first_line" in result
233
- assert isinstance(result["url_to_file.first_line"], str)
234
- self.assertEqual(result["url_to_file.first_line"], "chr1\t248387328")
270
+ def test_string_file_coercion(self, tmp_path: Path) -> None:
271
+ """
272
+ Test if input Files can be coerced to string and back.
273
+ """
274
+ with get_data("test/wdl/testfiles/string_file_coercion.wdl") as wdl:
275
+ with get_data("test/wdl/testfiles/string_file_coercion.json") as json_file:
276
+ result_json = subprocess.check_output(
277
+ self.base_command
278
+ + [
279
+ str(wdl),
280
+ str(json_file),
281
+ "-o",
282
+ str(tmp_path),
283
+ "--logInfo",
284
+ "--retryCount=0"
285
+ ]
286
+ )
287
+ result = json.loads(result_json)
288
+
289
+ assert "StringFileCoercion.output_file" in result
235
290
 
236
291
  @needs_docker
237
- def test_wait(self):
292
+ def test_wait(self, tmp_path: Path) -> None:
238
293
  """
239
294
  Test if Bash "wait" works in WDL scripts.
240
295
  """
241
- wdl = os.path.abspath("src/toil/test/wdl/testfiles/wait.wdl")
296
+ with get_data("test/wdl/testfiles/wait.wdl") as wdl:
297
+ result_json = subprocess.check_output(
298
+ self.base_command
299
+ + [
300
+ str(wdl),
301
+ "-o",
302
+ str(tmp_path),
303
+ "--logInfo",
304
+ "--retryCount=0",
305
+ "--wdlContainer=docker",
306
+ ]
307
+ )
308
+ result = json.loads(result_json)
242
309
 
243
- result_json = subprocess.check_output(
244
- self.base_command
245
- + [
246
- wdl,
247
- "-o",
248
- self.output_dir,
249
- "--logInfo",
250
- "--retryCount=0",
251
- "--wdlContainer=docker",
310
+ assert "wait.result" in result
311
+ assert isinstance(result["wait.result"], str)
312
+ assert result["wait.result"] == "waited"
313
+
314
+ def test_restart(self, tmp_path: Path) -> None:
315
+ """
316
+ Test if a WDL workflow can be restarted and finish successfully.
317
+ """
318
+ with get_data("test/wdl/testfiles/read_file.wdl") as wdl:
319
+ out_dir = tmp_path / "out"
320
+ file_path = tmp_path / "file"
321
+ jobstore_path = tmp_path / "tree"
322
+ command = (
323
+ self.base_command
324
+ + [
325
+ str(wdl),
326
+ "-o",
327
+ str(out_dir),
328
+ "-i",
329
+ json.dumps({"read_file.input_string": str(file_path)}),
330
+ "--jobStore",
331
+ str(jobstore_path),
332
+ "--retryCount=0"
333
+ ]
334
+ )
335
+ with pytest.raises(subprocess.CalledProcessError):
336
+ # The first time we run it, it should fail because it's trying
337
+ # to work on a nonexistent file from a string path.
338
+ result_json = subprocess.check_output(
339
+ command + ["--logCritical"]
340
+ )
341
+
342
+ # Then create the file
343
+ with open(file_path, "w") as f:
344
+ f.write("This is a line\n")
345
+ f.write("This is a different line")
346
+
347
+ # Now it should work
348
+ result_json = subprocess.check_output(
349
+ command + ["--restart"]
350
+ )
351
+ result = json.loads(result_json)
352
+
353
+ assert "read_file.lines" in result
354
+ assert isinstance(result["read_file.lines"], list)
355
+ assert result["read_file.lines"] == [
356
+ "This is a line",
357
+ "This is a different line"
252
358
  ]
253
- )
254
- result = json.loads(result_json)
255
359
 
256
- assert "wait.result" in result
257
- assert isinstance(result["wait.result"], str)
258
- self.assertEqual(result["wait.result"], "waited")
360
+ # Since we were catching
361
+ # <https://github.com/DataBiosphere/toil/issues/5247> at file
362
+ # export, make sure we actually exported a file.
363
+ assert "read_file.remade_file" in result
364
+ assert isinstance(result["read_file.remade_file"], str)
365
+ assert os.path.exists(result["read_file.remade_file"])
259
366
 
260
367
  @needs_singularity_or_docker
261
- def test_all_call_outputs(self):
368
+ def test_workflow_file_deletion(self, tmp_path: Path) -> None:
262
369
  """
263
- Test if Toil can collect all call outputs from a workflow that doesn't expose them.
370
+ Test if Toil can delete non-output outputs at the end of a workflow.
264
371
  """
265
- wdl = os.path.abspath("src/toil/test/wdl/testfiles/not_enough_outputs.wdl")
372
+ # Keep a job store around to inspect for files.
373
+ (tmp_path / "jobStore").mkdir()
374
+ job_store = tmp_path / "jobStore" / "tree"
375
+
376
+ # Make a working directory to run in
377
+ work_dir = tmp_path / "workDir"
378
+ work_dir.mkdir()
379
+ # Make the file that will be imported from a string in the workflow
380
+ referenced_file = work_dir / "localfile.txt"
381
+ with referenced_file.open("w") as f:
382
+ f.write("This file is imported by local path in the workflow")
383
+ # Make the file to pass as input
384
+ sent_in_file = work_dir / "sent_in.txt"
385
+ with sent_in_file.open("w") as f:
386
+ f.write("This file is sent in as input")
387
+
388
+ with get_data("test/wdl/testfiles/drop_files.wdl") as wdl:
389
+ result_json = subprocess.check_output(
390
+ self.base_command
391
+ + [
392
+ str(wdl),
393
+ "-o",
394
+ str(tmp_path / "output"),
395
+ "--jobStore",
396
+ job_store,
397
+ "--clean=never",
398
+ "--logInfo",
399
+ "--retryCount=0",
400
+ '--input={"file_in": "' + str(sent_in_file) + '"}',
401
+ ],
402
+ cwd=work_dir,
403
+ )
404
+ result = json.loads(result_json)
266
405
 
267
- # With no flag we don't include the call outputs
268
- result_json = subprocess.check_output(
269
- self.base_command
270
- + [wdl, "-o", self.output_dir, "--logInfo", "--retryCount=0"]
271
- )
272
- result = json.loads(result_json)
406
+ # Get all the file values in the job store.
407
+ all_file_values = set()
408
+ for directory, _, files in os.walk(
409
+ job_store
410
+ ): # can't switch to job_store.walk() until Python 3.12 is the minimum version
411
+ for filename in files:
412
+ with (Path(directory) / filename).open(
413
+ encoding="utf-8", errors="replace"
414
+ ) as f:
415
+ all_file_values.add(f.read().rstrip())
416
+
417
+ # Make sure the files with the right contents are in the job store and
418
+ # the files with the wrong contents aren't anymore.
419
+ #
420
+ # This assumes no top-level cleanup in the main driver script.
273
421
 
274
- assert "wf.only_result" in result
275
- assert "wf.do_math.square" not in result
276
- assert "wf.do_math.cube" not in result
277
- assert "wf.should_never_output" not in result
422
+ # These are all created inside the workflow and not output
423
+ assert (
424
+ "This file is imported by local path in the workflow"
425
+ not in all_file_values
426
+ )
427
+ assert "This file is consumed by a task call" not in all_file_values
428
+ assert (
429
+ "This file is created in a task inputs section" not in all_file_values
430
+ )
431
+ assert "This file is created in a runtime section" not in all_file_values
432
+ assert "This task file is not used" not in all_file_values
433
+ assert "This file should be discarded" not in all_file_values
434
+ assert "This file is dropped by a subworkflow" not in all_file_values
435
+ assert "This file gets stored in a variable" not in all_file_values
436
+ assert "This file never gets stored in a variable" not in all_file_values
437
+
438
+ # These are created inside the workflow and output
439
+ assert "3" in all_file_values
440
+ assert "This file is collected as a task output twice" in all_file_values
441
+ assert "This file should be kept" in all_file_values
442
+ assert "This file is kept by a subworkflow" in all_file_values
443
+
444
+ # These are sent into the workflow from the enclosing environment and
445
+ # should not be deleted.
446
+ assert "This file is sent in as input" in all_file_values
447
+
448
+ # Make sure we didn't somehow delete the file sent as input
449
+ assert sent_in_file.exists()
278
450
 
279
- # With flag off we don't include the call outputs
280
- result_json = subprocess.check_output(
281
- self.base_command
282
- + [
283
- wdl,
284
- "-o",
285
- self.output_dir,
286
- "--logInfo",
287
- "--retryCount=0",
288
- "--allCallOutputs=false",
289
- ]
290
- )
291
- result = json.loads(result_json)
451
+ @needs_singularity_or_docker
452
+ def test_all_call_outputs(self, tmp_path: Path) -> None:
453
+ """
454
+ Test if Toil can collect all call outputs from a workflow that doesn't expose them.
455
+ """
456
+ with get_data("test/wdl/testfiles/not_enough_outputs.wdl") as wdl:
457
+ # With no flag we don't include the call outputs
458
+ result_json = subprocess.check_output(
459
+ self.base_command
460
+ + [str(wdl), "-o", str(tmp_path), "--logInfo", "--retryCount=0"]
461
+ )
462
+ result = json.loads(result_json)
292
463
 
293
- assert "wf.only_result" in result
294
- assert "wf.do_math.square" not in result
295
- assert "wf.do_math.cube" not in result
296
- assert "wf.should_never_output" not in result
464
+ assert "wf.only_result" in result
465
+ assert "wf.do_math.square" not in result
466
+ assert "wf.do_math.cube" not in result
467
+ assert "wf.should_never_output" not in result
297
468
 
298
- # With flag on we do include the call outputs
299
- result_json = subprocess.check_output(
300
- self.base_command
301
- + [
302
- wdl,
303
- "-o",
304
- self.output_dir,
305
- "--logInfo",
306
- "--retryCount=0",
307
- "--allCallOutputs=on",
308
- ]
309
- )
310
- result = json.loads(result_json)
469
+ # With flag off we don't include the call outputs
470
+ result_json = subprocess.check_output(
471
+ self.base_command
472
+ + [
473
+ str(wdl),
474
+ "-o",
475
+ str(tmp_path),
476
+ "--logInfo",
477
+ "--retryCount=0",
478
+ "--allCallOutputs=false",
479
+ ]
480
+ )
481
+ result = json.loads(result_json)
482
+
483
+ assert "wf.only_result" in result
484
+ assert "wf.do_math.square" not in result
485
+ assert "wf.do_math.cube" not in result
486
+ assert "wf.should_never_output" not in result
487
+
488
+ # With flag on we do include the call outputs
489
+ result_json = subprocess.check_output(
490
+ self.base_command
491
+ + [
492
+ str(wdl),
493
+ "-o",
494
+ str(tmp_path),
495
+ "--logInfo",
496
+ "--retryCount=0",
497
+ "--allCallOutputs=on",
498
+ ]
499
+ )
500
+ result = json.loads(result_json)
311
501
 
312
- assert "wf.only_result" in result
313
- assert "wf.do_math.square" in result
314
- assert "wf.do_math.cube" in result
315
- assert "wf.should_never_output" not in result
502
+ assert "wf.only_result" in result
503
+ assert "wf.do_math.square" in result
504
+ assert "wf.do_math.cube" in result
505
+ assert "wf.should_never_output" not in result
316
506
 
317
507
  @needs_singularity_or_docker
318
- def test_croo_detection(self):
508
+ def test_croo_detection(self, tmp_path: Path) -> None:
319
509
  """
320
510
  Test if Toil can detect and do something sensible with Cromwell Output Organizer workflows.
321
511
  """
322
- wdl = os.path.abspath("src/toil/test/wdl/testfiles/croo.wdl")
323
-
324
- # With no flag we should include all task outputs
325
- result_json = subprocess.check_output(
326
- self.base_command
327
- + [wdl, "-o", self.output_dir, "--logInfo", "--retryCount=0"]
328
- )
329
- result = json.loads(result_json)
512
+ with get_data("test/wdl/testfiles/croo.wdl") as wdl:
513
+ # With no flag we should include all task outputs
514
+ result_json = subprocess.check_output(
515
+ self.base_command
516
+ + [str(wdl), "-o", str(tmp_path), "--logInfo", "--retryCount=0"]
517
+ )
518
+ result = json.loads(result_json)
330
519
 
331
- assert "wf.only_result" in result
332
- assert "wf.do_math.square" in result
333
- assert "wf.do_math.cube" in result
334
- assert "wf.should_never_output" not in result
520
+ assert "wf.only_result" in result
521
+ assert "wf.do_math.square" in result
522
+ assert "wf.do_math.cube" in result
523
+ assert "wf.should_never_output" not in result
335
524
 
336
- # With flag off we obey the WDL spec even if we're suspicious
337
- result_json = subprocess.check_output(
338
- self.base_command
339
- + [
340
- wdl,
341
- "-o",
342
- self.output_dir,
343
- "--logInfo",
344
- "--retryCount=0",
345
- "--allCallOutputs=off",
346
- ]
347
- )
348
- result = json.loads(result_json)
525
+ # With flag off we obey the WDL spec even if we're suspicious
526
+ result_json = subprocess.check_output(
527
+ self.base_command
528
+ + [
529
+ str(wdl),
530
+ "-o",
531
+ str(tmp_path),
532
+ "--logInfo",
533
+ "--retryCount=0",
534
+ "--allCallOutputs=off",
535
+ ]
536
+ )
537
+ result = json.loads(result_json)
349
538
 
350
- assert "wf.only_result" in result
351
- assert "wf.do_math.square" not in result
352
- assert "wf.do_math.cube" not in result
353
- assert "wf.should_never_output" not in result
539
+ assert "wf.only_result" in result
540
+ assert "wf.do_math.square" not in result
541
+ assert "wf.do_math.cube" not in result
542
+ assert "wf.should_never_output" not in result
354
543
 
355
544
  @needs_singularity_or_docker
356
- def test_caching(self):
545
+ def test_caching(self, tmp_path: Path) -> None:
357
546
  """
358
547
  Test if Toil can cache task runs.
359
548
  """
360
- wdl = os.path.abspath('src/toil/test/wdl/testfiles/random.wdl')
361
-
362
- caching_env = dict(os.environ)
363
- caching_env["MINIWDL__CALL_CACHE__GET"] = "true"
364
- caching_env["MINIWDL__CALL_CACHE__PUT"] = "true"
365
- caching_env["MINIWDL__CALL_CACHE__DIR"] = self._createTempDir("cache")
366
-
367
- result_json = subprocess.check_output(
368
- self.base_command + [wdl, '-o', self.output_dir, '--logInfo', '--retryCount=0', '--inputs={"random.task_1_input": 1, "random.task_2_input": 1}'],
369
- env=caching_env)
370
- result_initial = json.loads(result_json)
371
-
372
- assert 'random.value_seen' in result_initial
373
- assert 'random.value_written' in result_initial
374
-
375
- result_json = subprocess.check_output(
376
- self.base_command + [wdl, '-o', self.output_dir, '--logInfo', '--retryCount=0', '--inputs={"random.task_1_input": 1, "random.task_2_input": 1}'],
377
- env=caching_env)
378
- result_cached = json.loads(result_json)
379
-
380
- assert 'random.value_seen' in result_cached
381
- assert 'random.value_written' in result_cached
382
-
383
- assert result_cached['random.value_seen'] == result_initial['random.value_seen']
384
- assert result_cached['random.value_written'] == result_initial['random.value_written']
385
-
386
- result_json = subprocess.check_output(
387
- self.base_command + [wdl, '-o', self.output_dir, '--logInfo', '--retryCount=0', '--inputs={"random.task_1_input": 2, "random.task_2_input": 1}'],
388
- env=caching_env)
389
- result_not_cached = json.loads(result_json)
390
-
391
- assert 'random.value_seen' in result_not_cached
392
- assert 'random.value_written' in result_not_cached
549
+ with get_data("test/wdl/testfiles/random.wdl") as wdl:
550
+ cachedir = tmp_path / "cache"
551
+ cachedir.mkdir()
552
+ caching_env = dict(os.environ)
553
+ caching_env["MINIWDL__CALL_CACHE__GET"] = "true"
554
+ caching_env["MINIWDL__CALL_CACHE__PUT"] = "true"
555
+ caching_env["MINIWDL__CALL_CACHE__DIR"] = str(cachedir)
393
556
 
394
- assert result_not_cached['random.value_seen'] != result_initial['random.value_seen']
395
- assert result_not_cached['random.value_written'] != result_initial['random.value_written']
557
+ result_json = subprocess.check_output(
558
+ self.base_command
559
+ + [
560
+ str(wdl),
561
+ "-o",
562
+ str(tmp_path / "out1"),
563
+ "--logInfo",
564
+ "--retryCount=0",
565
+ '--inputs={"random.task_1_input": 1, "random.task_2_input": 1}',
566
+ ],
567
+ env=caching_env,
568
+ )
569
+ result_initial = json.loads(result_json)
396
570
 
397
- result_json = subprocess.check_output(
398
- self.base_command + [wdl, '-o', self.output_dir, '--logInfo', '--retryCount=0', '--inputs={"random.task_1_input": 1, "random.task_2_input": 2}'],
399
- env=caching_env)
400
- result_part_cached = json.loads(result_json)
571
+ assert "random.value_seen" in result_initial
572
+ assert "random.value_written" in result_initial
401
573
 
402
- assert 'random.value_seen' in result_part_cached
403
- assert 'random.value_written' in result_part_cached
574
+ result_json = subprocess.check_output(
575
+ self.base_command
576
+ + [
577
+ str(wdl),
578
+ "-o",
579
+ str(tmp_path / "out2"),
580
+ "--logInfo",
581
+ "--retryCount=0",
582
+ '--inputs={"random.task_1_input": 1, "random.task_2_input": 1}',
583
+ ],
584
+ env=caching_env,
585
+ )
586
+ result_cached = json.loads(result_json)
404
587
 
405
- assert result_part_cached['random.value_seen'] == result_initial['random.value_seen']
406
- assert result_part_cached['random.value_written'] != result_initial['random.value_written']
407
- assert result_part_cached['random.value_written'] != result_not_cached['random.value_written']
588
+ assert "random.value_seen" in result_cached
589
+ assert "random.value_written" in result_cached
408
590
 
591
+ assert (
592
+ result_cached["random.value_seen"]
593
+ == result_initial["random.value_seen"]
594
+ )
595
+ assert (
596
+ result_cached["random.value_written"]
597
+ == result_initial["random.value_written"]
598
+ )
409
599
 
600
+ result_json = subprocess.check_output(
601
+ self.base_command
602
+ + [
603
+ str(wdl),
604
+ "-o",
605
+ str(tmp_path / "out3"),
606
+ "--logInfo",
607
+ "--retryCount=0",
608
+ '--inputs={"random.task_1_input": 2, "random.task_2_input": 1}',
609
+ ],
610
+ env=caching_env,
611
+ )
612
+ result_not_cached = json.loads(result_json)
410
613
 
411
- def test_url_to_optional_file(self):
412
- """
413
- Test if missing and error-producing URLs are handled correctly for optional File? values.
414
- """
415
- wdl = os.path.abspath("src/toil/test/wdl/testfiles/url_to_optional_file.wdl")
614
+ assert "random.value_seen" in result_not_cached
615
+ assert "random.value_written" in result_not_cached
416
616
 
417
- def run_for_code(code: int) -> dict:
418
- """
419
- Run a workflow coercing URL to File? where the URL returns the given status code.
617
+ assert (
618
+ result_not_cached["random.value_seen"]
619
+ != result_initial["random.value_seen"]
620
+ )
621
+ assert (
622
+ result_not_cached["random.value_written"]
623
+ != result_initial["random.value_written"]
624
+ )
420
625
 
421
- Return the parsed output.
422
- """
423
- logger.info("Test optional file with HTTP code %s", code)
424
- json_value = '{"url_to_optional_file.http_code": %d}' % code
425
626
  result_json = subprocess.check_output(
426
627
  self.base_command
427
628
  + [
428
- wdl,
429
- json_value,
629
+ str(wdl),
430
630
  "-o",
431
- self.output_dir,
631
+ str(tmp_path / "out4"),
432
632
  "--logInfo",
433
633
  "--retryCount=0",
434
- ]
634
+ '--inputs={"random.task_1_input": 1, "random.task_2_input": 2}',
635
+ ],
636
+ env=caching_env,
435
637
  )
436
- result = json.loads(result_json)
437
- return result
638
+ result_part_cached = json.loads(result_json)
438
639
 
439
- # Check files that exist
440
- result = run_for_code(200)
441
- assert "url_to_optional_file.out_file" in result
442
- self.assertNotEqual(result["url_to_optional_file.out_file"], None)
640
+ assert "random.value_seen" in result_part_cached
641
+ assert "random.value_written" in result_part_cached
443
642
 
444
- for code in (404, 410):
445
- # Check files that definitely don't
446
- result = run_for_code(code)
643
+ assert (
644
+ result_part_cached["random.value_seen"]
645
+ == result_initial["random.value_seen"]
646
+ )
647
+ assert (
648
+ result_part_cached["random.value_written"]
649
+ != result_initial["random.value_written"]
650
+ )
651
+ assert (
652
+ result_part_cached["random.value_written"]
653
+ != result_not_cached["random.value_written"]
654
+ )
655
+
656
+ def test_url_to_optional_file(self, tmp_path: Path, httpserver: HTTPServer) -> None:
657
+ """
658
+ Test if missing and error-producing URLs are handled correctly for optional File? values.
659
+ """
660
+ with get_data("test/wdl/testfiles/url_to_optional_file.wdl") as wdl:
661
+
662
+ def run_for_code(code: int) -> dict[str, Any]:
663
+ """
664
+ Run a workflow coercing URL to File? where the URL returns the given status code.
665
+
666
+ Return the parsed output.
667
+ """
668
+ logger.info("Test optional file with HTTP code %s", code)
669
+ httpserver.expect_request(
670
+ "/" + str(code)
671
+ ).respond_with_data(
672
+ "Some data",
673
+ status=code,
674
+ content_type="text/plain"
675
+ )
676
+ base_url = httpserver.url_for("/")
677
+ json_value = '{"url_to_optional_file.http_code": %d, "url_to_optional_file.base_url": "%s"}' % (code, base_url)
678
+ result_json = subprocess.check_output(
679
+ self.base_command
680
+ + [
681
+ str(wdl),
682
+ json_value,
683
+ "-o",
684
+ str(tmp_path),
685
+ "--logInfo",
686
+ "--retryCount=0",
687
+ ]
688
+ )
689
+ result = json.loads(result_json)
690
+ return cast(dict[str, Any], json.loads(result_json))
691
+
692
+ # Check files that exist
693
+ result = run_for_code(200)
447
694
  assert "url_to_optional_file.out_file" in result
448
- self.assertEqual(result["url_to_optional_file.out_file"], None)
695
+ assert result["url_to_optional_file.out_file"] is not None
696
+
697
+ for code in (404, 410):
698
+ # Check files that definitely don't
699
+ result = run_for_code(code)
700
+ assert "url_to_optional_file.out_file" in result
701
+ assert result["url_to_optional_file.out_file"] is None
449
702
 
450
- for code in (402, 418, 500, 502):
451
- # Check that cases where the server refuses to say if the file
452
- # exists stop the workflow.
453
- with self.assertRaises(subprocess.CalledProcessError):
454
- run_for_code(code)
703
+ for code in (402, 418, 500, 502):
704
+ # Check that cases where the server refuses to say if the file
705
+ # exists stop the workflow.
706
+ with pytest.raises(subprocess.CalledProcessError):
707
+ run_for_code(code)
455
708
 
456
- def test_missing_output_directory(self):
709
+ def test_missing_output_directory(self, tmp_path: Path) -> None:
457
710
  """
458
711
  Test if Toil can run a WDL workflow into a new directory.
459
712
  """
460
- wdl = os.path.abspath("src/toil/test/wdl/md5sum/md5sum.1.0.wdl")
461
- json_file = os.path.abspath("src/toil/test/wdl/md5sum/md5sum.json")
462
- subprocess.check_call(
463
- self.base_command
464
- + [
465
- wdl,
466
- json_file,
467
- "-o",
468
- os.path.join(self.output_dir, "does", "not", "exist"),
469
- "--logDebug",
470
- "--retryCount=0",
471
- ]
472
- )
713
+ with get_data("test/wdl/md5sum/md5sum.1.0.wdl") as wdl:
714
+ with get_data("test/wdl/md5sum/md5sum.json") as json_file:
715
+ subprocess.check_call(
716
+ self.base_command
717
+ + [
718
+ str(wdl),
719
+ str(json_file),
720
+ "-o",
721
+ str(tmp_path / "does" / "not" / "exist"),
722
+ "--logDebug",
723
+ "--retryCount=0",
724
+ ]
725
+ )
473
726
 
474
727
  @needs_singularity_or_docker
475
- def test_miniwdl_self_test(self, extra_args: Optional[list[str]] = None) -> None:
728
+ def test_miniwdl_self_test(
729
+ self, tmp_path: Path, extra_args: Optional[list[str]] = None
730
+ ) -> None:
476
731
  """Test if the MiniWDL self test runs and produces the expected output."""
477
- wdl_file = os.path.abspath("src/toil/test/wdl/miniwdl_self_test/self_test.wdl")
478
- json_file = os.path.abspath("src/toil/test/wdl/miniwdl_self_test/inputs.json")
732
+ with get_data("test/wdl/miniwdl_self_test/self_test.wdl") as wdl_file:
733
+ with get_data("test/wdl/miniwdl_self_test/inputs.json") as json_file:
734
+
735
+ result_json = subprocess.check_output(
736
+ self.base_command
737
+ + [
738
+ str(wdl_file),
739
+ str(json_file),
740
+ "--logDebug",
741
+ "-o",
742
+ str(tmp_path),
743
+ "--outputDialect",
744
+ "miniwdl",
745
+ ]
746
+ + (extra_args or [])
747
+ )
748
+ result = json.loads(result_json)
749
+
750
+ # Expect MiniWDL-style output with a designated "dir"
751
+
752
+ assert "dir" in result
753
+ assert isinstance(result["dir"], str)
754
+ out_dir = result["dir"]
755
+
756
+ assert "outputs" in result
757
+ assert isinstance(result["outputs"], dict)
758
+ outputs = result["outputs"]
759
+
760
+ assert "hello_caller.message_files" in outputs
761
+ assert isinstance(outputs["hello_caller.message_files"], list)
762
+ assert len(outputs["hello_caller.message_files"]) == 2
763
+ for item in outputs["hello_caller.message_files"]:
764
+ # All the files should be strings in the "out" directory
765
+ assert isinstance(item, str), "File output must be a string"
766
+ assert item.startswith(
767
+ out_dir
768
+ ), "File output must be in the output directory"
769
+
770
+ # Look at the filename within that directory
771
+ name_in_out_dir = item[len(out_dir) :]
772
+
773
+ # Ity should contain the job name of "hello", so they are human-readable.
774
+ assert (
775
+ "hello" in name_in_out_dir
776
+ ), f"File output {name_in_out_dir} should have the originating task name in it"
777
+
778
+ # And it should not contain non-human-readable content.
779
+ #
780
+ # We use a threshold number of digits as a proxy for this, but
781
+ # don't try and get around this by just rolling other random
782
+ # strings; we want these outputs to be human-readable!!!
783
+ digit_count = len(
784
+ [c for c in name_in_out_dir if c in string.digits]
785
+ )
786
+ assert (
787
+ digit_count < 3
788
+ ), f"File output {name_in_out_dir} has {digit_count} digits, which is too many to be plausibly human-readable"
789
+
790
+ assert "hello_caller.messages" in outputs
791
+ assert outputs["hello_caller.messages"] == [
792
+ "Hello, Alyssa P. Hacker!",
793
+ "Hello, Ben Bitdiddle!",
794
+ ]
795
+
796
+ @needs_singularity_or_docker
797
+ def test_miniwdl_self_test_by_reference(self, tmp_path: Path) -> None:
798
+ """
799
+ Test if the MiniWDL self test works when passing input files by URL reference.
800
+ """
801
+ self.test_miniwdl_self_test(
802
+ tmp_path=tmp_path, extra_args=["--referenceInputs=True"]
803
+ )
804
+
805
+ @pytest.mark.integrative
806
+ @needs_singularity_or_docker
807
+ def test_dockstore_trs(
808
+ self, tmp_path: Path, extra_args: Optional[list[str]] = None
809
+ ) -> None:
810
+ wdl_file = "#workflow/github.com/dockstore/bcc2020-training/HelloWorld:master"
811
+ # Needs an input but doesn't provide a good one.
812
+ json_input = json.dumps(
813
+ {
814
+ "hello_world.hello.myName": "https://raw.githubusercontent.com/dockstore/bcc2020-training/refs/heads/master/wdl-training/exercise1/name.txt"
815
+ }
816
+ )
479
817
 
480
818
  result_json = subprocess.check_output(
481
819
  self.base_command
482
820
  + [
483
821
  wdl_file,
484
- json_file,
822
+ json_input,
485
823
  "--logDebug",
486
824
  "-o",
487
- self.output_dir,
825
+ str(tmp_path),
488
826
  "--outputDialect",
489
827
  "miniwdl",
490
828
  ]
@@ -492,89 +830,67 @@ class WDLTests(BaseWDLTest):
492
830
  )
493
831
  result = json.loads(result_json)
494
832
 
495
- # Expect MiniWDL-style output with a designated "dir"
496
-
497
- assert "dir" in result
498
- assert isinstance(result["dir"], str)
499
- out_dir = result["dir"]
500
-
501
- assert "outputs" in result
502
- assert isinstance(result["outputs"], dict)
503
- outputs = result["outputs"]
504
-
505
- assert "hello_caller.message_files" in outputs
506
- assert isinstance(outputs["hello_caller.message_files"], list)
507
- assert len(outputs["hello_caller.message_files"]) == 2
508
- for item in outputs["hello_caller.message_files"]:
509
- # All the files should be strings in the "out" directory
510
- assert isinstance(item, str), "File output must be a string"
511
- assert item.startswith(
512
- out_dir
513
- ), "File output must be in the output directory"
514
-
515
- # Look at the filename within that directory
516
- name_in_out_dir = item[len(out_dir) :]
517
-
518
- # Ity should contain the job name of "hello", so they are human-readable.
519
- assert (
520
- "hello" in name_in_out_dir
521
- ), f"File output {name_in_out_dir} should have the originating task name in it"
522
-
523
- # And it should not contain non-human-readable content.
524
- #
525
- # We use a threshold number of digits as a proxy for this, but
526
- # don't try and get around this by just rolling other random
527
- # strings; we want these outputs to be human-readable!!!
528
- digit_count = len([c for c in name_in_out_dir if c in string.digits])
529
- assert (
530
- digit_count < 3
531
- ), f"File output {name_in_out_dir} has {digit_count} digits, which is too many to be plausibly human-readable"
833
+ with open(result.get("outputs", {}).get("hello_world.helloFile")) as f:
834
+ result_text = f.read().strip()
532
835
 
533
- assert "hello_caller.messages" in outputs
534
- assert outputs["hello_caller.messages"] == [
535
- "Hello, Alyssa P. Hacker!",
536
- "Hello, Ben Bitdiddle!",
537
- ]
538
-
539
- @needs_singularity_or_docker
540
- def test_miniwdl_self_test_by_reference(self) -> None:
541
- """
542
- Test if the MiniWDL self test works when passing input files by URL reference.
543
- """
544
- self.test_miniwdl_self_test(extra_args=["--referenceInputs=True"])
836
+ assert result_text == "Hello World!\nMy name is potato."
545
837
 
838
+ # TODO: Should this move to the TRS/Dockstore tests file?
546
839
  @pytest.mark.integrative
547
840
  @needs_singularity_or_docker
548
- def test_dockstore_trs(self, extra_args: Optional[list[str]] = None) -> None:
841
+ def test_dockstore_metrics_publication(
842
+ self, tmp_path: Path, extra_args: Optional[list[str]] = None
843
+ ) -> None:
549
844
  wdl_file = "#workflow/github.com/dockstore/bcc2020-training/HelloWorld:master"
550
845
  # Needs an input but doesn't provide a good one.
551
- json_input = json.dumps({"hello_world.hello.myName": "https://raw.githubusercontent.com/dockstore/bcc2020-training/refs/heads/master/wdl-training/exercise1/name.txt"})
846
+ json_input = json.dumps(
847
+ {
848
+ "hello_world.hello.myName": "https://raw.githubusercontent.com/dockstore/bcc2020-training/refs/heads/master/wdl-training/exercise1/name.txt"
849
+ }
850
+ )
552
851
 
553
- result_json = subprocess.check_output(
554
- self.base_command + [wdl_file, json_input, '--logDebug', '-o', self.output_dir, '--outputDialect',
555
- 'miniwdl'] + (extra_args or []))
556
- result = json.loads(result_json)
852
+ env = dict(os.environ)
853
+ # Set credentials we got permission to publish from the Dockstore team,
854
+ # and work on the staging Dockstore.
855
+ env["TOIL_TRS_ROOT"] = "https://staging.dockstore.org"
856
+ env["TOIL_DOCKSTORE_TOKEN"] = "99cf5578ebe94b194d7864630a86258fa3d6cedcc17d757b5dd49e64ee3b68c3"
857
+ # Enable history for when <https://github.com/DataBiosphere/toil/pull/5258> merges
858
+ env["TOIL_HISTORY"] = "True"
557
859
 
558
- with open(result.get("outputs", {}).get("hello_world.helloFile")) as f:
559
- result_text = f.read().strip()
860
+ output_log = subprocess.check_output(
861
+ self.base_command
862
+ + [
863
+ wdl_file,
864
+ json_input,
865
+ "--logDebug",
866
+ "-o",
867
+ str(tmp_path),
868
+ "--outputDialect",
869
+ "miniwdl",
870
+ "--publishWorkflowMetrics=current",
871
+ ]
872
+ + (extra_args or []),
873
+ stderr=subprocess.STDOUT,
874
+ env=env,
875
+ )
560
876
 
561
- self.assertEqual(result_text, "Hello World!\nMy name is potato.")
877
+ assert b'Workflow metrics were accepted by Dockstore.' in output_log
562
878
 
563
879
  @slow
564
880
  @needs_docker_cuda
565
- def test_giraffe_deepvariant(self):
881
+ def test_giraffe_deepvariant(self, tmp_path: Path) -> None:
566
882
  """Test if Giraffe and GPU DeepVariant run. This could take 25 minutes."""
567
883
  # TODO: enable test if nvidia-container-runtime and Singularity are installed but Docker isn't.
568
884
 
569
- json_dir = self._createTempDir()
885
+ json_dir = tmp_path / "json"
886
+ json_dir.mkdir()
570
887
  base_uri = "https://raw.githubusercontent.com/vgteam/vg_wdl/65dd739aae765f5c4dedd14f2e42d5a263f9267a"
571
888
 
572
889
  wdl_file = f"{base_uri}/workflows/giraffe_and_deepvariant.wdl"
573
- json_file = os.path.abspath(os.path.join(json_dir, "inputs.json"))
574
- with open(json_file, "w") as fp:
890
+ json_file = json_dir / "inputs.json"
891
+ with json_file.open("w") as fp:
575
892
  # Write some inputs. We need to override the example inputs to use a GPU container, but that means we need absolute input URLs.
576
893
  json.dump(
577
- fp,
578
894
  {
579
895
  "GiraffeDeepVariant.INPUT_READ_FILE_1": f"{base_uri}/tests/small_sim_graph/reads_1.fastq.gz",
580
896
  "GiraffeDeepVariant.INPUT_READ_FILE_2": f"{base_uri}/tests/small_sim_graph/reads_2.fastq.gz",
@@ -587,11 +903,19 @@ class WDLTests(BaseWDLTest):
587
903
  "GiraffeDeepVariant.OUTPUT_GAF": True,
588
904
  "GiraffeDeepVariant.runDeepVariantCallVariants.in_dv_gpu_container": "google/deepvariant:1.3.0-gpu",
589
905
  },
906
+ fp,
590
907
  )
591
908
 
592
909
  result_json = subprocess.check_output(
593
910
  self.base_command
594
- + [wdl_file, json_file, "-o", self.output_dir, "--outputDialect", "miniwdl"]
911
+ + [
912
+ wdl_file,
913
+ json_file,
914
+ "-o",
915
+ str(tmp_path / "out"),
916
+ "--outputDialect",
917
+ "miniwdl",
918
+ ]
595
919
  )
596
920
  result = json.loads(result_json)
597
921
 
@@ -611,13 +935,12 @@ class WDLTests(BaseWDLTest):
611
935
 
612
936
  @slow
613
937
  @needs_singularity_or_docker
614
- def test_giraffe(self):
938
+ def test_giraffe(self, tmp_path: Path) -> None:
615
939
  """Test if Giraffe runs. This could take 12 minutes. Also we scale it down but it still demands lots of memory."""
616
940
  # TODO: enable test if nvidia-container-runtime and Singularity are installed but Docker isn't.
617
941
  # TODO: Reduce memory requests with custom/smaller inputs.
618
942
  # TODO: Skip if node lacks enough memory.
619
943
 
620
- json_dir = self._createTempDir()
621
944
  base_uri = "https://raw.githubusercontent.com/vgteam/vg_wdl/65dd739aae765f5c4dedd14f2e42d5a263f9267a"
622
945
  wdl_file = f"{base_uri}/workflows/giraffe.wdl"
623
946
  json_file = f"{base_uri}/params/giraffe.json"
@@ -628,7 +951,7 @@ class WDLTests(BaseWDLTest):
628
951
  wdl_file,
629
952
  json_file,
630
953
  "-o",
631
- self.output_dir,
954
+ str(tmp_path),
632
955
  "--outputDialect",
633
956
  "miniwdl",
634
957
  "--scale",
@@ -654,26 +977,43 @@ class WDLTests(BaseWDLTest):
654
977
 
655
978
  @needs_singularity_or_docker
656
979
  @needs_google_storage
657
- def test_gs_uri(self):
980
+ def test_gs_uri(self, tmp_path: Path) -> None:
658
981
  """Test if Toil can access Google Storage URIs."""
659
- wdl = os.path.abspath("src/toil/test/wdl/md5sum/md5sum.1.0.wdl")
660
- json_file = os.path.abspath("src/toil/test/wdl/md5sum/md5sum-gs.json")
661
-
662
- result_json = subprocess.check_output(
663
- self.base_command + [wdl, json_file, "-o", self.output_dir, "--logDebug"]
664
- )
665
- result = json.loads(result_json)
666
-
667
- assert "ga4ghMd5.value" in result
668
- assert isinstance(result["ga4ghMd5.value"], str)
669
- assert os.path.exists(result["ga4ghMd5.value"])
670
- assert os.path.basename(result["ga4ghMd5.value"]) == "md5sum.txt"
671
-
672
-
673
- class WDLToilBenchTests(ToilTest):
982
+ with get_data("test/wdl/md5sum/md5sum.1.0.wdl") as wdl:
983
+ with get_data("test/wdl/md5sum/md5sum-gs.json") as json_file:
984
+ result_json = subprocess.check_output(
985
+ self.base_command
986
+ + [str(wdl), str(json_file), "-o", str(tmp_path), "--logDebug"]
987
+ )
988
+ result = json.loads(result_json)
989
+
990
+ assert "ga4ghMd5.value" in result
991
+ assert isinstance(result["ga4ghMd5.value"], str)
992
+ assert os.path.exists(result["ga4ghMd5.value"])
993
+ assert os.path.basename(result["ga4ghMd5.value"]) == "md5sum.txt"
994
+
995
+ def test_check(self, tmp_path: Path) -> None:
996
+ """Test that Toil's lint check works"""
997
+ with get_data("test/wdl/lint_error.wdl") as wdl:
998
+ out = subprocess.check_output(
999
+ self.base_command + [str(wdl), "-o", str(tmp_path), "--logInfo"], stderr=subprocess.STDOUT)
1000
+
1001
+ assert b'UnnecessaryQuantifier' in out
1002
+
1003
+ p = subprocess.Popen(
1004
+ self.base_command + [wdl, "--strict=True", "--logCritical"], stderr=subprocess.PIPE)
1005
+ # Not actually a test assert; we need this to teach MyPy that we
1006
+ # get an stderr when we pass stderr=subprocess.PIPE.
1007
+ assert p.stderr is not None
1008
+ stderr = p.stderr.read()
1009
+ p.wait()
1010
+ assert p.returncode == 2
1011
+ assert b'Workflow did not pass linting in strict mode' in stderr
1012
+
1013
+ class TestWDLToilBench(unittest.TestCase):
674
1014
  """Tests for Toil's MiniWDL-based implementation that don't run workflows."""
675
1015
 
676
- def test_coalesce(self):
1016
+ def test_coalesce(self) -> None:
677
1017
  """
678
1018
  Test if WDLSectionJob can coalesce WDL decls.
679
1019
 
@@ -779,7 +1119,7 @@ class WDLToilBenchTests(ToilTest):
779
1119
  assert "decl2" in result[0]
780
1120
  assert "successor" in result[1]
781
1121
 
782
- def make_string_expr(self, to_parse: str) -> WDL.Expr.String:
1122
+ def make_string_expr(self, to_parse: str, expr_type: type[WDL.Expr.String] = WDL.Expr.String) -> WDL.Expr.String:
783
1123
  """
784
1124
  Parse pseudo-WDL for testing whitespace removal.
785
1125
  """
@@ -790,124 +1130,9 @@ class WDLToilBenchTests(ToilTest):
790
1130
  for i in range(1, len(parts), 2):
791
1131
  parts[i] = WDL.Expr.Placeholder(pos, {}, WDL.Expr.Null(pos))
792
1132
 
793
- return WDL.Expr.String(pos, parts)
794
-
795
- def test_remove_common_leading_whitespace(self):
796
- """
797
- Make sure leading whitespace removal works properly.
798
- """
799
-
800
- # For a single line, we remove its leading whitespace
801
- expr = self.make_string_expr(" a ~{b} c")
802
- trimmed = remove_common_leading_whitespace(expr)
803
- assert len(trimmed.parts) == 3
804
- assert trimmed.parts[0] == "a "
805
- assert trimmed.parts[2] == " c"
806
-
807
- # Whitespace removed isn't affected by totally blank lines
808
- expr = self.make_string_expr(" \n\n a\n ~{stuff}\n b\n\n")
809
- trimmed = remove_common_leading_whitespace(expr)
810
- assert len(trimmed.parts) == 3
811
- assert trimmed.parts[0] == "\n\na\n"
812
- assert trimmed.parts[2] == "\nb\n\n"
813
-
814
- # Unless blank toleration is off
815
- expr = self.make_string_expr(" \n\n a\n ~{stuff}\n b\n\n")
816
- trimmed = remove_common_leading_whitespace(expr, tolerate_blanks=False)
817
- assert len(trimmed.parts) == 3
818
- assert trimmed.parts[0] == " \n\n a\n "
819
- assert trimmed.parts[2] == "\n b\n\n"
820
-
821
- # Whitespace is still removed if the first line doesn't have it before the newline
822
- expr = self.make_string_expr("\n a\n ~{stuff}\n b\n")
823
- trimmed = remove_common_leading_whitespace(expr)
824
- assert len(trimmed.parts) == 3
825
- assert trimmed.parts[0] == "\na\n"
826
- assert trimmed.parts[2] == "\nb\n"
827
-
828
- # Whitespace is not removed if actual content is dedented
829
- expr = self.make_string_expr(" \n\n a\n ~{stuff}\nuhoh\n b\n\n")
830
- trimmed = remove_common_leading_whitespace(expr)
831
- assert len(trimmed.parts) == 3
832
- assert trimmed.parts[0] == " \n\n a\n "
833
- assert trimmed.parts[2] == "\nuhoh\n b\n\n"
834
-
835
- # Unless dedents are tolerated
836
- expr = self.make_string_expr(" \n\n a\n ~{stuff}\nuhoh\n b\n\n")
837
- trimmed = remove_common_leading_whitespace(expr, tolerate_dedents=True)
838
- assert len(trimmed.parts) == 3
839
- assert trimmed.parts[0] == "\n\na\n"
840
- assert trimmed.parts[2] == "\nuhoh\nb\n\n"
841
-
842
- # Whitespace is still removed if all-whitespace lines have less of it
843
- expr = self.make_string_expr("\n a\n ~{stuff}\n \n b\n")
844
- trimmed = remove_common_leading_whitespace(expr)
845
- assert len(trimmed.parts) == 3
846
- assert trimmed.parts[0] == "\na\n"
847
- assert trimmed.parts[2] == "\n\nb\n"
848
-
849
- # Unless all-whitespace lines are not tolerated
850
- expr = self.make_string_expr("\n a\n ~{stuff}\n \n b\n")
851
- trimmed = remove_common_leading_whitespace(expr, tolerate_all_whitespace=False)
852
- assert len(trimmed.parts) == 3
853
- assert trimmed.parts[0] == "\n a\n "
854
- assert trimmed.parts[2] == "\n\n b\n"
855
-
856
- # When mixed tabs and spaces are detected, nothing is changed.
857
- expr = self.make_string_expr("\n a\n\t~{stuff}\n b\n")
858
- trimmed = remove_common_leading_whitespace(expr)
859
- assert len(trimmed.parts) == 3
860
- assert trimmed.parts[0] == "\n a\n\t"
861
- assert trimmed.parts[2] == "\n b\n"
862
-
863
- # When mixed tabs and spaces are not in the prefix, whitespace is removed.
864
- expr = self.make_string_expr("\n\ta\n\t~{stuff} \n\tb\n")
865
- trimmed = remove_common_leading_whitespace(expr)
866
- assert len(trimmed.parts) == 3
867
- assert trimmed.parts[0] == "\na\n"
868
- assert trimmed.parts[2] == " \nb\n"
869
-
870
- # An empty string works
871
- expr = self.make_string_expr("")
872
- trimmed = remove_common_leading_whitespace(expr)
873
- assert len(trimmed.parts) == 1
874
- assert trimmed.parts[0] == ""
875
-
876
- # A string of only whitespace is preserved as an all-whitespece line
877
- expr = self.make_string_expr("\t\t\t")
878
- trimmed = remove_common_leading_whitespace(expr)
879
- assert len(trimmed.parts) == 1
880
- assert trimmed.parts[0] == "\t\t\t"
881
-
882
- # A string of only whitespace is trimmed when all-whitespace lines are not tolerated
883
- expr = self.make_string_expr("\t\t\t")
884
- trimmed = remove_common_leading_whitespace(expr, tolerate_all_whitespace=False)
885
- assert len(trimmed.parts) == 1
886
- assert trimmed.parts[0] == ""
887
-
888
- # An empty expression works
889
- expr = WDL.Expr.String(
890
- WDL.Error.SourcePosition("nowhere", "nowhere", 0, 0, 0, 0), []
891
- )
892
- trimmed = remove_common_leading_whitespace(expr)
893
- assert len(trimmed.parts) == 0
894
-
895
- # An expression of only placeholders works
896
- expr = self.make_string_expr("~{AAA}")
897
- trimmed = remove_common_leading_whitespace(expr)
898
- assert len(trimmed.parts) == 3
899
- assert trimmed.parts[0] == ""
900
- assert trimmed.parts[2] == ""
901
-
902
- # The command flag is preserved
903
- expr = self.make_string_expr(" a ~{b} c")
904
- trimmed = remove_common_leading_whitespace(expr)
905
- assert trimmed.command == False
906
- expr.command = True
907
- trimmed = remove_common_leading_whitespace(expr)
908
- assert trimmed.command == True
909
-
910
- def test_choose_human_readable_directory(self):
1133
+ return expr_type(pos, parts)
1134
+
1135
+ def test_choose_human_readable_directory(self) -> None:
911
1136
  """
912
1137
  Test to make sure that we pick sensible but non-colliding directories to put files in.
913
1138
  """
@@ -919,7 +1144,7 @@ class WDLToilBenchTests(ToilTest):
919
1144
 
920
1145
  state: DirectoryNamingStateDict = {}
921
1146
 
922
- # The first time we should get apath with the task name and without the ID
1147
+ # The first time we should get a path with the task name and without the ID
923
1148
  first_chosen = choose_human_readable_directory(
924
1149
  "root", "taskname", "111-222-333", state
925
1150
  )
@@ -931,18 +1156,18 @@ class WDLToilBenchTests(ToilTest):
931
1156
  same_id = choose_human_readable_directory(
932
1157
  "root", "taskname", "111-222-333", state
933
1158
  )
934
- self.assertEqual(same_id, first_chosen)
1159
+ assert same_id == first_chosen
935
1160
 
936
1161
  # If we use a different ID we should get a different result still obeying the constraints
937
1162
  diff_id = choose_human_readable_directory(
938
1163
  "root", "taskname", "222-333-444", state
939
1164
  )
940
- self.assertNotEqual(diff_id, first_chosen)
1165
+ assert diff_id != first_chosen
941
1166
  assert diff_id.startswith("root")
942
1167
  assert "taskname" in diff_id
943
1168
  assert "222-333-444" not in diff_id
944
1169
 
945
- def test_uri_packing(self):
1170
+ def test_uri_packing(self) -> None:
946
1171
  """
947
1172
  Test to make sure Toil URI packing brings through the required information.
948
1173
  """
@@ -960,53 +1185,50 @@ class WDLToilBenchTests(ToilTest):
960
1185
  unpacked = unpack_toil_uri(uri)
961
1186
 
962
1187
  # Make sure we got what we put in
963
- self.assertEqual(unpacked[0], file_id)
964
- self.assertEqual(unpacked[0].size, file_id.size)
965
- self.assertEqual(unpacked[0].executable, file_id.executable)
1188
+ assert unpacked[0] == file_id
1189
+ assert unpacked[0].size == file_id.size
1190
+ assert unpacked[0].executable == file_id.executable
966
1191
 
967
- self.assertEqual(unpacked[1], task_path)
1192
+ assert unpacked[1] == task_path
968
1193
 
969
1194
  # TODO: We don't make the UUIDs back into UUID objects
970
- self.assertEqual(unpacked[2], str(dir_id))
1195
+ assert unpacked[2] == str(dir_id)
971
1196
 
972
- self.assertEqual(unpacked[3], file_basename)
1197
+ assert unpacked[3] == file_basename
973
1198
 
974
- def test_disk_parse(self):
1199
+ def test_disk_parse(self) -> None:
975
1200
  """
976
1201
  Test to make sure the disk parsing is correct
977
1202
  """
978
1203
  # Test cromwell compatibility
979
1204
  spec = "local-disk 5 SSD"
980
1205
  specified_mount_point, part_size, part_suffix = parse_disks(spec, spec)
981
- self.assertEqual(specified_mount_point, None)
982
- self.assertEqual(part_size, 5)
983
- self.assertEqual(part_suffix, "GB")
1206
+ assert specified_mount_point is None
1207
+ assert part_size == 5
1208
+ assert part_suffix == "GB"
984
1209
 
985
1210
  # Test spec conformance
986
1211
  # https://github.com/openwdl/wdl/blob/e43e042104b728df1f1ad6e6145945d2b32331a6/SPEC.md?plain=1#L5072-L5082
987
1212
  spec = "10"
988
1213
  specified_mount_point, part_size, part_suffix = parse_disks(spec, spec)
989
- self.assertEqual(specified_mount_point, None)
990
- self.assertEqual(part_size, 10)
991
- self.assertEqual(part_suffix, "GiB") # WDL spec default
1214
+ assert specified_mount_point is None
1215
+ assert part_size == 10
1216
+ assert part_suffix == "GiB" # WDL spec default
992
1217
 
993
1218
  spec = "1 MB"
994
1219
  specified_mount_point, part_size, part_suffix = parse_disks(spec, spec)
995
- self.assertEqual(specified_mount_point, None)
996
- self.assertEqual(part_size, 1)
997
- self.assertEqual(part_suffix, "MB")
1220
+ assert specified_mount_point is None
1221
+ assert part_size == 1
1222
+ assert part_suffix == "MB"
998
1223
 
999
1224
  spec = "MOUNT_POINT 3"
1000
1225
  specified_mount_point, part_size, part_suffix = parse_disks(spec, spec)
1001
- self.assertEqual(specified_mount_point, "MOUNT_POINT")
1002
- self.assertEqual(part_size, 3)
1003
- self.assertEqual(part_suffix, "GiB")
1226
+ assert specified_mount_point == "MOUNT_POINT"
1227
+ assert part_size == 3
1228
+ assert part_suffix == "GiB"
1004
1229
 
1005
1230
  spec = "MOUNT_POINT 2 MB"
1006
1231
  specified_mount_point, part_size, part_suffix = parse_disks(spec, spec)
1007
- self.assertEqual(specified_mount_point, "MOUNT_POINT")
1008
- self.assertEqual(part_size, 2)
1009
- self.assertEqual(part_suffix, "MB")
1010
-
1011
- if __name__ == "__main__":
1012
- unittest.main() # run all tests
1232
+ assert specified_mount_point == "MOUNT_POINT"
1233
+ assert part_size == 2
1234
+ assert part_suffix == "MB"