toil 8.0.0__py3-none-any.whl → 8.2.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 (270) hide show
  1. toil/__init__.py +4 -39
  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/options.py +1 -0
  11. toil/batchSystems/singleMachine.py +1 -1
  12. toil/batchSystems/slurm.py +229 -84
  13. toil/bus.py +5 -3
  14. toil/common.py +198 -54
  15. toil/cwl/cwltoil.py +32 -11
  16. toil/job.py +110 -86
  17. toil/jobStores/abstractJobStore.py +24 -3
  18. toil/jobStores/aws/jobStore.py +46 -10
  19. toil/jobStores/fileJobStore.py +25 -1
  20. toil/jobStores/googleJobStore.py +104 -30
  21. toil/leader.py +9 -0
  22. toil/lib/accelerators.py +3 -1
  23. toil/lib/aws/session.py +14 -3
  24. toil/lib/aws/utils.py +92 -35
  25. toil/lib/aws/utils.py.orig +504 -0
  26. toil/lib/bioio.py +1 -1
  27. toil/lib/docker.py +252 -91
  28. toil/lib/dockstore.py +387 -0
  29. toil/lib/ec2nodes.py +3 -2
  30. toil/lib/exceptions.py +5 -3
  31. toil/lib/history.py +1345 -0
  32. toil/lib/history_submission.py +695 -0
  33. toil/lib/io.py +56 -23
  34. toil/lib/misc.py +25 -1
  35. toil/lib/resources.py +2 -1
  36. toil/lib/retry.py +10 -10
  37. toil/lib/threading.py +11 -10
  38. toil/lib/{integration.py → trs.py} +95 -46
  39. toil/lib/web.py +38 -0
  40. toil/options/common.py +25 -2
  41. toil/options/cwl.py +10 -0
  42. toil/options/wdl.py +11 -0
  43. toil/provisioners/gceProvisioner.py +4 -4
  44. toil/server/api_spec/LICENSE +201 -0
  45. toil/server/api_spec/README.rst +5 -0
  46. toil/server/cli/wes_cwl_runner.py +5 -4
  47. toil/server/utils.py +2 -3
  48. toil/statsAndLogging.py +35 -1
  49. toil/test/__init__.py +275 -115
  50. toil/test/batchSystems/batchSystemTest.py +227 -205
  51. toil/test/batchSystems/test_slurm.py +199 -2
  52. toil/test/cactus/pestis.tar.gz +0 -0
  53. toil/test/conftest.py +7 -0
  54. toil/test/cwl/2.fasta +11 -0
  55. toil/test/cwl/2.fastq +12 -0
  56. toil/test/cwl/conftest.py +39 -0
  57. toil/test/cwl/cwlTest.py +1015 -780
  58. toil/test/cwl/directory/directory/file.txt +15 -0
  59. toil/test/cwl/download_directory_file.json +4 -0
  60. toil/test/cwl/download_directory_s3.json +4 -0
  61. toil/test/cwl/download_file.json +6 -0
  62. toil/test/cwl/download_http.json +6 -0
  63. toil/test/cwl/download_https.json +6 -0
  64. toil/test/cwl/download_s3.json +6 -0
  65. toil/test/cwl/download_subdirectory_file.json +5 -0
  66. toil/test/cwl/download_subdirectory_s3.json +5 -0
  67. toil/test/cwl/empty.json +1 -0
  68. toil/test/cwl/mock_mpi/fake_mpi.yml +8 -0
  69. toil/test/cwl/mock_mpi/fake_mpi_run.py +42 -0
  70. toil/test/cwl/optional-file-exists.json +6 -0
  71. toil/test/cwl/optional-file-missing.json +6 -0
  72. toil/test/cwl/optional-file.cwl +18 -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/lib/aws/test_iam.py +3 -1
  123. toil/test/lib/dockerTest.py +205 -122
  124. toil/test/lib/test_history.py +236 -0
  125. toil/test/lib/test_trs.py +161 -0
  126. toil/test/provisioners/aws/awsProvisionerTest.py +12 -9
  127. toil/test/provisioners/clusterTest.py +4 -4
  128. toil/test/provisioners/gceProvisionerTest.py +16 -14
  129. toil/test/sort/sort.py +4 -1
  130. toil/test/src/busTest.py +17 -17
  131. toil/test/src/deferredFunctionTest.py +145 -132
  132. toil/test/src/importExportFileTest.py +71 -63
  133. toil/test/src/jobEncapsulationTest.py +27 -28
  134. toil/test/src/jobServiceTest.py +149 -133
  135. toil/test/src/jobTest.py +219 -211
  136. toil/test/src/miscTests.py +66 -60
  137. toil/test/src/promisedRequirementTest.py +163 -169
  138. toil/test/src/regularLogTest.py +24 -24
  139. toil/test/src/resourceTest.py +82 -76
  140. toil/test/src/restartDAGTest.py +51 -47
  141. toil/test/src/resumabilityTest.py +24 -19
  142. toil/test/src/retainTempDirTest.py +60 -57
  143. toil/test/src/systemTest.py +17 -13
  144. toil/test/src/threadingTest.py +29 -32
  145. toil/test/utils/ABCWorkflowDebug/B_file.txt +1 -0
  146. toil/test/utils/ABCWorkflowDebug/debugWorkflow.py +204 -0
  147. toil/test/utils/ABCWorkflowDebug/mkFile.py +16 -0
  148. toil/test/utils/ABCWorkflowDebug/sleep.cwl +12 -0
  149. toil/test/utils/ABCWorkflowDebug/sleep.yaml +1 -0
  150. toil/test/utils/toilDebugTest.py +117 -102
  151. toil/test/utils/toilKillTest.py +54 -53
  152. toil/test/utils/utilsTest.py +303 -229
  153. toil/test/wdl/lint_error.wdl +9 -0
  154. toil/test/wdl/md5sum/empty_file.json +1 -0
  155. toil/test/wdl/md5sum/md5sum-gs.json +1 -0
  156. toil/test/wdl/md5sum/md5sum.1.0.wdl +32 -0
  157. toil/test/wdl/md5sum/md5sum.input +1 -0
  158. toil/test/wdl/md5sum/md5sum.json +1 -0
  159. toil/test/wdl/md5sum/md5sum.wdl +25 -0
  160. toil/test/wdl/miniwdl_self_test/inputs-namespaced.json +1 -0
  161. toil/test/wdl/miniwdl_self_test/inputs.json +1 -0
  162. toil/test/wdl/miniwdl_self_test/self_test.wdl +40 -0
  163. toil/test/wdl/standard_library/as_map.json +16 -0
  164. toil/test/wdl/standard_library/as_map_as_input.wdl +23 -0
  165. toil/test/wdl/standard_library/as_pairs.json +7 -0
  166. toil/test/wdl/standard_library/as_pairs_as_input.wdl +23 -0
  167. toil/test/wdl/standard_library/ceil.json +3 -0
  168. toil/test/wdl/standard_library/ceil_as_command.wdl +16 -0
  169. toil/test/wdl/standard_library/ceil_as_input.wdl +16 -0
  170. toil/test/wdl/standard_library/collect_by_key.json +1 -0
  171. toil/test/wdl/standard_library/collect_by_key_as_input.wdl +23 -0
  172. toil/test/wdl/standard_library/cross.json +11 -0
  173. toil/test/wdl/standard_library/cross_as_input.wdl +19 -0
  174. toil/test/wdl/standard_library/flatten.json +7 -0
  175. toil/test/wdl/standard_library/flatten_as_input.wdl +18 -0
  176. toil/test/wdl/standard_library/floor.json +3 -0
  177. toil/test/wdl/standard_library/floor_as_command.wdl +16 -0
  178. toil/test/wdl/standard_library/floor_as_input.wdl +16 -0
  179. toil/test/wdl/standard_library/keys.json +8 -0
  180. toil/test/wdl/standard_library/keys_as_input.wdl +24 -0
  181. toil/test/wdl/standard_library/length.json +7 -0
  182. toil/test/wdl/standard_library/length_as_input.wdl +16 -0
  183. toil/test/wdl/standard_library/length_as_input_with_map.json +7 -0
  184. toil/test/wdl/standard_library/length_as_input_with_map.wdl +17 -0
  185. toil/test/wdl/standard_library/length_invalid.json +3 -0
  186. toil/test/wdl/standard_library/range.json +3 -0
  187. toil/test/wdl/standard_library/range_0.json +3 -0
  188. toil/test/wdl/standard_library/range_as_input.wdl +17 -0
  189. toil/test/wdl/standard_library/range_invalid.json +3 -0
  190. toil/test/wdl/standard_library/read_boolean.json +3 -0
  191. toil/test/wdl/standard_library/read_boolean_as_command.wdl +17 -0
  192. toil/test/wdl/standard_library/read_float.json +3 -0
  193. toil/test/wdl/standard_library/read_float_as_command.wdl +17 -0
  194. toil/test/wdl/standard_library/read_int.json +3 -0
  195. toil/test/wdl/standard_library/read_int_as_command.wdl +17 -0
  196. toil/test/wdl/standard_library/read_json.json +3 -0
  197. toil/test/wdl/standard_library/read_json_as_output.wdl +31 -0
  198. toil/test/wdl/standard_library/read_lines.json +3 -0
  199. toil/test/wdl/standard_library/read_lines_as_output.wdl +31 -0
  200. toil/test/wdl/standard_library/read_map.json +3 -0
  201. toil/test/wdl/standard_library/read_map_as_output.wdl +31 -0
  202. toil/test/wdl/standard_library/read_string.json +3 -0
  203. toil/test/wdl/standard_library/read_string_as_command.wdl +17 -0
  204. toil/test/wdl/standard_library/read_tsv.json +3 -0
  205. toil/test/wdl/standard_library/read_tsv_as_output.wdl +31 -0
  206. toil/test/wdl/standard_library/round.json +3 -0
  207. toil/test/wdl/standard_library/round_as_command.wdl +16 -0
  208. toil/test/wdl/standard_library/round_as_input.wdl +16 -0
  209. toil/test/wdl/standard_library/size.json +3 -0
  210. toil/test/wdl/standard_library/size_as_command.wdl +17 -0
  211. toil/test/wdl/standard_library/size_as_output.wdl +36 -0
  212. toil/test/wdl/standard_library/stderr.json +3 -0
  213. toil/test/wdl/standard_library/stderr_as_output.wdl +30 -0
  214. toil/test/wdl/standard_library/stdout.json +3 -0
  215. toil/test/wdl/standard_library/stdout_as_output.wdl +30 -0
  216. toil/test/wdl/standard_library/sub.json +3 -0
  217. toil/test/wdl/standard_library/sub_as_input.wdl +17 -0
  218. toil/test/wdl/standard_library/sub_as_input_with_file.wdl +17 -0
  219. toil/test/wdl/standard_library/transpose.json +6 -0
  220. toil/test/wdl/standard_library/transpose_as_input.wdl +18 -0
  221. toil/test/wdl/standard_library/write_json.json +6 -0
  222. toil/test/wdl/standard_library/write_json_as_command.wdl +17 -0
  223. toil/test/wdl/standard_library/write_lines.json +7 -0
  224. toil/test/wdl/standard_library/write_lines_as_command.wdl +17 -0
  225. toil/test/wdl/standard_library/write_map.json +6 -0
  226. toil/test/wdl/standard_library/write_map_as_command.wdl +17 -0
  227. toil/test/wdl/standard_library/write_tsv.json +6 -0
  228. toil/test/wdl/standard_library/write_tsv_as_command.wdl +17 -0
  229. toil/test/wdl/standard_library/zip.json +12 -0
  230. toil/test/wdl/standard_library/zip_as_input.wdl +19 -0
  231. toil/test/wdl/test.csv +3 -0
  232. toil/test/wdl/test.tsv +3 -0
  233. toil/test/wdl/testfiles/croo.wdl +38 -0
  234. toil/test/wdl/testfiles/drop_files.wdl +62 -0
  235. toil/test/wdl/testfiles/drop_files_subworkflow.wdl +13 -0
  236. toil/test/wdl/testfiles/empty.txt +0 -0
  237. toil/test/wdl/testfiles/not_enough_outputs.wdl +33 -0
  238. toil/test/wdl/testfiles/random.wdl +66 -0
  239. toil/test/wdl/testfiles/string_file_coercion.json +1 -0
  240. toil/test/wdl/testfiles/string_file_coercion.wdl +35 -0
  241. toil/test/wdl/testfiles/test.json +4 -0
  242. toil/test/wdl/testfiles/test_boolean.txt +1 -0
  243. toil/test/wdl/testfiles/test_float.txt +1 -0
  244. toil/test/wdl/testfiles/test_int.txt +1 -0
  245. toil/test/wdl/testfiles/test_lines.txt +5 -0
  246. toil/test/wdl/testfiles/test_map.txt +2 -0
  247. toil/test/wdl/testfiles/test_string.txt +1 -0
  248. toil/test/wdl/testfiles/url_to_file.wdl +13 -0
  249. toil/test/wdl/testfiles/url_to_optional_file.wdl +13 -0
  250. toil/test/wdl/testfiles/vocab.json +1 -0
  251. toil/test/wdl/testfiles/vocab.wdl +66 -0
  252. toil/test/wdl/testfiles/wait.wdl +34 -0
  253. toil/test/wdl/wdl_specification/type_pair.json +23 -0
  254. toil/test/wdl/wdl_specification/type_pair_basic.wdl +36 -0
  255. toil/test/wdl/wdl_specification/type_pair_with_files.wdl +36 -0
  256. toil/test/wdl/wdl_specification/v1_spec.json +1 -0
  257. toil/test/wdl/wdl_specification/v1_spec_declaration.wdl +39 -0
  258. toil/test/wdl/wdltoil_test.py +681 -408
  259. toil/test/wdl/wdltoil_test_kubernetes.py +2 -2
  260. toil/version.py +10 -10
  261. toil/wdl/wdltoil.py +350 -123
  262. toil/worker.py +113 -33
  263. {toil-8.0.0.dist-info → toil-8.2.0.dist-info}/METADATA +13 -7
  264. toil-8.2.0.dist-info/RECORD +439 -0
  265. {toil-8.0.0.dist-info → toil-8.2.0.dist-info}/WHEEL +1 -1
  266. toil/test/lib/test_integration.py +0 -104
  267. toil-8.0.0.dist-info/RECORD +0 -253
  268. {toil-8.0.0.dist-info → toil-8.2.0.dist-info}/entry_points.txt +0 -0
  269. {toil-8.0.0.dist-info → toil-8.2.0.dist-info/licenses}/LICENSE +0 -0
  270. {toil-8.0.0.dist-info → toil-8.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,236 @@
1
+ # Copyright (C) 2015-2025 Regents of the University of California
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ from collections.abc import Generator
16
+ from pathlib import Path
17
+ import logging
18
+ import time
19
+
20
+ from toil.lib.history import HistoryManager
21
+
22
+ import pytest
23
+
24
+ logger = logging.getLogger(__name__)
25
+ logging.basicConfig(level=logging.DEBUG)
26
+
27
+
28
+ class TestHistory:
29
+ """
30
+ Tests for Toil history tracking.
31
+
32
+ Each test gets its own history database.
33
+ """
34
+
35
+ @pytest.fixture(autouse=True, scope="function")
36
+ def private_history_manager(
37
+ self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
38
+ ) -> Generator[None]:
39
+ try:
40
+ with monkeypatch.context() as m:
41
+ m.setattr(
42
+ HistoryManager,
43
+ "database_path_override",
44
+ str(tmp_path / "test-db.sqlite"),
45
+ )
46
+ m.setattr(HistoryManager, "enabled", lambda: True)
47
+ m.setattr(HistoryManager, "enabled_job", lambda: True)
48
+ yield
49
+ finally:
50
+ pass # no cleanup needed
51
+
52
+ def make_fake_workflow(self, workflow_id: str) -> None:
53
+ # Make a fake workflow
54
+ workflow_jobstore_spec = "file:/tmp/tree"
55
+ HistoryManager.record_workflow_creation(workflow_id, workflow_jobstore_spec)
56
+ workflow_name = "SuperCoolWF"
57
+ workflow_trs_spec = "#wf:v1"
58
+ HistoryManager.record_workflow_metadata(
59
+ workflow_id, workflow_name, workflow_trs_spec
60
+ )
61
+
62
+ # Give it a job
63
+ workflow_attempt_number = 1
64
+ job_name = "DoThing"
65
+ succeeded = True
66
+ start_time = time.time()
67
+ runtime = 0.1
68
+ HistoryManager.record_job_attempt(
69
+ workflow_id,
70
+ workflow_attempt_number,
71
+ job_name,
72
+ succeeded,
73
+ start_time,
74
+ runtime,
75
+ )
76
+
77
+ # Give it a workflow attempt with the same details.
78
+ HistoryManager.record_workflow_attempt(
79
+ workflow_id,
80
+ workflow_attempt_number,
81
+ succeeded,
82
+ start_time,
83
+ runtime,
84
+ )
85
+
86
+ def test_history_submittable_detection(self) -> None:
87
+ """
88
+ Make sure that a submittable workflow shows up as such before
89
+ submission and doesn't afterward.
90
+ """
91
+ workflow_id = "123"
92
+ self.make_fake_workflow(workflow_id)
93
+ workflow_attempt_number = 1
94
+
95
+ # Make sure we have data
96
+ assert HistoryManager.count_workflows() == 1
97
+ assert HistoryManager.count_workflow_attempts() == 1
98
+ assert HistoryManager.count_job_attempts() == 1
99
+
100
+ # Make sure we see it as submittable
101
+ submittable_workflow_attempts = (
102
+ HistoryManager.get_submittable_workflow_attempts()
103
+ )
104
+ assert len(submittable_workflow_attempts) == 1
105
+
106
+ # Make sure we see its jobs as submittable
107
+ with_submittable_job_attempts = (
108
+ HistoryManager.get_workflow_attempts_with_submittable_job_attempts()
109
+ )
110
+ assert len(with_submittable_job_attempts) == 1
111
+
112
+ # Make sure we actually see the job
113
+ submittable_job_attempts = HistoryManager.get_unsubmitted_job_attempts(
114
+ workflow_id, workflow_attempt_number
115
+ )
116
+ assert len(submittable_job_attempts) == 1
117
+
118
+ # Pretend we submitted them.
119
+ HistoryManager.mark_job_attempts_submitted(
120
+ [j.id for j in submittable_job_attempts]
121
+ )
122
+ HistoryManager.mark_workflow_attempt_submitted(
123
+ workflow_id, workflow_attempt_number
124
+ )
125
+
126
+ # Make sure they are no longer matching
127
+ assert len(HistoryManager.get_submittable_workflow_attempts()) == 0
128
+ assert (
129
+ len(HistoryManager.get_workflow_attempts_with_submittable_job_attempts())
130
+ == 0
131
+ )
132
+ assert (
133
+ len(
134
+ HistoryManager.get_unsubmitted_job_attempts(
135
+ workflow_id, workflow_attempt_number
136
+ )
137
+ )
138
+ == 0
139
+ )
140
+
141
+ # Make sure we still have data
142
+ assert HistoryManager.count_workflows() == 1
143
+ assert HistoryManager.count_workflow_attempts() == 1
144
+ assert HistoryManager.count_job_attempts() == 1
145
+
146
+ def test_history_deletion(self) -> None:
147
+ workflow_id = "123"
148
+ self.make_fake_workflow(workflow_id)
149
+ workflow_attempt_number = 1
150
+
151
+ # Make sure we can see the workflow for deletion by age but not by done-ness
152
+ assert len(HistoryManager.get_oldest_workflow_ids()) == 1
153
+ assert len(HistoryManager.get_fully_submitted_workflow_ids()) == 0
154
+
155
+ # Pretend we submitted the workflow.
156
+ HistoryManager.mark_job_attempts_submitted(
157
+ [
158
+ j.id
159
+ for j in HistoryManager.get_unsubmitted_job_attempts(
160
+ workflow_id, workflow_attempt_number
161
+ )
162
+ ]
163
+ )
164
+ HistoryManager.mark_workflow_attempt_submitted(
165
+ workflow_id, workflow_attempt_number
166
+ )
167
+
168
+ # Make sure we can see the workflow for deletion by done-ness
169
+ assert len(HistoryManager.get_fully_submitted_workflow_ids()) == 1
170
+
171
+ # Add a new workflow
172
+ other_workflow_id = "456"
173
+ self.make_fake_workflow(other_workflow_id)
174
+
175
+ # Make sure we can see the both for deletion by age but only one by done-ness
176
+ assert len(HistoryManager.get_oldest_workflow_ids()) == 2
177
+ assert len(HistoryManager.get_fully_submitted_workflow_ids()) == 1
178
+
179
+ # Make sure the older workflow is first.
180
+ assert HistoryManager.get_oldest_workflow_ids() == [
181
+ workflow_id,
182
+ other_workflow_id,
183
+ ]
184
+
185
+ # Delete the new workflow
186
+ HistoryManager.delete_workflow(other_workflow_id)
187
+
188
+ # Make sure we can see the old one
189
+ assert HistoryManager.get_oldest_workflow_ids() == [workflow_id]
190
+ assert HistoryManager.get_fully_submitted_workflow_ids() == [workflow_id]
191
+
192
+ # Delete the old workflow
193
+ HistoryManager.delete_workflow(workflow_id)
194
+
195
+ # Make sure we have no data
196
+ assert HistoryManager.count_workflows() == 0
197
+ assert HistoryManager.count_workflow_attempts() == 0
198
+ assert HistoryManager.count_job_attempts() == 0
199
+
200
+ def test_history_size_limit(self) -> None:
201
+ """
202
+ Make sure the database size can be controlled.
203
+ """
204
+
205
+ for workflow_id in (
206
+ "WorkflowThatTakesUpSomeSpace,ActuallyMoreThanTheLaterOnesTake" + str(i)
207
+ for i in range(10)
208
+ ):
209
+ self.make_fake_workflow(workflow_id)
210
+
211
+ # We should see the workflows.
212
+ assert HistoryManager.count_workflows() == 10
213
+ # And they take up space.
214
+ small_size = HistoryManager.get_database_byte_size()
215
+ assert small_size > 0
216
+
217
+ # Add a bunch more
218
+ for workflow_id in ("WorkflowThatTakesUpSpace" + str(i) for i in range(50)):
219
+ self.make_fake_workflow(workflow_id)
220
+
221
+ # We should see that this is now a much larger database
222
+ large_size = HistoryManager.get_database_byte_size()
223
+ logger.info("Increased database size from %s to %s", small_size, large_size)
224
+ large_size > small_size
225
+
226
+ # We should be able to shrink it back down
227
+ HistoryManager.enforce_byte_size_limit(small_size)
228
+
229
+ reduced_size = HistoryManager.get_database_byte_size()
230
+ logger.info("Decreased database size from %s to %s", large_size, reduced_size)
231
+ # The database should be small enough
232
+ reduced_size <= small_size
233
+ # There should still be some workflow attempts left in the smaller database (though probably not the first ones)
234
+ remaining_workflows = HistoryManager.count_workflows()
235
+ logger.info("Still have %s workflows", remaining_workflows)
236
+ assert remaining_workflows > 0
@@ -0,0 +1,161 @@
1
+ # Copyright (C) 2015-2024 Regents of the University of California
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ import io
16
+ import logging
17
+ import pytest
18
+ from typing import IO
19
+ import urllib.request
20
+ from urllib.error import URLError
21
+
22
+ from toil.lib.retry import retry
23
+ from toil.lib.trs import find_workflow, fetch_workflow
24
+ from toil.test import ToilTest, needs_online
25
+
26
+ logger = logging.getLogger(__name__)
27
+ logging.basicConfig(level=logging.DEBUG)
28
+
29
+ @pytest.mark.integrative
30
+ @needs_online
31
+ class DockstoreLookupTest(ToilTest):
32
+ """
33
+ Make sure we can look up workflows on Dockstore.
34
+ """
35
+
36
+ @retry(errors=[URLError, RuntimeError])
37
+ def read_result(self, url_or_path: str) -> IO[bytes]:
38
+ """
39
+ Read a file or URL.
40
+
41
+ Binary mode to allow testing for binary file support.
42
+
43
+ This lets us test that we have the right workflow contents and not care
44
+ how we are being shown them.
45
+ """
46
+ if url_or_path.startswith("http://") or url_or_path.startswith("https://"):
47
+ response = urllib.request.urlopen(url_or_path)
48
+ if response.status != 200:
49
+ raise RuntimeError(f"HTTP error response: {response}")
50
+ return response
51
+ else:
52
+ return open(url_or_path, "rb")
53
+
54
+ # TODO: Tests that definitely test a clear cache
55
+
56
+ def test_lookup_from_page_url(self) -> None:
57
+ PAGE_URL = "https://dockstore.org/workflows/github.com/dockstore/bcc2020-training/HelloWorld:master?tab=info"
58
+ trs_id, trs_version, language = find_workflow(PAGE_URL)
59
+
60
+ self.assertEqual(trs_id, "#workflow/github.com/dockstore/bcc2020-training/HelloWorld")
61
+ self.assertEqual(trs_version, "master")
62
+ self.assertEqual(language, "WDL")
63
+
64
+ def test_lookup_from_trs_with_version(self) -> None:
65
+ TRS_ID = "#workflow/github.com/dockstore-testing/md5sum-checker"
66
+ TRS_VERSION = "master"
67
+ trs_id, trs_version, language = find_workflow(f"{TRS_ID}:{TRS_VERSION}")
68
+
69
+ self.assertEqual(trs_id, TRS_ID)
70
+ self.assertEqual(trs_version, TRS_VERSION)
71
+ self.assertEqual(language, "CWL")
72
+
73
+ def test_lookup_from_trs_no_version(self) -> None:
74
+ TRS_ID = "#workflow/github.com/dockstore-testing/md5sum-checker"
75
+ with pytest.raises(ValueError):
76
+ # We don't yet have a way to read Dockstore's default version info,
77
+ # so it's not safe to apply any default version when multiple
78
+ # versions exist.
79
+ trs_id, trs_version, language = find_workflow(TRS_ID)
80
+
81
+ # TODO: Add a test with a workflow that we know has and will only ever
82
+ # have one version, to test version auto-detection in that case.
83
+
84
+ def test_get(self) -> None:
85
+ TRS_ID = "#workflow/github.com/dockstore-testing/md5sum-checker"
86
+ TRS_VERSION = "master"
87
+ LANGUAGE = "CWL"
88
+ # Despite "-checker" in the ID, this actually refers to the base md5sum
89
+ # workflow that just happens to have a checker *available*, not to the
90
+ # checker workflow itself.
91
+ WORKFLOW_URL = "https://raw.githubusercontent.com/dockstore-testing/md5sum-checker/master/md5sum/md5sum-workflow.cwl"
92
+ looked_up = fetch_workflow(TRS_ID, TRS_VERSION, LANGUAGE)
93
+
94
+ data_from_lookup = self.read_result(looked_up).read()
95
+ data_from_source = self.read_result(WORKFLOW_URL).read()
96
+ self.assertEqual(data_from_lookup, data_from_source)
97
+
98
+ def test_get_from_trs_cached(self) -> None:
99
+ TRS_ID = "#workflow/github.com/dockstore-testing/md5sum-checker"
100
+ TRS_VERSION = "master"
101
+ LANGUAGE = "CWL"
102
+ WORKFLOW_URL = "https://raw.githubusercontent.com/dockstore-testing/md5sum-checker/master/md5sum/md5sum-workflow.cwl"
103
+ # This lookup may or may not be cached
104
+ fetch_workflow(TRS_ID, TRS_VERSION, LANGUAGE)
105
+ # This lookup is definitely cached
106
+ looked_up = fetch_workflow(TRS_ID, TRS_VERSION, LANGUAGE)
107
+
108
+ data_from_lookup = self.read_result(looked_up).read()
109
+ data_from_source = self.read_result(WORKFLOW_URL).read()
110
+ self.assertEqual(data_from_lookup, data_from_source)
111
+
112
+ def test_lookup_from_trs_with_version(self) -> None:
113
+ TRS_VERSIONED_ID = "#workflow/github.com/dockstore-testing/md5sum-checker:workflowWithHTTPImport"
114
+ trs_id, trs_version, language = find_workflow(TRS_VERSIONED_ID)
115
+
116
+ parts = TRS_VERSIONED_ID.split(":")
117
+
118
+ self.assertEqual(trs_id, parts[0])
119
+ self.assertEqual(trs_version, parts[1])
120
+ self.assertEqual(language, "CWL")
121
+
122
+ def test_lookup_from_trs_nonexistent_workflow(self) -> None:
123
+ TRS_VERSIONED_ID = "#workflow/github.com/adamnovak/veryfakerepo:notARealVersion"
124
+ with self.assertRaises(FileNotFoundError):
125
+ looked_up = find_workflow(TRS_VERSIONED_ID)
126
+
127
+ def test_lookup_from_trs_nonexistent_workflow_bad_format(self) -> None:
128
+ TRS_VERSIONED_ID = "#workflow/AbsoluteGarbage:notARealVersion"
129
+ with self.assertRaises(FileNotFoundError):
130
+ looked_up = find_workflow(TRS_VERSIONED_ID)
131
+
132
+ def test_lookup_from_trs_nonexistent_version(self) -> None:
133
+ TRS_VERSIONED_ID = "#workflow/github.com/dockstore-testing/md5sum-checker:notARealVersion"
134
+ with self.assertRaises(FileNotFoundError):
135
+ looked_up = find_workflow(TRS_VERSIONED_ID)
136
+
137
+ def test_get_nonexistent_workflow(self) -> None:
138
+ TRS_ID = "#workflow/github.com/adamnovak/veryfakerepo"
139
+ TRS_VERSION = "notARealVersion"
140
+ LANGUAGE = "CWL"
141
+ with self.assertRaises(FileNotFoundError):
142
+ looked_up = fetch_workflow(TRS_ID, TRS_VERSION, LANGUAGE)
143
+
144
+ def test_get_nonexistent_version(self) -> None:
145
+ TRS_ID = "#workflow/github.com/dockstore-testing/md5sum-checker"
146
+ TRS_VERSION = "notARealVersion"
147
+ LANGUAGE = "CWL"
148
+ with self.assertRaises(FileNotFoundError):
149
+ looked_up = fetch_workflow(TRS_ID, TRS_VERSION, LANGUAGE)
150
+
151
+ def test_get_nonexistent_workflow_bad_format(self) -> None:
152
+ # Dockstore enforces an ID pattern and blames your request if you ask
153
+ # about something that doesn't follow it. So don't follow it.
154
+ TRS_ID = "#workflow/AbsoluteGarbage"
155
+ TRS_VERSION = "notARealVersion"
156
+ LANGUAGE = "CWL"
157
+ with self.assertRaises(FileNotFoundError):
158
+ looked_up = fetch_workflow(TRS_ID, TRS_VERSION, LANGUAGE)
159
+
160
+
161
+
@@ -28,6 +28,7 @@ from toil.provisioners import cluster_factory
28
28
  from toil.provisioners.aws.awsProvisioner import AWSProvisioner
29
29
  from toil.test import (
30
30
  ToilTest,
31
+ get_data,
31
32
  integrative,
32
33
  needs_aws_ec2,
33
34
  needs_fetchable_appliance,
@@ -135,7 +136,7 @@ class AbstractAWSAutoscaleTest(AbstractClusterTest):
135
136
  """
136
137
  return os.path.join(self.dataDir, filename)
137
138
 
138
- def rsyncUtil(self, src, dest):
139
+ def rsyncUtil(self, src: str, dest: str) -> None:
139
140
  subprocess.check_call(
140
141
  [
141
142
  "toil",
@@ -285,10 +286,11 @@ class AWSAutoscaleTest(AbstractAWSAutoscaleTest):
285
286
  with open(fileToSort, "w") as f:
286
287
  # Fixme: making this file larger causes the test to hang
287
288
  f.write("01234567890123456789012345678901")
288
- self.rsyncUtil(
289
- os.path.join(self._projectRootPath(), "src/toil/test/sort/sort.py"),
290
- ":" + self.script(),
291
- )
289
+ with get_data("test/sort/sort.py") as sort_py:
290
+ self.rsyncUtil(
291
+ sort_py,
292
+ ":" + self.script(),
293
+ )
292
294
  self.rsyncUtil(fileToSort, ":" + self.data("sortFile"))
293
295
  os.unlink(fileToSort)
294
296
 
@@ -501,10 +503,11 @@ class AWSAutoscaleTestMultipleNodeTypes(AbstractAWSAutoscaleTest):
501
503
  sseKeyFile = os.path.join(os.getcwd(), "keyFile")
502
504
  with open(sseKeyFile, "w") as f:
503
505
  f.write("01234567890123456789012345678901")
504
- self.rsyncUtil(
505
- os.path.join(self._projectRootPath(), "src/toil/test/sort/sort.py"),
506
- ":" + self.script(),
507
- )
506
+ with get_data("test/sort/sort.py") as sort_py:
507
+ self.rsyncUtil(
508
+ sort_py,
509
+ ":" + self.script(),
510
+ )
508
511
  self.rsyncUtil(sseKeyFile, ":" + self.data("keyFile"))
509
512
  os.unlink(sseKeyFile)
510
513
 
@@ -39,7 +39,7 @@ log = logging.getLogger(__name__)
39
39
  class AbstractClusterTest(ToilTest):
40
40
  def __init__(self, methodName: str) -> None:
41
41
  super().__init__(methodName=methodName)
42
- self.keyName = os.getenv("TOIL_AWS_KEYNAME").strip() or "id_rsa"
42
+ self.keyName = os.getenv("TOIL_AWS_KEYNAME", "id_rsa").strip()
43
43
  self.clusterName = f"aws-provisioner-test-{uuid4()}"
44
44
  self.leaderNodeType = "t2.medium"
45
45
  self.clusterType = "mesos"
@@ -276,12 +276,12 @@ class CWLOnARMTest(AbstractClusterTest):
276
276
  ]
277
277
  )
278
278
 
279
- # Runs the CWLv12Test on an ARM instance
279
+ # Runs the TestCWLv12 on an ARM instance
280
280
  self.sshUtil(
281
281
  [
282
282
  "bash",
283
283
  "-c",
284
- 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",
284
+ f". .{self.venvDir}/bin/activate && cd {self.cwl_test_dir}/toil && pytest --log-cli-level DEBUG -r s src/toil/test/cwl/cwlTest.py::TestCWLv12::test_run_conformance",
285
285
  ]
286
286
  )
287
287
 
@@ -289,5 +289,5 @@ class CWLOnARMTest(AbstractClusterTest):
289
289
  # Bring it back to be an artifact.
290
290
  self.rsync_util(
291
291
  f":{self.cwl_test_dir}/toil/conformance-1.2.junit.xml",
292
- os.path.join(self._projectRootPath(), "arm-conformance-1.2.junit.xml"),
292
+ str(self._rootpath / "arm-conformance-1.2.junit.xml"),
293
293
  )
@@ -21,6 +21,7 @@ import pytest
21
21
 
22
22
  from toil.test import (
23
23
  ToilTest,
24
+ get_data,
24
25
  integrative,
25
26
  needs_fetchable_appliance,
26
27
  needs_google_project,
@@ -214,10 +215,11 @@ class GCEAutoscaleTest(AbstractGCEAutoscaleTest):
214
215
  with open(fileToSort, "w") as f:
215
216
  # Fixme: making this file larger causes the test to hang
216
217
  f.write("01234567890123456789012345678901")
217
- self.rsyncUtil(
218
- os.path.join(self._projectRootPath(), "src/toil/test/sort/sort.py"),
219
- ":/home/sort.py",
220
- )
218
+ with get_data("test/sort/sort.py") as sort_py:
219
+ self.rsyncUtil(
220
+ sort_py,
221
+ ":/home/sort.py",
222
+ )
221
223
  self.rsyncUtil(fileToSort, ":/home/sortFile")
222
224
  os.unlink(fileToSort)
223
225
 
@@ -324,10 +326,11 @@ class GCEAutoscaleTestMultipleNodeTypes(AbstractGCEAutoscaleTest):
324
326
  sseKeyFile = os.path.join(os.getcwd(), "keyFile")
325
327
  with open(sseKeyFile, "w") as f:
326
328
  f.write("01234567890123456789012345678901")
327
- self.rsyncUtil(
328
- os.path.join(self._projectRootPath(), "src/toil/test/sort/sort.py"),
329
- ":/home/sort.py",
330
- )
329
+ with get_data("test/sort/sort.py") as sort_py:
330
+ self.rsyncUtil(
331
+ sort_py,
332
+ ":/home/sort.py",
333
+ )
331
334
  self.rsyncUtil(sseKeyFile, ":/home/keyFile")
332
335
  os.unlink(sseKeyFile)
333
336
 
@@ -376,12 +379,11 @@ class GCERestartTest(AbstractGCEAutoscaleTest):
376
379
  self.jobStore = f"google:{self.projectID}:restart-{uuid4()}"
377
380
 
378
381
  def _getScript(self):
379
- self.rsyncUtil(
380
- os.path.join(
381
- self._projectRootPath(), "src/toil/test/provisioners/restartScript.py"
382
- ),
383
- ":" + self.scriptName,
384
- )
382
+ with get_data("test/provisioners/restartScript.py") as restartScript:
383
+ self.rsyncUtil(
384
+ restartScript,
385
+ ":" + self.scriptName,
386
+ )
385
387
 
386
388
  def _runScript(self, toilOptions):
387
389
  # clean = onSuccess
toil/test/sort/sort.py CHANGED
@@ -23,6 +23,7 @@ from configargparse import ArgumentParser
23
23
 
24
24
  from toil.common import Toil
25
25
  from toil.job import Job
26
+ from toil.lib.misc import StrPath
26
27
  from toil.realtimeLogger import RealtimeLogger
27
28
 
28
29
  defaultLines = 1000
@@ -207,7 +208,9 @@ def getMidPoint(file, fileStart, fileEnd):
207
208
  return len(line) + fileStart - 1
208
209
 
209
210
 
210
- def makeFileToSort(fileName, lines=defaultLines, lineLen=defaultLineLen):
211
+ def makeFileToSort(
212
+ fileName: StrPath, lines: int = defaultLines, lineLen: int = defaultLineLen
213
+ ) -> None:
211
214
  with open(fileName, "w") as f:
212
215
  for _ in range(lines):
213
216
  line = (
toil/test/src/busTest.py CHANGED
@@ -14,7 +14,9 @@
14
14
 
15
15
  import logging
16
16
  import os
17
+ from pathlib import Path
17
18
  from threading import Thread, current_thread
19
+ from typing import NoReturn
18
20
 
19
21
  from toil.batchSystems.abstractBatchSystem import BatchJobExitReason
20
22
  from toil.bus import (
@@ -26,18 +28,17 @@ from toil.bus import (
26
28
  from toil.common import Toil
27
29
  from toil.exceptions import FailedJobsException
28
30
  from toil.job import Job
29
- from toil.test import ToilTest, get_temp_file
30
31
 
31
32
  logger = logging.getLogger(__name__)
32
33
 
33
34
 
34
- class MessageBusTest(ToilTest):
35
+ class TestMessageBus:
35
36
 
36
- def test_enum_ints_in_file(self) -> None:
37
+ def test_enum_ints_in_file(self, tmp_path: Path) -> None:
37
38
  """
38
39
  Make sure writing bus messages to files works with enums.
39
40
  """
40
- bus_file = get_temp_file()
41
+ bus_file = tmp_path / "bus"
41
42
 
42
43
  bus = MessageBus()
43
44
  # Connect the handler and hold the result to protect it from GC
@@ -73,7 +74,7 @@ class MessageBusTest(ToilTest):
73
74
  # Message should always arrive in the main thread.
74
75
  nonlocal message_count
75
76
  logger.debug("Got message: %s", received)
76
- self.assertEqual(current_thread(), main_thread)
77
+ assert current_thread() == main_thread
77
78
  message_count += 1
78
79
 
79
80
  bus.subscribe(JobIssuedMessage, handler)
@@ -101,28 +102,27 @@ class MessageBusTest(ToilTest):
101
102
  t.join()
102
103
 
103
104
  # We should ge tone message per thread, plus our own
104
- self.assertEqual(box.count(JobIssuedMessage), 11)
105
+ assert box.count(JobIssuedMessage) == 11
105
106
  # And having polled for those, our handler should have run
106
- self.assertEqual(message_count, 11)
107
+ assert message_count == 11
107
108
 
108
- def test_restart_without_bus_path(self) -> None:
109
+ def test_restart_without_bus_path(self, tmp_path: Path) -> None:
109
110
  """
110
111
  Test the ability to restart a workflow when the message bus path used
111
112
  by the previous attempt is gone.
112
113
  """
113
- temp_dir = self._createTempDir(purpose="tempDir")
114
- job_store = self._getTestJobStorePath()
114
+ temp_dir = tmp_path / "tempDir"
115
+ temp_dir.mkdir()
116
+ job_store = tmp_path / "jobstore"
115
117
 
116
- bus_holder_dir = os.path.join(temp_dir, "bus_holder")
117
- os.mkdir(bus_holder_dir)
118
+ bus_holder_dir = temp_dir / "bus_holder"
119
+ bus_holder_dir.mkdir()
118
120
 
119
121
  start_options = Job.Runner.getDefaultOptions(job_store)
120
122
  start_options.logLevel = "DEBUG"
121
123
  start_options.retryCount = 0
122
124
  start_options.clean = "never"
123
- start_options.write_messages = os.path.abspath(
124
- os.path.join(bus_holder_dir, "messagebus.txt")
125
- )
125
+ start_options.write_messages = str(bus_holder_dir / "messagebus.txt")
126
126
 
127
127
  root = Job.wrapJobFn(failing_job_fn)
128
128
 
@@ -137,7 +137,7 @@ class MessageBusTest(ToilTest):
137
137
 
138
138
  # Get rid of the bus
139
139
  os.unlink(start_options.write_messages)
140
- os.rmdir(bus_holder_dir)
140
+ bus_holder_dir.rmdir()
141
141
 
142
142
  logger.info("Making second attempt")
143
143
 
@@ -158,7 +158,7 @@ class MessageBusTest(ToilTest):
158
158
  logger.info("Second attempt successfully failed")
159
159
 
160
160
 
161
- def failing_job_fn(job: Job) -> None:
161
+ def failing_job_fn(job: Job) -> NoReturn:
162
162
  """
163
163
  This function is guaranteed to fail.
164
164
  """