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