toil 8.1.0b1__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.
- toil/__init__.py +0 -35
- toil/batchSystems/abstractBatchSystem.py +1 -1
- toil/batchSystems/abstractGridEngineBatchSystem.py +1 -1
- toil/batchSystems/awsBatch.py +1 -1
- toil/batchSystems/cleanup_support.py +1 -1
- toil/batchSystems/kubernetes.py +53 -7
- toil/batchSystems/local_support.py +1 -1
- toil/batchSystems/mesos/batchSystem.py +13 -8
- toil/batchSystems/mesos/test/__init__.py +3 -2
- toil/batchSystems/singleMachine.py +1 -1
- toil/batchSystems/slurm.py +27 -26
- toil/bus.py +5 -3
- toil/common.py +39 -11
- toil/cwl/cwltoil.py +1 -1
- toil/job.py +64 -49
- toil/jobStores/abstractJobStore.py +24 -3
- toil/jobStores/fileJobStore.py +25 -1
- toil/jobStores/googleJobStore.py +104 -30
- toil/leader.py +9 -0
- toil/lib/accelerators.py +3 -1
- toil/lib/aws/utils.py.orig +504 -0
- toil/lib/bioio.py +1 -1
- toil/lib/docker.py +252 -91
- toil/lib/dockstore.py +11 -3
- toil/lib/exceptions.py +5 -3
- toil/lib/history.py +87 -13
- toil/lib/history_submission.py +23 -9
- toil/lib/io.py +34 -22
- toil/lib/misc.py +7 -1
- toil/lib/resources.py +2 -1
- toil/lib/threading.py +11 -10
- toil/options/common.py +8 -0
- toil/options/wdl.py +11 -0
- toil/server/api_spec/LICENSE +201 -0
- toil/server/api_spec/README.rst +5 -0
- toil/server/cli/wes_cwl_runner.py +2 -1
- toil/test/__init__.py +275 -115
- toil/test/batchSystems/batchSystemTest.py +227 -205
- toil/test/batchSystems/test_slurm.py +27 -0
- toil/test/cactus/pestis.tar.gz +0 -0
- toil/test/conftest.py +7 -0
- toil/test/cwl/2.fasta +11 -0
- toil/test/cwl/2.fastq +12 -0
- toil/test/cwl/conftest.py +1 -1
- toil/test/cwl/cwlTest.py +999 -867
- toil/test/cwl/directory/directory/file.txt +15 -0
- toil/test/cwl/download_directory_file.json +4 -0
- toil/test/cwl/download_directory_s3.json +4 -0
- toil/test/cwl/download_file.json +6 -0
- toil/test/cwl/download_http.json +6 -0
- toil/test/cwl/download_https.json +6 -0
- toil/test/cwl/download_s3.json +6 -0
- toil/test/cwl/download_subdirectory_file.json +5 -0
- toil/test/cwl/download_subdirectory_s3.json +5 -0
- toil/test/cwl/empty.json +1 -0
- toil/test/cwl/mock_mpi/fake_mpi.yml +8 -0
- toil/test/cwl/mock_mpi/fake_mpi_run.py +42 -0
- toil/test/cwl/optional-file-exists.json +6 -0
- toil/test/cwl/optional-file-missing.json +6 -0
- toil/test/cwl/preemptible_expression.json +1 -0
- toil/test/cwl/revsort-job-missing.json +6 -0
- toil/test/cwl/revsort-job.json +6 -0
- toil/test/cwl/s3_secondary_file.json +16 -0
- toil/test/cwl/seqtk_seq_job.json +6 -0
- toil/test/cwl/stream.json +6 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.dat +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f0 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f1 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f1i +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f2 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f2_TSM0 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f3 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f3_TSM0 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f4 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f4_TSM0 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.f5 +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.info +0 -0
- toil/test/cwl/test_filename_conflict_resolution.ms/table.lock +0 -0
- toil/test/cwl/whale.txt +16 -0
- toil/test/docs/scripts/example_alwaysfail.py +38 -0
- toil/test/docs/scripts/example_alwaysfail_with_files.wdl +33 -0
- toil/test/docs/scripts/example_cachingbenchmark.py +117 -0
- toil/test/docs/scripts/stagingExampleFiles/in.txt +1 -0
- toil/test/docs/scripts/stagingExampleFiles/out.txt +2 -0
- toil/test/docs/scripts/tutorial_arguments.py +23 -0
- toil/test/docs/scripts/tutorial_debugging.patch +12 -0
- toil/test/docs/scripts/tutorial_debugging_hangs.wdl +126 -0
- toil/test/docs/scripts/tutorial_debugging_works.wdl +129 -0
- toil/test/docs/scripts/tutorial_docker.py +20 -0
- toil/test/docs/scripts/tutorial_dynamic.py +24 -0
- toil/test/docs/scripts/tutorial_encapsulation.py +28 -0
- toil/test/docs/scripts/tutorial_encapsulation2.py +29 -0
- toil/test/docs/scripts/tutorial_helloworld.py +15 -0
- toil/test/docs/scripts/tutorial_invokeworkflow.py +27 -0
- toil/test/docs/scripts/tutorial_invokeworkflow2.py +30 -0
- toil/test/docs/scripts/tutorial_jobfunctions.py +22 -0
- toil/test/docs/scripts/tutorial_managing.py +29 -0
- toil/test/docs/scripts/tutorial_managing2.py +56 -0
- toil/test/docs/scripts/tutorial_multiplejobs.py +25 -0
- toil/test/docs/scripts/tutorial_multiplejobs2.py +21 -0
- toil/test/docs/scripts/tutorial_multiplejobs3.py +22 -0
- toil/test/docs/scripts/tutorial_promises.py +25 -0
- toil/test/docs/scripts/tutorial_promises2.py +30 -0
- toil/test/docs/scripts/tutorial_quickstart.py +22 -0
- toil/test/docs/scripts/tutorial_requirements.py +44 -0
- toil/test/docs/scripts/tutorial_services.py +45 -0
- toil/test/docs/scripts/tutorial_staging.py +45 -0
- toil/test/docs/scripts/tutorial_stats.py +64 -0
- toil/test/lib/aws/test_iam.py +3 -1
- toil/test/lib/dockerTest.py +205 -122
- toil/test/lib/test_history.py +101 -77
- toil/test/provisioners/aws/awsProvisionerTest.py +12 -9
- toil/test/provisioners/clusterTest.py +4 -4
- toil/test/provisioners/gceProvisionerTest.py +16 -14
- toil/test/sort/sort.py +4 -1
- toil/test/src/busTest.py +17 -17
- toil/test/src/deferredFunctionTest.py +145 -132
- toil/test/src/importExportFileTest.py +71 -63
- toil/test/src/jobEncapsulationTest.py +27 -28
- toil/test/src/jobServiceTest.py +149 -133
- toil/test/src/jobTest.py +219 -211
- toil/test/src/miscTests.py +66 -60
- toil/test/src/promisedRequirementTest.py +163 -169
- toil/test/src/regularLogTest.py +24 -24
- toil/test/src/resourceTest.py +82 -76
- toil/test/src/restartDAGTest.py +51 -47
- toil/test/src/resumabilityTest.py +24 -19
- toil/test/src/retainTempDirTest.py +60 -57
- toil/test/src/systemTest.py +17 -13
- toil/test/src/threadingTest.py +29 -32
- toil/test/utils/ABCWorkflowDebug/B_file.txt +1 -0
- toil/test/utils/ABCWorkflowDebug/debugWorkflow.py +204 -0
- toil/test/utils/ABCWorkflowDebug/mkFile.py +16 -0
- toil/test/utils/ABCWorkflowDebug/sleep.cwl +12 -0
- toil/test/utils/ABCWorkflowDebug/sleep.yaml +1 -0
- toil/test/utils/toilDebugTest.py +117 -102
- toil/test/utils/toilKillTest.py +54 -53
- toil/test/utils/utilsTest.py +303 -229
- toil/test/wdl/lint_error.wdl +9 -0
- toil/test/wdl/md5sum/empty_file.json +1 -0
- toil/test/wdl/md5sum/md5sum-gs.json +1 -0
- toil/test/wdl/md5sum/md5sum.1.0.wdl +32 -0
- toil/test/wdl/md5sum/md5sum.input +1 -0
- toil/test/wdl/md5sum/md5sum.json +1 -0
- toil/test/wdl/md5sum/md5sum.wdl +25 -0
- toil/test/wdl/miniwdl_self_test/inputs-namespaced.json +1 -0
- toil/test/wdl/miniwdl_self_test/inputs.json +1 -0
- toil/test/wdl/miniwdl_self_test/self_test.wdl +40 -0
- toil/test/wdl/standard_library/as_map.json +16 -0
- toil/test/wdl/standard_library/as_map_as_input.wdl +23 -0
- toil/test/wdl/standard_library/as_pairs.json +7 -0
- toil/test/wdl/standard_library/as_pairs_as_input.wdl +23 -0
- toil/test/wdl/standard_library/ceil.json +3 -0
- toil/test/wdl/standard_library/ceil_as_command.wdl +16 -0
- toil/test/wdl/standard_library/ceil_as_input.wdl +16 -0
- toil/test/wdl/standard_library/collect_by_key.json +1 -0
- toil/test/wdl/standard_library/collect_by_key_as_input.wdl +23 -0
- toil/test/wdl/standard_library/cross.json +11 -0
- toil/test/wdl/standard_library/cross_as_input.wdl +19 -0
- toil/test/wdl/standard_library/flatten.json +7 -0
- toil/test/wdl/standard_library/flatten_as_input.wdl +18 -0
- toil/test/wdl/standard_library/floor.json +3 -0
- toil/test/wdl/standard_library/floor_as_command.wdl +16 -0
- toil/test/wdl/standard_library/floor_as_input.wdl +16 -0
- toil/test/wdl/standard_library/keys.json +8 -0
- toil/test/wdl/standard_library/keys_as_input.wdl +24 -0
- toil/test/wdl/standard_library/length.json +7 -0
- toil/test/wdl/standard_library/length_as_input.wdl +16 -0
- toil/test/wdl/standard_library/length_as_input_with_map.json +7 -0
- toil/test/wdl/standard_library/length_as_input_with_map.wdl +17 -0
- toil/test/wdl/standard_library/length_invalid.json +3 -0
- toil/test/wdl/standard_library/range.json +3 -0
- toil/test/wdl/standard_library/range_0.json +3 -0
- toil/test/wdl/standard_library/range_as_input.wdl +17 -0
- toil/test/wdl/standard_library/range_invalid.json +3 -0
- toil/test/wdl/standard_library/read_boolean.json +3 -0
- toil/test/wdl/standard_library/read_boolean_as_command.wdl +17 -0
- toil/test/wdl/standard_library/read_float.json +3 -0
- toil/test/wdl/standard_library/read_float_as_command.wdl +17 -0
- toil/test/wdl/standard_library/read_int.json +3 -0
- toil/test/wdl/standard_library/read_int_as_command.wdl +17 -0
- toil/test/wdl/standard_library/read_json.json +3 -0
- toil/test/wdl/standard_library/read_json_as_output.wdl +31 -0
- toil/test/wdl/standard_library/read_lines.json +3 -0
- toil/test/wdl/standard_library/read_lines_as_output.wdl +31 -0
- toil/test/wdl/standard_library/read_map.json +3 -0
- toil/test/wdl/standard_library/read_map_as_output.wdl +31 -0
- toil/test/wdl/standard_library/read_string.json +3 -0
- toil/test/wdl/standard_library/read_string_as_command.wdl +17 -0
- toil/test/wdl/standard_library/read_tsv.json +3 -0
- toil/test/wdl/standard_library/read_tsv_as_output.wdl +31 -0
- toil/test/wdl/standard_library/round.json +3 -0
- toil/test/wdl/standard_library/round_as_command.wdl +16 -0
- toil/test/wdl/standard_library/round_as_input.wdl +16 -0
- toil/test/wdl/standard_library/size.json +3 -0
- toil/test/wdl/standard_library/size_as_command.wdl +17 -0
- toil/test/wdl/standard_library/size_as_output.wdl +36 -0
- toil/test/wdl/standard_library/stderr.json +3 -0
- toil/test/wdl/standard_library/stderr_as_output.wdl +30 -0
- toil/test/wdl/standard_library/stdout.json +3 -0
- toil/test/wdl/standard_library/stdout_as_output.wdl +30 -0
- toil/test/wdl/standard_library/sub.json +3 -0
- toil/test/wdl/standard_library/sub_as_input.wdl +17 -0
- toil/test/wdl/standard_library/sub_as_input_with_file.wdl +17 -0
- toil/test/wdl/standard_library/transpose.json +6 -0
- toil/test/wdl/standard_library/transpose_as_input.wdl +18 -0
- toil/test/wdl/standard_library/write_json.json +6 -0
- toil/test/wdl/standard_library/write_json_as_command.wdl +17 -0
- toil/test/wdl/standard_library/write_lines.json +7 -0
- toil/test/wdl/standard_library/write_lines_as_command.wdl +17 -0
- toil/test/wdl/standard_library/write_map.json +6 -0
- toil/test/wdl/standard_library/write_map_as_command.wdl +17 -0
- toil/test/wdl/standard_library/write_tsv.json +6 -0
- toil/test/wdl/standard_library/write_tsv_as_command.wdl +17 -0
- toil/test/wdl/standard_library/zip.json +12 -0
- toil/test/wdl/standard_library/zip_as_input.wdl +19 -0
- toil/test/wdl/test.csv +3 -0
- toil/test/wdl/test.tsv +3 -0
- toil/test/wdl/testfiles/croo.wdl +38 -0
- toil/test/wdl/testfiles/drop_files.wdl +62 -0
- toil/test/wdl/testfiles/drop_files_subworkflow.wdl +13 -0
- toil/test/wdl/testfiles/empty.txt +0 -0
- toil/test/wdl/testfiles/not_enough_outputs.wdl +33 -0
- toil/test/wdl/testfiles/random.wdl +66 -0
- toil/test/wdl/testfiles/string_file_coercion.json +1 -0
- toil/test/wdl/testfiles/string_file_coercion.wdl +35 -0
- toil/test/wdl/testfiles/test.json +4 -0
- toil/test/wdl/testfiles/test_boolean.txt +1 -0
- toil/test/wdl/testfiles/test_float.txt +1 -0
- toil/test/wdl/testfiles/test_int.txt +1 -0
- toil/test/wdl/testfiles/test_lines.txt +5 -0
- toil/test/wdl/testfiles/test_map.txt +2 -0
- toil/test/wdl/testfiles/test_string.txt +1 -0
- toil/test/wdl/testfiles/url_to_file.wdl +13 -0
- toil/test/wdl/testfiles/url_to_optional_file.wdl +13 -0
- toil/test/wdl/testfiles/vocab.json +1 -0
- toil/test/wdl/testfiles/vocab.wdl +66 -0
- toil/test/wdl/testfiles/wait.wdl +34 -0
- toil/test/wdl/wdl_specification/type_pair.json +23 -0
- toil/test/wdl/wdl_specification/type_pair_basic.wdl +36 -0
- toil/test/wdl/wdl_specification/type_pair_with_files.wdl +36 -0
- toil/test/wdl/wdl_specification/v1_spec.json +1 -0
- toil/test/wdl/wdl_specification/v1_spec_declaration.wdl +39 -0
- toil/test/wdl/wdltoil_test.py +680 -407
- toil/test/wdl/wdltoil_test_kubernetes.py +2 -2
- toil/version.py +9 -9
- toil/wdl/wdltoil.py +336 -123
- {toil-8.1.0b1.dist-info → toil-8.2.0.dist-info}/METADATA +5 -4
- toil-8.2.0.dist-info/RECORD +439 -0
- {toil-8.1.0b1.dist-info → toil-8.2.0.dist-info}/WHEEL +1 -1
- toil-8.1.0b1.dist-info/RECORD +0 -259
- {toil-8.1.0b1.dist-info → toil-8.2.0.dist-info}/entry_points.txt +0 -0
- {toil-8.1.0b1.dist-info → toil-8.2.0.dist-info/licenses}/LICENSE +0 -0
- {toil-8.1.0b1.dist-info → toil-8.2.0.dist-info}/top_level.txt +0 -0
toil/test/src/jobTest.py
CHANGED
|
@@ -11,32 +11,38 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
from collections.abc import Callable
|
|
14
15
|
import collections
|
|
15
16
|
import logging
|
|
16
17
|
import os
|
|
18
|
+
from pathlib import Path
|
|
17
19
|
import random
|
|
18
|
-
import
|
|
20
|
+
from typing import cast, Optional, Union, Any, NoReturn
|
|
19
21
|
|
|
20
22
|
import pytest
|
|
21
23
|
|
|
22
24
|
from toil.common import Toil
|
|
23
25
|
from toil.exceptions import FailedJobsException
|
|
24
|
-
from toil.job import
|
|
25
|
-
|
|
26
|
+
from toil.job import (
|
|
27
|
+
FunctionWrappingJob,
|
|
28
|
+
Job,
|
|
29
|
+
JobFunctionWrappingJob,
|
|
30
|
+
JobGraphDeadlockException,
|
|
31
|
+
ServiceHostJob,
|
|
32
|
+
Promise,
|
|
33
|
+
)
|
|
34
|
+
from toil.lib.misc import FileDescriptorOrPath
|
|
35
|
+
from toil.test import pslow as slow
|
|
26
36
|
|
|
27
|
-
|
|
37
|
+
from pytest_subtests import SubTests
|
|
28
38
|
|
|
29
39
|
|
|
30
|
-
class
|
|
40
|
+
class TestJob:
|
|
31
41
|
"""Tests the job class."""
|
|
32
42
|
|
|
33
|
-
@classmethod
|
|
34
|
-
def setUpClass(cls):
|
|
35
|
-
super().setUpClass()
|
|
36
|
-
logging.basicConfig(level=logging.DEBUG)
|
|
37
|
-
|
|
38
43
|
@slow
|
|
39
|
-
|
|
44
|
+
@pytest.mark.slow
|
|
45
|
+
def testStatic(self, tmp_path: Path) -> None:
|
|
40
46
|
r"""
|
|
41
47
|
Create a DAG of jobs non-dynamically and run it. DAG is::
|
|
42
48
|
|
|
@@ -48,39 +54,35 @@ class JobTest(ToilTest):
|
|
|
48
54
|
|
|
49
55
|
Follow on is marked by ``->``
|
|
50
56
|
"""
|
|
51
|
-
outFile =
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# Run the workflow, the return value being the number of failed jobs
|
|
76
|
-
Job.Runner.startToil(A, options)
|
|
57
|
+
outFile = tmp_path / "out"
|
|
58
|
+
# Create the jobs
|
|
59
|
+
A = Job.wrapFn(fn1Test, "A", outFile)
|
|
60
|
+
B = Job.wrapFn(fn1Test, A.rv(), outFile)
|
|
61
|
+
C = Job.wrapFn(fn1Test, B.rv(), outFile)
|
|
62
|
+
D = Job.wrapFn(fn1Test, C.rv(), outFile)
|
|
63
|
+
E = Job.wrapFn(fn1Test, D.rv(), outFile)
|
|
64
|
+
F = Job.wrapFn(fn1Test, E.rv(), outFile)
|
|
65
|
+
# Connect them into a workflow
|
|
66
|
+
A.addChild(B)
|
|
67
|
+
A.addChild(C)
|
|
68
|
+
B.addChild(C)
|
|
69
|
+
B.addFollowOn(E)
|
|
70
|
+
C.addFollowOn(D)
|
|
71
|
+
A.addFollowOn(F)
|
|
72
|
+
|
|
73
|
+
# Create the runner for the workflow.
|
|
74
|
+
options = Job.Runner.getDefaultOptions(tmp_path / "jobstore")
|
|
75
|
+
options.logLevel = "INFO"
|
|
76
|
+
options.retryCount = 100
|
|
77
|
+
options.badWorker = 0.5
|
|
78
|
+
options.badWorkerFailInterval = 0.01
|
|
79
|
+
# Run the workflow, the return value being the number of failed jobs
|
|
80
|
+
Job.Runner.startToil(A, options)
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
finally:
|
|
81
|
-
os.remove(outFile)
|
|
82
|
+
# Check output
|
|
83
|
+
assert open(outFile).readline() == "ABCDEFG"
|
|
82
84
|
|
|
83
|
-
def testStatic2(self):
|
|
85
|
+
def testStatic2(self, tmp_path: Path) -> None:
|
|
84
86
|
r"""
|
|
85
87
|
Create a DAG of jobs non-dynamically and run it. DAG is::
|
|
86
88
|
|
|
@@ -92,84 +94,68 @@ class JobTest(ToilTest):
|
|
|
92
94
|
|
|
93
95
|
Follow on is marked by ``->``
|
|
94
96
|
"""
|
|
95
|
-
outFile =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
D = Job.wrapFn(fn1Test, C.rv(), outFile)
|
|
103
|
-
|
|
104
|
-
# Connect them into a workflow
|
|
105
|
-
A.addChild(B)
|
|
106
|
-
A.addFollowOn(C)
|
|
107
|
-
C.addChild(D)
|
|
108
|
-
|
|
109
|
-
# Create the runner for the workflow.
|
|
110
|
-
options = Job.Runner.getDefaultOptions(self._getTestJobStorePath())
|
|
111
|
-
options.logLevel = "INFO"
|
|
112
|
-
options.retryCount = 100
|
|
113
|
-
options.badWorker = 0.5
|
|
114
|
-
options.badWorkerFailInterval = 0.01
|
|
115
|
-
# Run the workflow, the return value being the number of failed jobs
|
|
116
|
-
Job.Runner.startToil(A, options)
|
|
97
|
+
outFile = tmp_path / "out"
|
|
98
|
+
|
|
99
|
+
# Create the jobs
|
|
100
|
+
A = Job.wrapFn(fn1Test, "A", outFile)
|
|
101
|
+
B = Job.wrapFn(fn1Test, A.rv(), outFile)
|
|
102
|
+
C = Job.wrapFn(fn1Test, B.rv(), outFile)
|
|
103
|
+
D = Job.wrapFn(fn1Test, C.rv(), outFile)
|
|
117
104
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
105
|
+
# Connect them into a workflow
|
|
106
|
+
A.addChild(B)
|
|
107
|
+
A.addFollowOn(C)
|
|
108
|
+
C.addChild(D)
|
|
109
|
+
|
|
110
|
+
# Create the runner for the workflow.
|
|
111
|
+
options = Job.Runner.getDefaultOptions(tmp_path / "jobstore")
|
|
112
|
+
options.logLevel = "INFO"
|
|
113
|
+
options.retryCount = 100
|
|
114
|
+
options.badWorker = 0.5
|
|
115
|
+
options.badWorkerFailInterval = 0.01
|
|
116
|
+
# Run the workflow, the return value being the number of failed jobs
|
|
117
|
+
Job.Runner.startToil(A, options)
|
|
118
|
+
|
|
119
|
+
# Check output
|
|
120
|
+
assert open(outFile).readline() == "ABCDE"
|
|
122
121
|
|
|
123
122
|
@slow
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
@pytest.mark.slow
|
|
124
|
+
def testTrivialDAGConsistency(self, tmp_path: Path) -> None:
|
|
125
|
+
options = Job.Runner.getDefaultOptions(tmp_path / "jobStore")
|
|
126
126
|
options.clean = "always"
|
|
127
127
|
options.logLevel = "debug"
|
|
128
128
|
i = Job.wrapJobFn(trivialParent)
|
|
129
129
|
with Toil(options) as toil:
|
|
130
|
-
|
|
130
|
+
with pytest.raises(FailedJobsException):
|
|
131
131
|
toil.start(i)
|
|
132
|
-
except FailedJobsException:
|
|
133
|
-
# we expect this exception to be raised
|
|
134
|
-
pass
|
|
135
|
-
else:
|
|
136
|
-
self.fail()
|
|
137
132
|
|
|
138
133
|
@pytest.mark.timeout(300)
|
|
139
|
-
def testDAGConsistency(self):
|
|
140
|
-
options = Job.Runner.getDefaultOptions(
|
|
134
|
+
def testDAGConsistency(self, tmp_path: Path) -> None:
|
|
135
|
+
options = Job.Runner.getDefaultOptions(tmp_path / "jobStore")
|
|
141
136
|
options.clean = "always"
|
|
142
137
|
options.logLevel = "debug"
|
|
143
138
|
i = Job.wrapJobFn(parent)
|
|
144
139
|
with Toil(options) as toil:
|
|
145
|
-
|
|
140
|
+
with pytest.raises(FailedJobsException):
|
|
146
141
|
toil.start(i)
|
|
147
|
-
except FailedJobsException:
|
|
148
|
-
# we expect this exception to be raised
|
|
149
|
-
pass
|
|
150
|
-
else:
|
|
151
|
-
self.fail()
|
|
152
142
|
|
|
153
143
|
@slow
|
|
154
|
-
|
|
144
|
+
@pytest.mark.slow
|
|
145
|
+
def testSiblingDAGConsistency(self, tmp_path: Path) -> None:
|
|
155
146
|
"""
|
|
156
147
|
Slightly more complex case. The stranded job's predecessors are siblings instead of
|
|
157
148
|
parent/child.
|
|
158
149
|
"""
|
|
159
|
-
options = Job.Runner.getDefaultOptions(
|
|
150
|
+
options = Job.Runner.getDefaultOptions(tmp_path / "jobStore")
|
|
160
151
|
options.clean = "always"
|
|
161
152
|
options.logLevel = "debug"
|
|
162
153
|
i = Job.wrapJobFn(diamond)
|
|
163
154
|
with Toil(options) as toil:
|
|
164
|
-
|
|
155
|
+
with pytest.raises(FailedJobsException):
|
|
165
156
|
toil.start(i)
|
|
166
|
-
except FailedJobsException:
|
|
167
|
-
# we expect this exception to be raised
|
|
168
|
-
pass
|
|
169
|
-
else:
|
|
170
|
-
self.fail()
|
|
171
157
|
|
|
172
|
-
def testDeadlockDetection(self):
|
|
158
|
+
def testDeadlockDetection(self) -> None:
|
|
173
159
|
"""
|
|
174
160
|
Randomly generate job graphs with various types of cycle in them and
|
|
175
161
|
check they cause an exception properly. Also check that multiple roots
|
|
@@ -181,19 +167,19 @@ class JobTest(ToilTest):
|
|
|
181
167
|
childEdges = self.makeRandomDAG(nodeNumber)
|
|
182
168
|
# Get an adjacency list representation and check is acyclic
|
|
183
169
|
adjacencyList = self.getAdjacencyList(nodeNumber, childEdges)
|
|
184
|
-
self.
|
|
170
|
+
assert self.isAcyclic(adjacencyList)
|
|
185
171
|
|
|
186
172
|
# Add in follow-on edges - these are returned as a list, and as a set of augmented
|
|
187
173
|
# edges in the adjacency list
|
|
188
174
|
# edges in the adjacency list
|
|
189
175
|
followOnEdges = self.addRandomFollowOnEdges(adjacencyList)
|
|
190
|
-
self.
|
|
176
|
+
assert self.isAcyclic(adjacencyList)
|
|
191
177
|
# Make the job graph
|
|
192
178
|
rootJob = self.makeJobGraph(nodeNumber, childEdges, followOnEdges, None)
|
|
193
179
|
rootJob.checkJobGraphAcylic() # This should not throw an exception
|
|
194
180
|
rootJob.checkJobGraphConnected() # Nor this
|
|
195
181
|
# Check root detection explicitly
|
|
196
|
-
|
|
182
|
+
assert rootJob.getRootJobs() == {rootJob}
|
|
197
183
|
|
|
198
184
|
# Test making multiple roots
|
|
199
185
|
childEdges2 = childEdges.copy()
|
|
@@ -203,23 +189,17 @@ class JobTest(ToilTest):
|
|
|
203
189
|
rootJob2 = self.makeJobGraph(
|
|
204
190
|
nodeNumber + 1, childEdges2, followOnEdges, None, False
|
|
205
191
|
)
|
|
206
|
-
|
|
192
|
+
with pytest.raises(JobGraphDeadlockException):
|
|
207
193
|
rootJob2.checkJobGraphConnected()
|
|
208
|
-
self.assertTrue(False) # Multiple roots were not detected
|
|
209
|
-
except JobGraphDeadlockException:
|
|
210
|
-
pass # This is the expected behaviour
|
|
211
194
|
|
|
212
|
-
def checkChildEdgeCycleDetection(fNode, tNode):
|
|
195
|
+
def checkChildEdgeCycleDetection(fNode: int, tNode: int) -> None:
|
|
213
196
|
childEdges.add((fNode, tNode)) # Create a cycle
|
|
214
197
|
adjacencyList[fNode].add(tNode)
|
|
215
|
-
|
|
216
|
-
|
|
198
|
+
assert not self.isAcyclic(adjacencyList)
|
|
199
|
+
with pytest.raises(JobGraphDeadlockException):
|
|
217
200
|
self.makeJobGraph(
|
|
218
201
|
nodeNumber, childEdges, followOnEdges, None
|
|
219
202
|
).checkJobGraphAcylic()
|
|
220
|
-
self.assertTrue(False) # A cycle was not detected
|
|
221
|
-
except JobGraphDeadlockException:
|
|
222
|
-
pass # This is the expected behaviour
|
|
223
203
|
# Remove the edges
|
|
224
204
|
childEdges.remove((fNode, tNode))
|
|
225
205
|
adjacencyList[fNode].remove(tNode)
|
|
@@ -228,15 +208,12 @@ class JobTest(ToilTest):
|
|
|
228
208
|
nodeNumber, childEdges, followOnEdges, None, False
|
|
229
209
|
).checkJobGraphAcylic()
|
|
230
210
|
|
|
231
|
-
def checkFollowOnEdgeCycleDetection(fNode, tNode):
|
|
211
|
+
def checkFollowOnEdgeCycleDetection(fNode: int, tNode: int) -> None:
|
|
232
212
|
followOnEdges.add((fNode, tNode)) # Create a cycle
|
|
233
|
-
|
|
213
|
+
with pytest.raises(JobGraphDeadlockException):
|
|
234
214
|
self.makeJobGraph(
|
|
235
215
|
nodeNumber, childEdges, followOnEdges, None, False
|
|
236
216
|
).checkJobGraphAcylic()
|
|
237
|
-
# self.assertTrue(False) #The cycle was not detected
|
|
238
|
-
except JobGraphDeadlockException:
|
|
239
|
-
pass # This is the expected behaviour
|
|
240
217
|
# Remove the edges
|
|
241
218
|
followOnEdges.remove((fNode, tNode))
|
|
242
219
|
# Check is now acyclic again
|
|
@@ -279,7 +256,10 @@ class JobTest(ToilTest):
|
|
|
279
256
|
checkFollowOnEdgeCycleDetection(fNode, tNode)
|
|
280
257
|
|
|
281
258
|
@slow
|
|
282
|
-
|
|
259
|
+
@pytest.mark.slow
|
|
260
|
+
def testNewCheckpointIsLeafVertexNonRootCase(
|
|
261
|
+
self, tmp_path: Path, subtests: SubTests
|
|
262
|
+
) -> None:
|
|
283
263
|
"""
|
|
284
264
|
Test for issue #1465: Detection of checkpoint jobs that are not leaf vertices
|
|
285
265
|
identifies leaf vertices incorrectly
|
|
@@ -293,17 +273,20 @@ class JobTest(ToilTest):
|
|
|
293
273
|
|
|
294
274
|
"""
|
|
295
275
|
|
|
296
|
-
def createWorkflow():
|
|
276
|
+
def createWorkflow() -> tuple[Job, FunctionWrappingJob]:
|
|
297
277
|
rootJob = Job.wrapJobFn(simpleJobFn, "Parent")
|
|
298
278
|
childCheckpointJob = rootJob.addChildJobFn(
|
|
299
279
|
simpleJobFn, "Child", checkpoint=True
|
|
300
280
|
)
|
|
301
281
|
return rootJob, childCheckpointJob
|
|
302
282
|
|
|
303
|
-
self.runNewCheckpointIsLeafVertexTest(createWorkflow)
|
|
283
|
+
self.runNewCheckpointIsLeafVertexTest(tmp_path, subtests, createWorkflow)
|
|
304
284
|
|
|
305
285
|
@slow
|
|
306
|
-
|
|
286
|
+
@pytest.mark.slow
|
|
287
|
+
def testNewCheckpointIsLeafVertexRootCase(
|
|
288
|
+
self, tmp_path: Path, subtests: SubTests
|
|
289
|
+
) -> None:
|
|
307
290
|
"""
|
|
308
291
|
Test for issue #1466: Detection of checkpoint jobs that are not leaf vertices
|
|
309
292
|
omits the workflow root job
|
|
@@ -315,13 +298,18 @@ class JobTest(ToilTest):
|
|
|
315
298
|
|
|
316
299
|
"""
|
|
317
300
|
|
|
318
|
-
def createWorkflow():
|
|
301
|
+
def createWorkflow() -> tuple[Job, Job]:
|
|
319
302
|
rootJob = Job.wrapJobFn(simpleJobFn, "Root", checkpoint=True)
|
|
320
303
|
return rootJob, rootJob
|
|
321
304
|
|
|
322
|
-
self.runNewCheckpointIsLeafVertexTest(createWorkflow)
|
|
305
|
+
self.runNewCheckpointIsLeafVertexTest(tmp_path, subtests, createWorkflow)
|
|
323
306
|
|
|
324
|
-
def runNewCheckpointIsLeafVertexTest(
|
|
307
|
+
def runNewCheckpointIsLeafVertexTest(
|
|
308
|
+
self,
|
|
309
|
+
tmp_path: Path,
|
|
310
|
+
subtests: SubTests,
|
|
311
|
+
createWorkflowFn: Callable[[], tuple[Job, Job]],
|
|
312
|
+
) -> None:
|
|
325
313
|
"""
|
|
326
314
|
Test verification that a checkpoint job is a leaf vertex using both
|
|
327
315
|
valid and invalid cases.
|
|
@@ -333,51 +321,65 @@ class JobTest(ToilTest):
|
|
|
333
321
|
|
|
334
322
|
"""
|
|
335
323
|
|
|
336
|
-
|
|
337
|
-
|
|
324
|
+
with subtests.test(msg="Test checkpoint job that is a leaf vertex"):
|
|
325
|
+
sub_tmp_path1 = tmp_path / "1"
|
|
326
|
+
sub_tmp_path1.mkdir()
|
|
327
|
+
self.runCheckpointVertexTest(
|
|
328
|
+
*createWorkflowFn(), tmp_path=sub_tmp_path1, expectedException=None
|
|
329
|
+
)
|
|
338
330
|
|
|
339
|
-
|
|
340
|
-
"Test checkpoint job that is not a leaf vertex due to the presence of a service"
|
|
341
|
-
)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
331
|
+
with subtests.test(
|
|
332
|
+
msg="Test checkpoint job that is not a leaf vertex due to the presence of a service"
|
|
333
|
+
):
|
|
334
|
+
sub_tmp_path2 = tmp_path / "2"
|
|
335
|
+
sub_tmp_path2.mkdir()
|
|
336
|
+
self.runCheckpointVertexTest(
|
|
337
|
+
*createWorkflowFn(),
|
|
338
|
+
tmp_path=sub_tmp_path2,
|
|
339
|
+
checkpointJobService=TrivialService("LeafTestService"),
|
|
340
|
+
expectedException=JobGraphDeadlockException,
|
|
341
|
+
)
|
|
347
342
|
|
|
348
|
-
|
|
349
|
-
"Test checkpoint job that is not a leaf vertex due to the presence of a child job"
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
343
|
+
with subtests.test(
|
|
344
|
+
msg="Test checkpoint job that is not a leaf vertex due to the presence of a child job"
|
|
345
|
+
):
|
|
346
|
+
sub_tmp_path3 = tmp_path / "3"
|
|
347
|
+
sub_tmp_path3.mkdir()
|
|
348
|
+
self.runCheckpointVertexTest(
|
|
349
|
+
*createWorkflowFn(),
|
|
350
|
+
tmp_path=sub_tmp_path3,
|
|
351
|
+
checkpointJobChild=Job.wrapJobFn(simpleJobFn, "LeafTestChild"),
|
|
352
|
+
expectedException=JobGraphDeadlockException,
|
|
353
|
+
)
|
|
356
354
|
|
|
357
|
-
|
|
358
|
-
"Test checkpoint job that is not a leaf vertex due to the presence of a follow-on job"
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
355
|
+
with subtests.test(
|
|
356
|
+
msg="Test checkpoint job that is not a leaf vertex due to the presence of a follow-on job"
|
|
357
|
+
):
|
|
358
|
+
sub_tmp_path4 = tmp_path / "4"
|
|
359
|
+
sub_tmp_path4.mkdir()
|
|
360
|
+
self.runCheckpointVertexTest(
|
|
361
|
+
*createWorkflowFn(),
|
|
362
|
+
tmp_path=sub_tmp_path4,
|
|
363
|
+
checkpointJobFollowOn=Job.wrapJobFn(simpleJobFn, "LeafTestFollowOn"),
|
|
364
|
+
expectedException=JobGraphDeadlockException,
|
|
365
|
+
)
|
|
365
366
|
|
|
366
367
|
def runCheckpointVertexTest(
|
|
367
368
|
self,
|
|
368
|
-
workflowRootJob,
|
|
369
|
-
checkpointJob,
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
369
|
+
workflowRootJob: Job,
|
|
370
|
+
checkpointJob: Job,
|
|
371
|
+
tmp_path: Path,
|
|
372
|
+
checkpointJobService: Optional[Job.Service] = None,
|
|
373
|
+
checkpointJobChild: Optional[Job] = None,
|
|
374
|
+
checkpointJobFollowOn: Optional[Job] = None,
|
|
375
|
+
expectedException: Optional[type[Exception]] = None,
|
|
376
|
+
) -> None:
|
|
375
377
|
"""
|
|
376
378
|
Modifies the checkpoint job according to the given parameters
|
|
377
379
|
then runs the workflow, checking for the expected exception, if any.
|
|
378
380
|
"""
|
|
379
381
|
|
|
380
|
-
|
|
382
|
+
assert checkpointJob.checkpoint
|
|
381
383
|
|
|
382
384
|
if checkpointJobService is not None:
|
|
383
385
|
checkpointJob.addService(checkpointJobService)
|
|
@@ -387,38 +389,37 @@ class JobTest(ToilTest):
|
|
|
387
389
|
checkpointJob.addFollowOn(checkpointJobFollowOn)
|
|
388
390
|
|
|
389
391
|
# Run the workflow and check for the expected behavior
|
|
390
|
-
options = Job.Runner.getDefaultOptions(
|
|
392
|
+
options = Job.Runner.getDefaultOptions(tmp_path / "jobstore")
|
|
391
393
|
options.logLevel = "INFO"
|
|
392
394
|
if expectedException is None:
|
|
393
395
|
Job.Runner.startToil(workflowRootJob, options)
|
|
394
396
|
else:
|
|
395
|
-
|
|
397
|
+
with pytest.raises(expectedException):
|
|
396
398
|
Job.Runner.startToil(workflowRootJob, options)
|
|
397
|
-
self.fail("The expected exception was not thrown")
|
|
398
|
-
except expectedException as ex:
|
|
399
|
-
logger.debug("The expected exception was thrown: %s", repr(ex))
|
|
400
399
|
|
|
401
400
|
@slow
|
|
402
|
-
|
|
401
|
+
@pytest.mark.slow
|
|
402
|
+
def testEvaluatingRandomDAG(self, tmp_path: Path) -> None:
|
|
403
403
|
"""
|
|
404
404
|
Randomly generate test input then check that the job graph can be
|
|
405
405
|
run successfully, using the existence of promises
|
|
406
406
|
to validate the run.
|
|
407
407
|
"""
|
|
408
|
-
jobStore =
|
|
408
|
+
jobStore = tmp_path / "jobstore"
|
|
409
409
|
for test in range(5):
|
|
410
410
|
# Temporary file
|
|
411
|
-
tempDir =
|
|
411
|
+
tempDir = tmp_path / f"tempDir{test}"
|
|
412
|
+
tempDir.mkdir()
|
|
412
413
|
# Make a random DAG for the set of child edges
|
|
413
414
|
nodeNumber = random.choice(range(2, 8))
|
|
414
415
|
childEdges = self.makeRandomDAG(nodeNumber)
|
|
415
416
|
# Get an adjacency list representation and check is acyclic
|
|
416
417
|
adjacencyList = self.getAdjacencyList(nodeNumber, childEdges)
|
|
417
|
-
self.
|
|
418
|
+
assert self.isAcyclic(adjacencyList)
|
|
418
419
|
# Add in follow on edges - these are returned as a list, and as a set of augmented
|
|
419
420
|
# edges in the adjacency list
|
|
420
421
|
followOnEdges = self.addRandomFollowOnEdges(adjacencyList)
|
|
421
|
-
self.
|
|
422
|
+
assert self.isAcyclic(adjacencyList)
|
|
422
423
|
# Make the job graph
|
|
423
424
|
rootJob = self.makeJobGraph(nodeNumber, childEdges, followOnEdges, tempDir)
|
|
424
425
|
# Run the job graph
|
|
@@ -450,7 +451,7 @@ class JobTest(ToilTest):
|
|
|
450
451
|
except FailedJobsException as e:
|
|
451
452
|
numberOfFailedJobs = e.numberOfFailedJobs
|
|
452
453
|
if totalTrys > 32: # p(fail after this many restarts) ~= 0.5**32
|
|
453
|
-
|
|
454
|
+
pytest.fail("Exceeded a reasonable number of restarts")
|
|
454
455
|
totalTrys += 1
|
|
455
456
|
|
|
456
457
|
# For each job check it created a valid output file and add the ordering
|
|
@@ -458,9 +459,9 @@ class JobTest(ToilTest):
|
|
|
458
459
|
# so we can check they are compatible with the relationships defined by the job DAG.
|
|
459
460
|
ordering = None
|
|
460
461
|
for i in range(nodeNumber):
|
|
461
|
-
with
|
|
462
|
+
with (tempDir / str(i)).open() as fH:
|
|
462
463
|
ordering = list(map(int, fH.readline().split()))
|
|
463
|
-
|
|
464
|
+
assert int(ordering[-1]) == i
|
|
464
465
|
for j in ordering[:-1]:
|
|
465
466
|
adjacencyList[int(j)].add(i)
|
|
466
467
|
# Check the ordering retains an acyclic graph
|
|
@@ -469,16 +470,16 @@ class JobTest(ToilTest):
|
|
|
469
470
|
print("CHILD EDGES", childEdges)
|
|
470
471
|
print("FOLLOW ON EDGES", followOnEdges)
|
|
471
472
|
print("ADJACENCY LIST", adjacencyList)
|
|
472
|
-
self.
|
|
473
|
+
assert self.isAcyclic(adjacencyList)
|
|
473
474
|
|
|
474
475
|
@staticmethod
|
|
475
|
-
def getRandomEdge(nodeNumber):
|
|
476
|
+
def getRandomEdge(nodeNumber: int) -> tuple[int, int]:
|
|
476
477
|
assert nodeNumber > 1
|
|
477
478
|
fNode = random.choice(range(nodeNumber - 1))
|
|
478
479
|
return fNode, random.choice(range(fNode + 1, nodeNumber))
|
|
479
480
|
|
|
480
481
|
@staticmethod
|
|
481
|
-
def makeRandomDAG(nodeNumber):
|
|
482
|
+
def makeRandomDAG(nodeNumber: int) -> set[tuple[int, int]]:
|
|
482
483
|
"""
|
|
483
484
|
Makes a random dag with "nodeNumber" nodes in which all nodes are connected. Return value
|
|
484
485
|
is list of edges, each of form (a, b), where a and b are integers >= 0 < nodeNumber
|
|
@@ -492,56 +493,58 @@ class JobTest(ToilTest):
|
|
|
492
493
|
edges = {(random.choice(range(i)), i) for i in range(1, nodeNumber)}
|
|
493
494
|
# Add extra random edges until there are edgeNumber edges
|
|
494
495
|
while len(edges) < edgeNumber:
|
|
495
|
-
edges.add(
|
|
496
|
+
edges.add(TestJob.getRandomEdge(nodeNumber))
|
|
496
497
|
return edges
|
|
497
498
|
|
|
498
499
|
@staticmethod
|
|
499
|
-
def getAdjacencyList(
|
|
500
|
+
def getAdjacencyList(
|
|
501
|
+
nodeNumber: int, edges: set[tuple[int, int]]
|
|
502
|
+
) -> list[set[int]]:
|
|
500
503
|
"""
|
|
501
504
|
Make adjacency list representation of edges
|
|
502
505
|
"""
|
|
503
|
-
adjacencyList = [set() for _ in range(nodeNumber)]
|
|
506
|
+
adjacencyList: list[set[int]] = [set() for _ in range(nodeNumber)]
|
|
504
507
|
for fNode, tNode in edges:
|
|
505
508
|
adjacencyList[fNode].add(tNode)
|
|
506
509
|
return adjacencyList
|
|
507
510
|
|
|
508
|
-
def reachable(self, node, adjacencyList
|
|
511
|
+
def reachable(self, node: int, adjacencyList: list[set[int]]) -> set[int]:
|
|
509
512
|
"""
|
|
510
513
|
Find the set of nodes reachable from this node (including the node). Return is a set of
|
|
511
514
|
integers.
|
|
512
515
|
"""
|
|
513
|
-
visited = set()
|
|
516
|
+
visited: set[int] = set()
|
|
514
517
|
|
|
515
|
-
def dfs(fNode):
|
|
518
|
+
def dfs(fNode: int) -> None:
|
|
516
519
|
if fNode not in visited:
|
|
517
520
|
visited.add(fNode)
|
|
518
521
|
list(map(dfs, adjacencyList[fNode]))
|
|
519
|
-
if followOnAdjacencyList is not None:
|
|
520
|
-
list(map(dfs, followOnAdjacencyList[fNode]))
|
|
521
522
|
|
|
522
523
|
dfs(node)
|
|
523
524
|
return visited
|
|
524
525
|
|
|
525
|
-
def addRandomFollowOnEdges(
|
|
526
|
+
def addRandomFollowOnEdges(
|
|
527
|
+
self, childAdjacencyList: list[set[int]]
|
|
528
|
+
) -> set[tuple[int, int]]:
|
|
526
529
|
"""
|
|
527
530
|
Adds random follow on edges to the graph, represented as an adjacency list. The follow on
|
|
528
531
|
edges are returned as a set and their augmented edges are added to the adjacency list.
|
|
529
532
|
"""
|
|
530
533
|
|
|
531
|
-
def makeAugmentedAdjacencyList():
|
|
534
|
+
def makeAugmentedAdjacencyList() -> list[set[int]]:
|
|
532
535
|
augmentedAdjacencyList = [
|
|
533
536
|
childAdjacencyList[i].union(followOnAdjacencyList[i])
|
|
534
537
|
for i in range(len(childAdjacencyList))
|
|
535
538
|
]
|
|
536
539
|
|
|
537
|
-
def addImpliedEdges(node, followOnEdges):
|
|
540
|
+
def addImpliedEdges(node: int, followOnEdges: set[int]) -> None:
|
|
538
541
|
# Let node2 be a child of node or a successor of a child of node.
|
|
539
542
|
# For all node2 the following adds an edge to the augmented
|
|
540
543
|
# adjacency list from node2 to each followOn of node
|
|
541
544
|
|
|
542
|
-
visited = set()
|
|
545
|
+
visited: set[int] = set()
|
|
543
546
|
|
|
544
|
-
def f(node2):
|
|
547
|
+
def f(node2: int) -> None:
|
|
545
548
|
if node2 not in visited:
|
|
546
549
|
visited.add(node2)
|
|
547
550
|
for i in followOnEdges:
|
|
@@ -556,10 +559,10 @@ class JobTest(ToilTest):
|
|
|
556
559
|
return augmentedAdjacencyList
|
|
557
560
|
|
|
558
561
|
followOnEdges = set()
|
|
559
|
-
followOnAdjacencyList = [set() for i in childAdjacencyList]
|
|
562
|
+
followOnAdjacencyList: list[set[int]] = [set() for i in childAdjacencyList]
|
|
560
563
|
# Loop to create the follow on edges (try 1000 times)
|
|
561
564
|
while random.random() > 0.001:
|
|
562
|
-
fNode, tNode =
|
|
565
|
+
fNode, tNode = TestJob.getRandomEdge(len(childAdjacencyList))
|
|
563
566
|
|
|
564
567
|
# Make an adjacency list including augmented edges and proposed
|
|
565
568
|
# follow on edge
|
|
@@ -583,17 +586,22 @@ class JobTest(ToilTest):
|
|
|
583
586
|
return followOnEdges
|
|
584
587
|
|
|
585
588
|
def makeJobGraph(
|
|
586
|
-
self,
|
|
587
|
-
|
|
589
|
+
self,
|
|
590
|
+
nodeNumber: int,
|
|
591
|
+
childEdges: set[tuple[int, int]],
|
|
592
|
+
followOnEdges: set[tuple[int, int]],
|
|
593
|
+
outPath: Optional[Path],
|
|
594
|
+
addServices: bool = True,
|
|
595
|
+
) -> Job:
|
|
588
596
|
"""
|
|
589
597
|
Converts a DAG into a job graph. childEdges and followOnEdges are the lists of child and
|
|
590
598
|
followOn edges.
|
|
591
599
|
"""
|
|
592
600
|
# Map of jobs to the list of promises they have
|
|
593
|
-
jobsToPromisesMap = {}
|
|
601
|
+
jobsToPromisesMap: dict[FunctionWrappingJob, list[Promise]] = {}
|
|
594
602
|
|
|
595
|
-
def makeJob(string):
|
|
596
|
-
promises = []
|
|
603
|
+
def makeJob(string: str) -> FunctionWrappingJob:
|
|
604
|
+
promises: list[Promise] = []
|
|
597
605
|
job = Job.wrapFn(
|
|
598
606
|
fn2Test,
|
|
599
607
|
promises,
|
|
@@ -610,7 +618,9 @@ class JobTest(ToilTest):
|
|
|
610
618
|
jobs = [makeJob(str(i)) for i in range(nodeNumber)]
|
|
611
619
|
|
|
612
620
|
# Record predecessors for sampling
|
|
613
|
-
predecessors =
|
|
621
|
+
predecessors: dict[FunctionWrappingJob, list[FunctionWrappingJob]] = (
|
|
622
|
+
collections.defaultdict(list)
|
|
623
|
+
)
|
|
614
624
|
|
|
615
625
|
# Make the edges
|
|
616
626
|
for fNode, tNode in childEdges:
|
|
@@ -624,7 +634,9 @@ class JobTest(ToilTest):
|
|
|
624
634
|
jobsToRvs = {
|
|
625
635
|
job: (
|
|
626
636
|
job.addService(
|
|
627
|
-
TrivialService(
|
|
637
|
+
TrivialService(
|
|
638
|
+
cast(str, job.rv()), cores=0.1, memory="0.5G", disk="0.1G"
|
|
639
|
+
)
|
|
628
640
|
)
|
|
629
641
|
if addServices
|
|
630
642
|
else job.rv()
|
|
@@ -632,7 +644,7 @@ class JobTest(ToilTest):
|
|
|
632
644
|
for job in jobs
|
|
633
645
|
}
|
|
634
646
|
|
|
635
|
-
def getRandomPredecessor(job):
|
|
647
|
+
def getRandomPredecessor(job: FunctionWrappingJob) -> FunctionWrappingJob:
|
|
636
648
|
predecessor = random.choice(list(predecessors[job]))
|
|
637
649
|
while random.random() > 0.5 and len(predecessors[predecessor]) > 0:
|
|
638
650
|
predecessor = random.choice(list(predecessors[predecessor]))
|
|
@@ -648,13 +660,13 @@ class JobTest(ToilTest):
|
|
|
648
660
|
|
|
649
661
|
return jobs[0]
|
|
650
662
|
|
|
651
|
-
def isAcyclic(self, adjacencyList):
|
|
663
|
+
def isAcyclic(self, adjacencyList: list[set[int]]) -> bool:
|
|
652
664
|
"""
|
|
653
665
|
Returns true if there are any cycles in the graph, which is represented as an adjacency
|
|
654
666
|
list.
|
|
655
667
|
"""
|
|
656
668
|
|
|
657
|
-
def cyclic(fNode, visited, stack):
|
|
669
|
+
def cyclic(fNode: int, visited: set[int], stack: list[int]) -> Union[bool, int]:
|
|
658
670
|
if fNode not in visited:
|
|
659
671
|
visited.add(fNode)
|
|
660
672
|
assert fNode not in stack
|
|
@@ -665,18 +677,18 @@ class JobTest(ToilTest):
|
|
|
665
677
|
assert stack.pop() == fNode
|
|
666
678
|
return fNode in stack
|
|
667
679
|
|
|
668
|
-
visited = set()
|
|
680
|
+
visited: set[int] = set()
|
|
669
681
|
for i in range(len(adjacencyList)):
|
|
670
682
|
if cyclic(i, visited, []):
|
|
671
683
|
return False
|
|
672
684
|
return True
|
|
673
685
|
|
|
674
686
|
|
|
675
|
-
def simpleJobFn(job, value):
|
|
687
|
+
def simpleJobFn(job: ServiceHostJob, value: str) -> None:
|
|
676
688
|
job.fileStore.log_to_leader(value)
|
|
677
689
|
|
|
678
690
|
|
|
679
|
-
def fn1Test(string, outputFile):
|
|
691
|
+
def fn1Test(string: str, outputFile: FileDescriptorOrPath) -> str:
|
|
680
692
|
"""
|
|
681
693
|
Function appends the next character after the last character in the given
|
|
682
694
|
string to the string, writes the string to a file, and returns it. For
|
|
@@ -689,7 +701,7 @@ def fn1Test(string, outputFile):
|
|
|
689
701
|
return rV
|
|
690
702
|
|
|
691
703
|
|
|
692
|
-
def fn2Test(pStrings, s, outputFile):
|
|
704
|
+
def fn2Test(pStrings: list[str], s: str, outputFile: Path) -> str:
|
|
693
705
|
"""
|
|
694
706
|
Function concatenates the strings in pStrings and s, in that order, and writes the result to
|
|
695
707
|
the output file. Returns s.
|
|
@@ -699,7 +711,7 @@ def fn2Test(pStrings, s, outputFile):
|
|
|
699
711
|
return s
|
|
700
712
|
|
|
701
713
|
|
|
702
|
-
def trivialParent(job):
|
|
714
|
+
def trivialParent(job: Job) -> None:
|
|
703
715
|
strandedJob = JobFunctionWrappingJob(child)
|
|
704
716
|
failingJob = JobFunctionWrappingJob(errorChild)
|
|
705
717
|
|
|
@@ -708,7 +720,7 @@ def trivialParent(job):
|
|
|
708
720
|
failingJob.addChild(strandedJob)
|
|
709
721
|
|
|
710
722
|
|
|
711
|
-
def parent(job):
|
|
723
|
+
def parent(job: Job) -> None:
|
|
712
724
|
childJob = JobFunctionWrappingJob(child)
|
|
713
725
|
strandedJob = JobFunctionWrappingJob(child)
|
|
714
726
|
failingJob = JobFunctionWrappingJob(errorChild)
|
|
@@ -719,7 +731,7 @@ def parent(job):
|
|
|
719
731
|
failingJob.addChild(strandedJob)
|
|
720
732
|
|
|
721
733
|
|
|
722
|
-
def diamond(job):
|
|
734
|
+
def diamond(job: Job) -> None:
|
|
723
735
|
childJob = JobFunctionWrappingJob(child)
|
|
724
736
|
strandedJob = JobFunctionWrappingJob(child)
|
|
725
737
|
failingJob = JobFunctionWrappingJob(errorChild)
|
|
@@ -730,32 +742,28 @@ def diamond(job):
|
|
|
730
742
|
failingJob.addChild(strandedJob)
|
|
731
743
|
|
|
732
744
|
|
|
733
|
-
def child(job):
|
|
745
|
+
def child(job: Job) -> None:
|
|
734
746
|
assert job.cores is not None
|
|
735
747
|
assert job.disk is not None
|
|
736
748
|
assert job.memory is not None
|
|
737
749
|
assert job.preemptible is not None
|
|
738
750
|
|
|
739
751
|
|
|
740
|
-
def errorChild(job):
|
|
752
|
+
def errorChild(job: Job) -> NoReturn:
|
|
741
753
|
raise RuntimeError("Child failure")
|
|
742
754
|
|
|
743
755
|
|
|
744
756
|
class TrivialService(Job.Service):
|
|
745
|
-
def __init__(self, message, *args, **kwargs):
|
|
757
|
+
def __init__(self, message: str, *args: Any, **kwargs: Any) -> None:
|
|
746
758
|
"""Service that does nothing, used to check for deadlocks"""
|
|
747
759
|
Job.Service.__init__(self, *args, **kwargs)
|
|
748
760
|
self.message = message
|
|
749
761
|
|
|
750
|
-
def start(self, job):
|
|
762
|
+
def start(self, job: ServiceHostJob) -> str:
|
|
751
763
|
return self.message
|
|
752
764
|
|
|
753
|
-
def stop(self, job):
|
|
765
|
+
def stop(self, job: ServiceHostJob) -> None:
|
|
754
766
|
pass
|
|
755
767
|
|
|
756
|
-
def check(self):
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
if __name__ == "__main__":
|
|
761
|
-
unittest.main()
|
|
768
|
+
def check(self) -> bool:
|
|
769
|
+
return True
|