toil 6.1.0__py3-none-any.whl → 7.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 +1 -232
- toil/batchSystems/abstractBatchSystem.py +22 -13
- toil/batchSystems/abstractGridEngineBatchSystem.py +59 -45
- toil/batchSystems/awsBatch.py +8 -8
- toil/batchSystems/contained_executor.py +4 -5
- toil/batchSystems/gridengine.py +1 -1
- toil/batchSystems/htcondor.py +5 -5
- toil/batchSystems/kubernetes.py +25 -11
- toil/batchSystems/local_support.py +3 -3
- toil/batchSystems/lsf.py +2 -2
- toil/batchSystems/mesos/batchSystem.py +4 -4
- toil/batchSystems/mesos/executor.py +3 -2
- toil/batchSystems/options.py +9 -0
- toil/batchSystems/singleMachine.py +11 -10
- toil/batchSystems/slurm.py +64 -22
- toil/batchSystems/torque.py +1 -1
- toil/bus.py +7 -3
- toil/common.py +36 -13
- toil/cwl/cwltoil.py +365 -312
- toil/deferred.py +1 -1
- toil/fileStores/abstractFileStore.py +17 -17
- toil/fileStores/cachingFileStore.py +2 -2
- toil/fileStores/nonCachingFileStore.py +1 -1
- toil/job.py +228 -60
- toil/jobStores/abstractJobStore.py +18 -10
- toil/jobStores/aws/jobStore.py +280 -218
- toil/jobStores/aws/utils.py +57 -29
- toil/jobStores/conftest.py +2 -2
- toil/jobStores/fileJobStore.py +2 -2
- toil/jobStores/googleJobStore.py +3 -4
- toil/leader.py +72 -24
- toil/lib/aws/__init__.py +26 -10
- toil/lib/aws/iam.py +2 -2
- toil/lib/aws/session.py +62 -22
- toil/lib/aws/utils.py +73 -37
- toil/lib/conversions.py +5 -1
- toil/lib/ec2.py +118 -69
- toil/lib/expando.py +1 -1
- toil/lib/io.py +14 -2
- toil/lib/misc.py +1 -3
- toil/lib/resources.py +55 -21
- toil/lib/retry.py +12 -5
- toil/lib/threading.py +2 -2
- toil/lib/throttle.py +1 -1
- toil/options/common.py +27 -24
- toil/provisioners/__init__.py +9 -3
- toil/provisioners/abstractProvisioner.py +9 -7
- toil/provisioners/aws/__init__.py +20 -15
- toil/provisioners/aws/awsProvisioner.py +406 -329
- toil/provisioners/gceProvisioner.py +2 -2
- toil/provisioners/node.py +13 -5
- toil/server/app.py +1 -1
- toil/statsAndLogging.py +58 -16
- toil/test/__init__.py +27 -12
- toil/test/batchSystems/batchSystemTest.py +40 -33
- toil/test/batchSystems/batch_system_plugin_test.py +79 -0
- toil/test/batchSystems/test_slurm.py +1 -1
- toil/test/cwl/cwlTest.py +8 -91
- toil/test/cwl/seqtk_seq.cwl +1 -1
- toil/test/docs/scriptsTest.py +10 -13
- toil/test/jobStores/jobStoreTest.py +33 -49
- toil/test/lib/aws/test_iam.py +2 -2
- toil/test/provisioners/aws/awsProvisionerTest.py +51 -34
- toil/test/provisioners/clusterTest.py +90 -8
- toil/test/server/serverTest.py +2 -2
- toil/test/src/autoDeploymentTest.py +1 -1
- toil/test/src/dockerCheckTest.py +2 -1
- toil/test/src/environmentTest.py +125 -0
- toil/test/src/fileStoreTest.py +1 -1
- toil/test/src/jobDescriptionTest.py +18 -8
- toil/test/src/jobTest.py +1 -1
- toil/test/src/realtimeLoggerTest.py +4 -0
- toil/test/src/workerTest.py +52 -19
- toil/test/utils/toilDebugTest.py +61 -3
- toil/test/utils/utilsTest.py +20 -18
- toil/test/wdl/wdltoil_test.py +24 -71
- toil/test/wdl/wdltoil_test_kubernetes.py +77 -0
- toil/toilState.py +68 -9
- toil/utils/toilDebugJob.py +153 -26
- toil/utils/toilLaunchCluster.py +12 -2
- toil/utils/toilRsyncCluster.py +7 -2
- toil/utils/toilSshCluster.py +7 -3
- toil/utils/toilStats.py +2 -1
- toil/utils/toilStatus.py +97 -51
- toil/version.py +10 -10
- toil/wdl/wdltoil.py +318 -51
- toil/worker.py +96 -69
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/LICENSE +25 -0
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/METADATA +55 -21
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/RECORD +93 -90
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/WHEEL +1 -1
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/entry_points.txt +0 -0
- {toil-6.1.0.dist-info → toil-7.0.0.dist-info}/top_level.txt +0 -0
|
@@ -42,13 +42,13 @@ class GCEProvisioner(AbstractProvisioner):
|
|
|
42
42
|
NODE_BOTO_PATH = "/root/.boto" # boto file path on instances
|
|
43
43
|
SOURCE_IMAGE = b'projects/kinvolk-public/global/images/family/flatcar-stable'
|
|
44
44
|
|
|
45
|
-
def __init__(self, clusterName, clusterType, zone, nodeStorage, nodeStorageOverrides, sseKey):
|
|
45
|
+
def __init__(self, clusterName, clusterType, zone, nodeStorage, nodeStorageOverrides, sseKey, enable_fuse):
|
|
46
46
|
self.cloud = 'gce'
|
|
47
47
|
self._sseKey = sseKey
|
|
48
48
|
|
|
49
49
|
# Call base class constructor, which will call createClusterSettings()
|
|
50
50
|
# or readClusterSettings()
|
|
51
|
-
super().__init__(clusterName, clusterType, zone, nodeStorage, nodeStorageOverrides)
|
|
51
|
+
super().__init__(clusterName, clusterType, zone, nodeStorage, nodeStorageOverrides, enable_fuse)
|
|
52
52
|
|
|
53
53
|
def supportedClusterTypes(self):
|
|
54
54
|
return {'mesos'}
|
toil/provisioners/node.py
CHANGED
|
@@ -18,6 +18,7 @@ import socket
|
|
|
18
18
|
import subprocess
|
|
19
19
|
import time
|
|
20
20
|
from itertools import count
|
|
21
|
+
from typing import Union, Dict, Optional, List, Any
|
|
21
22
|
|
|
22
23
|
from toil.lib.memoize import parse_iso_utc
|
|
23
24
|
|
|
@@ -29,7 +30,8 @@ logger = logging.getLogger(__name__)
|
|
|
29
30
|
class Node:
|
|
30
31
|
maxWaitTime = 7 * 60
|
|
31
32
|
|
|
32
|
-
def __init__(self, publicIP, privateIP, name, launchTime
|
|
33
|
+
def __init__(self, publicIP: str, privateIP: str, name: str, launchTime: Union[datetime.datetime, str],
|
|
34
|
+
nodeType: Optional[str], preemptible: bool, tags: Optional[Dict[str, str]] = None, use_private_ip: Optional[bool] = None) -> None:
|
|
33
35
|
self.publicIP = publicIP
|
|
34
36
|
self.privateIP = privateIP
|
|
35
37
|
if use_private_ip:
|
|
@@ -37,7 +39,13 @@ class Node:
|
|
|
37
39
|
else:
|
|
38
40
|
self.effectiveIP = self.publicIP or self.privateIP
|
|
39
41
|
self.name = name
|
|
40
|
-
|
|
42
|
+
if isinstance(launchTime, datetime.datetime):
|
|
43
|
+
self.launchTime = launchTime
|
|
44
|
+
else:
|
|
45
|
+
try:
|
|
46
|
+
self.launchTime = parse_iso_utc(launchTime)
|
|
47
|
+
except ValueError:
|
|
48
|
+
self.launchTime = datetime.datetime.fromisoformat(launchTime)
|
|
41
49
|
self.nodeType = nodeType
|
|
42
50
|
self.preemptible = preemptible
|
|
43
51
|
self.tags = tags
|
|
@@ -65,12 +73,12 @@ class Node:
|
|
|
65
73
|
"""
|
|
66
74
|
if self.launchTime:
|
|
67
75
|
now = datetime.datetime.utcnow()
|
|
68
|
-
delta = now -
|
|
76
|
+
delta = now - self.launchTime
|
|
69
77
|
return 1 - delta.total_seconds() / 3600.0 % 1.0
|
|
70
78
|
else:
|
|
71
79
|
return 1
|
|
72
80
|
|
|
73
|
-
def waitForNode(self, role, keyName='core'):
|
|
81
|
+
def waitForNode(self, role: str, keyName: str='core') -> None:
|
|
74
82
|
self._waitForSSHPort()
|
|
75
83
|
# wait here so docker commands can be used reliably afterwards
|
|
76
84
|
self._waitForSSHKeys(keyName=keyName)
|
|
@@ -288,7 +296,7 @@ class Node:
|
|
|
288
296
|
% (' '.join(args), exit_code, stdout, stderr))
|
|
289
297
|
return stdout
|
|
290
298
|
|
|
291
|
-
def coreRsync(self, args, applianceName='toil_leader', **kwargs):
|
|
299
|
+
def coreRsync(self, args: List[str], applianceName: str = 'toil_leader', **kwargs: Any) -> int:
|
|
292
300
|
remoteRsync = "docker exec -i %s rsync -v" % applianceName # Access rsync inside appliance
|
|
293
301
|
parsedArgs = []
|
|
294
302
|
sshCommand = "ssh"
|
toil/server/app.py
CHANGED
|
@@ -87,7 +87,7 @@ def create_app(args: argparse.Namespace) -> "connexion.FlaskApp":
|
|
|
87
87
|
|
|
88
88
|
if args.cors:
|
|
89
89
|
# enable cross origin resource sharing
|
|
90
|
-
from flask_cors import CORS
|
|
90
|
+
from flask_cors import CORS
|
|
91
91
|
CORS(flask_app.app, resources={r"/ga4gh/*": {"origins": args.cors_origins}})
|
|
92
92
|
|
|
93
93
|
# add workflow execution service (WES) API endpoints
|
toil/statsAndLogging.py
CHANGED
|
@@ -22,8 +22,9 @@ from logging.handlers import RotatingFileHandler
|
|
|
22
22
|
from threading import Event, Thread
|
|
23
23
|
from typing import IO, TYPE_CHECKING, Any, Callable, List, Optional, Union
|
|
24
24
|
|
|
25
|
+
from toil.lib.conversions import strtobool
|
|
25
26
|
from toil.lib.expando import Expando
|
|
26
|
-
from toil.lib.resources import
|
|
27
|
+
from toil.lib.resources import ResourceMonitor
|
|
27
28
|
|
|
28
29
|
if TYPE_CHECKING:
|
|
29
30
|
from toil.common import Config
|
|
@@ -39,6 +40,7 @@ __loggingFiles = []
|
|
|
39
40
|
|
|
40
41
|
class StatsAndLogging:
|
|
41
42
|
"""A thread to aggregate statistics and logging."""
|
|
43
|
+
|
|
42
44
|
def __init__(self, jobStore: 'AbstractJobStore', config: 'Config') -> None:
|
|
43
45
|
self._stop = Event()
|
|
44
46
|
self._worker = Thread(target=self.statsAndLoggingAggregator,
|
|
@@ -74,10 +76,10 @@ class StatsAndLogging:
|
|
|
74
76
|
|
|
75
77
|
return '\n'.join(lines)
|
|
76
78
|
|
|
77
|
-
|
|
78
79
|
@classmethod
|
|
79
|
-
def logWithFormatting(cls, stream_name: str, jobLogs: Union[IO[str], IO[bytes]],
|
|
80
|
-
|
|
80
|
+
def logWithFormatting(cls, stream_name: str, jobLogs: Union[IO[str], IO[bytes]],
|
|
81
|
+
method: Callable[[str], None] = logger.debug,
|
|
82
|
+
message: Optional[str] = None) -> None:
|
|
81
83
|
if message is not None:
|
|
82
84
|
method(message)
|
|
83
85
|
|
|
@@ -100,7 +102,7 @@ class StatsAndLogging:
|
|
|
100
102
|
fullName = os.path.join(logPath, logName + suffix)
|
|
101
103
|
# The maximum file name size in the default HFS+ file system is 255 UTF-16 encoding units, so basically 255 characters
|
|
102
104
|
if len(fullName) >= 255:
|
|
103
|
-
return fullName[:(255-len(suffix))] + suffix
|
|
105
|
+
return fullName[:(255 - len(suffix))] + suffix
|
|
104
106
|
if not os.path.exists(fullName):
|
|
105
107
|
return fullName
|
|
106
108
|
counter += 1
|
|
@@ -145,7 +147,7 @@ class StatsAndLogging:
|
|
|
145
147
|
"""
|
|
146
148
|
# Overall timing
|
|
147
149
|
startTime = time.time()
|
|
148
|
-
startClock = get_total_cpu_time()
|
|
150
|
+
startClock = ResourceMonitor.get_total_cpu_time()
|
|
149
151
|
|
|
150
152
|
def callback(fileHandle: Union[IO[bytes], IO[str]]) -> None:
|
|
151
153
|
statsStr = fileHandle.read()
|
|
@@ -210,7 +212,7 @@ class StatsAndLogging:
|
|
|
210
212
|
|
|
211
213
|
# Finish the stats file
|
|
212
214
|
text = json.dumps(dict(total_time=str(time.time() - startTime),
|
|
213
|
-
total_clock=str(get_total_cpu_time() - startClock)), ensure_ascii=True)
|
|
215
|
+
total_clock=str(ResourceMonitor.get_total_cpu_time() - startClock)), ensure_ascii=True)
|
|
214
216
|
jobStore.write_logs(text)
|
|
215
217
|
|
|
216
218
|
def check(self) -> None:
|
|
@@ -236,32 +238,70 @@ def set_log_level(level: str, set_logger: Optional[logging.Logger] = None) -> No
|
|
|
236
238
|
level = "CRITICAL" if level.upper() == "OFF" else level.upper()
|
|
237
239
|
set_logger = set_logger if set_logger else root_logger
|
|
238
240
|
set_logger.setLevel(level)
|
|
239
|
-
|
|
240
241
|
# Suppress any random loggers introduced by libraries we use.
|
|
241
242
|
# Especially boto/boto3. They print too much. -__-
|
|
242
243
|
suppress_exotic_logging(__name__)
|
|
243
244
|
|
|
244
245
|
|
|
245
|
-
def
|
|
246
|
-
"""
|
|
246
|
+
def install_log_color(set_logger: Optional[logging.Logger] = None) -> None:
|
|
247
|
+
"""Make logs colored."""
|
|
248
|
+
# Most of this code is taken from miniwdl
|
|
249
|
+
# delayed import
|
|
250
|
+
import coloredlogs # type: ignore[import-untyped]
|
|
251
|
+
|
|
252
|
+
level_styles = dict(coloredlogs.DEFAULT_LEVEL_STYLES)
|
|
253
|
+
level_styles["debug"]["color"] = 242
|
|
254
|
+
level_styles["notice"] = {"color": "green", "bold": True}
|
|
255
|
+
level_styles["error"]["bold"] = True
|
|
256
|
+
level_styles["warning"]["bold"] = True
|
|
257
|
+
level_styles["info"] = {}
|
|
258
|
+
field_styles = dict(coloredlogs.DEFAULT_FIELD_STYLES)
|
|
259
|
+
field_styles["asctime"] = {"color": "blue"}
|
|
260
|
+
field_styles["name"] = {"color": "magenta"}
|
|
261
|
+
field_styles["levelname"] = {"color": "blue"}
|
|
262
|
+
field_styles["threadName"] = {"color": "blue"}
|
|
263
|
+
fmt = "[%(asctime)s] [%(threadName)s] [%(levelname).1s] [%(name)s] %(message)s" # mimic old toil logging format
|
|
264
|
+
set_logger = set_logger if set_logger else root_logger
|
|
265
|
+
coloredlogs.install(
|
|
266
|
+
level=set_logger.getEffectiveLevel(),
|
|
267
|
+
logger=set_logger,
|
|
268
|
+
level_styles=level_styles,
|
|
269
|
+
field_styles=field_styles,
|
|
270
|
+
datefmt="%Y-%m-%dT%H:%M:%S%z", # mimic old toil date format
|
|
271
|
+
fmt=fmt,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def add_logging_options(parser: ArgumentParser, default_level: Optional[int] = None) -> None:
|
|
276
|
+
"""
|
|
277
|
+
Add logging options to set the global log level.
|
|
278
|
+
|
|
279
|
+
:param default_level: A logging level, like logging.INFO, to use as the default.
|
|
280
|
+
"""
|
|
281
|
+
if default_level is None:
|
|
282
|
+
# Make sure we have a log levle to make the default
|
|
283
|
+
default_level = DEFAULT_LOGLEVEL
|
|
284
|
+
default_level_name = logging.getLevelName(default_level)
|
|
285
|
+
|
|
247
286
|
group = parser.add_argument_group("Logging Options")
|
|
248
|
-
default_loglevel = logging.getLevelName(DEFAULT_LOGLEVEL)
|
|
249
287
|
|
|
250
288
|
levels = ['Critical', 'Error', 'Warning', 'Debug', 'Info']
|
|
251
289
|
for level in levels:
|
|
252
|
-
group.add_argument(f"--log{level}", dest="logLevel", default=
|
|
253
|
-
const=level, help=f"Turn on loglevel {level}. Default: {
|
|
290
|
+
group.add_argument(f"--log{level}", dest="logLevel", default=default_level_name, action="store_const",
|
|
291
|
+
const=level, help=f"Turn on loglevel {level}. Default: {default_level_name}.")
|
|
254
292
|
|
|
255
293
|
levels += [l.lower() for l in levels] + [l.upper() for l in levels]
|
|
256
|
-
group.add_argument("--logOff", dest="logLevel", default=
|
|
294
|
+
group.add_argument("--logOff", dest="logLevel", default=default_level_name,
|
|
257
295
|
action="store_const", const="CRITICAL", help="Same as --logCRITICAL.")
|
|
258
296
|
# Maybe deprecate the above in favor of --logLevel?
|
|
259
297
|
|
|
260
|
-
group.add_argument("--logLevel", dest="logLevel", default=
|
|
261
|
-
help=f"Set the log level. Default: {
|
|
298
|
+
group.add_argument("--logLevel", dest="logLevel", default=default_level_name, choices=levels,
|
|
299
|
+
help=f"Set the log level. Default: {default_level_name}. Options: {levels}.")
|
|
262
300
|
group.add_argument("--logFile", dest="logFile", help="File to log in.")
|
|
263
301
|
group.add_argument("--rotatingLogging", dest="logRotating", action="store_true", default=False,
|
|
264
302
|
help="Turn on rotating logging, which prevents log files from getting too big.")
|
|
303
|
+
group.add_argument("--logColors", dest="colored_logs", default=True, type=strtobool, metavar="BOOL",
|
|
304
|
+
help="Enable or disable colored logging. Default: %(default)s")
|
|
265
305
|
|
|
266
306
|
|
|
267
307
|
def configure_root_logger() -> None:
|
|
@@ -292,6 +332,8 @@ def set_logging_from_options(options: Union["Config", Namespace]) -> None:
|
|
|
292
332
|
configure_root_logger()
|
|
293
333
|
options.logLevel = options.logLevel or logging.getLevelName(root_logger.getEffectiveLevel())
|
|
294
334
|
set_log_level(options.logLevel)
|
|
335
|
+
if options.colored_logs:
|
|
336
|
+
install_log_color()
|
|
295
337
|
logger.debug(f"Root logger is at level '{logging.getLevelName(root_logger.getEffectiveLevel())}', "
|
|
296
338
|
f"'toil' logger at level '{logging.getLevelName(toil_logger.getEffectiveLevel())}'.")
|
|
297
339
|
|
toil/test/__init__.py
CHANGED
|
@@ -36,6 +36,7 @@ from typing import (Any,
|
|
|
36
36
|
Dict,
|
|
37
37
|
Generator,
|
|
38
38
|
List,
|
|
39
|
+
Literal,
|
|
39
40
|
Optional,
|
|
40
41
|
Tuple,
|
|
41
42
|
Type,
|
|
@@ -46,17 +47,15 @@ from unittest.util import strclass
|
|
|
46
47
|
from urllib.error import HTTPError, URLError
|
|
47
48
|
from urllib.request import urlopen
|
|
48
49
|
|
|
49
|
-
import pytz
|
|
50
50
|
|
|
51
|
-
if sys.version_info >= (3,
|
|
52
|
-
|
|
51
|
+
if sys.version_info >= (3, 9):
|
|
52
|
+
import zoneinfo
|
|
53
53
|
else:
|
|
54
|
-
from
|
|
54
|
+
from backports import zoneinfo
|
|
55
55
|
|
|
56
56
|
from toil import ApplianceImageNotFound, applianceSelf, toilPackageDirPath
|
|
57
57
|
from toil.lib.accelerators import (have_working_nvidia_docker_runtime,
|
|
58
58
|
have_working_nvidia_smi)
|
|
59
|
-
from toil.lib.aws import running_on_ec2
|
|
60
59
|
from toil.lib.io import mkdtemp
|
|
61
60
|
from toil.lib.iterables import concat
|
|
62
61
|
from toil.lib.memoize import memoize
|
|
@@ -86,8 +85,8 @@ class ToilTest(unittest.TestCase):
|
|
|
86
85
|
_tempDirs: List[str] = []
|
|
87
86
|
|
|
88
87
|
def setup_method(self, method: Any) -> None:
|
|
89
|
-
western =
|
|
90
|
-
california_time =
|
|
88
|
+
western = zoneinfo.ZoneInfo("America/Los_Angeles")
|
|
89
|
+
california_time = datetime.datetime.now(tz=western)
|
|
91
90
|
timestamp = california_time.strftime("%b %d %Y %H:%M:%S:%f %Z")
|
|
92
91
|
print(f"\n\n[TEST] {strclass(self.__class__)}:{self._testMethodName} ({timestamp})\n\n")
|
|
93
92
|
|
|
@@ -127,6 +126,7 @@ class ToilTest(unittest.TestCase):
|
|
|
127
126
|
Use us-west-2 unless running on EC2, in which case use the region in which
|
|
128
127
|
the instance is located
|
|
129
128
|
"""
|
|
129
|
+
from toil.lib.aws import running_on_ec2
|
|
130
130
|
return cls._region() if running_on_ec2() else 'us-west-2'
|
|
131
131
|
|
|
132
132
|
@classmethod
|
|
@@ -372,14 +372,15 @@ def needs_aws_s3(test_item: MT) -> MT:
|
|
|
372
372
|
# TODO: we just check for generic access to the AWS account
|
|
373
373
|
test_item = _mark_test('aws-s3', needs_online(test_item))
|
|
374
374
|
try:
|
|
375
|
-
from
|
|
376
|
-
|
|
375
|
+
from boto3 import Session
|
|
376
|
+
session = Session()
|
|
377
|
+
boto3_credentials = session.get_credentials()
|
|
377
378
|
except ImportError:
|
|
378
379
|
return unittest.skip("Install Toil with the 'aws' extra to include this test.")(
|
|
379
380
|
test_item
|
|
380
381
|
)
|
|
381
|
-
|
|
382
|
-
if not (
|
|
382
|
+
from toil.lib.aws import running_on_ec2
|
|
383
|
+
if not (boto3_credentials or os.path.exists(os.path.expanduser('~/.aws/credentials')) or running_on_ec2()):
|
|
383
384
|
return unittest.skip("Configure AWS credentials to include this test.")(test_item)
|
|
384
385
|
return test_item
|
|
385
386
|
|
|
@@ -632,6 +633,20 @@ def needs_cwl(test_item: MT) -> MT:
|
|
|
632
633
|
else:
|
|
633
634
|
return test_item
|
|
634
635
|
|
|
636
|
+
def needs_wdl(test_item: MT) -> MT:
|
|
637
|
+
"""
|
|
638
|
+
Use as a decorator before test classes or methods to only run them if miniwdl is installed
|
|
639
|
+
and configured.
|
|
640
|
+
"""
|
|
641
|
+
test_item = _mark_test('wdl', test_item)
|
|
642
|
+
try:
|
|
643
|
+
# noinspection PyUnresolvedReferences
|
|
644
|
+
import WDL # noqa
|
|
645
|
+
except ImportError:
|
|
646
|
+
return unittest.skip("Install Toil with the 'wdl' extra to include this test.")(test_item)
|
|
647
|
+
else:
|
|
648
|
+
return test_item
|
|
649
|
+
|
|
635
650
|
|
|
636
651
|
def needs_server(test_item: MT) -> MT:
|
|
637
652
|
"""
|
|
@@ -747,7 +762,7 @@ def integrative(test_item: MT) -> MT:
|
|
|
747
762
|
return test_item
|
|
748
763
|
else:
|
|
749
764
|
return unittest.skip(
|
|
750
|
-
'Set TOIL_TEST_INTEGRATIVE=
|
|
765
|
+
'Set TOIL_TEST_INTEGRATIVE=True to include this integration test, '
|
|
751
766
|
"or run `make integration_test_local` to run all integration tests."
|
|
752
767
|
)(test_item)
|
|
753
768
|
|
|
@@ -140,19 +140,16 @@ class hidden:
|
|
|
140
140
|
"""
|
|
141
141
|
return self.createConfig()
|
|
142
142
|
|
|
143
|
-
def _mockJobDescription(self, jobStoreID=None,
|
|
143
|
+
def _mockJobDescription(self, jobStoreID=None, **kwargs):
|
|
144
144
|
"""
|
|
145
|
-
Create a mock-up JobDescription with the given ID
|
|
145
|
+
Create a mock-up JobDescription with the given ID and other parameters.
|
|
146
146
|
"""
|
|
147
147
|
|
|
148
148
|
# TODO: Use a real unittest.Mock? For now we make a real instance and just hack it up.
|
|
149
149
|
|
|
150
150
|
desc = JobDescription(**kwargs)
|
|
151
|
-
# Normally we can't pass in
|
|
152
|
-
#
|
|
153
|
-
# here.
|
|
154
|
-
if command is not None:
|
|
155
|
-
desc.command = command
|
|
151
|
+
# Normally we can't pass in an ID, and the job serialization logic
|
|
152
|
+
# takes care of filling it in. We set it here.
|
|
156
153
|
if jobStoreID is not None:
|
|
157
154
|
desc.jobStoreID = jobStoreID
|
|
158
155
|
|
|
@@ -185,12 +182,12 @@ class hidden:
|
|
|
185
182
|
|
|
186
183
|
@retry_flaky_test(prepare=[tearDown, setUp])
|
|
187
184
|
def test_run_jobs(self):
|
|
188
|
-
jobDesc1 = self._mockJobDescription(
|
|
185
|
+
jobDesc1 = self._mockJobDescription(jobName='test1', unitName=None,
|
|
189
186
|
jobStoreID='1', requirements=defaultRequirements)
|
|
190
|
-
jobDesc2 = self._mockJobDescription(
|
|
187
|
+
jobDesc2 = self._mockJobDescription(jobName='test2', unitName=None,
|
|
191
188
|
jobStoreID='2', requirements=defaultRequirements)
|
|
192
|
-
job1 = self.batchSystem.issueBatchJob(jobDesc1)
|
|
193
|
-
job2 = self.batchSystem.issueBatchJob(jobDesc2)
|
|
189
|
+
job1 = self.batchSystem.issueBatchJob('sleep 1000', jobDesc1)
|
|
190
|
+
job2 = self.batchSystem.issueBatchJob('sleep 1000', jobDesc2)
|
|
194
191
|
|
|
195
192
|
issuedIDs = self._waitForJobsToIssue(2)
|
|
196
193
|
self.assertEqual(set(issuedIDs), {job1, job2})
|
|
@@ -219,9 +216,9 @@ class hidden:
|
|
|
219
216
|
# then check for it having happened, but we can't guarantee that
|
|
220
217
|
# the batch system will run against the same filesystem we are
|
|
221
218
|
# looking at.
|
|
222
|
-
jobDesc3 = self._mockJobDescription(
|
|
219
|
+
jobDesc3 = self._mockJobDescription(jobName='test3', unitName=None,
|
|
223
220
|
jobStoreID='3', requirements=defaultRequirements)
|
|
224
|
-
job3 = self.batchSystem.issueBatchJob(jobDesc3)
|
|
221
|
+
job3 = self.batchSystem.issueBatchJob("mktemp -d", jobDesc3)
|
|
225
222
|
|
|
226
223
|
jobUpdateInfo = self.batchSystem.getUpdatedBatchJob(maxWait=1000)
|
|
227
224
|
jobID, exitStatus, wallTime = jobUpdateInfo.jobID, jobUpdateInfo.exitStatus, jobUpdateInfo.wallTime
|
|
@@ -252,18 +249,18 @@ class hidden:
|
|
|
252
249
|
|
|
253
250
|
# Turn into a string which convinces bash to take all args and paste them back together and run them
|
|
254
251
|
command = "bash -c \"\\${@}\" bash eval " + script_protected
|
|
255
|
-
jobDesc4 = self._mockJobDescription(
|
|
252
|
+
jobDesc4 = self._mockJobDescription(jobName='test4', unitName=None,
|
|
256
253
|
jobStoreID='4', requirements=defaultRequirements)
|
|
257
|
-
job4 = self.batchSystem.issueBatchJob(jobDesc4)
|
|
254
|
+
job4 = self.batchSystem.issueBatchJob(command, jobDesc4)
|
|
258
255
|
jobUpdateInfo = self.batchSystem.getUpdatedBatchJob(maxWait=1000)
|
|
259
256
|
jobID, exitStatus, wallTime = jobUpdateInfo.jobID, jobUpdateInfo.exitStatus, jobUpdateInfo.wallTime
|
|
260
257
|
self.assertEqual(exitStatus, 42)
|
|
261
258
|
self.assertEqual(jobID, job4)
|
|
262
259
|
# Now set the variable and ensure that it is present
|
|
263
260
|
self.batchSystem.setEnv('FOO', 'bar')
|
|
264
|
-
jobDesc5 = self._mockJobDescription(
|
|
261
|
+
jobDesc5 = self._mockJobDescription(jobName='test5', unitName=None,
|
|
265
262
|
jobStoreID='5', requirements=defaultRequirements)
|
|
266
|
-
job5 = self.batchSystem.issueBatchJob(jobDesc5)
|
|
263
|
+
job5 = self.batchSystem.issueBatchJob(command, jobDesc5)
|
|
267
264
|
jobUpdateInfo = self.batchSystem.getUpdatedBatchJob(maxWait=1000)
|
|
268
265
|
self.assertEqual(jobUpdateInfo.exitStatus, 23)
|
|
269
266
|
self.assertEqual(jobUpdateInfo.jobID, job5)
|
|
@@ -274,18 +271,18 @@ class hidden:
|
|
|
274
271
|
command = "bash -c \"\\${@}\" bash eval " + script.replace(';', r'\;')
|
|
275
272
|
|
|
276
273
|
# Issue a job with a job environment variable
|
|
277
|
-
job_desc_6 = self._mockJobDescription(
|
|
274
|
+
job_desc_6 = self._mockJobDescription(jobName='test6', unitName=None,
|
|
278
275
|
jobStoreID='6', requirements=defaultRequirements)
|
|
279
|
-
job6 = self.batchSystem.issueBatchJob(job_desc_6, job_environment={
|
|
276
|
+
job6 = self.batchSystem.issueBatchJob(command, job_desc_6, job_environment={
|
|
280
277
|
'FOO': 'bar'
|
|
281
278
|
})
|
|
282
279
|
job_update_info = self.batchSystem.getUpdatedBatchJob(maxWait=1000)
|
|
283
280
|
self.assertEqual(job_update_info.exitStatus, 23) # this should succeed
|
|
284
281
|
self.assertEqual(job_update_info.jobID, job6)
|
|
285
282
|
# Now check that the environment variable doesn't exist for other jobs
|
|
286
|
-
job_desc_7 = self._mockJobDescription(
|
|
283
|
+
job_desc_7 = self._mockJobDescription(jobName='test7', unitName=None,
|
|
287
284
|
jobStoreID='7', requirements=defaultRequirements)
|
|
288
|
-
job7 = self.batchSystem.issueBatchJob(job_desc_7)
|
|
285
|
+
job7 = self.batchSystem.issueBatchJob(command, job_desc_7)
|
|
289
286
|
job_update_info = self.batchSystem.getUpdatedBatchJob(maxWait=1000)
|
|
290
287
|
self.assertEqual(job_update_info.exitStatus, 42)
|
|
291
288
|
self.assertEqual(job_update_info.jobID, job7)
|
|
@@ -619,9 +616,9 @@ class MesosBatchSystemTest(hidden.AbstractBatchSystemTest, MesosTestSupport):
|
|
|
619
616
|
|
|
620
617
|
def testIgnoreNode(self):
|
|
621
618
|
self.batchSystem.ignoreNode('localhost')
|
|
622
|
-
jobDesc = self._mockJobDescription(
|
|
619
|
+
jobDesc = self._mockJobDescription(jobName='test2', unitName=None,
|
|
623
620
|
jobStoreID='1', requirements=defaultRequirements)
|
|
624
|
-
job = self.batchSystem.issueBatchJob(jobDesc)
|
|
621
|
+
job = self.batchSystem.issueBatchJob('sleep 1000', jobDesc)
|
|
625
622
|
|
|
626
623
|
issuedID = self._waitForJobsToIssue(1)
|
|
627
624
|
self.assertEqual(set(issuedID), {job})
|
|
@@ -731,8 +728,13 @@ class SingleMachineBatchSystemTest(hidden.AbstractBatchSystemTest):
|
|
|
731
728
|
command += ' hide'
|
|
732
729
|
|
|
733
730
|
# Start the job
|
|
734
|
-
self.batchSystem.issueBatchJob(
|
|
735
|
-
|
|
731
|
+
self.batchSystem.issueBatchJob(
|
|
732
|
+
command,
|
|
733
|
+
self._mockJobDescription(
|
|
734
|
+
jobName='fork',
|
|
735
|
+
jobStoreID='1',
|
|
736
|
+
requirements=defaultRequirements)
|
|
737
|
+
)
|
|
736
738
|
# Wait
|
|
737
739
|
time.sleep(10)
|
|
738
740
|
|
|
@@ -863,13 +865,18 @@ class MaxCoresSingleMachineBatchSystemTest(ToilTest):
|
|
|
863
865
|
try:
|
|
864
866
|
jobIds = set()
|
|
865
867
|
for i in range(0, int(jobs)):
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
868
|
+
desc = JobDescription(
|
|
869
|
+
requirements=dict(
|
|
870
|
+
cores=float(coresPerJob),
|
|
871
|
+
memory=1,
|
|
872
|
+
disk=1,
|
|
873
|
+
accelerators=[],
|
|
874
|
+
preemptible=preemptible
|
|
875
|
+
),
|
|
876
|
+
jobName=str(i),
|
|
877
|
+
unitName=''
|
|
878
|
+
)
|
|
879
|
+
jobIds.add(bs.issueBatchJob(self.scriptCommand(), desc))
|
|
873
880
|
self.assertEqual(len(jobIds), jobs)
|
|
874
881
|
while jobIds:
|
|
875
882
|
job = bs.getUpdatedBatchJob(maxWait=10)
|
|
@@ -894,7 +901,7 @@ class MaxCoresSingleMachineBatchSystemTest(ToilTest):
|
|
|
894
901
|
@skipIf(SingleMachineBatchSystem.numCores < 3, 'Need at least three cores to run this test')
|
|
895
902
|
def testServices(self):
|
|
896
903
|
options = Job.Runner.getDefaultOptions(self._getTestJobStorePath())
|
|
897
|
-
options.
|
|
904
|
+
options.logLevel = "DEBUG"
|
|
898
905
|
options.maxCores = 3
|
|
899
906
|
self.assertTrue(options.maxCores <= SingleMachineBatchSystem.numCores)
|
|
900
907
|
Job.Runner.startToil(Job.wrapJobFn(parentJob, self.scriptCommand()), options)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# Copyright (C) 2015-2021 Regents of the University of California
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Optional, Dict, List, Type
|
|
16
|
+
from configargparse import ArgParser, ArgumentParser
|
|
17
|
+
|
|
18
|
+
from toil.batchSystems.abstractBatchSystem import (AbstractBatchSystem, UpdatedBatchJobInfo)
|
|
19
|
+
from toil.batchSystems.cleanup_support import BatchSystemCleanupSupport
|
|
20
|
+
from toil.batchSystems.options import OptionSetter
|
|
21
|
+
from toil.batchSystems.registry import add_batch_system_factory
|
|
22
|
+
from toil.common import Toil, addOptions
|
|
23
|
+
from toil.job import JobDescription
|
|
24
|
+
from toil.test import ToilTest
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FakeBatchSystem(BatchSystemCleanupSupport):
|
|
30
|
+
@classmethod
|
|
31
|
+
def supportsAutoDeployment(cls) -> bool:
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def issueBatchJob(self, command: str, job_desc: JobDescription, job_environment: Optional[Dict[str, str]] = None) -> int:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def killBatchJobs(self, jobIDs: List[int]) -> None:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def getIssuedBatchJobIDs(self) -> List[int]:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def getRunningBatchJobIDs(self) -> Dict[int, float]:
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def getUpdatedBatchJob(self, maxWait: int) -> Optional[UpdatedBatchJobInfo]:
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def shutdown(self) -> None:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def add_options(cls, parser: ArgumentParser) -> None:
|
|
54
|
+
parser.add_argument("--fake_argument", default="exists")
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def setOptions(cls, setOption: OptionSetter) -> None:
|
|
58
|
+
setOption("fake_argument")
|
|
59
|
+
|
|
60
|
+
class BatchSystemPluginTest(ToilTest):
|
|
61
|
+
def test_batchsystem_plugin_installable(self):
|
|
62
|
+
"""
|
|
63
|
+
Test that installing a batch system plugin works.
|
|
64
|
+
:return:
|
|
65
|
+
"""
|
|
66
|
+
def fake_batch_system_factory() -> Type[AbstractBatchSystem]:
|
|
67
|
+
return FakeBatchSystem
|
|
68
|
+
|
|
69
|
+
add_batch_system_factory("fake", fake_batch_system_factory)
|
|
70
|
+
|
|
71
|
+
parser = ArgParser()
|
|
72
|
+
addOptions(parser)
|
|
73
|
+
|
|
74
|
+
options = parser.parse_args(["test-jobstore", "--clean=always"])
|
|
75
|
+
|
|
76
|
+
# try to install a batchsystem plugin with some arguments
|
|
77
|
+
# if the arguments exists, the values should also exist in the config
|
|
78
|
+
with Toil(options) as toil:
|
|
79
|
+
self.assertEqual(toil.config.fake_argument == "exists", True)
|
|
@@ -193,7 +193,7 @@ class SlurmTest(ToilTest):
|
|
|
193
193
|
|
|
194
194
|
def setUp(self):
|
|
195
195
|
self.monkeypatch = pytest.MonkeyPatch()
|
|
196
|
-
self.worker = toil.batchSystems.slurm.SlurmBatchSystem.
|
|
196
|
+
self.worker = toil.batchSystems.slurm.SlurmBatchSystem.GridEngineThread(
|
|
197
197
|
newJobsQueue=Queue(),
|
|
198
198
|
updatedJobsQueue=Queue(),
|
|
199
199
|
killQueue=Queue(),
|