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/cwl/cwlTest.py
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
# See the License for the specific language governing permissions and
|
|
14
14
|
# limitations under the License.
|
|
15
|
+
from collections.abc import Generator
|
|
15
16
|
import json
|
|
16
17
|
import logging
|
|
17
18
|
import os
|
|
@@ -20,7 +21,6 @@ import shutil
|
|
|
20
21
|
import stat
|
|
21
22
|
import subprocess
|
|
22
23
|
import sys
|
|
23
|
-
import unittest
|
|
24
24
|
import uuid
|
|
25
25
|
import zipfile
|
|
26
26
|
from functools import partial
|
|
@@ -50,22 +50,23 @@ from toil.fileStores import FileID
|
|
|
50
50
|
from toil.fileStores.abstractFileStore import AbstractFileStore
|
|
51
51
|
from toil.lib.threading import cpu_count
|
|
52
52
|
from toil.test import (
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
53
|
+
get_data,
|
|
54
|
+
)
|
|
55
|
+
from toil.test import (
|
|
56
|
+
pslow as slow,
|
|
57
|
+
pneeds_docker as needs_docker,
|
|
58
|
+
pneeds_cwl as needs_cwl,
|
|
59
|
+
pneeds_aws_s3 as needs_aws_s3,
|
|
60
|
+
pneeds_docker_cuda as needs_docker_cuda,
|
|
61
|
+
pneeds_gridengine as needs_gridengine,
|
|
62
|
+
pneeds_kubernetes as needs_kubernetes,
|
|
63
|
+
pneeds_local_cuda as needs_local_cuda,
|
|
64
|
+
pneeds_lsf as needs_lsf,
|
|
65
|
+
pneeds_mesos as needs_mesos,
|
|
66
|
+
pneeds_online as needs_online,
|
|
67
|
+
pneeds_slurm as needs_slurm,
|
|
68
|
+
pneeds_torque as needs_torque,
|
|
69
|
+
pneeds_wes_server as needs_wes_server,
|
|
69
70
|
)
|
|
70
71
|
|
|
71
72
|
log = logging.getLogger(__name__)
|
|
@@ -222,72 +223,58 @@ def run_conformance_tests(
|
|
|
222
223
|
log.info("Unsuccessful return code is OK")
|
|
223
224
|
|
|
224
225
|
|
|
225
|
-
TesterFuncType = Callable[[
|
|
226
|
+
TesterFuncType = Callable[[Path, Path, "CWLObjectType", Path], None]
|
|
226
227
|
|
|
227
228
|
|
|
228
229
|
@needs_cwl
|
|
229
|
-
|
|
230
|
+
@pytest.mark.cwl
|
|
231
|
+
class TestCWLWorkflow:
|
|
230
232
|
"""
|
|
231
233
|
CWL tests included in Toil that don't involve the whole CWL conformance
|
|
232
234
|
test suite. Tests Toil-specific functions like URL types supported for
|
|
233
235
|
inputs.
|
|
234
236
|
"""
|
|
235
237
|
|
|
236
|
-
def setUp(self) -> None:
|
|
237
|
-
"""Runs anew before each test to create farm fresh temp dirs."""
|
|
238
|
-
self.outDir = f"/tmp/toil-cwl-test-{str(uuid.uuid4())}"
|
|
239
|
-
os.makedirs(self.outDir)
|
|
240
|
-
self.rootDir = self._projectRootPath()
|
|
241
|
-
self.jobStoreDir = f"./jobstore-{str(uuid.uuid4())}"
|
|
242
|
-
|
|
243
|
-
def tearDown(self) -> None:
|
|
244
|
-
"""Clean up outputs."""
|
|
245
|
-
if os.path.exists(self.outDir):
|
|
246
|
-
shutil.rmtree(self.outDir)
|
|
247
|
-
if os.path.exists(self.jobStoreDir):
|
|
248
|
-
shutil.rmtree(self.jobStoreDir)
|
|
249
|
-
unittest.TestCase.tearDown(self)
|
|
250
|
-
|
|
251
238
|
def test_cwl_cmdline_input(self) -> None:
|
|
252
239
|
"""
|
|
253
240
|
Test that running a CWL workflow with inputs specified on the command line passes.
|
|
254
241
|
"""
|
|
255
242
|
from toil.cwl import cwltoil
|
|
256
243
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
244
|
+
with get_data("test/cwl/conditional_wf.cwl") as cwlfile:
|
|
245
|
+
args = [str(cwlfile), "--message", "str", "--sleep", "2"]
|
|
246
|
+
st = StringIO()
|
|
247
|
+
# If the workflow runs, it must have had options
|
|
248
|
+
cwltoil.main(args, stdout=st)
|
|
262
249
|
|
|
263
250
|
def _tester(
|
|
264
251
|
self,
|
|
265
|
-
cwlfile:
|
|
266
|
-
jobfile:
|
|
252
|
+
cwlfile: Path,
|
|
253
|
+
jobfile: Path,
|
|
267
254
|
expect: "CWLObjectType",
|
|
268
|
-
|
|
255
|
+
outdir: Path,
|
|
269
256
|
out_name: str = "output",
|
|
270
|
-
|
|
257
|
+
main_args: Optional[list[str]] = None,
|
|
271
258
|
) -> None:
|
|
272
259
|
from toil.cwl import cwltoil
|
|
273
260
|
|
|
274
261
|
st = StringIO()
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
# Don't just dump output in the working directory.
|
|
278
|
-
main_args.extend(["--logDebug", "--outdir", self.outDir])
|
|
279
|
-
main_args.extend(
|
|
262
|
+
real_main_args = main_args or []
|
|
263
|
+
real_main_args.extend(
|
|
280
264
|
[
|
|
281
|
-
|
|
282
|
-
|
|
265
|
+
"--logDebug",
|
|
266
|
+
"--outdir",
|
|
267
|
+
str(outdir),
|
|
268
|
+
str(cwlfile),
|
|
269
|
+
str(jobfile),
|
|
283
270
|
]
|
|
284
271
|
)
|
|
285
|
-
cwltoil.main(
|
|
272
|
+
cwltoil.main(real_main_args, stdout=st)
|
|
286
273
|
out = json.loads(st.getvalue())
|
|
287
274
|
out.get(out_name, {}).pop("http://commonwl.org/cwltool#generation", None)
|
|
288
275
|
out.get(out_name, {}).pop("nameext", None)
|
|
289
276
|
out.get(out_name, {}).pop("nameroot", None)
|
|
290
|
-
|
|
277
|
+
assert out == expect
|
|
291
278
|
|
|
292
279
|
for k, v in expect.items():
|
|
293
280
|
if (
|
|
@@ -298,11 +285,11 @@ class CWLWorkflowTest(ToilTest):
|
|
|
298
285
|
):
|
|
299
286
|
# This is a top-level output file.
|
|
300
287
|
# None of our output files should be executable.
|
|
301
|
-
|
|
302
|
-
|
|
288
|
+
assert os.path.exists(v["path"]) is True
|
|
289
|
+
assert (os.stat(v["path"]).st_mode & stat.S_IXUSR) == 0
|
|
303
290
|
|
|
304
291
|
def _debug_worker_tester(
|
|
305
|
-
self, cwlfile:
|
|
292
|
+
self, cwlfile: Path, jobfile: Path, expect: "CWLObjectType", outdir: Path
|
|
306
293
|
) -> None:
|
|
307
294
|
from toil.cwl import cwltoil
|
|
308
295
|
|
|
@@ -311,9 +298,9 @@ class CWLWorkflowTest(ToilTest):
|
|
|
311
298
|
[
|
|
312
299
|
"--debugWorker",
|
|
313
300
|
"--outdir",
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
301
|
+
str(outdir),
|
|
302
|
+
str(cwlfile),
|
|
303
|
+
str(jobfile),
|
|
317
304
|
],
|
|
318
305
|
stdout=st,
|
|
319
306
|
)
|
|
@@ -321,172 +308,216 @@ class CWLWorkflowTest(ToilTest):
|
|
|
321
308
|
out["output"].pop("http://commonwl.org/cwltool#generation", None)
|
|
322
309
|
out["output"].pop("nameext", None)
|
|
323
310
|
out["output"].pop("nameroot", None)
|
|
324
|
-
|
|
311
|
+
assert out == expect
|
|
325
312
|
|
|
326
|
-
def revsort(
|
|
327
|
-
tester_fn
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
)
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
tester_fn
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
313
|
+
def revsort(
|
|
314
|
+
self, cwl_filename: str, tester_fn: TesterFuncType, out_dir: Path
|
|
315
|
+
) -> None:
|
|
316
|
+
with get_data(f"test/cwl/{cwl_filename}") as cwl_file:
|
|
317
|
+
with get_data("test/cwl/revsort-job.json") as job_file:
|
|
318
|
+
tester_fn(
|
|
319
|
+
cwl_file, job_file, self._expected_revsort_output(out_dir), out_dir
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def revsort_no_checksum(
|
|
323
|
+
self, cwl_filename: str, tester_fn: TesterFuncType, out_dir: Path
|
|
324
|
+
) -> None:
|
|
325
|
+
with get_data(f"test/cwl/{cwl_filename}") as cwl_file:
|
|
326
|
+
with get_data("test/cwl/revsort-job.json") as job_file:
|
|
327
|
+
tester_fn(
|
|
328
|
+
cwl_file,
|
|
329
|
+
job_file,
|
|
330
|
+
self._expected_revsort_nochecksum_output(out_dir),
|
|
331
|
+
out_dir,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def download(self, inputs: str, tester_fn: TesterFuncType, out_dir: Path) -> None:
|
|
335
|
+
with get_data(f"test/cwl/{inputs}") as input_location:
|
|
336
|
+
with get_data("test/cwl/download.cwl") as cwl_file:
|
|
337
|
+
tester_fn(
|
|
338
|
+
cwl_file,
|
|
339
|
+
input_location,
|
|
340
|
+
self._expected_download_output(out_dir),
|
|
341
|
+
out_dir,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
def load_contents(
|
|
345
|
+
self, inputs: str, tester_fn: TesterFuncType, out_dir: Path
|
|
346
|
+
) -> None:
|
|
347
|
+
with get_data(f"test/cwl/{inputs}") as input_location:
|
|
348
|
+
with get_data("test/cwl/load_contents.cwl") as cwl_file:
|
|
349
|
+
tester_fn(
|
|
350
|
+
cwl_file,
|
|
351
|
+
input_location,
|
|
352
|
+
self._expected_load_contents_output(out_dir),
|
|
353
|
+
out_dir,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
def download_directory(
|
|
357
|
+
self, inputs: str, tester_fn: TesterFuncType, out_dir: Path
|
|
358
|
+
) -> None:
|
|
359
|
+
with get_data(f"test/cwl/{inputs}") as input_location:
|
|
360
|
+
with get_data("test/cwl/download_directory.cwl") as cwl_file:
|
|
361
|
+
tester_fn(
|
|
362
|
+
cwl_file,
|
|
363
|
+
input_location,
|
|
364
|
+
self._expected_download_output(out_dir),
|
|
365
|
+
out_dir,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
def download_subdirectory(
|
|
369
|
+
self, inputs: str, tester_fn: TesterFuncType, out_dir: Path
|
|
370
|
+
) -> None:
|
|
371
|
+
with get_data(f"test/cwl/{inputs}") as input_location:
|
|
372
|
+
with get_data("test/cwl/download_subdirectory.cwl") as cwl_file:
|
|
373
|
+
tester_fn(
|
|
374
|
+
cwl_file,
|
|
375
|
+
input_location,
|
|
376
|
+
self._expected_download_output(out_dir),
|
|
377
|
+
out_dir,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
def test_mpi(self, tmp_path: Path) -> None:
|
|
373
381
|
from toil.cwl import cwltoil
|
|
374
382
|
|
|
375
383
|
stdout = StringIO()
|
|
376
|
-
|
|
377
|
-
"
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
384
|
+
with get_data("test/cwl/mock_mpi/fake_mpi.yml") as mpi_config_file:
|
|
385
|
+
with get_data("test/cwl/mpi_simple.cwl") as cwl_file:
|
|
386
|
+
with get_data("test/cwl/mock_mpi/fake_mpi_run.py") as fake_mpi_run:
|
|
387
|
+
main_args = [
|
|
388
|
+
"--logDebug",
|
|
389
|
+
"--outdir",
|
|
390
|
+
str(tmp_path),
|
|
391
|
+
"--enable-dev",
|
|
392
|
+
"--enable-ext",
|
|
393
|
+
"--mpi-config-file",
|
|
394
|
+
str(mpi_config_file),
|
|
395
|
+
str(cwl_file),
|
|
396
|
+
]
|
|
397
|
+
path = os.environ["PATH"]
|
|
398
|
+
os.environ["PATH"] = f"{path}:{fake_mpi_run.parent}"
|
|
399
|
+
cwltoil.main(main_args, stdout=stdout)
|
|
400
|
+
os.environ["PATH"] = path
|
|
401
|
+
stdout_text = stdout.getvalue()
|
|
402
|
+
assert "pids" in stdout_text
|
|
403
|
+
out = json.loads(stdout_text)
|
|
404
|
+
with open(
|
|
405
|
+
out.get("pids", {}).get("location")[len("file://") :]
|
|
406
|
+
) as f:
|
|
407
|
+
two_pids = [int(i) for i in f.read().split()]
|
|
408
|
+
assert len(two_pids) == 2
|
|
409
|
+
assert isinstance(two_pids[0], int)
|
|
410
|
+
assert isinstance(two_pids[1], int)
|
|
395
411
|
|
|
396
412
|
@needs_aws_s3
|
|
397
|
-
|
|
413
|
+
@pytest.mark.aws_s3
|
|
414
|
+
@pytest.mark.online
|
|
415
|
+
def test_s3_as_secondary_file(self, tmp_path: Path) -> None:
|
|
398
416
|
from toil.cwl import cwltoil
|
|
399
417
|
|
|
400
418
|
stdout = StringIO()
|
|
401
|
-
|
|
402
|
-
"
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
self.assertEqual(f.read().strip(), "When is s4 coming out?")
|
|
419
|
+
with get_data("test/cwl/s3_secondary_file.cwl") as cwl_file:
|
|
420
|
+
with get_data("test/cwl/s3_secondary_file.json") as inputs_file:
|
|
421
|
+
main_args = ["--outdir", str(tmp_path), str(cwl_file), str(inputs_file)]
|
|
422
|
+
cwltoil.main(main_args, stdout=stdout)
|
|
423
|
+
out = json.loads(stdout.getvalue())
|
|
424
|
+
assert (
|
|
425
|
+
out["output"]["checksum"]
|
|
426
|
+
== "sha1$d14dd02e354918b4776b941d154c18ebc15b9b38"
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
assert out["output"]["size"] == 24
|
|
430
|
+
with open(out["output"]["location"][len("file://") :]) as f:
|
|
431
|
+
assert f.read().strip() == "When is s4 coming out?"
|
|
415
432
|
|
|
416
|
-
|
|
417
|
-
|
|
433
|
+
@needs_docker
|
|
434
|
+
@pytest.mark.docker
|
|
435
|
+
@pytest.mark.online
|
|
436
|
+
def test_run_revsort(self, tmp_path: Path) -> None:
|
|
437
|
+
self.revsort("revsort.cwl", self._tester, tmp_path)
|
|
418
438
|
|
|
419
|
-
|
|
439
|
+
@needs_docker
|
|
440
|
+
@pytest.mark.docker
|
|
441
|
+
@pytest.mark.online
|
|
442
|
+
def test_run_revsort_nochecksum(self, tmp_path: Path) -> None:
|
|
420
443
|
self.revsort_no_checksum(
|
|
421
|
-
"revsort.cwl",
|
|
444
|
+
"revsort.cwl",
|
|
445
|
+
partial(self._tester, main_args=["--no-compute-checksum"]),
|
|
446
|
+
tmp_path,
|
|
422
447
|
)
|
|
423
448
|
|
|
424
|
-
def test_run_revsort_no_container(self) -> None:
|
|
449
|
+
def test_run_revsort_no_container(self, tmp_path: Path) -> None:
|
|
425
450
|
self.revsort(
|
|
426
|
-
"revsort.cwl", partial(self._tester, main_args=["--no-container"])
|
|
451
|
+
"revsort.cwl", partial(self._tester, main_args=["--no-container"]), tmp_path
|
|
427
452
|
)
|
|
428
453
|
|
|
429
|
-
|
|
430
|
-
|
|
454
|
+
@needs_docker
|
|
455
|
+
@pytest.mark.docker
|
|
456
|
+
@pytest.mark.online
|
|
457
|
+
def test_run_revsort2(self, tmp_path: Path) -> None:
|
|
458
|
+
self.revsort("revsort2.cwl", self._tester, tmp_path)
|
|
431
459
|
|
|
432
|
-
|
|
433
|
-
|
|
460
|
+
@needs_docker
|
|
461
|
+
@pytest.mark.docker
|
|
462
|
+
@pytest.mark.online
|
|
463
|
+
def test_run_revsort_debug_worker(self, tmp_path: Path) -> None:
|
|
464
|
+
self.revsort("revsort.cwl", self._debug_worker_tester, tmp_path)
|
|
465
|
+
|
|
466
|
+
@needs_docker
|
|
467
|
+
@pytest.mark.docker
|
|
468
|
+
@pytest.mark.online
|
|
469
|
+
def test_run_colon_output(self, tmp_path: Path) -> None:
|
|
470
|
+
with get_data("test/cwl/colon_test_output.cwl") as cwl_file:
|
|
471
|
+
with get_data("test/cwl/colon_test_output_job.yaml") as inputs_file:
|
|
472
|
+
self._tester(
|
|
473
|
+
cwl_file,
|
|
474
|
+
inputs_file,
|
|
475
|
+
self._expected_colon_output(tmp_path),
|
|
476
|
+
tmp_path,
|
|
477
|
+
out_name="result",
|
|
478
|
+
)
|
|
434
479
|
|
|
435
|
-
def test_run_colon_output(self) -> None:
|
|
436
|
-
self._tester(
|
|
437
|
-
"src/toil/test/cwl/colon_test_output.cwl",
|
|
438
|
-
"src/toil/test/cwl/colon_test_output_job.yaml",
|
|
439
|
-
self._expected_colon_output(self.outDir),
|
|
440
|
-
out_name="result",
|
|
441
|
-
)
|
|
442
|
-
|
|
443
480
|
@pytest.mark.integrative
|
|
444
|
-
@
|
|
445
|
-
|
|
481
|
+
@needs_docker
|
|
482
|
+
@pytest.mark.docker
|
|
483
|
+
@pytest.mark.online
|
|
484
|
+
def test_run_dockstore_trs(self, tmp_path: Path) -> None:
|
|
446
485
|
from toil.cwl import cwltoil
|
|
447
486
|
|
|
448
487
|
stdout = StringIO()
|
|
449
488
|
main_args = [
|
|
450
489
|
"--outdir",
|
|
451
|
-
|
|
490
|
+
str(tmp_path),
|
|
452
491
|
"#workflow/github.com/dockstore-testing/md5sum-checker:master",
|
|
453
|
-
"https://raw.githubusercontent.com/dockstore-testing/md5sum-checker/refs/heads/master/md5sum/md5sum-input-cwl.json"
|
|
492
|
+
"https://raw.githubusercontent.com/dockstore-testing/md5sum-checker/refs/heads/master/md5sum/md5sum-input-cwl.json",
|
|
454
493
|
]
|
|
455
494
|
cwltoil.main(main_args, stdout=stdout)
|
|
456
495
|
out = json.loads(stdout.getvalue())
|
|
457
496
|
with open(out.get("output_file", {}).get("location")[len("file://") :]) as f:
|
|
458
497
|
computed_hash = f.read().strip()
|
|
459
|
-
|
|
498
|
+
assert computed_hash == "00579a00e3e7fa0674428ac7049423e2"
|
|
460
499
|
|
|
461
|
-
def test_glob_dir_bypass_file_store(self) -> None:
|
|
500
|
+
def test_glob_dir_bypass_file_store(self, tmp_path: Path) -> None:
|
|
462
501
|
self.maxDiff = 1000
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
# Clean up anything we made in the current directory.
|
|
475
|
-
try:
|
|
476
|
-
shutil.rmtree(os.path.join(os.getcwd(), "shouldmake"))
|
|
477
|
-
except FileNotFoundError:
|
|
478
|
-
pass
|
|
479
|
-
|
|
480
|
-
def test_required_input_condition_protection(self) -> None:
|
|
502
|
+
with get_data("test/cwl/glob_dir.cwl") as cwl_file:
|
|
503
|
+
with get_data("test/cwl/empty.json") as inputs_file:
|
|
504
|
+
self._tester(
|
|
505
|
+
cwl_file,
|
|
506
|
+
inputs_file,
|
|
507
|
+
self._expected_glob_dir_output(tmp_path),
|
|
508
|
+
tmp_path,
|
|
509
|
+
main_args=["--bypass-file-store"],
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
def test_required_input_condition_protection(self, tmp_path: Path) -> None:
|
|
481
513
|
# This doesn't run containerized
|
|
482
|
-
|
|
483
|
-
"
|
|
484
|
-
|
|
485
|
-
{},
|
|
486
|
-
)
|
|
514
|
+
with get_data("test/cwl/not_run_required_input.cwl") as cwl_file:
|
|
515
|
+
with get_data("test/cwl/empty.json") as inputs_file:
|
|
516
|
+
self._tester(cwl_file, inputs_file, {}, tmp_path)
|
|
487
517
|
|
|
488
518
|
@needs_slurm
|
|
489
|
-
|
|
519
|
+
@pytest.mark.slurm
|
|
520
|
+
def test_slurm_node_memory(self, tmp_path: Path) -> None:
|
|
490
521
|
pass
|
|
491
522
|
|
|
492
523
|
# Run the workflow. This will either finish quickly and tell us the
|
|
@@ -497,28 +528,29 @@ class CWLWorkflowTest(ToilTest):
|
|
|
497
528
|
# And if we run out of time we need to stop the workflow gracefully and
|
|
498
529
|
# cancel the Slurm jobs.
|
|
499
530
|
|
|
500
|
-
main_args = [
|
|
501
|
-
f"--jobStore={self.jobStoreDir}",
|
|
502
|
-
# Avoid racing to toil kill before the jobstore is removed
|
|
503
|
-
"--clean=never",
|
|
504
|
-
"--batchSystem=slurm",
|
|
505
|
-
"--no-cwl-default-ram",
|
|
506
|
-
"--slurmDefaultAllMem=True",
|
|
507
|
-
"--outdir",
|
|
508
|
-
self.outDir,
|
|
509
|
-
os.path.join(self.rootDir, "src/toil/test/cwl/measure_default_memory.cwl"),
|
|
510
|
-
]
|
|
511
531
|
try:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
532
|
+
with get_data("test/cwl/measure_default_memory.cwl") as cwl_file:
|
|
533
|
+
main_args = [
|
|
534
|
+
f"--jobStore={str(tmp_path / 'jobStoreDir')}",
|
|
535
|
+
# Avoid racing to toil kill before the jobstore is removed
|
|
536
|
+
"--clean=never",
|
|
537
|
+
"--batchSystem=slurm",
|
|
538
|
+
"--no-cwl-default-ram",
|
|
539
|
+
"--slurmDefaultAllMem=True",
|
|
540
|
+
"--outdir",
|
|
541
|
+
str(tmp_path / "outdir"),
|
|
542
|
+
str(cwl_file),
|
|
543
|
+
]
|
|
544
|
+
log.debug("Start test workflow")
|
|
545
|
+
child = subprocess.Popen(
|
|
546
|
+
["toil-cwl-runner"] + main_args, stdout=subprocess.PIPE
|
|
547
|
+
)
|
|
548
|
+
output, _ = child.communicate(timeout=60)
|
|
517
549
|
except subprocess.TimeoutExpired:
|
|
518
550
|
# The job didn't finish quickly; presumably waiting for a full node.
|
|
519
551
|
# Stop the workflow
|
|
520
552
|
log.debug("Workflow might be waiting for a full node. Stop it.")
|
|
521
|
-
subprocess.check_call(["toil", "kill",
|
|
553
|
+
subprocess.check_call(["toil", "kill", str(tmp_path / "jobStoreDir")])
|
|
522
554
|
# Wait another little bit for it to clean up, making sure to collect output in case it is blocked on writing
|
|
523
555
|
child.communicate(timeout=20)
|
|
524
556
|
# Kill it off in case it is still running
|
|
@@ -538,113 +570,151 @@ class CWLWorkflowTest(ToilTest):
|
|
|
538
570
|
else:
|
|
539
571
|
result = int(memory_string)
|
|
540
572
|
# We should see more than the CWL default or the Toil default, assuming Slurm nodes of reasonable size (3 GiB).
|
|
541
|
-
|
|
573
|
+
assert result > (3 * 1024 * 1024)
|
|
542
574
|
|
|
543
575
|
@needs_aws_s3
|
|
544
|
-
|
|
545
|
-
|
|
576
|
+
@pytest.mark.aws_s3
|
|
577
|
+
@pytest.mark.online
|
|
578
|
+
def test_download_s3(self, tmp_path: Path) -> None:
|
|
579
|
+
self.download("download_s3.json", self._tester, tmp_path)
|
|
546
580
|
|
|
547
|
-
def test_download_http(self) -> None:
|
|
548
|
-
self.download("download_http.json", self._tester)
|
|
581
|
+
def test_download_http(self, tmp_path: Path) -> None:
|
|
582
|
+
self.download("download_http.json", self._tester, tmp_path)
|
|
549
583
|
|
|
550
|
-
def test_download_https(self) -> None:
|
|
551
|
-
self.download("download_https.json", self._tester)
|
|
584
|
+
def test_download_https(self, tmp_path: Path) -> None:
|
|
585
|
+
self.download("download_https.json", self._tester, tmp_path)
|
|
552
586
|
|
|
553
|
-
def test_download_https_reference(self) -> None:
|
|
587
|
+
def test_download_https_reference(self, tmp_path: Path) -> None:
|
|
554
588
|
self.download(
|
|
555
589
|
"download_https.json",
|
|
556
590
|
partial(self._tester, main_args=["--reference-inputs"]),
|
|
591
|
+
tmp_path,
|
|
557
592
|
)
|
|
558
593
|
|
|
559
|
-
def test_download_file(self) -> None:
|
|
560
|
-
self.download("download_file.json", self._tester)
|
|
594
|
+
def test_download_file(self, tmp_path: Path) -> None:
|
|
595
|
+
self.download("download_file.json", self._tester, tmp_path)
|
|
561
596
|
|
|
562
597
|
@needs_aws_s3
|
|
563
|
-
|
|
564
|
-
|
|
598
|
+
@pytest.mark.aws_s3
|
|
599
|
+
@pytest.mark.online
|
|
600
|
+
def test_download_directory_s3(self, tmp_path: Path) -> None:
|
|
601
|
+
self.download_directory("download_directory_s3.json", self._tester, tmp_path)
|
|
565
602
|
|
|
566
603
|
@needs_aws_s3
|
|
567
|
-
|
|
604
|
+
@pytest.mark.aws_s3
|
|
605
|
+
@pytest.mark.online
|
|
606
|
+
def test_download_directory_s3_reference(self, tmp_path: Path) -> None:
|
|
568
607
|
self.download_directory(
|
|
569
608
|
"download_directory_s3.json",
|
|
570
609
|
partial(self._tester, main_args=["--reference-inputs"]),
|
|
610
|
+
tmp_path,
|
|
571
611
|
)
|
|
572
612
|
|
|
573
|
-
def test_download_directory_file(self) -> None:
|
|
574
|
-
self.download_directory("download_directory_file.json", self._tester)
|
|
613
|
+
def test_download_directory_file(self, tmp_path: Path) -> None:
|
|
614
|
+
self.download_directory("download_directory_file.json", self._tester, tmp_path)
|
|
575
615
|
|
|
576
616
|
@needs_aws_s3
|
|
577
|
-
|
|
578
|
-
|
|
617
|
+
@pytest.mark.aws_s3
|
|
618
|
+
@pytest.mark.online
|
|
619
|
+
def test_download_subdirectory_s3(self, tmp_path: Path) -> None:
|
|
620
|
+
self.download_subdirectory(
|
|
621
|
+
"download_subdirectory_s3.json", self._tester, tmp_path
|
|
622
|
+
)
|
|
579
623
|
|
|
580
|
-
def test_download_subdirectory_file(self) -> None:
|
|
581
|
-
self.download_subdirectory(
|
|
624
|
+
def test_download_subdirectory_file(self, tmp_path: Path) -> None:
|
|
625
|
+
self.download_subdirectory(
|
|
626
|
+
"download_subdirectory_file.json", self._tester, tmp_path
|
|
627
|
+
)
|
|
582
628
|
|
|
583
629
|
# We also want to make sure we can run a bare tool with loadContents on the inputs, which requires accessing the input data early in the leader.
|
|
584
630
|
|
|
585
631
|
@needs_aws_s3
|
|
586
|
-
|
|
587
|
-
|
|
632
|
+
@pytest.mark.aws_s3
|
|
633
|
+
@pytest.mark.online
|
|
634
|
+
def test_load_contents_s3(self, tmp_path: Path) -> None:
|
|
635
|
+
self.load_contents("download_s3.json", self._tester, tmp_path)
|
|
588
636
|
|
|
589
|
-
def test_load_contents_http(self) -> None:
|
|
590
|
-
self.load_contents("download_http.json", self._tester)
|
|
637
|
+
def test_load_contents_http(self, tmp_path: Path) -> None:
|
|
638
|
+
self.load_contents("download_http.json", self._tester, tmp_path)
|
|
591
639
|
|
|
592
|
-
def test_load_contents_https(self) -> None:
|
|
593
|
-
self.load_contents("download_https.json", self._tester)
|
|
640
|
+
def test_load_contents_https(self, tmp_path: Path) -> None:
|
|
641
|
+
self.load_contents("download_https.json", self._tester, tmp_path)
|
|
594
642
|
|
|
595
|
-
def test_load_contents_file(self) -> None:
|
|
596
|
-
self.load_contents("download_file.json", self._tester)
|
|
643
|
+
def test_load_contents_file(self, tmp_path: Path) -> None:
|
|
644
|
+
self.load_contents("download_file.json", self._tester, tmp_path)
|
|
597
645
|
|
|
598
646
|
@slow
|
|
599
647
|
@pytest.mark.integrative
|
|
600
|
-
@
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
"
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
648
|
+
@pytest.mark.slow
|
|
649
|
+
@pytest.mark.skip("Fails too often due to remote service")
|
|
650
|
+
def test_bioconda(self, tmp_path: Path) -> None:
|
|
651
|
+
with get_data("test/cwl/seqtk_seq.cwl") as cwl_file:
|
|
652
|
+
with get_data("test/cwl/seqtk_seq_job.json") as inputs_file:
|
|
653
|
+
self._tester(
|
|
654
|
+
cwl_file,
|
|
655
|
+
inputs_file,
|
|
656
|
+
self._expected_seqtk_output(tmp_path),
|
|
657
|
+
tmp_path,
|
|
658
|
+
main_args=["--beta-conda-dependencies"],
|
|
659
|
+
out_name="output1",
|
|
660
|
+
)
|
|
609
661
|
|
|
610
662
|
@needs_docker
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
663
|
+
@pytest.mark.docker
|
|
664
|
+
@pytest.mark.online
|
|
665
|
+
def test_default_args(self, tmp_path: Path) -> None:
|
|
666
|
+
with get_data("test/cwl/seqtk_seq.cwl") as cwl_file:
|
|
667
|
+
with get_data("test/cwl/seqtk_seq_job.json") as inputs_file:
|
|
668
|
+
self._tester(
|
|
669
|
+
cwl_file,
|
|
670
|
+
inputs_file,
|
|
671
|
+
self._expected_seqtk_output(tmp_path),
|
|
672
|
+
tmp_path,
|
|
673
|
+
main_args=[
|
|
674
|
+
"--default-container",
|
|
675
|
+
"quay.io/biocontainers/seqtk:1.4--he4a0461_1",
|
|
676
|
+
],
|
|
677
|
+
out_name="output1",
|
|
678
|
+
)
|
|
622
679
|
|
|
623
680
|
@needs_docker
|
|
681
|
+
@pytest.mark.docker
|
|
624
682
|
@pytest.mark.integrative
|
|
625
|
-
@
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
"
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
683
|
+
@pytest.mark.online
|
|
684
|
+
@pytest.mark.skip(reason="Fails too often due to remote service")
|
|
685
|
+
def test_biocontainers(self, tmp_path: Path) -> None:
|
|
686
|
+
with get_data("test/cwl/seqtk_seq.cwl") as cwl_file:
|
|
687
|
+
with get_data("test/cwl/seqtk_seq_job.json") as inputs_file:
|
|
688
|
+
self._tester(
|
|
689
|
+
cwl_file,
|
|
690
|
+
inputs_file,
|
|
691
|
+
self._expected_seqtk_output(tmp_path),
|
|
692
|
+
tmp_path,
|
|
693
|
+
main_args=["--beta-use-biocontainers"],
|
|
694
|
+
out_name="output1",
|
|
695
|
+
)
|
|
634
696
|
|
|
635
697
|
@needs_docker
|
|
636
698
|
@needs_docker_cuda
|
|
637
699
|
@needs_local_cuda
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
700
|
+
@pytest.mark.docker
|
|
701
|
+
@pytest.mark.online
|
|
702
|
+
@pytest.mark.docker_cuda
|
|
703
|
+
@pytest.mark.local_cuda
|
|
704
|
+
def test_cuda(self, tmp_path: Path) -> None:
|
|
705
|
+
with get_data("test/cwl/nvidia_smi.cwl") as cwl_file:
|
|
706
|
+
with get_data("test/cwl/empty.json") as inputs_file:
|
|
707
|
+
self._tester(
|
|
708
|
+
cwl_file,
|
|
709
|
+
inputs_file,
|
|
710
|
+
{},
|
|
711
|
+
tmp_path,
|
|
712
|
+
out_name="result",
|
|
713
|
+
)
|
|
645
714
|
|
|
646
715
|
@slow
|
|
647
|
-
|
|
716
|
+
@pytest.mark.slow
|
|
717
|
+
def test_restart(self, tmp_path: Path) -> None:
|
|
648
718
|
"""
|
|
649
719
|
Enable restarts with toil-cwl-runner -- run failing test, re-run correct test.
|
|
650
720
|
Only implemented for single machine.
|
|
@@ -652,195 +722,203 @@ class CWLWorkflowTest(ToilTest):
|
|
|
652
722
|
log.info("Running CWL Test Restart. Expecting failure, then success.")
|
|
653
723
|
from toil.cwl import cwltoil
|
|
654
724
|
|
|
655
|
-
outDir =
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
["
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
725
|
+
outDir = tmp_path / "outDir"
|
|
726
|
+
outDir.mkdir()
|
|
727
|
+
jobStore = tmp_path / "jobStore"
|
|
728
|
+
with get_data("test/cwl/revsort.cwl") as cwl_file:
|
|
729
|
+
with get_data("test/cwl/revsort-job.json") as job_file:
|
|
730
|
+
cmd = [
|
|
731
|
+
"--outdir",
|
|
732
|
+
str(outDir),
|
|
733
|
+
"--jobStore",
|
|
734
|
+
str(jobStore),
|
|
735
|
+
"--no-container",
|
|
736
|
+
str(cwl_file),
|
|
737
|
+
str(job_file),
|
|
738
|
+
]
|
|
739
|
+
|
|
740
|
+
# create a fake rev bin that actually points to the "date" binary
|
|
741
|
+
cal_path = [
|
|
742
|
+
d
|
|
743
|
+
for d in os.environ["PATH"].split(":")
|
|
744
|
+
if os.path.exists(os.path.join(d, "date"))
|
|
745
|
+
][-1]
|
|
746
|
+
os.symlink(
|
|
747
|
+
os.path.realpath(os.path.join(cal_path, "date")), outDir / "rev"
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
def path_with_bogus_rev() -> str:
|
|
751
|
+
# append to the front of the PATH so that we check there first
|
|
752
|
+
return f"{str(outDir)}:" + os.environ["PATH"]
|
|
753
|
+
|
|
754
|
+
orig_path = os.environ["PATH"]
|
|
755
|
+
# Force a failure by trying to use an incorrect version of `rev` from the PATH
|
|
756
|
+
os.environ["PATH"] = path_with_bogus_rev()
|
|
757
|
+
try:
|
|
758
|
+
subprocess.check_output(
|
|
759
|
+
["toil-cwl-runner"] + cmd,
|
|
760
|
+
env=os.environ.copy(),
|
|
761
|
+
stderr=subprocess.STDOUT,
|
|
762
|
+
)
|
|
763
|
+
pytest.fail("Expected problem job with incorrect PATH did not fail")
|
|
764
|
+
except subprocess.CalledProcessError:
|
|
765
|
+
pass
|
|
766
|
+
# Finish the job with a correct PATH
|
|
767
|
+
os.environ["PATH"] = orig_path
|
|
768
|
+
cmd.insert(0, "--restart")
|
|
769
|
+
cwltoil.main(cmd)
|
|
770
|
+
# Should fail because previous job completed successfully
|
|
771
|
+
try:
|
|
772
|
+
subprocess.check_output(
|
|
773
|
+
["toil-cwl-runner"] + cmd,
|
|
774
|
+
env=os.environ.copy(),
|
|
775
|
+
stderr=subprocess.STDOUT,
|
|
776
|
+
)
|
|
777
|
+
pytest.fail("Restart with missing directory did not fail")
|
|
778
|
+
except subprocess.CalledProcessError:
|
|
779
|
+
pass
|
|
780
|
+
|
|
781
|
+
def test_caching(self, tmp_path: Path) -> None:
|
|
707
782
|
log.info("Running CWL caching test.")
|
|
708
783
|
from toil.cwl import cwltoil
|
|
709
784
|
|
|
710
|
-
outDir =
|
|
711
|
-
cacheDir =
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
785
|
+
outDir = tmp_path / "outDir"
|
|
786
|
+
cacheDir = tmp_path / "cacheDir"
|
|
787
|
+
log_path = outDir / "log"
|
|
788
|
+
|
|
789
|
+
with get_data("test/cwl/revsort.cwl") as cwl_file:
|
|
790
|
+
with get_data("test/cwl/revsort-job.json") as job_file:
|
|
791
|
+
cmd = [
|
|
792
|
+
"--outdir",
|
|
793
|
+
str(outDir),
|
|
794
|
+
"--jobStore",
|
|
795
|
+
str(tmp_path / "jobStore"),
|
|
796
|
+
"--clean=always",
|
|
797
|
+
"--no-container",
|
|
798
|
+
"--cachedir",
|
|
799
|
+
str(cacheDir),
|
|
800
|
+
str(cwl_file),
|
|
801
|
+
str(job_file),
|
|
802
|
+
]
|
|
803
|
+
st = StringIO()
|
|
804
|
+
ret = cwltoil.main(cmd, stdout=st)
|
|
805
|
+
assert ret == 0
|
|
806
|
+
# cwltool hashes certain steps into directories, ensure it exists
|
|
807
|
+
# since cwltool caches per task and revsort has 2 cwl tasks, there should be 2 directories and 2 status files
|
|
808
|
+
assert sum(1 for _ in cacheDir.iterdir()) == 4
|
|
809
|
+
|
|
810
|
+
# Rerun the workflow to ensure there is a cache hit and that we don't rerun the tools
|
|
811
|
+
st = StringIO()
|
|
812
|
+
cmd = [
|
|
813
|
+
"--writeLogsFromAllJobs=True",
|
|
814
|
+
"--writeLogs",
|
|
815
|
+
str(log_path),
|
|
816
|
+
] + cmd
|
|
817
|
+
ret = cwltoil.main(cmd, stdout=st)
|
|
818
|
+
assert ret == 0
|
|
819
|
+
|
|
820
|
+
# Ensure all of the worker logs are using their cached outputs
|
|
821
|
+
for file in log_path.iterdir():
|
|
822
|
+
assert "Using cached output" in file.read_text(encoding="utf-8")
|
|
749
823
|
|
|
750
824
|
@needs_aws_s3
|
|
751
|
-
|
|
825
|
+
@pytest.mark.aws_s3
|
|
826
|
+
@pytest.mark.online
|
|
827
|
+
def test_streamable(
|
|
828
|
+
self, tmp_path: Path, extra_args: Optional[list[str]] = None
|
|
829
|
+
) -> None:
|
|
752
830
|
"""
|
|
753
831
|
Test that a file with 'streamable'=True is a named pipe.
|
|
754
832
|
This is a CWL1.2 feature.
|
|
755
833
|
"""
|
|
756
|
-
cwlfile = "src/toil/test/cwl/stream.cwl"
|
|
757
|
-
jobfile = "src/toil/test/cwl/stream.json"
|
|
758
|
-
out_name = "output"
|
|
759
|
-
jobstore = f"--jobStore=aws:us-west-1:toil-stream-{uuid.uuid4()}"
|
|
760
|
-
from toil.cwl import cwltoil
|
|
761
|
-
|
|
762
834
|
st = StringIO()
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
"
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
835
|
+
outDir = tmp_path / "outDir"
|
|
836
|
+
with get_data("test/cwl/stream.cwl") as cwlfile:
|
|
837
|
+
with get_data("test/cwl/stream.json") as jobfile:
|
|
838
|
+
out_name = "output"
|
|
839
|
+
jobstore = f"--jobStore=aws:us-west-1:toil-stream-{uuid.uuid4()}"
|
|
840
|
+
from toil.cwl import cwltoil
|
|
841
|
+
|
|
842
|
+
args = [
|
|
843
|
+
"--logDebug",
|
|
844
|
+
"--outdir",
|
|
845
|
+
str(outDir),
|
|
846
|
+
jobstore,
|
|
847
|
+
str(cwlfile),
|
|
848
|
+
str(jobfile),
|
|
849
|
+
]
|
|
850
|
+
if extra_args:
|
|
851
|
+
args = extra_args + args
|
|
852
|
+
log.info("Run CWL run: %s", " ".join(args))
|
|
853
|
+
cwltoil.main(args, stdout=st)
|
|
775
854
|
out = json.loads(st.getvalue())
|
|
776
855
|
out[out_name].pop("http://commonwl.org/cwltool#generation", None)
|
|
777
856
|
out[out_name].pop("nameext", None)
|
|
778
857
|
out[out_name].pop("nameroot", None)
|
|
779
|
-
|
|
858
|
+
assert out == self._expected_streaming_output(outDir)
|
|
780
859
|
with open(out[out_name]["location"][len("file://") :]) as f:
|
|
781
|
-
|
|
860
|
+
assert f.read().strip() == "When is s4 coming out?"
|
|
782
861
|
|
|
783
862
|
@needs_aws_s3
|
|
784
|
-
|
|
863
|
+
@pytest.mark.aws_s3
|
|
864
|
+
@pytest.mark.online
|
|
865
|
+
def test_streamable_reference(self, tmp_path: Path) -> None:
|
|
785
866
|
"""
|
|
786
867
|
Test that a streamable file is a stream even when passed around by URI.
|
|
787
868
|
"""
|
|
788
|
-
self.test_streamable(extra_args=["--reference-inputs"])
|
|
869
|
+
self.test_streamable(tmp_path=tmp_path, extra_args=["--reference-inputs"])
|
|
789
870
|
|
|
790
|
-
def test_preemptible(self) -> None:
|
|
871
|
+
def test_preemptible(self, tmp_path: Path) -> None:
|
|
791
872
|
"""
|
|
792
873
|
Tests that the http://arvados.org/cwl#UsePreemptible extension is supported.
|
|
793
874
|
"""
|
|
794
|
-
cwlfile = "src/toil/test/cwl/preemptible.cwl"
|
|
795
|
-
jobfile = "src/toil/test/cwl/empty.json"
|
|
796
|
-
out_name = "output"
|
|
797
875
|
from toil.cwl import cwltoil
|
|
798
876
|
|
|
799
877
|
st = StringIO()
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
878
|
+
out_name = "output"
|
|
879
|
+
with get_data("test/cwl/preemptible.cwl") as cwlfile:
|
|
880
|
+
with get_data("test/cwl/empty.json") as jobfile:
|
|
881
|
+
args = [
|
|
882
|
+
"--outdir",
|
|
883
|
+
str(tmp_path / "outDir"),
|
|
884
|
+
str(cwlfile),
|
|
885
|
+
str(jobfile),
|
|
886
|
+
]
|
|
887
|
+
cwltoil.main(args, stdout=st)
|
|
807
888
|
out = json.loads(st.getvalue())
|
|
808
889
|
out[out_name].pop("http://commonwl.org/cwltool#generation", None)
|
|
809
890
|
out[out_name].pop("nameext", None)
|
|
810
891
|
out[out_name].pop("nameroot", None)
|
|
811
892
|
with open(out[out_name]["location"][len("file://") :]) as f:
|
|
812
|
-
|
|
893
|
+
assert f.read().strip() == "hello"
|
|
813
894
|
|
|
814
|
-
def test_preemptible_expression(self) -> None:
|
|
895
|
+
def test_preemptible_expression(self, tmp_path: Path) -> None:
|
|
815
896
|
"""
|
|
816
897
|
Tests that the http://arvados.org/cwl#UsePreemptible extension is validated.
|
|
817
898
|
"""
|
|
818
|
-
cwlfile = "src/toil/test/cwl/preemptible_expression.cwl"
|
|
819
|
-
jobfile = "src/toil/test/cwl/preemptible_expression.json"
|
|
820
899
|
from toil.cwl import cwltoil
|
|
821
900
|
|
|
822
901
|
st = StringIO()
|
|
823
|
-
|
|
824
|
-
"
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
902
|
+
with get_data("test/cwl/preemptible_expression.cwl") as cwlfile:
|
|
903
|
+
with get_data("test/cwl/preemptible_expression.json") as jobfile:
|
|
904
|
+
args = [
|
|
905
|
+
"--outdir",
|
|
906
|
+
str(tmp_path),
|
|
907
|
+
str(cwlfile),
|
|
908
|
+
str(jobfile),
|
|
909
|
+
]
|
|
910
|
+
with pytest.raises(
|
|
911
|
+
ValidationException, match=re.escape("expressions are not allowed")
|
|
912
|
+
):
|
|
913
|
+
cwltoil.main(args, stdout=st)
|
|
835
914
|
|
|
836
915
|
@staticmethod
|
|
837
|
-
def _expected_seqtk_output(outDir:
|
|
838
|
-
path =
|
|
839
|
-
loc = "file://" + path
|
|
916
|
+
def _expected_seqtk_output(outDir: Path) -> "CWLObjectType":
|
|
917
|
+
path = outDir / "out"
|
|
840
918
|
return {
|
|
841
919
|
"output1": {
|
|
842
|
-
"location":
|
|
843
|
-
"path": path,
|
|
920
|
+
"location": path.as_uri(),
|
|
921
|
+
"path": str(path),
|
|
844
922
|
"checksum": "sha1$322e001e5a99f19abdce9f02ad0f02a17b5066c2",
|
|
845
923
|
"basename": "out",
|
|
846
924
|
"class": "File",
|
|
@@ -849,13 +927,12 @@ class CWLWorkflowTest(ToilTest):
|
|
|
849
927
|
}
|
|
850
928
|
|
|
851
929
|
@staticmethod
|
|
852
|
-
def _expected_revsort_output(outDir:
|
|
853
|
-
path =
|
|
854
|
-
loc = "file://" + path
|
|
930
|
+
def _expected_revsort_output(outDir: Path) -> "CWLObjectType":
|
|
931
|
+
path = outDir / "output.txt"
|
|
855
932
|
return {
|
|
856
933
|
"output": {
|
|
857
|
-
"location":
|
|
858
|
-
"path": path,
|
|
934
|
+
"location": path.as_uri(),
|
|
935
|
+
"path": str(path),
|
|
859
936
|
"basename": "output.txt",
|
|
860
937
|
"size": 1111,
|
|
861
938
|
"class": "File",
|
|
@@ -864,13 +941,12 @@ class CWLWorkflowTest(ToilTest):
|
|
|
864
941
|
}
|
|
865
942
|
|
|
866
943
|
@staticmethod
|
|
867
|
-
def _expected_revsort_nochecksum_output(outDir:
|
|
868
|
-
path =
|
|
869
|
-
loc = "file://" + path
|
|
944
|
+
def _expected_revsort_nochecksum_output(outDir: Path) -> "CWLObjectType":
|
|
945
|
+
path = outDir / "output.txt"
|
|
870
946
|
return {
|
|
871
947
|
"output": {
|
|
872
|
-
"location":
|
|
873
|
-
"path": path,
|
|
948
|
+
"location": path.as_uri(),
|
|
949
|
+
"path": str(path),
|
|
874
950
|
"basename": "output.txt",
|
|
875
951
|
"size": 1111,
|
|
876
952
|
"class": "File",
|
|
@@ -878,30 +954,29 @@ class CWLWorkflowTest(ToilTest):
|
|
|
878
954
|
}
|
|
879
955
|
|
|
880
956
|
@staticmethod
|
|
881
|
-
def _expected_download_output(outDir:
|
|
882
|
-
path =
|
|
883
|
-
loc = "file://" + path
|
|
957
|
+
def _expected_download_output(outDir: Path) -> "CWLObjectType":
|
|
958
|
+
path = outDir / "output.txt"
|
|
884
959
|
return {
|
|
885
960
|
"output": {
|
|
886
|
-
"location":
|
|
961
|
+
"location": path.as_uri(),
|
|
887
962
|
"basename": "output.txt",
|
|
888
963
|
"size": 0,
|
|
889
964
|
"class": "File",
|
|
890
965
|
"checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
|
891
|
-
"path": path,
|
|
966
|
+
"path": str(path),
|
|
892
967
|
}
|
|
893
968
|
}
|
|
894
969
|
|
|
895
970
|
@staticmethod
|
|
896
|
-
def _expected_glob_dir_output(out_dir:
|
|
897
|
-
dir_path =
|
|
898
|
-
dir_loc =
|
|
899
|
-
file_path =
|
|
900
|
-
file_loc =
|
|
971
|
+
def _expected_glob_dir_output(out_dir: Path) -> "CWLObjectType":
|
|
972
|
+
dir_path = out_dir / "shouldmake"
|
|
973
|
+
dir_loc = dir_path.as_uri()
|
|
974
|
+
file_path = dir_path / "test.txt"
|
|
975
|
+
file_loc = file_path.as_uri()
|
|
901
976
|
return {
|
|
902
977
|
"shouldmake": {
|
|
903
978
|
"location": dir_loc,
|
|
904
|
-
"path": dir_path,
|
|
979
|
+
"path": str(dir_path),
|
|
905
980
|
"basename": "shouldmake",
|
|
906
981
|
"nameroot": "shouldmake",
|
|
907
982
|
"nameext": "",
|
|
@@ -910,7 +985,7 @@ class CWLWorkflowTest(ToilTest):
|
|
|
910
985
|
{
|
|
911
986
|
"class": "File",
|
|
912
987
|
"location": file_loc,
|
|
913
|
-
"path": file_path,
|
|
988
|
+
"path": str(file_path),
|
|
914
989
|
"basename": "test.txt",
|
|
915
990
|
"checksum": "sha1$da39a3ee5e6b4b0d3255bfef95601890afd80709",
|
|
916
991
|
"size": 0,
|
|
@@ -922,7 +997,7 @@ class CWLWorkflowTest(ToilTest):
|
|
|
922
997
|
}
|
|
923
998
|
|
|
924
999
|
@classmethod
|
|
925
|
-
def _expected_load_contents_output(cls, out_dir:
|
|
1000
|
+
def _expected_load_contents_output(cls, out_dir: Path) -> "CWLObjectType":
|
|
926
1001
|
"""
|
|
927
1002
|
Generate the putput we expect from load_contents.cwl, when sending
|
|
928
1003
|
output files to the given directory.
|
|
@@ -932,13 +1007,15 @@ class CWLWorkflowTest(ToilTest):
|
|
|
932
1007
|
return expected
|
|
933
1008
|
|
|
934
1009
|
@staticmethod
|
|
935
|
-
def _expected_colon_output(outDir:
|
|
936
|
-
path =
|
|
937
|
-
loc = "file://" + os.path.join(
|
|
1010
|
+
def _expected_colon_output(outDir: Path) -> "CWLObjectType":
|
|
1011
|
+
path = outDir / "A:Gln2Cys_result"
|
|
1012
|
+
loc = "file://" + os.path.join(
|
|
1013
|
+
outDir, "A%3AGln2Cys_result"
|
|
1014
|
+
) # not using .as_uri to ensure the expected escaping
|
|
938
1015
|
return {
|
|
939
1016
|
"result": {
|
|
940
1017
|
"location": loc,
|
|
941
|
-
"path": path,
|
|
1018
|
+
"path": str(path),
|
|
942
1019
|
"basename": "A:Gln2Cys_result",
|
|
943
1020
|
"class": "Directory",
|
|
944
1021
|
"listing": [
|
|
@@ -956,13 +1033,12 @@ class CWLWorkflowTest(ToilTest):
|
|
|
956
1033
|
}
|
|
957
1034
|
}
|
|
958
1035
|
|
|
959
|
-
def _expected_streaming_output(self, outDir:
|
|
960
|
-
path =
|
|
961
|
-
loc = "file://" + path
|
|
1036
|
+
def _expected_streaming_output(self, outDir: Path) -> "CWLObjectType":
|
|
1037
|
+
path = outDir / "output.txt"
|
|
962
1038
|
return {
|
|
963
1039
|
"output": {
|
|
964
|
-
"location":
|
|
965
|
-
"path": path,
|
|
1040
|
+
"location": path.as_uri(),
|
|
1041
|
+
"path": str(path),
|
|
966
1042
|
"basename": "output.txt",
|
|
967
1043
|
"size": 24,
|
|
968
1044
|
"class": "File",
|
|
@@ -970,102 +1046,100 @@ class CWLWorkflowTest(ToilTest):
|
|
|
970
1046
|
}
|
|
971
1047
|
}
|
|
972
1048
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1049
|
+
@needs_docker
|
|
1050
|
+
@pytest.mark.docker
|
|
1051
|
+
@pytest.mark.online
|
|
1052
|
+
def test_missing_import(self, tmp_path: Path) -> None:
|
|
1053
|
+
with get_data("test/cwl/revsort.cwl") as cwl_file:
|
|
1054
|
+
with get_data("test/cwl/revsort-job-missing.json") as inputs_file:
|
|
1055
|
+
cmd = [
|
|
1056
|
+
"toil-cwl-runner",
|
|
1057
|
+
f"--outdir={str(tmp_path)}",
|
|
1058
|
+
"--clean=always",
|
|
1059
|
+
str(cwl_file),
|
|
1060
|
+
str(inputs_file),
|
|
1061
|
+
]
|
|
1062
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
1063
|
+
# Make sure that the missing file is mentioned in the log so the user knows
|
|
1064
|
+
assert p.returncode == 1, p.stderr
|
|
1065
|
+
assert "missing.txt" in p.stderr
|
|
987
1066
|
|
|
988
1067
|
@needs_aws_s3
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
cwlfile = "src/toil/test/cwl/optional-file.cwl"
|
|
994
|
-
jobfile = "src/toil/test/cwl/optional-file-exists.json"
|
|
995
|
-
|
|
996
|
-
args = [
|
|
997
|
-
os.path.join(self.rootDir, cwlfile),
|
|
998
|
-
os.path.join(self.rootDir, jobfile),
|
|
999
|
-
f"--outdir={out_dir}"
|
|
1000
|
-
]
|
|
1068
|
+
@pytest.mark.aws_s3
|
|
1069
|
+
@pytest.mark.online
|
|
1070
|
+
def test_optional_secondary_files_exists(self, tmp_path: Path) -> None:
|
|
1001
1071
|
from toil.cwl import cwltoil
|
|
1002
1072
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1073
|
+
with get_data("test/cwl/optional-file.cwl") as cwlfile:
|
|
1074
|
+
with get_data("test/cwl/optional-file-exists.json") as jobfile:
|
|
1075
|
+
args = [str(cwlfile), str(jobfile), f"--outdir={str(tmp_path)}"]
|
|
1076
|
+
ret = cwltoil.main(args)
|
|
1077
|
+
assert ret == 0
|
|
1078
|
+
assert (tmp_path / "wdl_templates_old.zip").exists()
|
|
1006
1079
|
|
|
1007
1080
|
@needs_aws_s3
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1081
|
+
@pytest.mark.aws_s3
|
|
1082
|
+
@pytest.mark.online
|
|
1083
|
+
def test_optional_secondary_files_missing(self, tmp_path: Path) -> None:
|
|
1084
|
+
from toil.cwl import cwltoil
|
|
1011
1085
|
|
|
1012
|
-
|
|
1013
|
-
|
|
1086
|
+
with get_data("test/cwl/optional-file.cwl") as cwlfile:
|
|
1087
|
+
with get_data("test/cwl/optional-file-missing.json") as jobfile:
|
|
1088
|
+
args = [str(cwlfile), str(jobfile), f"--outdir={str(tmp_path)}"]
|
|
1089
|
+
ret = cwltoil.main(args)
|
|
1090
|
+
assert ret == 0
|
|
1091
|
+
assert not (tmp_path / "hello_old.zip").exists()
|
|
1014
1092
|
|
|
1015
|
-
args = [
|
|
1016
|
-
os.path.join(self.rootDir, cwlfile),
|
|
1017
|
-
os.path.join(self.rootDir, jobfile),
|
|
1018
|
-
f"--outdir={out_dir}"
|
|
1019
|
-
]
|
|
1020
|
-
from toil.cwl import cwltoil
|
|
1021
1093
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1094
|
+
@pytest.fixture(scope="function")
|
|
1095
|
+
def cwl_v1_0_spec(tmp_path: Path) -> Generator[Path]:
|
|
1096
|
+
# The latest cwl git commit hash from https://github.com/common-workflow-language/common-workflow-language.
|
|
1097
|
+
# Update it to get the latest tests.
|
|
1098
|
+
testhash = (
|
|
1099
|
+
"6a955874ade22080b8ef962b4e0d6e408112c1ef" # Date: Tue Dec 16 2020 8:43pm PST
|
|
1100
|
+
)
|
|
1101
|
+
url = (
|
|
1102
|
+
"https://github.com/common-workflow-language/common-workflow-language/archive/%s.zip"
|
|
1103
|
+
% testhash
|
|
1104
|
+
)
|
|
1105
|
+
urlretrieve(url, "spec.zip")
|
|
1106
|
+
with zipfile.ZipFile("spec.zip", "r") as z:
|
|
1107
|
+
z.extractall()
|
|
1108
|
+
shutil.move("common-workflow-language-%s" % testhash, str(tmp_path))
|
|
1109
|
+
os.remove("spec.zip")
|
|
1110
|
+
try:
|
|
1111
|
+
yield tmp_path / ("common-workflow-language-%s" % testhash)
|
|
1112
|
+
finally:
|
|
1113
|
+
pass # no cleanup
|
|
1114
|
+
|
|
1025
1115
|
|
|
1026
1116
|
@needs_cwl
|
|
1027
1117
|
@needs_online
|
|
1028
|
-
|
|
1118
|
+
@pytest.mark.cwl
|
|
1119
|
+
@pytest.mark.online
|
|
1120
|
+
class TestCWLv10:
|
|
1029
1121
|
"""
|
|
1030
1122
|
Run the CWL 1.0 conformance tests in various environments.
|
|
1031
1123
|
"""
|
|
1032
1124
|
|
|
1033
|
-
def setUp(self) -> None:
|
|
1034
|
-
"""Runs anew before each test to create farm fresh temp dirs."""
|
|
1035
|
-
self.outDir = f"/tmp/toil-cwl-test-{str(uuid.uuid4())}"
|
|
1036
|
-
os.makedirs(self.outDir)
|
|
1037
|
-
self.rootDir = self._projectRootPath()
|
|
1038
|
-
self.cwlSpec = os.path.join(self.rootDir, "src/toil/test/cwl/spec")
|
|
1039
|
-
self.workDir = os.path.join(self.cwlSpec, "v1.0")
|
|
1040
|
-
# The latest cwl git commit hash from https://github.com/common-workflow-language/common-workflow-language.
|
|
1041
|
-
# Update it to get the latest tests.
|
|
1042
|
-
testhash = "6a955874ade22080b8ef962b4e0d6e408112c1ef" # Date: Tue Dec 16 2020 8:43pm PST
|
|
1043
|
-
url = (
|
|
1044
|
-
"https://github.com/common-workflow-language/common-workflow-language/archive/%s.zip"
|
|
1045
|
-
% testhash
|
|
1046
|
-
)
|
|
1047
|
-
if not os.path.exists(self.cwlSpec):
|
|
1048
|
-
urlretrieve(url, "spec.zip")
|
|
1049
|
-
with zipfile.ZipFile("spec.zip", "r") as z:
|
|
1050
|
-
z.extractall()
|
|
1051
|
-
shutil.move("common-workflow-language-%s" % testhash, self.cwlSpec)
|
|
1052
|
-
os.remove("spec.zip")
|
|
1053
|
-
|
|
1054
|
-
def tearDown(self) -> None:
|
|
1055
|
-
"""Clean up outputs."""
|
|
1056
|
-
if os.path.exists(self.outDir):
|
|
1057
|
-
shutil.rmtree(self.outDir)
|
|
1058
|
-
unittest.TestCase.tearDown(self)
|
|
1059
|
-
|
|
1060
1125
|
@slow
|
|
1126
|
+
@needs_docker
|
|
1127
|
+
@pytest.mark.slow
|
|
1128
|
+
@pytest.mark.docker
|
|
1129
|
+
@pytest.mark.online
|
|
1061
1130
|
@pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
|
|
1062
|
-
def test_run_conformance_with_caching(self) -> None:
|
|
1063
|
-
self.test_run_conformance(caching=True)
|
|
1131
|
+
def test_run_conformance_with_caching(self, cwl_v1_0_spec: Path) -> None:
|
|
1132
|
+
self.test_run_conformance(cwl_v1_0_spec, caching=True)
|
|
1064
1133
|
|
|
1065
1134
|
@slow
|
|
1135
|
+
@needs_docker
|
|
1136
|
+
@pytest.mark.slow
|
|
1137
|
+
@pytest.mark.docker
|
|
1138
|
+
@pytest.mark.online
|
|
1066
1139
|
@pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
|
|
1067
1140
|
def test_run_conformance(
|
|
1068
1141
|
self,
|
|
1142
|
+
cwl_v1_0_spec: Path,
|
|
1069
1143
|
batchSystem: Optional[str] = None,
|
|
1070
1144
|
caching: bool = False,
|
|
1071
1145
|
selected_tests: Optional[str] = None,
|
|
@@ -1073,8 +1147,8 @@ class CWLv10Test(ToilTest):
|
|
|
1073
1147
|
extra_args: Optional[list[str]] = None,
|
|
1074
1148
|
) -> None:
|
|
1075
1149
|
run_conformance_tests(
|
|
1076
|
-
workDir=
|
|
1077
|
-
yml="conformance_test_v1.0.yaml",
|
|
1150
|
+
workDir=str(cwl_v1_0_spec / "v1.0"),
|
|
1151
|
+
yml=str(cwl_v1_0_spec / "v1.0" / "conformance_test_v1.0.yaml"),
|
|
1078
1152
|
caching=caching,
|
|
1079
1153
|
batchSystem=batchSystem,
|
|
1080
1154
|
selected_tests=selected_tests,
|
|
@@ -1084,38 +1158,66 @@ class CWLv10Test(ToilTest):
|
|
|
1084
1158
|
|
|
1085
1159
|
@slow
|
|
1086
1160
|
@needs_lsf
|
|
1087
|
-
@
|
|
1088
|
-
|
|
1089
|
-
|
|
1161
|
+
@pytest.mark.slow
|
|
1162
|
+
@pytest.mark.lsf
|
|
1163
|
+
@pytest.mark.skip("Not run")
|
|
1164
|
+
def test_lsf_cwl_conformance(
|
|
1165
|
+
self, cwl_v1_0_spec: Path, caching: bool = False
|
|
1166
|
+
) -> None:
|
|
1167
|
+
self.test_run_conformance(cwl_v1_0_spec, batchSystem="lsf", caching=caching)
|
|
1090
1168
|
|
|
1091
1169
|
@slow
|
|
1092
1170
|
@needs_slurm
|
|
1093
|
-
@
|
|
1094
|
-
|
|
1095
|
-
|
|
1171
|
+
@pytest.mark.slow
|
|
1172
|
+
@pytest.mark.slurm
|
|
1173
|
+
@pytest.mark.skip("Not run")
|
|
1174
|
+
def test_slurm_cwl_conformance(
|
|
1175
|
+
self, cwl_v1_0_spec: Path, caching: bool = False
|
|
1176
|
+
) -> None:
|
|
1177
|
+
self.test_run_conformance(cwl_v1_0_spec, batchSystem="slurm", caching=caching)
|
|
1096
1178
|
|
|
1097
1179
|
@slow
|
|
1098
1180
|
@needs_torque
|
|
1099
|
-
@
|
|
1100
|
-
|
|
1101
|
-
|
|
1181
|
+
@pytest.mark.slow
|
|
1182
|
+
@pytest.mark.torque
|
|
1183
|
+
@pytest.mark.skip("Not run")
|
|
1184
|
+
def test_torque_cwl_conformance(
|
|
1185
|
+
self, cwl_v1_0_spec: Path, caching: bool = False
|
|
1186
|
+
) -> None:
|
|
1187
|
+
self.test_run_conformance(cwl_v1_0_spec, batchSystem="torque", caching=caching)
|
|
1102
1188
|
|
|
1103
1189
|
@slow
|
|
1104
1190
|
@needs_gridengine
|
|
1105
|
-
@
|
|
1106
|
-
|
|
1107
|
-
|
|
1191
|
+
@pytest.mark.slow
|
|
1192
|
+
@pytest.mark.gridengine
|
|
1193
|
+
@pytest.mark.skip("Not run")
|
|
1194
|
+
def test_gridengine_cwl_conformance(
|
|
1195
|
+
self, cwl_v1_0_spec: Path, caching: bool = False
|
|
1196
|
+
) -> None:
|
|
1197
|
+
self.test_run_conformance(
|
|
1198
|
+
cwl_v1_0_spec, batchSystem="grid_engine", caching=caching
|
|
1199
|
+
)
|
|
1108
1200
|
|
|
1109
1201
|
@slow
|
|
1110
1202
|
@needs_mesos
|
|
1111
|
-
@
|
|
1112
|
-
|
|
1113
|
-
|
|
1203
|
+
@pytest.mark.slow
|
|
1204
|
+
@pytest.mark.mesos
|
|
1205
|
+
@pytest.mark.skip("Not run")
|
|
1206
|
+
def test_mesos_cwl_conformance(
|
|
1207
|
+
self, cwl_v1_0_spec: Path, caching: bool = False
|
|
1208
|
+
) -> None:
|
|
1209
|
+
self.test_run_conformance(cwl_v1_0_spec, batchSystem="mesos", caching=caching)
|
|
1114
1210
|
|
|
1115
1211
|
@slow
|
|
1116
1212
|
@needs_kubernetes
|
|
1117
|
-
|
|
1213
|
+
@pytest.mark.slow
|
|
1214
|
+
@pytest.mark.kubernetes
|
|
1215
|
+
@pytest.mark.online
|
|
1216
|
+
def test_kubernetes_cwl_conformance(
|
|
1217
|
+
self, cwl_v1_0_spec: Path, caching: bool = False
|
|
1218
|
+
) -> None:
|
|
1118
1219
|
self.test_run_conformance(
|
|
1220
|
+
cwl_v1_0_spec,
|
|
1119
1221
|
caching=caching,
|
|
1120
1222
|
batchSystem="kubernetes",
|
|
1121
1223
|
extra_args=["--retryCount=3"],
|
|
@@ -1127,82 +1229,98 @@ class CWLv10Test(ToilTest):
|
|
|
1127
1229
|
|
|
1128
1230
|
@slow
|
|
1129
1231
|
@needs_lsf
|
|
1130
|
-
@
|
|
1131
|
-
|
|
1132
|
-
|
|
1232
|
+
@pytest.mark.slow
|
|
1233
|
+
@pytest.mark.lsf
|
|
1234
|
+
@pytest.mark.skip(reason="Not run")
|
|
1235
|
+
def test_lsf_cwl_conformance_with_caching(self, cwl_v1_0_spec: Path) -> None:
|
|
1236
|
+
self.test_lsf_cwl_conformance(cwl_v1_0_spec, caching=True)
|
|
1133
1237
|
|
|
1134
1238
|
@slow
|
|
1135
1239
|
@needs_slurm
|
|
1136
|
-
@
|
|
1137
|
-
|
|
1138
|
-
|
|
1240
|
+
@pytest.mark.slow
|
|
1241
|
+
@pytest.mark.slurm
|
|
1242
|
+
@pytest.mark.skip(reason="Not run")
|
|
1243
|
+
def test_slurm_cwl_conformance_with_caching(self, cwl_v1_0_spec: Path) -> None:
|
|
1244
|
+
self.test_slurm_cwl_conformance(cwl_v1_0_spec, caching=True)
|
|
1139
1245
|
|
|
1140
1246
|
@slow
|
|
1141
1247
|
@needs_torque
|
|
1142
|
-
@
|
|
1143
|
-
|
|
1144
|
-
|
|
1248
|
+
@pytest.mark.slow
|
|
1249
|
+
@pytest.mark.torque
|
|
1250
|
+
@pytest.mark.skip(reason="Not run")
|
|
1251
|
+
def test_torque_cwl_conformance_with_caching(self, cwl_v1_0_spec: Path) -> None:
|
|
1252
|
+
self.test_torque_cwl_conformance(cwl_v1_0_spec, caching=True)
|
|
1145
1253
|
|
|
1146
1254
|
@slow
|
|
1147
1255
|
@needs_gridengine
|
|
1148
|
-
@
|
|
1149
|
-
|
|
1150
|
-
|
|
1256
|
+
@pytest.mark.slow
|
|
1257
|
+
@pytest.mark.gridengine
|
|
1258
|
+
@pytest.mark.skip(reason="Not run")
|
|
1259
|
+
def test_gridengine_cwl_conformance_with_caching(self, cwl_v1_0_spec: Path) -> None:
|
|
1260
|
+
self.test_gridengine_cwl_conformance(cwl_v1_0_spec, caching=True)
|
|
1151
1261
|
|
|
1152
1262
|
@slow
|
|
1153
1263
|
@needs_mesos
|
|
1154
|
-
@
|
|
1155
|
-
|
|
1156
|
-
|
|
1264
|
+
@pytest.mark.slow
|
|
1265
|
+
@pytest.mark.mesos
|
|
1266
|
+
@pytest.mark.skip(reason="Not run")
|
|
1267
|
+
def test_mesos_cwl_conformance_with_caching(self, cwl_v1_0_spec: Path) -> None:
|
|
1268
|
+
self.test_mesos_cwl_conformance(cwl_v1_0_spec, caching=True)
|
|
1157
1269
|
|
|
1158
1270
|
@slow
|
|
1159
1271
|
@needs_kubernetes
|
|
1160
|
-
|
|
1161
|
-
|
|
1272
|
+
@pytest.mark.slow
|
|
1273
|
+
@pytest.mark.kubernetes
|
|
1274
|
+
@pytest.mark.online
|
|
1275
|
+
def test_kubernetes_cwl_conformance_with_caching(self, cwl_v1_0_spec: Path) -> None:
|
|
1276
|
+
self.test_kubernetes_cwl_conformance(cwl_v1_0_spec, caching=True)
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
@pytest.fixture(scope="function")
|
|
1280
|
+
def cwl_v1_1_spec(tmp_path: Path) -> Generator[Path]:
|
|
1281
|
+
# The latest cwl git commit hash from https://github.com/common-workflow-language/cwl-v1.1
|
|
1282
|
+
# Update it to get the latest tests.
|
|
1283
|
+
testhash = "664835e83eb5e57eee18a04ce7b05fb9d70d77b7"
|
|
1284
|
+
url = (
|
|
1285
|
+
"https://github.com/common-workflow-language/cwl-v1.1/archive/%s.zip" % testhash
|
|
1286
|
+
)
|
|
1287
|
+
urlretrieve(url, "spec.zip")
|
|
1288
|
+
with zipfile.ZipFile("spec.zip", "r") as z:
|
|
1289
|
+
z.extractall()
|
|
1290
|
+
shutil.move("cwl-v1.1-%s" % testhash, str(tmp_path))
|
|
1291
|
+
os.remove("spec.zip")
|
|
1292
|
+
try:
|
|
1293
|
+
yield tmp_path / ("cwl-v1.1-%s" % testhash)
|
|
1294
|
+
finally:
|
|
1295
|
+
pass # no cleanup
|
|
1162
1296
|
|
|
1163
1297
|
|
|
1164
1298
|
@needs_cwl
|
|
1165
1299
|
@needs_online
|
|
1166
|
-
|
|
1300
|
+
@pytest.mark.cwl
|
|
1301
|
+
@pytest.mark.online
|
|
1302
|
+
class TestCWLv11:
|
|
1167
1303
|
"""
|
|
1168
1304
|
Run the CWL 1.1 conformance tests in various environments.
|
|
1169
1305
|
"""
|
|
1170
1306
|
|
|
1171
|
-
rootDir: str
|
|
1172
|
-
cwlSpec: str
|
|
1173
|
-
test_yaml: str
|
|
1174
|
-
|
|
1175
|
-
@classmethod
|
|
1176
|
-
def setUpClass(cls) -> None:
|
|
1177
|
-
"""Runs anew before each test."""
|
|
1178
|
-
cls.rootDir = cls._projectRootPath()
|
|
1179
|
-
cls.cwlSpec = os.path.join(cls.rootDir, "src/toil/test/cwl/spec_v11")
|
|
1180
|
-
cls.test_yaml = os.path.join(cls.cwlSpec, "conformance_tests.yaml")
|
|
1181
|
-
# TODO: Use a commit zip in case someone decides to rewrite master's history?
|
|
1182
|
-
url = "https://github.com/common-workflow-language/cwl-v1.1.git"
|
|
1183
|
-
commit = "664835e83eb5e57eee18a04ce7b05fb9d70d77b7"
|
|
1184
|
-
p = subprocess.Popen(
|
|
1185
|
-
f"git clone {url} {cls.cwlSpec} && cd {cls.cwlSpec} && git checkout {commit}",
|
|
1186
|
-
shell=True,
|
|
1187
|
-
)
|
|
1188
|
-
p.communicate()
|
|
1189
|
-
|
|
1190
|
-
def tearDown(self) -> None:
|
|
1191
|
-
"""Clean up outputs."""
|
|
1192
|
-
unittest.TestCase.tearDown(self)
|
|
1193
|
-
|
|
1194
1307
|
@slow
|
|
1308
|
+
@needs_docker
|
|
1309
|
+
@pytest.mark.slow
|
|
1310
|
+
@pytest.mark.docker
|
|
1311
|
+
@pytest.mark.online
|
|
1195
1312
|
@pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
|
|
1196
1313
|
def test_run_conformance(
|
|
1197
1314
|
self,
|
|
1315
|
+
cwl_v1_1_spec: Path,
|
|
1198
1316
|
caching: bool = False,
|
|
1199
1317
|
batchSystem: Optional[str] = None,
|
|
1200
1318
|
skipped_tests: Optional[str] = None,
|
|
1201
1319
|
extra_args: Optional[list[str]] = None,
|
|
1202
1320
|
) -> None:
|
|
1203
1321
|
run_conformance_tests(
|
|
1204
|
-
workDir=
|
|
1205
|
-
yml=
|
|
1322
|
+
workDir=str(cwl_v1_1_spec),
|
|
1323
|
+
yml=str(cwl_v1_1_spec / "conformance_tests.yaml"),
|
|
1206
1324
|
caching=caching,
|
|
1207
1325
|
batchSystem=batchSystem,
|
|
1208
1326
|
skipped_tests=skipped_tests,
|
|
@@ -1210,14 +1328,24 @@ class CWLv11Test(ToilTest):
|
|
|
1210
1328
|
)
|
|
1211
1329
|
|
|
1212
1330
|
@slow
|
|
1331
|
+
@needs_docker
|
|
1332
|
+
@pytest.mark.slow
|
|
1333
|
+
@pytest.mark.docker
|
|
1334
|
+
@pytest.mark.online
|
|
1213
1335
|
@pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
|
|
1214
|
-
def test_run_conformance_with_caching(self) -> None:
|
|
1215
|
-
self.test_run_conformance(caching=True)
|
|
1336
|
+
def test_run_conformance_with_caching(self, cwl_v1_1_spec: Path) -> None:
|
|
1337
|
+
self.test_run_conformance(cwl_v1_1_spec, caching=True)
|
|
1216
1338
|
|
|
1217
1339
|
@slow
|
|
1218
1340
|
@needs_kubernetes
|
|
1219
|
-
|
|
1341
|
+
@pytest.mark.slow
|
|
1342
|
+
@pytest.mark.kubernetes
|
|
1343
|
+
@pytest.mark.online
|
|
1344
|
+
def test_kubernetes_cwl_conformance(
|
|
1345
|
+
self, cwl_v1_1_spec: Path, caching: bool = False
|
|
1346
|
+
) -> None:
|
|
1220
1347
|
self.test_run_conformance(
|
|
1348
|
+
cwl_v1_1_spec,
|
|
1221
1349
|
batchSystem="kubernetes",
|
|
1222
1350
|
extra_args=["--retryCount=3"],
|
|
1223
1351
|
# These tests don't work with
|
|
@@ -1229,44 +1357,50 @@ class CWLv11Test(ToilTest):
|
|
|
1229
1357
|
|
|
1230
1358
|
@slow
|
|
1231
1359
|
@needs_kubernetes
|
|
1232
|
-
|
|
1233
|
-
|
|
1360
|
+
@pytest.mark.slow
|
|
1361
|
+
@pytest.mark.kubernetes
|
|
1362
|
+
@pytest.mark.online
|
|
1363
|
+
def test_kubernetes_cwl_conformance_with_caching(self, cwl_v1_1_spec: Path) -> None:
|
|
1364
|
+
self.test_kubernetes_cwl_conformance(cwl_v1_1_spec, caching=True)
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
@pytest.fixture(scope="function")
|
|
1368
|
+
def cwl_v1_2_spec(tmp_path: Path) -> Generator[Path]:
|
|
1369
|
+
# The latest cwl git commit hash from https://github.com/common-workflow-language/cwl-v1.2
|
|
1370
|
+
# Update it to get the latest tests.
|
|
1371
|
+
testhash = "0d538a0dbc5518f3c6083ce4571926f65cb84f76"
|
|
1372
|
+
url = (
|
|
1373
|
+
"https://github.com/common-workflow-language/cwl-v1.2/archive/%s.zip" % testhash
|
|
1374
|
+
)
|
|
1375
|
+
urlretrieve(url, "spec.zip")
|
|
1376
|
+
with zipfile.ZipFile("spec.zip", "r") as z:
|
|
1377
|
+
z.extractall()
|
|
1378
|
+
shutil.move("cwl-v1.2-%s" % testhash, str(tmp_path))
|
|
1379
|
+
os.remove("spec.zip")
|
|
1380
|
+
try:
|
|
1381
|
+
yield tmp_path / ("cwl-v1.2-%s" % testhash)
|
|
1382
|
+
finally:
|
|
1383
|
+
pass # no cleanup
|
|
1234
1384
|
|
|
1235
1385
|
|
|
1236
1386
|
@needs_cwl
|
|
1237
1387
|
@needs_online
|
|
1238
|
-
|
|
1388
|
+
@pytest.mark.cwl
|
|
1389
|
+
@pytest.mark.online
|
|
1390
|
+
class TestCWLv12:
|
|
1239
1391
|
"""
|
|
1240
1392
|
Run the CWL 1.2 conformance tests in various environments.
|
|
1241
1393
|
"""
|
|
1242
1394
|
|
|
1243
|
-
rootDir: str
|
|
1244
|
-
cwlSpec: str
|
|
1245
|
-
test_yaml: str
|
|
1246
|
-
|
|
1247
|
-
@classmethod
|
|
1248
|
-
def setUpClass(cls) -> None:
|
|
1249
|
-
"""Runs anew before each test."""
|
|
1250
|
-
cls.rootDir = cls._projectRootPath()
|
|
1251
|
-
cls.cwlSpec = os.path.join(cls.rootDir, "src/toil/test/cwl/spec_v12")
|
|
1252
|
-
cls.test_yaml = os.path.join(cls.cwlSpec, "conformance_tests.yaml")
|
|
1253
|
-
# TODO: Use a commit zip in case someone decides to rewrite master's history?
|
|
1254
|
-
url = "https://github.com/common-workflow-language/cwl-v1.2.git"
|
|
1255
|
-
commit = "0d538a0dbc5518f3c6083ce4571926f65cb84f76"
|
|
1256
|
-
p = subprocess.Popen(
|
|
1257
|
-
f"git clone {url} {cls.cwlSpec} && cd {cls.cwlSpec} && git checkout {commit}",
|
|
1258
|
-
shell=True,
|
|
1259
|
-
)
|
|
1260
|
-
p.communicate()
|
|
1261
|
-
|
|
1262
|
-
def tearDown(self) -> None:
|
|
1263
|
-
"""Clean up outputs."""
|
|
1264
|
-
unittest.TestCase.tearDown(self)
|
|
1265
|
-
|
|
1266
1395
|
@slow
|
|
1396
|
+
@needs_docker
|
|
1397
|
+
@pytest.mark.slow
|
|
1398
|
+
@pytest.mark.docker
|
|
1399
|
+
@pytest.mark.online
|
|
1267
1400
|
@pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
|
|
1268
1401
|
def test_run_conformance(
|
|
1269
1402
|
self,
|
|
1403
|
+
cwl_v1_2_spec: Path,
|
|
1270
1404
|
runner: Optional[str] = None,
|
|
1271
1405
|
caching: bool = False,
|
|
1272
1406
|
batchSystem: Optional[str] = None,
|
|
@@ -1277,10 +1411,10 @@ class CWLv12Test(ToilTest):
|
|
|
1277
1411
|
junit_file: Optional[str] = None,
|
|
1278
1412
|
) -> None:
|
|
1279
1413
|
if junit_file is None:
|
|
1280
|
-
junit_file = os.path.
|
|
1414
|
+
junit_file = os.path.abspath("conformance-1.2.junit.xml")
|
|
1281
1415
|
run_conformance_tests(
|
|
1282
|
-
workDir=
|
|
1283
|
-
yml=
|
|
1416
|
+
workDir=str(cwl_v1_2_spec),
|
|
1417
|
+
yml=str(cwl_v1_2_spec / "conformance_tests.yaml"),
|
|
1284
1418
|
runner=runner,
|
|
1285
1419
|
caching=caching,
|
|
1286
1420
|
batchSystem=batchSystem,
|
|
@@ -1290,48 +1424,67 @@ class CWLv12Test(ToilTest):
|
|
|
1290
1424
|
must_support_all_features=must_support_all_features,
|
|
1291
1425
|
junit_file=junit_file,
|
|
1292
1426
|
)
|
|
1427
|
+
|
|
1293
1428
|
@slow
|
|
1429
|
+
@needs_docker
|
|
1430
|
+
@pytest.mark.slow
|
|
1431
|
+
@pytest.mark.docker
|
|
1432
|
+
@pytest.mark.online
|
|
1294
1433
|
@pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
|
|
1295
|
-
def test_run_conformance_with_caching(self) -> None:
|
|
1434
|
+
def test_run_conformance_with_caching(self, cwl_v1_2_spec: Path) -> None:
|
|
1296
1435
|
self.test_run_conformance(
|
|
1436
|
+
cwl_v1_2_spec,
|
|
1297
1437
|
caching=True,
|
|
1298
|
-
junit_file=os.path.
|
|
1438
|
+
junit_file=os.path.abspath("caching-conformance-1.2.junit.xml"),
|
|
1299
1439
|
)
|
|
1300
1440
|
|
|
1301
1441
|
@slow
|
|
1442
|
+
@needs_docker
|
|
1443
|
+
@pytest.mark.slow
|
|
1444
|
+
@pytest.mark.docker
|
|
1302
1445
|
@pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
|
|
1303
|
-
def test_run_conformance_with_task_caching(
|
|
1446
|
+
def test_run_conformance_with_task_caching(
|
|
1447
|
+
self, cwl_v1_2_spec: Path, tmp_path: Path
|
|
1448
|
+
) -> None:
|
|
1304
1449
|
self.test_run_conformance(
|
|
1305
|
-
|
|
1306
|
-
|
|
1450
|
+
cwl_v1_2_spec,
|
|
1451
|
+
junit_file=os.path.abspath("task-caching-conformance-1.2.junit.xml"),
|
|
1452
|
+
extra_args=["--cachedir", str(tmp_path / "task_cache")],
|
|
1307
1453
|
)
|
|
1308
1454
|
|
|
1309
1455
|
@slow
|
|
1456
|
+
@needs_docker
|
|
1457
|
+
@pytest.mark.slow
|
|
1458
|
+
@pytest.mark.docker
|
|
1310
1459
|
@pytest.mark.timeout(CONFORMANCE_TEST_TIMEOUT)
|
|
1311
|
-
def test_run_conformance_with_in_place_update(self) -> None:
|
|
1460
|
+
def test_run_conformance_with_in_place_update(self, cwl_v1_2_spec: Path) -> None:
|
|
1312
1461
|
"""
|
|
1313
1462
|
Make sure that with --bypass-file-store we properly support in place
|
|
1314
1463
|
update on a single node, and that this doesn't break any other
|
|
1315
1464
|
features.
|
|
1316
1465
|
"""
|
|
1317
1466
|
self.test_run_conformance(
|
|
1467
|
+
cwl_v1_2_spec,
|
|
1318
1468
|
extra_args=["--bypass-file-store"],
|
|
1319
1469
|
must_support_all_features=True,
|
|
1320
|
-
junit_file=os.path.
|
|
1321
|
-
self.rootDir, "in-place-update-conformance-1.2.junit.xml"
|
|
1322
|
-
),
|
|
1470
|
+
junit_file=os.path.abspath("in-place-update-conformance-1.2.junit.xml"),
|
|
1323
1471
|
)
|
|
1324
1472
|
|
|
1325
1473
|
@slow
|
|
1326
1474
|
@needs_kubernetes
|
|
1475
|
+
@pytest.mark.slow
|
|
1476
|
+
@pytest.mark.kubernetes
|
|
1477
|
+
@pytest.mark.online
|
|
1327
1478
|
def test_kubernetes_cwl_conformance(
|
|
1328
|
-
self,
|
|
1479
|
+
self,
|
|
1480
|
+
cwl_v1_2_spec: Path,
|
|
1481
|
+
caching: bool = False,
|
|
1482
|
+
junit_file: Optional[str] = None,
|
|
1329
1483
|
) -> None:
|
|
1330
1484
|
if junit_file is None:
|
|
1331
|
-
junit_file = os.path.
|
|
1332
|
-
self.rootDir, "kubernetes-conformance-1.2.junit.xml"
|
|
1333
|
-
)
|
|
1485
|
+
junit_file = os.path.abspath("kubernetes-conformance-1.2.junit.xml")
|
|
1334
1486
|
self.test_run_conformance(
|
|
1487
|
+
cwl_v1_2_spec,
|
|
1335
1488
|
caching=caching,
|
|
1336
1489
|
batchSystem="kubernetes",
|
|
1337
1490
|
extra_args=["--retryCount=3"],
|
|
@@ -1346,17 +1499,22 @@ class CWLv12Test(ToilTest):
|
|
|
1346
1499
|
|
|
1347
1500
|
@slow
|
|
1348
1501
|
@needs_kubernetes
|
|
1349
|
-
|
|
1502
|
+
@pytest.mark.slow
|
|
1503
|
+
@pytest.mark.kubernetes
|
|
1504
|
+
@pytest.mark.online
|
|
1505
|
+
def test_kubernetes_cwl_conformance_with_caching(self, cwl_v1_2_spec: Path) -> None:
|
|
1350
1506
|
self.test_kubernetes_cwl_conformance(
|
|
1507
|
+
cwl_v1_2_spec,
|
|
1351
1508
|
caching=True,
|
|
1352
|
-
junit_file=os.path.
|
|
1353
|
-
self.rootDir, "kubernetes-caching-conformance-1.2.junit.xml"
|
|
1354
|
-
),
|
|
1509
|
+
junit_file=os.path.abspath("kubernetes-caching-conformance-1.2.junit.xml"),
|
|
1355
1510
|
)
|
|
1356
1511
|
|
|
1357
1512
|
@slow
|
|
1358
1513
|
@needs_wes_server
|
|
1359
|
-
|
|
1514
|
+
@pytest.mark.slow
|
|
1515
|
+
@pytest.mark.wes_server
|
|
1516
|
+
@pytest.mark.online
|
|
1517
|
+
def test_wes_server_cwl_conformance(self, cwl_v1_2_spec: Path) -> None:
|
|
1360
1518
|
"""
|
|
1361
1519
|
Run the CWL conformance tests via WES. TOIL_WES_ENDPOINT must be
|
|
1362
1520
|
specified. If the WES server requires authentication, set TOIL_WES_USER
|
|
@@ -1367,7 +1525,7 @@ class CWLv12Test(ToilTest):
|
|
|
1367
1525
|
TOIL_WES_ENDPOINT=http://localhost:8080 \
|
|
1368
1526
|
TOIL_WES_USER=test \
|
|
1369
1527
|
TOIL_WES_PASSWORD=password \
|
|
1370
|
-
python -m pytest src/toil/test/cwl/cwlTest.py::
|
|
1528
|
+
python -m pytest src/toil/test/cwl/cwlTest.py::TestCWLv12::test_wes_server_cwl_conformance -vv --log-level INFO --log-cli-level INFO
|
|
1371
1529
|
"""
|
|
1372
1530
|
endpoint = os.environ.get("TOIL_WES_ENDPOINT")
|
|
1373
1531
|
extra_args = [f"--wes_endpoint={endpoint}"]
|
|
@@ -1382,6 +1540,7 @@ class CWLv12Test(ToilTest):
|
|
|
1382
1540
|
# e.g.: https://github.com/common-workflow-language/cwl-v1.2/blob/1.2.1_proposed/tests/mixed-versions/wf-v10.cwl#L4-L10
|
|
1383
1541
|
|
|
1384
1542
|
self.test_run_conformance(
|
|
1543
|
+
cwl_v1_2_spec,
|
|
1385
1544
|
runner="toil-wes-cwl-runner",
|
|
1386
1545
|
selected_tests="1-309,313-337",
|
|
1387
1546
|
extra_args=extra_args,
|
|
@@ -1389,183 +1548,162 @@ class CWLv12Test(ToilTest):
|
|
|
1389
1548
|
|
|
1390
1549
|
|
|
1391
1550
|
@needs_cwl
|
|
1551
|
+
@pytest.mark.cwl
|
|
1392
1552
|
@pytest.mark.cwl_small_log_dir
|
|
1393
1553
|
def test_workflow_echo_string_scatter_stderr_log_dir(tmp_path: Path) -> None:
|
|
1394
1554
|
log_dir = tmp_path / "cwl-logs"
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
new_file_loc = out_base["location"]
|
|
1419
|
-
assert (
|
|
1420
|
-
new_file_loc == file["location"]
|
|
1421
|
-
), f"Toil should have detected conflicts for these stdout files {new_file_loc} and {file}"
|
|
1555
|
+
with get_data("test/cwl/echo_string_scatter_capture_stdout.cwl") as cwl_file:
|
|
1556
|
+
cmd = [
|
|
1557
|
+
"toil-cwl-runner",
|
|
1558
|
+
f"--jobStore={tmp_path / 'jobstore'}",
|
|
1559
|
+
"--strict-memory-limit",
|
|
1560
|
+
f"--log-dir={log_dir}",
|
|
1561
|
+
str(cwl_file),
|
|
1562
|
+
]
|
|
1563
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
1564
|
+
outputs = json.loads(p.stdout)
|
|
1565
|
+
out_list = outputs["list_out"]
|
|
1566
|
+
assert len(out_list) == 2, f"outList shoud have two file elements {out_list}"
|
|
1567
|
+
out_base = outputs["list_out"][0]
|
|
1568
|
+
# This is a test on the scatter functionality and stdout.
|
|
1569
|
+
# Each value of scatter should generate a separate file in the output.
|
|
1570
|
+
for index, file in enumerate(out_list):
|
|
1571
|
+
if index > 0:
|
|
1572
|
+
new_file_loc = out_base["location"] + f"_{index + 1}"
|
|
1573
|
+
else:
|
|
1574
|
+
new_file_loc = out_base["location"]
|
|
1575
|
+
assert (
|
|
1576
|
+
new_file_loc == file["location"]
|
|
1577
|
+
), f"Toil should have detected conflicts for these stdout files {new_file_loc} and {file}"
|
|
1422
1578
|
|
|
1423
|
-
|
|
1424
|
-
|
|
1579
|
+
assert "Finished toil run successfully" in p.stderr
|
|
1580
|
+
assert p.returncode == 0
|
|
1425
1581
|
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1582
|
+
assert log_dir.exists()
|
|
1583
|
+
scatter_0 = log_dir / "echo-test-scatter.0.scatter"
|
|
1584
|
+
scatter_1 = log_dir / "echo-test-scatter.1.scatter"
|
|
1585
|
+
list_0 = log_dir / "echo-test-scatter.0.list"
|
|
1586
|
+
list_1 = log_dir / "echo-test-scatter.1.list"
|
|
1587
|
+
assert scatter_0.exists()
|
|
1588
|
+
assert scatter_1.exists()
|
|
1589
|
+
assert list_0.exists()
|
|
1590
|
+
assert list_1.exists()
|
|
1435
1591
|
|
|
1436
1592
|
|
|
1437
1593
|
@needs_cwl
|
|
1594
|
+
@pytest.mark.cwl
|
|
1438
1595
|
@pytest.mark.cwl_small_log_dir
|
|
1439
1596
|
def test_log_dir_echo_no_output(tmp_path: Path) -> None:
|
|
1440
1597
|
log_dir = tmp_path / "cwl-logs"
|
|
1441
|
-
job_store = "test_log_dir_echo_no_output"
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
result = next(subdir.iterdir())
|
|
1463
|
-
assert result.name == "out.txt"
|
|
1464
|
-
output = open(result).read()
|
|
1465
|
-
assert "hello" in output
|
|
1598
|
+
job_store = tmp_path / "test_log_dir_echo_no_output"
|
|
1599
|
+
with get_data("test/cwl/echo-stdout-log-dir.cwl") as cwl_file:
|
|
1600
|
+
cmd = [
|
|
1601
|
+
"toil-cwl-runner",
|
|
1602
|
+
f"--jobStore={job_store}",
|
|
1603
|
+
"--strict-memory-limit",
|
|
1604
|
+
f"--log-dir={str(log_dir)}",
|
|
1605
|
+
str(cwl_file),
|
|
1606
|
+
]
|
|
1607
|
+
subprocess.run(cmd)
|
|
1608
|
+
|
|
1609
|
+
assert log_dir.exists()
|
|
1610
|
+
assert sum(1 for _ in log_dir.iterdir()) == 1
|
|
1611
|
+
|
|
1612
|
+
subdir = next(log_dir.iterdir())
|
|
1613
|
+
assert subdir.name == "echo"
|
|
1614
|
+
assert subdir.is_dir()
|
|
1615
|
+
assert sum(1 for _ in subdir.iterdir()) == 1
|
|
1616
|
+
result = next(subdir.iterdir())
|
|
1617
|
+
assert result.name == "out.txt"
|
|
1618
|
+
assert "hello" in result.read_text()
|
|
1466
1619
|
|
|
1467
1620
|
|
|
1468
1621
|
@needs_cwl
|
|
1622
|
+
@pytest.mark.cwl
|
|
1469
1623
|
@pytest.mark.cwl_small_log_dir
|
|
1470
1624
|
def test_log_dir_echo_stderr(tmp_path: Path) -> None:
|
|
1471
1625
|
log_dir = tmp_path / "cwl-logs"
|
|
1626
|
+
log_dir.mkdir()
|
|
1627
|
+
with get_data("test/cwl/echo-stderr.cwl") as cwl_file:
|
|
1628
|
+
cmd = [
|
|
1629
|
+
"toil-cwl-runner",
|
|
1630
|
+
f"--jobStore={str(tmp_path / 'test_log_dir_echo_stderr')}",
|
|
1631
|
+
"--strict-memory-limit",
|
|
1632
|
+
"--force-docker-pull",
|
|
1633
|
+
"--clean=always",
|
|
1634
|
+
f"--log-dir={str(log_dir)}",
|
|
1635
|
+
str(cwl_file),
|
|
1636
|
+
]
|
|
1637
|
+
subprocess.run(cmd)
|
|
1638
|
+
tmp_path = log_dir
|
|
1639
|
+
|
|
1640
|
+
assert len(list(tmp_path.iterdir())) == 1
|
|
1472
1641
|
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
cmd = [toil, jobstore, option_1, option_2, option_3, option_4, cwl]
|
|
1482
|
-
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
1483
|
-
stdout, stderr = p.communicate()
|
|
1484
|
-
tmp_path = log_dir
|
|
1485
|
-
|
|
1486
|
-
assert len(list(tmp_path.iterdir())) == 1
|
|
1487
|
-
|
|
1488
|
-
subdir = next(tmp_path.iterdir())
|
|
1489
|
-
assert subdir.name == "echo-stderr.cwl"
|
|
1490
|
-
assert subdir.is_dir()
|
|
1491
|
-
assert len(list(subdir.iterdir())) == 1
|
|
1492
|
-
result = next(subdir.iterdir())
|
|
1493
|
-
assert result.name == "out.txt"
|
|
1494
|
-
output = open(result).read()
|
|
1495
|
-
assert output == "hello\n"
|
|
1642
|
+
subdir = next(tmp_path.iterdir())
|
|
1643
|
+
assert subdir.name == "echo-stderr.cwl"
|
|
1644
|
+
assert subdir.is_dir()
|
|
1645
|
+
assert len(list(subdir.iterdir())) == 1
|
|
1646
|
+
result = next(subdir.iterdir())
|
|
1647
|
+
assert result.name == "out.txt"
|
|
1648
|
+
output = open(result).read()
|
|
1649
|
+
assert output == "hello\n"
|
|
1496
1650
|
|
|
1497
1651
|
|
|
1498
1652
|
# TODO: It's not clear how this test tests filename conflict resolution; it
|
|
1499
1653
|
# seems like it runs a python script to copy some files and makes sure the
|
|
1500
1654
|
# workflow doesn't fail.
|
|
1501
1655
|
@needs_cwl
|
|
1656
|
+
@pytest.mark.cwl
|
|
1502
1657
|
@pytest.mark.cwl_small_log_dir
|
|
1503
1658
|
def test_filename_conflict_resolution(tmp_path: Path) -> None:
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
cwl_inputs = ["--msin", input]
|
|
1517
|
-
cmd = [toil] + options + [cwl] + cwl_inputs
|
|
1518
|
-
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
1519
|
-
stdout, stderr = p.communicate()
|
|
1520
|
-
assert b"Finished toil run successfully" in stderr
|
|
1521
|
-
assert p.returncode == 0
|
|
1659
|
+
with get_data("test/cwl/test_filename_conflict_resolution.cwl") as cwl_file:
|
|
1660
|
+
with get_data("test/cwl/test_filename_conflict_resolution.ms") as msin:
|
|
1661
|
+
cmd = [
|
|
1662
|
+
"toil-cwl-runner",
|
|
1663
|
+
f"--outdir={tmp_path}",
|
|
1664
|
+
str(cwl_file),
|
|
1665
|
+
"--msin",
|
|
1666
|
+
str(msin),
|
|
1667
|
+
]
|
|
1668
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
1669
|
+
assert "Finished toil run successfully" in p.stderr
|
|
1670
|
+
assert p.returncode == 0
|
|
1522
1671
|
|
|
1523
1672
|
|
|
1524
1673
|
@needs_cwl
|
|
1674
|
+
@pytest.mark.cwl
|
|
1525
1675
|
@pytest.mark.cwl_small_log_dir
|
|
1526
1676
|
def test_filename_conflict_resolution_3_or_more(tmp_path: Path) -> None:
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
1536
|
-
stdout, stderr = p.communicate()
|
|
1537
|
-
assert b"Finished toil run successfully" in stderr
|
|
1538
|
-
assert p.returncode == 0
|
|
1539
|
-
assert (
|
|
1540
|
-
len(os.listdir(out_dir)) == 9
|
|
1541
|
-
), "All 9 files made by the scatter should be in the directory"
|
|
1677
|
+
with get_data("test/cwl/scatter_duplicate_outputs.cwl") as cwl_file:
|
|
1678
|
+
cmd = ["toil-cwl-runner", f"--outdir={tmp_path}", str(cwl_file)]
|
|
1679
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
1680
|
+
assert "Finished toil run successfully" in p.stderr
|
|
1681
|
+
assert p.returncode == 0
|
|
1682
|
+
assert (
|
|
1683
|
+
sum(1 for _ in tmp_path.iterdir()) == 9
|
|
1684
|
+
), f"All 9 files made by the scatter should be in the directory: {tmp_path}"
|
|
1542
1685
|
|
|
1543
1686
|
|
|
1544
1687
|
@needs_cwl
|
|
1545
1688
|
@needs_docker
|
|
1689
|
+
@pytest.mark.cwl
|
|
1690
|
+
@pytest.mark.docker
|
|
1546
1691
|
@pytest.mark.cwl_small_log_dir
|
|
1547
1692
|
def test_filename_conflict_detection(tmp_path: Path) -> None:
|
|
1548
1693
|
"""
|
|
1549
1694
|
Make sure we don't just stage files over each other when using a container.
|
|
1550
1695
|
"""
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
]
|
|
1557
|
-
cwl = os.path.join(
|
|
1558
|
-
os.path.dirname(__file__), "test_filename_conflict_detection.cwl"
|
|
1559
|
-
)
|
|
1560
|
-
cmd = [toil] + options + [cwl]
|
|
1561
|
-
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
1562
|
-
stdout, stderr = p.communicate()
|
|
1563
|
-
assert b"File staging conflict" in stderr
|
|
1564
|
-
assert p.returncode != 0
|
|
1696
|
+
with get_data("test/cwl/test_filename_conflict_detection.cwl") as cwl_file:
|
|
1697
|
+
cmd = ["toil-cwl-runner", f"--outdir={tmp_path}", str(cwl_file)]
|
|
1698
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
1699
|
+
assert "File staging conflict" in p.stderr
|
|
1700
|
+
assert p.returncode != 0
|
|
1565
1701
|
|
|
1566
1702
|
|
|
1567
1703
|
@needs_cwl
|
|
1568
1704
|
@needs_docker
|
|
1705
|
+
@pytest.mark.cwl
|
|
1706
|
+
@pytest.mark.docker
|
|
1569
1707
|
@pytest.mark.cwl_small_log_dir
|
|
1570
1708
|
def test_filename_conflict_detection_at_root(tmp_path: Path) -> None:
|
|
1571
1709
|
"""
|
|
@@ -1573,101 +1711,89 @@ def test_filename_conflict_detection_at_root(tmp_path: Path) -> None:
|
|
|
1573
1711
|
|
|
1574
1712
|
Specifically, when using a container and the files are at the root of the work dir.
|
|
1575
1713
|
"""
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
]
|
|
1582
|
-
cwl = os.path.join(
|
|
1583
|
-
os.path.dirname(__file__), "test_filename_conflict_detection_at_root.cwl"
|
|
1584
|
-
)
|
|
1585
|
-
cmd = [toil] + options + [cwl]
|
|
1586
|
-
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
1587
|
-
stdout, stderr = p.communicate()
|
|
1588
|
-
assert b"File staging conflict" in stderr
|
|
1589
|
-
assert p.returncode != 0
|
|
1714
|
+
with get_data("test/cwl/test_filename_conflict_detection_at_root.cwl") as cwl_file:
|
|
1715
|
+
cmd = ["toil-cwl-runner", f"--outdir={tmp_path}", str(cwl_file)]
|
|
1716
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
1717
|
+
assert "File staging conflict" in p.stderr
|
|
1718
|
+
assert p.returncode != 0
|
|
1590
1719
|
|
|
1591
1720
|
|
|
1592
1721
|
@needs_cwl
|
|
1722
|
+
@pytest.mark.cwl
|
|
1593
1723
|
@pytest.mark.cwl_small
|
|
1594
|
-
def test_pick_value_with_one_null_value(
|
|
1724
|
+
def test_pick_value_with_one_null_value(
|
|
1725
|
+
caplog: pytest.LogCaptureFixture, tmp_path: Path
|
|
1726
|
+
) -> None:
|
|
1595
1727
|
"""
|
|
1596
1728
|
Make sure toil-cwl-runner does not false log a warning when pickValue is
|
|
1597
1729
|
used but outputSource only contains one null value. See: #3991.
|
|
1598
1730
|
"""
|
|
1599
1731
|
from toil.cwl import cwltoil
|
|
1600
1732
|
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
not in line
|
|
1611
|
-
)
|
|
1733
|
+
with get_data("test/cwl/conditional_wf.cwl") as cwl_file:
|
|
1734
|
+
with get_data("test/cwl/conditional_wf.yaml") as job_file:
|
|
1735
|
+
with caplog.at_level(logging.WARNING, logger="toil.cwl.cwltoil"):
|
|
1736
|
+
cwltoil.main([f"--outdir={tmp_path}", str(cwl_file), str(job_file)])
|
|
1737
|
+
for line in caplog.messages:
|
|
1738
|
+
assert (
|
|
1739
|
+
"You had a conditional step that did not run, but you did not use pickValue to handle the skipped input."
|
|
1740
|
+
not in line
|
|
1741
|
+
)
|
|
1612
1742
|
|
|
1613
1743
|
|
|
1614
1744
|
@needs_cwl
|
|
1745
|
+
@pytest.mark.cwl
|
|
1615
1746
|
@pytest.mark.cwl_small
|
|
1616
|
-
def test_workflow_echo_string() -> None:
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
), f"Got wrong output: {stdout2}\nWith error: {stderr2}"
|
|
1631
|
-
assert "Finished toil run successfully" in stderr2
|
|
1632
|
-
assert p.returncode == 0
|
|
1747
|
+
def test_workflow_echo_string(tmp_path: Path) -> None:
|
|
1748
|
+
with get_data("test/cwl/echo_string.cwl") as cwl_file:
|
|
1749
|
+
cmd = [
|
|
1750
|
+
"toil-cwl-runner",
|
|
1751
|
+
f"--jobStore=file:{tmp_path / 'jobstore'}",
|
|
1752
|
+
"--strict-memory-limit",
|
|
1753
|
+
str(cwl_file),
|
|
1754
|
+
]
|
|
1755
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
1756
|
+
assert (
|
|
1757
|
+
p.stdout.strip() == "{}"
|
|
1758
|
+
), f"Got wrong output: {p.stdout}\nWith error: {p.stderr}"
|
|
1759
|
+
assert "Finished toil run successfully" in p.stderr
|
|
1760
|
+
assert p.returncode == 0
|
|
1633
1761
|
|
|
1634
1762
|
|
|
1635
1763
|
@needs_cwl
|
|
1764
|
+
@pytest.mark.cwl
|
|
1636
1765
|
@pytest.mark.cwl_small
|
|
1637
|
-
def test_workflow_echo_string_scatter_capture_stdout() -> None:
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
new_file_loc = out_base["location"]
|
|
1662
|
-
assert (
|
|
1663
|
-
new_file_loc == file["location"]
|
|
1664
|
-
), f"Toil should have detected conflicts for these stdout files {new_file_loc} and {file}"
|
|
1766
|
+
def test_workflow_echo_string_scatter_capture_stdout(tmp_path: Path) -> None:
|
|
1767
|
+
with get_data("test/cwl/echo_string_scatter_capture_stdout.cwl") as cwl_file:
|
|
1768
|
+
cmd = [
|
|
1769
|
+
"toil-cwl-runner",
|
|
1770
|
+
f"--jobStore=file:{tmp_path / 'jobStore'}",
|
|
1771
|
+
"--strict-memory-limit",
|
|
1772
|
+
str(cwl_file),
|
|
1773
|
+
]
|
|
1774
|
+
p = subprocess.run(cmd, capture_output=True, text=True)
|
|
1775
|
+
assert len(p.stdout) > 0
|
|
1776
|
+
outputs = json.loads(p.stdout)
|
|
1777
|
+
out_list = outputs["list_out"]
|
|
1778
|
+
assert len(out_list) == 2, f"outList shoud have two file elements {out_list}"
|
|
1779
|
+
out_base = outputs["list_out"][0]
|
|
1780
|
+
# This is a test on the scatter functionality and stdout.
|
|
1781
|
+
# Each value of scatter should generate a separate file in the output.
|
|
1782
|
+
for index, file in enumerate(out_list):
|
|
1783
|
+
if index > 0:
|
|
1784
|
+
new_file_loc = out_base["location"] + f"_{index + 1}"
|
|
1785
|
+
else:
|
|
1786
|
+
new_file_loc = out_base["location"]
|
|
1787
|
+
assert (
|
|
1788
|
+
new_file_loc == file["location"]
|
|
1789
|
+
), f"Toil should have detected conflicts for these stdout files {new_file_loc} and {file}"
|
|
1665
1790
|
|
|
1666
|
-
|
|
1667
|
-
|
|
1791
|
+
assert "Finished toil run successfully" in p.stderr
|
|
1792
|
+
assert p.returncode == 0
|
|
1668
1793
|
|
|
1669
1794
|
|
|
1670
1795
|
@needs_cwl
|
|
1796
|
+
@pytest.mark.cwl
|
|
1671
1797
|
@pytest.mark.cwl_small
|
|
1672
1798
|
def test_visit_top_cwl_class() -> None:
|
|
1673
1799
|
structure = {
|
|
@@ -1719,6 +1845,7 @@ def test_visit_top_cwl_class() -> None:
|
|
|
1719
1845
|
|
|
1720
1846
|
|
|
1721
1847
|
@needs_cwl
|
|
1848
|
+
@pytest.mark.cwl
|
|
1722
1849
|
@pytest.mark.cwl_small
|
|
1723
1850
|
def test_visit_cwl_class_and_reduce() -> None:
|
|
1724
1851
|
structure = {
|
|
@@ -1780,6 +1907,7 @@ def test_visit_cwl_class_and_reduce() -> None:
|
|
|
1780
1907
|
|
|
1781
1908
|
|
|
1782
1909
|
@needs_cwl
|
|
1910
|
+
@pytest.mark.cwl
|
|
1783
1911
|
@pytest.mark.cwl_small
|
|
1784
1912
|
def test_download_structure(tmp_path: Path) -> None:
|
|
1785
1913
|
"""
|
|
@@ -1804,7 +1932,7 @@ def test_download_structure(tmp_path: Path) -> None:
|
|
|
1804
1932
|
}
|
|
1805
1933
|
|
|
1806
1934
|
# Say where to put it on the filesystem
|
|
1807
|
-
to_dir =
|
|
1935
|
+
to_dir = tmp_path
|
|
1808
1936
|
|
|
1809
1937
|
# Make a fake file store
|
|
1810
1938
|
file_store = Mock(AbstractFileStore)
|
|
@@ -1817,7 +1945,7 @@ def test_download_structure(tmp_path: Path) -> None:
|
|
|
1817
1945
|
existing: dict[str, str] = {}
|
|
1818
1946
|
|
|
1819
1947
|
# Do the download
|
|
1820
|
-
download_structure(file_store, index, existing, structure, to_dir)
|
|
1948
|
+
download_structure(file_store, index, existing, structure, str(to_dir))
|
|
1821
1949
|
|
|
1822
1950
|
# Check the results
|
|
1823
1951
|
# 3 files should be made
|
|
@@ -1826,61 +1954,53 @@ def test_download_structure(tmp_path: Path) -> None:
|
|
|
1826
1954
|
assert len(existing) == 2
|
|
1827
1955
|
|
|
1828
1956
|
# Make sure that the index contents (path to URI) are correct
|
|
1829
|
-
assert
|
|
1830
|
-
assert
|
|
1831
|
-
assert
|
|
1957
|
+
assert str(to_dir / "dir1/dir2/f1") in index
|
|
1958
|
+
assert str(to_dir / "dir1/dir2/f1again") in index
|
|
1959
|
+
assert str(to_dir / "anotherfile") in index
|
|
1832
1960
|
assert (
|
|
1833
|
-
index[
|
|
1961
|
+
index[str(to_dir / "dir1/dir2/f1")]
|
|
1834
1962
|
== cast(
|
|
1835
1963
|
DirectoryStructure, cast(DirectoryStructure, structure["dir1"])["dir2"]
|
|
1836
1964
|
)["f1"]
|
|
1837
1965
|
)
|
|
1838
1966
|
assert (
|
|
1839
|
-
index[
|
|
1967
|
+
index[str(to_dir / "dir1/dir2/f1again")]
|
|
1840
1968
|
== cast(
|
|
1841
1969
|
DirectoryStructure, cast(DirectoryStructure, structure["dir1"])["dir2"]
|
|
1842
1970
|
)["f1again"]
|
|
1843
1971
|
)
|
|
1844
|
-
assert index[
|
|
1972
|
+
assert index[str(to_dir / "anotherfile")] == structure["anotherfile"]
|
|
1845
1973
|
|
|
1846
1974
|
# And the existing contents (URI to path)
|
|
1847
1975
|
assert "toilfile:" + fid1.pack() in existing
|
|
1848
1976
|
assert "toilfile:" + fid2.pack() in existing
|
|
1849
1977
|
assert existing["toilfile:" + fid1.pack()] in [
|
|
1850
|
-
|
|
1851
|
-
|
|
1978
|
+
str(to_dir / "dir1/dir2/f1"),
|
|
1979
|
+
str(to_dir / "dir1/dir2/f1again"),
|
|
1852
1980
|
]
|
|
1853
|
-
assert existing["toilfile:" + fid2.pack()] ==
|
|
1981
|
+
assert existing["toilfile:" + fid2.pack()] == str(to_dir / "anotherfile")
|
|
1854
1982
|
|
|
1855
1983
|
# The directory structure should be created for real
|
|
1856
|
-
assert
|
|
1857
|
-
assert
|
|
1858
|
-
assert
|
|
1859
|
-
assert
|
|
1984
|
+
assert (to_dir / "dir1").is_dir()
|
|
1985
|
+
assert (to_dir / "dir1/dir2").is_dir()
|
|
1986
|
+
assert (to_dir / "dir1/dir2/dir2sub").is_dir()
|
|
1987
|
+
assert (to_dir / "dir1/dir3").is_dir()
|
|
1860
1988
|
|
|
1861
1989
|
# The file store should have been asked to do the download
|
|
1862
1990
|
file_store.readGlobalFile.assert_has_calls(
|
|
1863
1991
|
[
|
|
1864
|
-
call(fid1,
|
|
1865
|
-
call(fid1,
|
|
1866
|
-
call(fid2,
|
|
1992
|
+
call(fid1, str(to_dir / "dir1/dir2/f1"), symlink=False),
|
|
1993
|
+
call(fid1, str(to_dir / "dir1/dir2/f1again"), symlink=False),
|
|
1994
|
+
call(fid2, str(to_dir / "anotherfile"), symlink=False),
|
|
1867
1995
|
],
|
|
1868
1996
|
any_order=True,
|
|
1869
1997
|
)
|
|
1870
1998
|
|
|
1871
1999
|
|
|
1872
2000
|
@needs_cwl
|
|
2001
|
+
@pytest.mark.cwl
|
|
1873
2002
|
@pytest.mark.timeout(300)
|
|
1874
2003
|
def test_import_on_workers() -> None:
|
|
1875
|
-
args = [
|
|
1876
|
-
"src/toil/test/cwl/download.cwl",
|
|
1877
|
-
"src/toil/test/cwl/download_file.json",
|
|
1878
|
-
"--runImportsOnWorkers",
|
|
1879
|
-
"--importWorkersDisk=10MiB",
|
|
1880
|
-
"--realTimeLogging=True",
|
|
1881
|
-
"--logLevel=INFO",
|
|
1882
|
-
"--logColors=False",
|
|
1883
|
-
]
|
|
1884
2004
|
from toil.cwl import cwltoil
|
|
1885
2005
|
|
|
1886
2006
|
detector = ImportWorkersMessageHandler()
|
|
@@ -1888,9 +2008,21 @@ def test_import_on_workers() -> None:
|
|
|
1888
2008
|
# Set up a log message detector to the root logger
|
|
1889
2009
|
logging.getLogger().addHandler(detector)
|
|
1890
2010
|
|
|
1891
|
-
|
|
2011
|
+
with get_data("test/cwl/download.cwl") as cwl_file:
|
|
2012
|
+
with get_data("test/cwl/directory/directory/file.txt") as file_path:
|
|
2013
|
+
args = [
|
|
2014
|
+
"--runImportsOnWorkers",
|
|
2015
|
+
"--importWorkersDisk=10MiB",
|
|
2016
|
+
"--realTimeLogging=True",
|
|
2017
|
+
"--logLevel=INFO",
|
|
2018
|
+
"--logColors=False",
|
|
2019
|
+
str(cwl_file),
|
|
2020
|
+
"--input",
|
|
2021
|
+
str(file_path),
|
|
2022
|
+
]
|
|
2023
|
+
cwltoil.main(args)
|
|
1892
2024
|
|
|
1893
|
-
|
|
2025
|
+
assert detector.detected is True
|
|
1894
2026
|
|
|
1895
2027
|
|
|
1896
2028
|
# StreamHandler is generic, _typeshed doesn't exist at runtime, do a bit of typing trickery, see https://github.com/python/typeshed/issues/5680
|