toil 6.1.0a1__py3-none-any.whl → 8.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 +122 -315
- toil/batchSystems/__init__.py +1 -0
- toil/batchSystems/abstractBatchSystem.py +173 -89
- toil/batchSystems/abstractGridEngineBatchSystem.py +272 -148
- toil/batchSystems/awsBatch.py +244 -135
- toil/batchSystems/cleanup_support.py +26 -16
- toil/batchSystems/contained_executor.py +31 -28
- toil/batchSystems/gridengine.py +86 -50
- toil/batchSystems/htcondor.py +166 -89
- toil/batchSystems/kubernetes.py +632 -382
- toil/batchSystems/local_support.py +20 -15
- toil/batchSystems/lsf.py +134 -81
- toil/batchSystems/lsfHelper.py +13 -11
- toil/batchSystems/mesos/__init__.py +41 -29
- toil/batchSystems/mesos/batchSystem.py +290 -151
- toil/batchSystems/mesos/executor.py +79 -50
- toil/batchSystems/mesos/test/__init__.py +31 -23
- toil/batchSystems/options.py +46 -28
- toil/batchSystems/registry.py +53 -19
- toil/batchSystems/singleMachine.py +296 -125
- toil/batchSystems/slurm.py +603 -138
- toil/batchSystems/torque.py +47 -33
- toil/bus.py +186 -76
- toil/common.py +664 -368
- toil/cwl/__init__.py +1 -1
- toil/cwl/cwltoil.py +1136 -483
- toil/cwl/utils.py +17 -22
- toil/deferred.py +63 -42
- toil/exceptions.py +5 -3
- toil/fileStores/__init__.py +5 -5
- toil/fileStores/abstractFileStore.py +140 -60
- toil/fileStores/cachingFileStore.py +717 -269
- toil/fileStores/nonCachingFileStore.py +116 -87
- toil/job.py +1225 -368
- toil/jobStores/abstractJobStore.py +416 -266
- toil/jobStores/aws/jobStore.py +863 -477
- toil/jobStores/aws/utils.py +201 -120
- toil/jobStores/conftest.py +3 -2
- toil/jobStores/fileJobStore.py +292 -154
- toil/jobStores/googleJobStore.py +140 -74
- toil/jobStores/utils.py +36 -15
- toil/leader.py +668 -272
- toil/lib/accelerators.py +115 -18
- toil/lib/aws/__init__.py +74 -31
- toil/lib/aws/ami.py +122 -87
- toil/lib/aws/iam.py +284 -108
- toil/lib/aws/s3.py +31 -0
- toil/lib/aws/session.py +214 -39
- toil/lib/aws/utils.py +287 -231
- toil/lib/bioio.py +13 -5
- toil/lib/compatibility.py +11 -6
- toil/lib/conversions.py +104 -47
- toil/lib/docker.py +131 -103
- toil/lib/ec2.py +361 -199
- toil/lib/ec2nodes.py +174 -106
- toil/lib/encryption/_dummy.py +5 -3
- toil/lib/encryption/_nacl.py +10 -6
- toil/lib/encryption/conftest.py +1 -0
- toil/lib/exceptions.py +26 -7
- toil/lib/expando.py +5 -3
- toil/lib/ftp_utils.py +217 -0
- toil/lib/generatedEC2Lists.py +127 -19
- toil/lib/humanize.py +6 -2
- toil/lib/integration.py +341 -0
- toil/lib/io.py +141 -15
- toil/lib/iterables.py +4 -2
- toil/lib/memoize.py +12 -8
- toil/lib/misc.py +66 -21
- toil/lib/objects.py +2 -2
- toil/lib/resources.py +68 -15
- toil/lib/retry.py +126 -81
- toil/lib/threading.py +299 -82
- toil/lib/throttle.py +16 -15
- toil/options/common.py +843 -409
- toil/options/cwl.py +175 -90
- toil/options/runner.py +50 -0
- toil/options/wdl.py +73 -17
- toil/provisioners/__init__.py +117 -46
- toil/provisioners/abstractProvisioner.py +332 -157
- toil/provisioners/aws/__init__.py +70 -33
- toil/provisioners/aws/awsProvisioner.py +1145 -715
- toil/provisioners/clusterScaler.py +541 -279
- toil/provisioners/gceProvisioner.py +282 -179
- toil/provisioners/node.py +155 -79
- toil/realtimeLogger.py +34 -22
- toil/resource.py +137 -75
- toil/server/app.py +128 -62
- toil/server/celery_app.py +3 -1
- toil/server/cli/wes_cwl_runner.py +82 -53
- toil/server/utils.py +54 -28
- toil/server/wes/abstract_backend.py +64 -26
- toil/server/wes/amazon_wes_utils.py +21 -15
- toil/server/wes/tasks.py +121 -63
- toil/server/wes/toil_backend.py +142 -107
- toil/server/wsgi_app.py +4 -3
- toil/serviceManager.py +58 -22
- toil/statsAndLogging.py +224 -70
- toil/test/__init__.py +282 -183
- toil/test/batchSystems/batchSystemTest.py +460 -210
- toil/test/batchSystems/batch_system_plugin_test.py +90 -0
- toil/test/batchSystems/test_gridengine.py +173 -0
- toil/test/batchSystems/test_lsf_helper.py +67 -58
- toil/test/batchSystems/test_slurm.py +110 -49
- toil/test/cactus/__init__.py +0 -0
- toil/test/cactus/test_cactus_integration.py +56 -0
- toil/test/cwl/cwlTest.py +496 -287
- toil/test/cwl/measure_default_memory.cwl +12 -0
- toil/test/cwl/not_run_required_input.cwl +29 -0
- toil/test/cwl/scatter_duplicate_outputs.cwl +40 -0
- toil/test/cwl/seqtk_seq.cwl +1 -1
- toil/test/docs/scriptsTest.py +69 -46
- toil/test/jobStores/jobStoreTest.py +427 -264
- toil/test/lib/aws/test_iam.py +118 -50
- toil/test/lib/aws/test_s3.py +16 -9
- toil/test/lib/aws/test_utils.py +5 -6
- toil/test/lib/dockerTest.py +118 -141
- toil/test/lib/test_conversions.py +113 -115
- toil/test/lib/test_ec2.py +58 -50
- toil/test/lib/test_integration.py +104 -0
- toil/test/lib/test_misc.py +12 -5
- toil/test/mesos/MesosDataStructuresTest.py +23 -10
- toil/test/mesos/helloWorld.py +7 -6
- toil/test/mesos/stress.py +25 -20
- toil/test/options/__init__.py +13 -0
- toil/test/options/options.py +42 -0
- toil/test/provisioners/aws/awsProvisionerTest.py +320 -150
- toil/test/provisioners/clusterScalerTest.py +440 -250
- toil/test/provisioners/clusterTest.py +166 -44
- toil/test/provisioners/gceProvisionerTest.py +174 -100
- toil/test/provisioners/provisionerTest.py +25 -13
- toil/test/provisioners/restartScript.py +5 -4
- toil/test/server/serverTest.py +188 -141
- toil/test/sort/restart_sort.py +137 -68
- toil/test/sort/sort.py +134 -66
- toil/test/sort/sortTest.py +91 -49
- toil/test/src/autoDeploymentTest.py +141 -101
- toil/test/src/busTest.py +20 -18
- toil/test/src/checkpointTest.py +8 -2
- toil/test/src/deferredFunctionTest.py +49 -35
- toil/test/src/dockerCheckTest.py +32 -24
- toil/test/src/environmentTest.py +135 -0
- toil/test/src/fileStoreTest.py +539 -272
- toil/test/src/helloWorldTest.py +7 -4
- toil/test/src/importExportFileTest.py +61 -31
- toil/test/src/jobDescriptionTest.py +46 -21
- toil/test/src/jobEncapsulationTest.py +2 -0
- toil/test/src/jobFileStoreTest.py +74 -50
- toil/test/src/jobServiceTest.py +187 -73
- toil/test/src/jobTest.py +121 -71
- toil/test/src/miscTests.py +19 -18
- toil/test/src/promisedRequirementTest.py +82 -36
- toil/test/src/promisesTest.py +7 -6
- toil/test/src/realtimeLoggerTest.py +10 -6
- toil/test/src/regularLogTest.py +71 -37
- toil/test/src/resourceTest.py +80 -49
- toil/test/src/restartDAGTest.py +36 -22
- toil/test/src/resumabilityTest.py +9 -2
- toil/test/src/retainTempDirTest.py +45 -14
- toil/test/src/systemTest.py +12 -8
- toil/test/src/threadingTest.py +44 -25
- toil/test/src/toilContextManagerTest.py +10 -7
- toil/test/src/userDefinedJobArgTypeTest.py +8 -5
- toil/test/src/workerTest.py +73 -23
- toil/test/utils/toilDebugTest.py +103 -33
- toil/test/utils/toilKillTest.py +4 -5
- toil/test/utils/utilsTest.py +245 -106
- toil/test/wdl/wdltoil_test.py +818 -149
- toil/test/wdl/wdltoil_test_kubernetes.py +91 -0
- toil/toilState.py +120 -35
- toil/utils/toilConfig.py +13 -4
- toil/utils/toilDebugFile.py +44 -27
- toil/utils/toilDebugJob.py +214 -27
- toil/utils/toilDestroyCluster.py +11 -6
- toil/utils/toilKill.py +8 -3
- toil/utils/toilLaunchCluster.py +256 -140
- toil/utils/toilMain.py +37 -16
- toil/utils/toilRsyncCluster.py +32 -14
- toil/utils/toilSshCluster.py +49 -22
- toil/utils/toilStats.py +356 -273
- toil/utils/toilStatus.py +292 -139
- toil/utils/toilUpdateEC2Instances.py +3 -1
- toil/version.py +12 -12
- toil/wdl/utils.py +5 -5
- toil/wdl/wdltoil.py +3913 -1033
- toil/worker.py +367 -184
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/LICENSE +25 -0
- toil-8.0.0.dist-info/METADATA +173 -0
- toil-8.0.0.dist-info/RECORD +253 -0
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/WHEEL +1 -1
- toil-6.1.0a1.dist-info/METADATA +0 -125
- toil-6.1.0a1.dist-info/RECORD +0 -237
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/entry_points.txt +0 -0
- {toil-6.1.0a1.dist-info → toil-8.0.0.dist-info}/top_level.txt +0 -0
|
@@ -28,7 +28,7 @@ from itertools import chain, islice
|
|
|
28
28
|
from queue import Queue
|
|
29
29
|
from tempfile import mkstemp
|
|
30
30
|
from threading import Thread
|
|
31
|
-
from typing import Any
|
|
31
|
+
from typing import Any
|
|
32
32
|
from urllib.request import Request, urlopen
|
|
33
33
|
|
|
34
34
|
import pytest
|
|
@@ -37,21 +37,21 @@ from stubserver import FTPStubServer
|
|
|
37
37
|
from toil.common import Config, Toil
|
|
38
38
|
from toil.fileStores import FileID
|
|
39
39
|
from toil.job import Job, JobDescription, TemporaryID
|
|
40
|
-
from toil.jobStores.abstractJobStore import
|
|
41
|
-
NoSuchJobException)
|
|
40
|
+
from toil.jobStores.abstractJobStore import NoSuchFileException, NoSuchJobException
|
|
42
41
|
from toil.jobStores.fileJobStore import FileJobStore
|
|
43
|
-
from toil.lib.aws.utils import create_s3_bucket, get_object_for_url
|
|
44
42
|
from toil.lib.io import mkdtemp
|
|
45
43
|
from toil.lib.memoize import memoize
|
|
46
44
|
from toil.lib.retry import retry
|
|
47
45
|
from toil.statsAndLogging import StatsAndLogging
|
|
48
|
-
from toil.test import (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
46
|
+
from toil.test import (
|
|
47
|
+
ToilTest,
|
|
48
|
+
make_tests,
|
|
49
|
+
needs_aws_s3,
|
|
50
|
+
needs_encryption,
|
|
51
|
+
needs_google_project,
|
|
52
|
+
needs_google_storage,
|
|
53
|
+
slow,
|
|
54
|
+
)
|
|
55
55
|
|
|
56
56
|
# noinspection PyPackageRequirements
|
|
57
57
|
# (installed by `make prepare`)
|
|
@@ -83,11 +83,11 @@ class AbstractJobStoreTest:
|
|
|
83
83
|
def setUpClass(cls):
|
|
84
84
|
super().setUpClass()
|
|
85
85
|
logging.basicConfig(level=logging.DEBUG)
|
|
86
|
-
logging.getLogger(
|
|
87
|
-
logging.getLogger(
|
|
88
|
-
logging.getLogger(
|
|
89
|
-
logging.getLogger(
|
|
90
|
-
logging.getLogger(
|
|
86
|
+
logging.getLogger("boto").setLevel(logging.CRITICAL)
|
|
87
|
+
logging.getLogger("boto").setLevel(logging.WARNING)
|
|
88
|
+
logging.getLogger("boto3.resources").setLevel(logging.WARNING)
|
|
89
|
+
logging.getLogger("botocore.auth").setLevel(logging.WARNING)
|
|
90
|
+
logging.getLogger("botocore.hooks").setLevel(logging.WARNING)
|
|
91
91
|
|
|
92
92
|
# The use of @memoize ensures that we only have one instance of per class even with the
|
|
93
93
|
# generative import/export tests attempts to instantiate more. This in turn enables us to
|
|
@@ -114,7 +114,7 @@ class AbstractJobStoreTest:
|
|
|
114
114
|
|
|
115
115
|
def setUp(self):
|
|
116
116
|
super().setUp()
|
|
117
|
-
self.namePrefix =
|
|
117
|
+
self.namePrefix = "jobstore-test-" + str(uuid.uuid4())
|
|
118
118
|
self.config = self._createConfig()
|
|
119
119
|
|
|
120
120
|
# Jobstores to be used in testing.
|
|
@@ -128,11 +128,16 @@ class AbstractJobStoreTest:
|
|
|
128
128
|
self.jobstore_resumed_noconfig.resume()
|
|
129
129
|
|
|
130
130
|
# Requirements for jobs to be created.
|
|
131
|
-
self.arbitraryRequirements = {
|
|
131
|
+
self.arbitraryRequirements = {
|
|
132
|
+
"memory": 1,
|
|
133
|
+
"disk": 2,
|
|
134
|
+
"cores": 1,
|
|
135
|
+
"preemptible": False,
|
|
136
|
+
}
|
|
132
137
|
# Function to make an arbitrary new job
|
|
133
|
-
self.arbitraryJob = lambda: JobDescription(
|
|
134
|
-
|
|
135
|
-
|
|
138
|
+
self.arbitraryJob = lambda: JobDescription(
|
|
139
|
+
jobName="arbitrary", requirements=self.arbitraryRequirements
|
|
140
|
+
)
|
|
136
141
|
|
|
137
142
|
self.parentJobReqs = dict(memory=12, cores=34, disk=35, preemptible=True)
|
|
138
143
|
self.childJobReqs1 = dict(memory=23, cores=45, disk=46, preemptible=True)
|
|
@@ -144,9 +149,13 @@ class AbstractJobStoreTest:
|
|
|
144
149
|
super().tearDown()
|
|
145
150
|
|
|
146
151
|
def testInitialState(self):
|
|
147
|
-
"""Ensure proper handling of
|
|
148
|
-
self.assertFalse(self.jobstore_initialized.job_exists(
|
|
149
|
-
self.assertRaises(
|
|
152
|
+
"""Ensure proper handling of nonexistent files."""
|
|
153
|
+
self.assertFalse(self.jobstore_initialized.job_exists("nonexistentFile"))
|
|
154
|
+
self.assertRaises(
|
|
155
|
+
NoSuchJobException,
|
|
156
|
+
self.jobstore_initialized.load_job,
|
|
157
|
+
"nonexistentFile",
|
|
158
|
+
)
|
|
150
159
|
|
|
151
160
|
def testJobCreation(self):
|
|
152
161
|
"""
|
|
@@ -159,9 +168,9 @@ class AbstractJobStoreTest:
|
|
|
159
168
|
jobstore = self.jobstore_initialized
|
|
160
169
|
|
|
161
170
|
# Create a job and verify its existence/properties
|
|
162
|
-
job = JobDescription(
|
|
163
|
-
|
|
164
|
-
|
|
171
|
+
job = JobDescription(
|
|
172
|
+
requirements=self.parentJobReqs, jobName="test1", unitName="onParent"
|
|
173
|
+
)
|
|
165
174
|
self.assertTrue(isinstance(job.jobStoreID, TemporaryID))
|
|
166
175
|
jobstore.assign_job_id(job)
|
|
167
176
|
self.assertFalse(isinstance(job.jobStoreID, TemporaryID))
|
|
@@ -170,13 +179,12 @@ class AbstractJobStoreTest:
|
|
|
170
179
|
self.assertEqual(created, job)
|
|
171
180
|
|
|
172
181
|
self.assertTrue(jobstore.job_exists(job.jobStoreID))
|
|
173
|
-
self.assertEqual(job.
|
|
174
|
-
self.assertEqual(job.
|
|
175
|
-
self.assertEqual(job.
|
|
176
|
-
self.assertEqual(job.
|
|
177
|
-
self.assertEqual(job.
|
|
178
|
-
self.assertEqual(job.
|
|
179
|
-
self.assertEqual(job.unitName, 'onParent')
|
|
182
|
+
self.assertEqual(job.memory, self.parentJobReqs["memory"])
|
|
183
|
+
self.assertEqual(job.cores, self.parentJobReqs["cores"])
|
|
184
|
+
self.assertEqual(job.disk, self.parentJobReqs["disk"])
|
|
185
|
+
self.assertEqual(job.preemptible, self.parentJobReqs["preemptible"])
|
|
186
|
+
self.assertEqual(job.jobName, "test1")
|
|
187
|
+
self.assertEqual(job.unitName, "onParent")
|
|
180
188
|
|
|
181
189
|
def testConfigEquality(self):
|
|
182
190
|
"""
|
|
@@ -195,26 +203,26 @@ class AbstractJobStoreTest:
|
|
|
195
203
|
"""Tests that a job created via one JobStore instance can be loaded from another."""
|
|
196
204
|
|
|
197
205
|
# Create a job on the first jobstore.
|
|
198
|
-
jobDesc1 = JobDescription(
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
jobDesc1 = JobDescription(
|
|
207
|
+
requirements=self.parentJobReqs, jobName="test1", unitName="onJS1"
|
|
208
|
+
)
|
|
201
209
|
self.jobstore_initialized.assign_job_id(jobDesc1)
|
|
202
210
|
self.jobstore_initialized.create_job(jobDesc1)
|
|
203
211
|
|
|
204
212
|
# Load it from the second jobstore
|
|
205
213
|
jobDesc2 = self.jobstore_resumed_noconfig.load_job(jobDesc1.jobStoreID)
|
|
206
214
|
|
|
207
|
-
self.assertEqual(jobDesc1.
|
|
215
|
+
self.assertEqual(jobDesc1._body, jobDesc2._body)
|
|
208
216
|
|
|
209
217
|
def testChildLoadingEquality(self):
|
|
210
218
|
"""Test that loading a child job operates as expected."""
|
|
211
|
-
job = JobDescription(
|
|
212
|
-
|
|
213
|
-
|
|
219
|
+
job = JobDescription(
|
|
220
|
+
requirements=self.parentJobReqs, jobName="test1", unitName="onParent"
|
|
221
|
+
)
|
|
214
222
|
|
|
215
|
-
childJob = JobDescription(
|
|
216
|
-
|
|
217
|
-
|
|
223
|
+
childJob = JobDescription(
|
|
224
|
+
requirements=self.childJobReqs1, jobName="test2", unitName="onChild1"
|
|
225
|
+
)
|
|
218
226
|
self.jobstore_initialized.assign_job_id(job)
|
|
219
227
|
self.jobstore_initialized.assign_job_id(childJob)
|
|
220
228
|
self.jobstore_initialized.create_job(job)
|
|
@@ -222,7 +230,10 @@ class AbstractJobStoreTest:
|
|
|
222
230
|
job.addChild(childJob.jobStoreID)
|
|
223
231
|
self.jobstore_initialized.update_job(job)
|
|
224
232
|
|
|
225
|
-
self.assertEqual(
|
|
233
|
+
self.assertEqual(
|
|
234
|
+
self.jobstore_initialized.load_job(list(job.allSuccessors())[0])._body,
|
|
235
|
+
childJob._body,
|
|
236
|
+
)
|
|
226
237
|
|
|
227
238
|
def testPersistantFilesToDelete(self):
|
|
228
239
|
"""
|
|
@@ -237,32 +248,35 @@ class AbstractJobStoreTest:
|
|
|
237
248
|
"""
|
|
238
249
|
|
|
239
250
|
# Create a job.
|
|
240
|
-
job = JobDescription(
|
|
241
|
-
|
|
242
|
-
|
|
251
|
+
job = JobDescription(
|
|
252
|
+
requirements=self.parentJobReqs, jobName="test1", unitName="onJS1"
|
|
253
|
+
)
|
|
243
254
|
|
|
244
255
|
self.jobstore_initialized.assign_job_id(job)
|
|
245
256
|
self.jobstore_initialized.create_job(job)
|
|
246
|
-
job.filesToDelete = [
|
|
257
|
+
job.filesToDelete = ["1", "2"]
|
|
247
258
|
self.jobstore_initialized.update_job(job)
|
|
248
|
-
self.assertEqual(
|
|
259
|
+
self.assertEqual(
|
|
260
|
+
self.jobstore_initialized.load_job(job.jobStoreID).filesToDelete,
|
|
261
|
+
["1", "2"],
|
|
262
|
+
)
|
|
249
263
|
|
|
250
264
|
def testUpdateBehavior(self):
|
|
251
265
|
"""Tests the proper behavior during updating jobs."""
|
|
252
266
|
jobstore1 = self.jobstore_initialized
|
|
253
267
|
jobstore2 = self.jobstore_resumed_noconfig
|
|
254
268
|
|
|
255
|
-
job1 = JobDescription(
|
|
256
|
-
|
|
257
|
-
|
|
269
|
+
job1 = JobDescription(
|
|
270
|
+
requirements=self.parentJobReqs, jobName="test1", unitName="onParent"
|
|
271
|
+
)
|
|
258
272
|
|
|
259
|
-
childJob1 = JobDescription(
|
|
260
|
-
|
|
261
|
-
|
|
273
|
+
childJob1 = JobDescription(
|
|
274
|
+
requirements=self.childJobReqs1, jobName="test2", unitName="onChild1"
|
|
275
|
+
)
|
|
262
276
|
|
|
263
|
-
childJob2 = JobDescription(
|
|
264
|
-
|
|
265
|
-
|
|
277
|
+
childJob2 = JobDescription(
|
|
278
|
+
requirements=self.childJobReqs2, jobName="test3", unitName="onChild2"
|
|
279
|
+
)
|
|
266
280
|
|
|
267
281
|
jobstore1.assign_job_id(job1)
|
|
268
282
|
jobstore1.create_job(job1)
|
|
@@ -281,7 +295,9 @@ class AbstractJobStoreTest:
|
|
|
281
295
|
|
|
282
296
|
# Check equivalence between jobstore1 and jobstore2.
|
|
283
297
|
# While job1 and job2 share a jobStoreID, job1 has not been "refreshed" to show the newly added child jobs.
|
|
284
|
-
self.assertNotEqual(
|
|
298
|
+
self.assertNotEqual(
|
|
299
|
+
sorted(job2.allSuccessors()), sorted(job1.allSuccessors())
|
|
300
|
+
)
|
|
285
301
|
|
|
286
302
|
# Reload parent job on jobstore, "refreshing" the job.
|
|
287
303
|
job1 = jobstore1.load_job(job1.jobStoreID)
|
|
@@ -298,21 +314,21 @@ class AbstractJobStoreTest:
|
|
|
298
314
|
"""Tests the consequences of deleting jobs."""
|
|
299
315
|
# A local jobstore object for testing.
|
|
300
316
|
jobstore = self.jobstore_initialized
|
|
301
|
-
job = JobDescription(
|
|
302
|
-
|
|
303
|
-
|
|
317
|
+
job = JobDescription(
|
|
318
|
+
requirements=self.parentJobReqs, jobName="test1", unitName="onJob"
|
|
319
|
+
)
|
|
304
320
|
# Create job
|
|
305
321
|
jobstore.assign_job_id(job)
|
|
306
322
|
jobstore.create_job(job)
|
|
307
323
|
|
|
308
324
|
# Create child Jobs
|
|
309
|
-
child1 = JobDescription(
|
|
310
|
-
|
|
311
|
-
|
|
325
|
+
child1 = JobDescription(
|
|
326
|
+
requirements=self.childJobReqs1, jobName="test2", unitName="onChild1"
|
|
327
|
+
)
|
|
312
328
|
|
|
313
|
-
child2 = JobDescription(
|
|
314
|
-
|
|
315
|
-
|
|
329
|
+
child2 = JobDescription(
|
|
330
|
+
requirements=self.childJobReqs2, jobName="test3", unitName="onChild2"
|
|
331
|
+
)
|
|
316
332
|
|
|
317
333
|
# Add children to parent.
|
|
318
334
|
jobstore.assign_job_id(child1)
|
|
@@ -323,9 +339,8 @@ class AbstractJobStoreTest:
|
|
|
323
339
|
job.addChild(child2.jobStoreID)
|
|
324
340
|
jobstore.update_job(job)
|
|
325
341
|
|
|
326
|
-
#
|
|
327
|
-
job.
|
|
328
|
-
jobstore.update_job(job)
|
|
342
|
+
# Parent must have no body to start on children
|
|
343
|
+
assert not job.has_body()
|
|
329
344
|
|
|
330
345
|
# Go get the children
|
|
331
346
|
childJobs = [jobstore.load_job(childID) for childID in job.nextSuccessors()]
|
|
@@ -335,7 +350,10 @@ class AbstractJobStoreTest:
|
|
|
335
350
|
# jobs that show up are a subset of all existing jobs. If we had deleted jobs before
|
|
336
351
|
# this we would have to worry about ghost jobs appearing and this assertion would not
|
|
337
352
|
# be valid
|
|
338
|
-
self.assertTrue(
|
|
353
|
+
self.assertTrue(
|
|
354
|
+
{j.jobStoreID for j in (childJobs + [job])}
|
|
355
|
+
>= {j.jobStoreID for j in jobstore.jobs()}
|
|
356
|
+
)
|
|
339
357
|
|
|
340
358
|
# Test job deletions
|
|
341
359
|
# First delete parent, this should have no effect on the children
|
|
@@ -348,12 +366,14 @@ class AbstractJobStoreTest:
|
|
|
348
366
|
self.assertTrue(jobstore.job_exists(childJob.jobStoreID))
|
|
349
367
|
jobstore.delete_job(childJob.jobStoreID)
|
|
350
368
|
self.assertFalse(jobstore.job_exists(childJob.jobStoreID))
|
|
351
|
-
self.assertRaises(
|
|
369
|
+
self.assertRaises(
|
|
370
|
+
NoSuchJobException, jobstore.load_job, childJob.jobStoreID
|
|
371
|
+
)
|
|
352
372
|
|
|
353
373
|
try:
|
|
354
|
-
with jobstore.read_shared_file_stream(
|
|
374
|
+
with jobstore.read_shared_file_stream("missing") as _:
|
|
355
375
|
pass
|
|
356
|
-
self.fail(
|
|
376
|
+
self.fail("Expecting NoSuchFileException")
|
|
357
377
|
except NoSuchFileException:
|
|
358
378
|
pass
|
|
359
379
|
|
|
@@ -362,36 +382,40 @@ class AbstractJobStoreTest:
|
|
|
362
382
|
jobstore1 = self.jobstore_initialized
|
|
363
383
|
jobstore2 = self.jobstore_resumed_noconfig
|
|
364
384
|
|
|
365
|
-
bar = b
|
|
385
|
+
bar = b"bar"
|
|
366
386
|
|
|
367
|
-
with jobstore1.write_shared_file_stream(
|
|
387
|
+
with jobstore1.write_shared_file_stream("foo") as f:
|
|
368
388
|
f.write(bar)
|
|
369
389
|
# ... read that file on worker, ...
|
|
370
|
-
with jobstore2.read_shared_file_stream(
|
|
390
|
+
with jobstore2.read_shared_file_stream("foo") as f:
|
|
371
391
|
self.assertEqual(bar, f.read())
|
|
372
392
|
# ... and read it again on jobstore1.
|
|
373
|
-
with jobstore1.read_shared_file_stream(
|
|
393
|
+
with jobstore1.read_shared_file_stream("foo") as f:
|
|
374
394
|
self.assertEqual(bar, f.read())
|
|
375
395
|
|
|
376
|
-
with jobstore1.write_shared_file_stream(
|
|
396
|
+
with jobstore1.write_shared_file_stream(
|
|
397
|
+
"nonEncrypted", encrypted=False
|
|
398
|
+
) as f:
|
|
377
399
|
f.write(bar)
|
|
378
|
-
self.assertUrl(jobstore1.get_shared_public_url(
|
|
379
|
-
self.assertRaises(
|
|
400
|
+
self.assertUrl(jobstore1.get_shared_public_url("nonEncrypted"))
|
|
401
|
+
self.assertRaises(
|
|
402
|
+
NoSuchFileException, jobstore1.get_shared_public_url, "missing"
|
|
403
|
+
)
|
|
380
404
|
|
|
381
405
|
def testReadWriteSharedFilesTextMode(self):
|
|
382
406
|
"""Checks if text mode is compatible for shared file streams."""
|
|
383
407
|
jobstore1 = self.jobstore_initialized
|
|
384
408
|
jobstore2 = self.jobstore_resumed_noconfig
|
|
385
409
|
|
|
386
|
-
bar =
|
|
410
|
+
bar = "bar"
|
|
387
411
|
|
|
388
|
-
with jobstore1.write_shared_file_stream(
|
|
412
|
+
with jobstore1.write_shared_file_stream("foo", encoding="utf-8") as f:
|
|
389
413
|
f.write(bar)
|
|
390
414
|
|
|
391
|
-
with jobstore2.read_shared_file_stream(
|
|
415
|
+
with jobstore2.read_shared_file_stream("foo", encoding="utf-8") as f:
|
|
392
416
|
self.assertEqual(bar, f.read())
|
|
393
417
|
|
|
394
|
-
with jobstore1.read_shared_file_stream(
|
|
418
|
+
with jobstore1.read_shared_file_stream("foo", encoding="utf-8") as f:
|
|
395
419
|
self.assertEqual(bar, f.read())
|
|
396
420
|
|
|
397
421
|
def testReadWriteFileStreamTextMode(self):
|
|
@@ -401,19 +425,22 @@ class AbstractJobStoreTest:
|
|
|
401
425
|
jobstore.assign_job_id(job)
|
|
402
426
|
jobstore.create_job(job)
|
|
403
427
|
|
|
404
|
-
foo =
|
|
405
|
-
bar =
|
|
428
|
+
foo = "foo"
|
|
429
|
+
bar = "bar"
|
|
406
430
|
|
|
407
|
-
with jobstore.write_file_stream(job.jobStoreID, encoding=
|
|
431
|
+
with jobstore.write_file_stream(job.jobStoreID, encoding="utf-8") as (
|
|
432
|
+
f,
|
|
433
|
+
fileID,
|
|
434
|
+
):
|
|
408
435
|
f.write(foo)
|
|
409
436
|
|
|
410
|
-
with jobstore.read_file_stream(fileID, encoding=
|
|
437
|
+
with jobstore.read_file_stream(fileID, encoding="utf-8") as f:
|
|
411
438
|
self.assertEqual(foo, f.read())
|
|
412
439
|
|
|
413
|
-
with jobstore.update_file_stream(fileID, encoding=
|
|
440
|
+
with jobstore.update_file_stream(fileID, encoding="utf-8") as f:
|
|
414
441
|
f.write(bar)
|
|
415
442
|
|
|
416
|
-
with jobstore.read_file_stream(fileID, encoding=
|
|
443
|
+
with jobstore.read_file_stream(fileID, encoding="utf-8") as f:
|
|
417
444
|
self.assertEqual(bar, f.read())
|
|
418
445
|
|
|
419
446
|
def testPerJobFiles(self):
|
|
@@ -422,20 +449,22 @@ class AbstractJobStoreTest:
|
|
|
422
449
|
jobstore2 = self.jobstore_resumed_noconfig
|
|
423
450
|
|
|
424
451
|
# Create jobNodeOnJS1
|
|
425
|
-
jobOnJobStore1 = JobDescription(
|
|
426
|
-
|
|
427
|
-
|
|
452
|
+
jobOnJobStore1 = JobDescription(
|
|
453
|
+
requirements=self.parentJobReqs, jobName="test1", unitName="onJobStore1"
|
|
454
|
+
)
|
|
428
455
|
|
|
429
456
|
# First recreate job
|
|
430
457
|
jobstore1.assign_job_id(jobOnJobStore1)
|
|
431
458
|
jobstore1.create_job(jobOnJobStore1)
|
|
432
|
-
fileOne = jobstore2.get_empty_file_store_id(
|
|
459
|
+
fileOne = jobstore2.get_empty_file_store_id(
|
|
460
|
+
jobOnJobStore1.jobStoreID, cleanup=True
|
|
461
|
+
)
|
|
433
462
|
# Check file exists
|
|
434
463
|
self.assertTrue(jobstore2.file_exists(fileOne))
|
|
435
464
|
self.assertTrue(jobstore1.file_exists(fileOne))
|
|
436
|
-
one = b
|
|
437
|
-
two = b
|
|
438
|
-
three = b
|
|
465
|
+
one = b"one"
|
|
466
|
+
two = b"two"
|
|
467
|
+
three = b"three"
|
|
439
468
|
# ... write to the file on jobstore2, ...
|
|
440
469
|
with jobstore2.update_file_stream(fileOne) as f:
|
|
441
470
|
f.write(one)
|
|
@@ -447,20 +476,22 @@ class AbstractJobStoreTest:
|
|
|
447
476
|
fh, path = mkstemp()
|
|
448
477
|
try:
|
|
449
478
|
os.close(fh)
|
|
450
|
-
tmpPath = path +
|
|
479
|
+
tmpPath = path + ".read-only"
|
|
451
480
|
jobstore1.read_file(fileOne, tmpPath)
|
|
452
481
|
try:
|
|
453
482
|
shutil.copyfile(tmpPath, path)
|
|
454
483
|
finally:
|
|
455
484
|
os.unlink(tmpPath)
|
|
456
|
-
with open(path,
|
|
485
|
+
with open(path, "rb+") as f:
|
|
457
486
|
self.assertEqual(f.read(), one)
|
|
458
487
|
# Write a different string to the local file ...
|
|
459
488
|
f.seek(0)
|
|
460
489
|
f.truncate(0)
|
|
461
490
|
f.write(two)
|
|
462
491
|
# ... and create a second file from the local file.
|
|
463
|
-
fileTwo = jobstore1.write_file(
|
|
492
|
+
fileTwo = jobstore1.write_file(
|
|
493
|
+
path, jobOnJobStore1.jobStoreID, cleanup=True
|
|
494
|
+
)
|
|
464
495
|
with jobstore2.read_file_stream(fileTwo) as f:
|
|
465
496
|
self.assertEqual(f.read(), two)
|
|
466
497
|
# Now update the first file from the local file ...
|
|
@@ -470,7 +501,9 @@ class AbstractJobStoreTest:
|
|
|
470
501
|
finally:
|
|
471
502
|
os.unlink(path)
|
|
472
503
|
# Create a third file to test the last remaining method.
|
|
473
|
-
with jobstore2.write_file_stream(
|
|
504
|
+
with jobstore2.write_file_stream(
|
|
505
|
+
jobOnJobStore1.jobStoreID, cleanup=True
|
|
506
|
+
) as (f, fileThree):
|
|
474
507
|
f.write(three)
|
|
475
508
|
with jobstore1.read_file_stream(fileThree) as f:
|
|
476
509
|
self.assertEqual(f.read(), three)
|
|
@@ -481,30 +514,30 @@ class AbstractJobStoreTest:
|
|
|
481
514
|
#
|
|
482
515
|
for store in jobstore2, jobstore1:
|
|
483
516
|
self.assertFalse(store.file_exists(fileOne))
|
|
484
|
-
self.assertRaises(NoSuchFileException, store.read_file, fileOne,
|
|
517
|
+
self.assertRaises(NoSuchFileException, store.read_file, fileOne, "")
|
|
485
518
|
try:
|
|
486
519
|
with store.read_file_stream(fileOne) as _:
|
|
487
520
|
pass
|
|
488
|
-
self.fail(
|
|
521
|
+
self.fail("Expecting NoSuchFileException")
|
|
489
522
|
except NoSuchFileException:
|
|
490
523
|
pass
|
|
491
524
|
|
|
492
525
|
def testStatsAndLogging(self):
|
|
493
|
-
"""Tests behavior of reading and
|
|
526
|
+
"""Tests behavior of reading and writing stats and logging."""
|
|
494
527
|
jobstore1 = self.jobstore_initialized
|
|
495
528
|
jobstore2 = self.jobstore_resumed_noconfig
|
|
496
529
|
|
|
497
|
-
jobOnJobStore1 = JobDescription(
|
|
498
|
-
|
|
499
|
-
|
|
530
|
+
jobOnJobStore1 = JobDescription(
|
|
531
|
+
requirements=self.parentJobReqs, jobName="test1", unitName="onJobStore1"
|
|
532
|
+
)
|
|
500
533
|
|
|
501
534
|
jobstore1.assign_job_id(jobOnJobStore1)
|
|
502
535
|
jobstore1.create_job(jobOnJobStore1)
|
|
503
536
|
|
|
504
537
|
# Test stats and logging
|
|
505
538
|
stats = None
|
|
506
|
-
one = b
|
|
507
|
-
two = b
|
|
539
|
+
one = b"one"
|
|
540
|
+
two = b"two"
|
|
508
541
|
|
|
509
542
|
# Allows stats to be read/written to/from in read/writeStatsAndLogging.
|
|
510
543
|
def callback(f2):
|
|
@@ -521,7 +554,9 @@ class AbstractJobStoreTest:
|
|
|
521
554
|
jobstore2.write_logs(one)
|
|
522
555
|
self.assertEqual(1, jobstore1.read_logs(callback))
|
|
523
556
|
self.assertEqual({one}, stats)
|
|
524
|
-
self.assertEqual(
|
|
557
|
+
self.assertEqual(
|
|
558
|
+
0, jobstore1.read_logs(callback)
|
|
559
|
+
) # read_logs purges saved stats etc
|
|
525
560
|
|
|
526
561
|
jobstore2.write_logs(one)
|
|
527
562
|
jobstore2.write_logs(two)
|
|
@@ -545,17 +580,21 @@ class AbstractJobStoreTest:
|
|
|
545
580
|
|
|
546
581
|
def testWriteLogFiles(self):
|
|
547
582
|
"""Test writing log files."""
|
|
548
|
-
jobNames = [
|
|
549
|
-
jobLogList = [
|
|
583
|
+
jobNames = ["testStatsAndLogging_writeLogFiles"]
|
|
584
|
+
jobLogList = ["string", b"bytes", "", b"newline\n"]
|
|
550
585
|
config = self._createConfig()
|
|
551
|
-
setattr(config,
|
|
552
|
-
setattr(config,
|
|
586
|
+
setattr(config, "writeLogs", self._createTempDir())
|
|
587
|
+
setattr(config, "writeLogsGzip", None)
|
|
553
588
|
StatsAndLogging.writeLogFiles(jobNames, jobLogList, config)
|
|
554
|
-
jobLogFile = os.path.join(config.writeLogs, jobNames[0] +
|
|
589
|
+
jobLogFile = os.path.join(config.writeLogs, jobNames[0] + "_000.log")
|
|
590
|
+
# The log directory should get exactly one file, names after this
|
|
591
|
+
# easy job name with no replacements needed.
|
|
592
|
+
self.assertEqual(
|
|
593
|
+
os.listdir(config.writeLogs), [os.path.basename(jobLogFile)]
|
|
594
|
+
)
|
|
555
595
|
self.assertTrue(os.path.isfile(jobLogFile))
|
|
556
596
|
with open(jobLogFile) as f:
|
|
557
|
-
self.assertEqual(f.read(),
|
|
558
|
-
os.remove(jobLogFile)
|
|
597
|
+
self.assertEqual(f.read(), "string\nbytes\n\nnewline\n")
|
|
559
598
|
|
|
560
599
|
def testBatchCreate(self):
|
|
561
600
|
"""Test creation of many jobs."""
|
|
@@ -564,9 +603,11 @@ class AbstractJobStoreTest:
|
|
|
564
603
|
jobs = []
|
|
565
604
|
with jobstore.batch():
|
|
566
605
|
for i in range(100):
|
|
567
|
-
overlargeJob = JobDescription(
|
|
568
|
-
|
|
569
|
-
|
|
606
|
+
overlargeJob = JobDescription(
|
|
607
|
+
requirements=jobRequirements,
|
|
608
|
+
jobName="test-overlarge",
|
|
609
|
+
unitName="onJobStore",
|
|
610
|
+
)
|
|
570
611
|
jobstore.assign_job_id(overlargeJob)
|
|
571
612
|
jobstore.create_job(overlargeJob)
|
|
572
613
|
jobs.append(overlargeJob)
|
|
@@ -634,16 +675,16 @@ class AbstractJobStoreTest:
|
|
|
634
675
|
try:
|
|
635
676
|
store = self.externalStoreCache[self]
|
|
636
677
|
except KeyError:
|
|
637
|
-
logger.debug(
|
|
678
|
+
logger.debug("Creating new external store for %s", self)
|
|
638
679
|
store = self.externalStoreCache[self] = self._createExternalStore()
|
|
639
680
|
else:
|
|
640
|
-
logger.debug(
|
|
681
|
+
logger.debug("Reusing external store for %s", self)
|
|
641
682
|
return store
|
|
642
683
|
|
|
643
684
|
@classmethod
|
|
644
685
|
def cleanUpExternalStores(cls):
|
|
645
686
|
for test, store in cls.externalStoreCache.items():
|
|
646
|
-
logger.debug(
|
|
687
|
+
logger.debug("Cleaning up external store for %s.", test)
|
|
647
688
|
test._cleanUpExternalStore(store)
|
|
648
689
|
|
|
649
690
|
mpTestPartSize = 5 << 20
|
|
@@ -653,9 +694,11 @@ class AbstractJobStoreTest:
|
|
|
653
694
|
|
|
654
695
|
testClasses = [FileJobStoreTest, AWSJobStoreTest, GoogleJobStoreTest]
|
|
655
696
|
|
|
656
|
-
activeTestClassesByName = {
|
|
657
|
-
|
|
658
|
-
|
|
697
|
+
activeTestClassesByName = {
|
|
698
|
+
testCls.__name__: testCls
|
|
699
|
+
for testCls in testClasses
|
|
700
|
+
if not getattr(testCls, "__unittest_skip__", False)
|
|
701
|
+
}
|
|
659
702
|
|
|
660
703
|
def testImportExportFile(self, otherCls, size, moveExports):
|
|
661
704
|
"""
|
|
@@ -667,7 +710,7 @@ class AbstractJobStoreTest:
|
|
|
667
710
|
:param int size: the size of the file to test importing/exporting with
|
|
668
711
|
"""
|
|
669
712
|
# Prepare test file in other job store
|
|
670
|
-
self.jobstore_initialized.
|
|
713
|
+
self.jobstore_initialized.part_size = cls.mpTestPartSize
|
|
671
714
|
self.jobstore_initialized.moveExports = moveExports
|
|
672
715
|
|
|
673
716
|
# Test assumes imports are not linked
|
|
@@ -675,7 +718,7 @@ class AbstractJobStoreTest:
|
|
|
675
718
|
|
|
676
719
|
# The string in otherCls() is arbitrary as long as it returns a class that has access
|
|
677
720
|
# to ._externalStore() and ._prepareTestFile()
|
|
678
|
-
other = otherCls(
|
|
721
|
+
other = otherCls("testSharedFiles")
|
|
679
722
|
store = other._externalStore()
|
|
680
723
|
|
|
681
724
|
srcUrl, srcMd5 = other._prepareTestFile(store, size)
|
|
@@ -690,9 +733,11 @@ class AbstractJobStoreTest:
|
|
|
690
733
|
self.jobstore_initialized.export_file(jobStoreFileID, dstUrl)
|
|
691
734
|
self.assertEqual(fileMD5, other._hashTestFile(dstUrl))
|
|
692
735
|
|
|
693
|
-
if otherCls.__name__ ==
|
|
736
|
+
if otherCls.__name__ == "FileJobStoreTest":
|
|
694
737
|
if isinstance(self.jobstore_initialized, FileJobStore):
|
|
695
|
-
jobStorePath = self.jobstore_initialized._get_file_path_from_id(
|
|
738
|
+
jobStorePath = self.jobstore_initialized._get_file_path_from_id(
|
|
739
|
+
jobStoreFileID
|
|
740
|
+
)
|
|
696
741
|
jobStoreHasLink = os.path.islink(jobStorePath)
|
|
697
742
|
if self.jobstore_initialized.moveExports:
|
|
698
743
|
# Ensure the export performed a move / link
|
|
@@ -706,14 +751,20 @@ class AbstractJobStoreTest:
|
|
|
706
751
|
os.remove(srcUrl[7:])
|
|
707
752
|
os.remove(dstUrl[7:])
|
|
708
753
|
|
|
709
|
-
make_tests(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
754
|
+
make_tests(
|
|
755
|
+
testImportExportFile,
|
|
756
|
+
cls,
|
|
757
|
+
otherCls=activeTestClassesByName,
|
|
758
|
+
size=dict(
|
|
759
|
+
zero=0,
|
|
760
|
+
one=1,
|
|
761
|
+
oneMiB=2**20,
|
|
762
|
+
partSizeMinusOne=cls.mpTestPartSize - 1,
|
|
763
|
+
partSize=cls.mpTestPartSize,
|
|
764
|
+
partSizePlusOne=cls.mpTestPartSize + 1,
|
|
765
|
+
),
|
|
766
|
+
moveExports={"deactivated": None, "activated": True},
|
|
767
|
+
)
|
|
717
768
|
|
|
718
769
|
def testImportSharedFile(self, otherCls):
|
|
719
770
|
"""
|
|
@@ -723,34 +774,37 @@ class AbstractJobStoreTest:
|
|
|
723
774
|
to import from or export to
|
|
724
775
|
"""
|
|
725
776
|
# Prepare test file in other job store
|
|
726
|
-
self.jobstore_initialized.
|
|
727
|
-
other = otherCls(
|
|
777
|
+
self.jobstore_initialized.part_size = cls.mpTestPartSize
|
|
778
|
+
other = otherCls("testSharedFiles")
|
|
728
779
|
store = other._externalStore()
|
|
729
780
|
|
|
730
781
|
srcUrl, srcMd5 = other._prepareTestFile(store, 42)
|
|
731
782
|
# Import into job store under test
|
|
732
|
-
self.assertIsNone(
|
|
733
|
-
|
|
783
|
+
self.assertIsNone(
|
|
784
|
+
self.jobstore_initialized.import_file(
|
|
785
|
+
srcUrl, shared_file_name="foo"
|
|
786
|
+
)
|
|
787
|
+
)
|
|
788
|
+
with self.jobstore_initialized.read_shared_file_stream("foo") as f:
|
|
734
789
|
fileMD5 = hashlib.md5(f.read()).hexdigest()
|
|
735
790
|
self.assertEqual(fileMD5, srcMd5)
|
|
736
|
-
if otherCls.__name__ ==
|
|
791
|
+
if otherCls.__name__ == "FileJobStoreTest": # Remove local Files
|
|
737
792
|
os.remove(srcUrl[7:])
|
|
738
793
|
|
|
739
|
-
make_tests(testImportSharedFile,
|
|
740
|
-
cls,
|
|
741
|
-
otherCls=activeTestClassesByName)
|
|
794
|
+
make_tests(testImportSharedFile, cls, otherCls=activeTestClassesByName)
|
|
742
795
|
|
|
743
796
|
def testImportHttpFile(self):
|
|
744
|
-
|
|
745
|
-
http = socketserver.TCPServer((
|
|
797
|
+
"""Test importing a file over HTTP."""
|
|
798
|
+
http = socketserver.TCPServer(("", 0), StubHttpRequestHandler)
|
|
746
799
|
try:
|
|
747
800
|
httpThread = threading.Thread(target=http.serve_forever)
|
|
748
801
|
httpThread.start()
|
|
749
802
|
try:
|
|
750
803
|
assignedPort = http.server_address[1]
|
|
751
|
-
url =
|
|
804
|
+
url = "http://localhost:%d" % assignedPort
|
|
752
805
|
with self.jobstore_initialized.read_file_stream(
|
|
753
|
-
|
|
806
|
+
self.jobstore_initialized.import_file(url)
|
|
807
|
+
) as readable:
|
|
754
808
|
f1 = readable.read()
|
|
755
809
|
f2 = StubHttpRequestHandler.fileContents
|
|
756
810
|
if isinstance(f1, bytes) and not isinstance(f2, bytes):
|
|
@@ -765,20 +819,25 @@ class AbstractJobStoreTest:
|
|
|
765
819
|
http.server_close()
|
|
766
820
|
|
|
767
821
|
def testImportFtpFile(self):
|
|
768
|
-
|
|
769
|
-
ftpfile = {
|
|
822
|
+
"""Test importing a file over FTP"""
|
|
823
|
+
ftpfile = {"name": "foo", "content": "foo bar baz qux"}
|
|
770
824
|
ftp = FTPStubServer(0)
|
|
771
825
|
ftp.run()
|
|
772
826
|
try:
|
|
773
827
|
ftp.add_file(**ftpfile)
|
|
774
828
|
assignedPort = ftp.server.server_address[1]
|
|
775
|
-
url =
|
|
776
|
-
|
|
829
|
+
url = "ftp://user1:passwd@localhost:%d/%s" % (
|
|
830
|
+
assignedPort,
|
|
831
|
+
ftpfile["name"],
|
|
832
|
+
)
|
|
833
|
+
with self.jobstore_initialized.read_file_stream(
|
|
834
|
+
self.jobstore_initialized.import_file(url)
|
|
835
|
+
) as readable:
|
|
777
836
|
imported_content = readable.read()
|
|
778
837
|
# python 2/3 string/bytestring compat
|
|
779
838
|
if isinstance(imported_content, bytes):
|
|
780
|
-
imported_content = imported_content.decode(
|
|
781
|
-
self.assertEqual(imported_content, ftpfile[
|
|
839
|
+
imported_content = imported_content.decode("utf-8")
|
|
840
|
+
self.assertEqual(imported_content, ftpfile["content"])
|
|
782
841
|
finally:
|
|
783
842
|
ftp.stop()
|
|
784
843
|
|
|
@@ -794,12 +853,19 @@ class AbstractJobStoreTest:
|
|
|
794
853
|
job = self.arbitraryJob()
|
|
795
854
|
self.jobstore_initialized.assign_job_id(job)
|
|
796
855
|
self.jobstore_initialized.create_job(job)
|
|
797
|
-
fileIDs = [
|
|
798
|
-
|
|
856
|
+
fileIDs = [
|
|
857
|
+
self.jobstore_initialized.get_empty_file_store_id(
|
|
858
|
+
job.jobStoreID, cleanup=True
|
|
859
|
+
)
|
|
860
|
+
for _ in range(0, numFiles)
|
|
861
|
+
]
|
|
799
862
|
self.jobstore_initialized.delete_job(job.jobStoreID)
|
|
800
863
|
for fileID in fileIDs:
|
|
801
864
|
# NB: the fooStream() methods return context managers
|
|
802
|
-
self.assertRaises(
|
|
865
|
+
self.assertRaises(
|
|
866
|
+
NoSuchFileException,
|
|
867
|
+
self.jobstore_initialized.read_file_stream(fileID).__enter__,
|
|
868
|
+
)
|
|
803
869
|
|
|
804
870
|
@slow
|
|
805
871
|
def testMultipartUploads(self):
|
|
@@ -834,9 +900,10 @@ class AbstractJobStoreTest:
|
|
|
834
900
|
checksumThread.start()
|
|
835
901
|
try:
|
|
836
902
|
# Should not block. On Linux, /dev/random blocks when it's running low on entropy
|
|
837
|
-
with open(
|
|
838
|
-
with self.jobstore_initialized.write_file_stream(
|
|
839
|
-
|
|
903
|
+
with open("/dev/urandom", "rb") as readable:
|
|
904
|
+
with self.jobstore_initialized.write_file_stream(
|
|
905
|
+
job.jobStoreID, cleanup=True
|
|
906
|
+
) as (writable, fileId):
|
|
840
907
|
for i in range(int(partSize * partsPerFile / bufSize)):
|
|
841
908
|
buf = readable.read(bufSize)
|
|
842
909
|
checksumQueue.put(buf)
|
|
@@ -861,13 +928,15 @@ class AbstractJobStoreTest:
|
|
|
861
928
|
checksum = hashlib.md5()
|
|
862
929
|
fh, path = mkstemp()
|
|
863
930
|
try:
|
|
864
|
-
with os.fdopen(fh,
|
|
865
|
-
with open(
|
|
931
|
+
with os.fdopen(fh, "wb+") as writable:
|
|
932
|
+
with open("/dev/urandom", "rb") as readable:
|
|
866
933
|
for i in range(int(partSize * partsPerFile / bufSize)):
|
|
867
934
|
buf = readable.read(bufSize)
|
|
868
935
|
writable.write(buf)
|
|
869
936
|
checksum.update(buf)
|
|
870
|
-
fileId = self.jobstore_initialized.write_file(
|
|
937
|
+
fileId = self.jobstore_initialized.write_file(
|
|
938
|
+
path, job.jobStoreID, cleanup=True
|
|
939
|
+
)
|
|
871
940
|
finally:
|
|
872
941
|
os.unlink(path)
|
|
873
942
|
before = checksum.hexdigest()
|
|
@@ -885,14 +954,18 @@ class AbstractJobStoreTest:
|
|
|
885
954
|
self.jobstore_initialized.delete_job(job.jobStoreID)
|
|
886
955
|
|
|
887
956
|
def testZeroLengthFiles(self):
|
|
888
|
-
|
|
957
|
+
"""Test reading and writing of empty files."""
|
|
889
958
|
job = self.arbitraryJob()
|
|
890
959
|
self.jobstore_initialized.assign_job_id(job)
|
|
891
960
|
self.jobstore_initialized.create_job(job)
|
|
892
|
-
nullFile = self.jobstore_initialized.write_file(
|
|
961
|
+
nullFile = self.jobstore_initialized.write_file(
|
|
962
|
+
"/dev/null", job.jobStoreID, cleanup=True
|
|
963
|
+
)
|
|
893
964
|
with self.jobstore_initialized.read_file_stream(nullFile) as f:
|
|
894
965
|
assert not f.read()
|
|
895
|
-
with self.jobstore_initialized.write_file_stream(
|
|
966
|
+
with self.jobstore_initialized.write_file_stream(
|
|
967
|
+
job.jobStoreID, cleanup=True
|
|
968
|
+
) as (f, nullStream):
|
|
896
969
|
pass
|
|
897
970
|
with self.jobstore_initialized.read_file_stream(nullStream) as f:
|
|
898
971
|
assert not f.read()
|
|
@@ -900,12 +973,12 @@ class AbstractJobStoreTest:
|
|
|
900
973
|
|
|
901
974
|
@slow
|
|
902
975
|
def testLargeFile(self):
|
|
903
|
-
|
|
976
|
+
"""Test the reading and writing of large files."""
|
|
904
977
|
# Write a large file.
|
|
905
978
|
dirPath = self._createTempDir()
|
|
906
|
-
filePath = os.path.join(dirPath,
|
|
979
|
+
filePath = os.path.join(dirPath, "large")
|
|
907
980
|
hashIn = hashlib.md5()
|
|
908
|
-
with open(filePath,
|
|
981
|
+
with open(filePath, "wb") as f:
|
|
909
982
|
for i in range(0, 10):
|
|
910
983
|
buf = os.urandom(self._partSize())
|
|
911
984
|
f.write(buf)
|
|
@@ -915,7 +988,9 @@ class AbstractJobStoreTest:
|
|
|
915
988
|
job = self.arbitraryJob()
|
|
916
989
|
self.jobstore_initialized.assign_job_id(job)
|
|
917
990
|
self.jobstore_initialized.create_job(job)
|
|
918
|
-
jobStoreFileID = self.jobstore_initialized.write_file(
|
|
991
|
+
jobStoreFileID = self.jobstore_initialized.write_file(
|
|
992
|
+
filePath, job.jobStoreID, cleanup=True
|
|
993
|
+
)
|
|
919
994
|
|
|
920
995
|
# Remove the local file.
|
|
921
996
|
os.unlink(filePath)
|
|
@@ -925,7 +1000,7 @@ class AbstractJobStoreTest:
|
|
|
925
1000
|
|
|
926
1001
|
# Reread the file to confirm success.
|
|
927
1002
|
hashOut = hashlib.md5()
|
|
928
|
-
with open(filePath,
|
|
1003
|
+
with open(filePath, "rb") as f:
|
|
929
1004
|
while True:
|
|
930
1005
|
buf = f.read(self._partSize())
|
|
931
1006
|
if not buf:
|
|
@@ -943,8 +1018,8 @@ class AbstractJobStoreTest:
|
|
|
943
1018
|
|
|
944
1019
|
def assertUrl(self, url):
|
|
945
1020
|
|
|
946
|
-
prefix, path = url.split(
|
|
947
|
-
if prefix ==
|
|
1021
|
+
prefix, path = url.split(":", 1)
|
|
1022
|
+
if prefix == "file":
|
|
948
1023
|
self.assertTrue(os.path.exists(path))
|
|
949
1024
|
else:
|
|
950
1025
|
try:
|
|
@@ -982,8 +1057,7 @@ class AbstractJobStoreTest:
|
|
|
982
1057
|
self.assertEqual(len(list(jobstore.jobs())), 101)
|
|
983
1058
|
|
|
984
1059
|
# See how long it takes to clean with cache
|
|
985
|
-
jobCache = {job.jobStoreID: job
|
|
986
|
-
for job in jobstore.jobs()}
|
|
1060
|
+
jobCache = {job.jobStoreID: job for job in jobstore.jobs()}
|
|
987
1061
|
cacheStart = time.time()
|
|
988
1062
|
jobstore.clean(jobCache)
|
|
989
1063
|
cacheEnd = time.time()
|
|
@@ -998,13 +1072,15 @@ class AbstractJobStoreTest:
|
|
|
998
1072
|
# NB: the 'thread' method seems to be needed here to actually
|
|
999
1073
|
# ensure the timeout is raised, probably because the only
|
|
1000
1074
|
# "live" thread doesn't hold the GIL.
|
|
1001
|
-
@pytest.mark.timeout(45, method=
|
|
1075
|
+
@pytest.mark.timeout(45, method="thread")
|
|
1002
1076
|
def testPartialReadFromStream(self):
|
|
1003
1077
|
"""Test whether readFileStream will deadlock on a partial read."""
|
|
1004
1078
|
job = self.arbitraryJob()
|
|
1005
1079
|
self.jobstore_initialized.assign_job_id(job)
|
|
1006
1080
|
self.jobstore_initialized.create_job(job)
|
|
1007
|
-
with self.jobstore_initialized.write_file_stream(
|
|
1081
|
+
with self.jobstore_initialized.write_file_stream(
|
|
1082
|
+
job.jobStoreID, cleanup=True
|
|
1083
|
+
) as (f, fileID):
|
|
1008
1084
|
# Write enough data to make sure the writer thread
|
|
1009
1085
|
# will get blocked on the write. Technically anything
|
|
1010
1086
|
# greater than the pipe buffer size plus the libc
|
|
@@ -1012,7 +1088,7 @@ class AbstractJobStoreTest:
|
|
|
1012
1088
|
# but this gives us a lot of extra room just to be sure.
|
|
1013
1089
|
|
|
1014
1090
|
# python 3 requires self.fileContents to be a bytestring
|
|
1015
|
-
a = b
|
|
1091
|
+
a = b"a"
|
|
1016
1092
|
f.write(a * 300000)
|
|
1017
1093
|
with self.jobstore_initialized.read_file_stream(fileID) as f:
|
|
1018
1094
|
self.assertEqual(f.read(1), a)
|
|
@@ -1085,9 +1161,9 @@ class AbstractEncryptedJobStoreTest:
|
|
|
1085
1161
|
|
|
1086
1162
|
def _createConfig(self):
|
|
1087
1163
|
config = super()._createConfig()
|
|
1088
|
-
sseKeyFile = os.path.join(self.sseKeyDir,
|
|
1089
|
-
with open(sseKeyFile,
|
|
1090
|
-
f.write(
|
|
1164
|
+
sseKeyFile = os.path.join(self.sseKeyDir, "keyFile")
|
|
1165
|
+
with open(sseKeyFile, "w") as f:
|
|
1166
|
+
f.write("01234567890123456789012345678901")
|
|
1091
1167
|
config.sseKey = sseKeyFile
|
|
1092
1168
|
# config.attrib['sse_key'] = sseKeyFile
|
|
1093
1169
|
return config
|
|
@@ -1097,9 +1173,11 @@ class AbstractEncryptedJobStoreTest:
|
|
|
1097
1173
|
Create an encrypted file. Read it in encrypted mode then try with encryption off
|
|
1098
1174
|
to ensure that it fails.
|
|
1099
1175
|
"""
|
|
1100
|
-
phrase = b
|
|
1101
|
-
fileName =
|
|
1102
|
-
with self.jobstore_initialized.write_shared_file_stream(
|
|
1176
|
+
phrase = b"This file is encrypted."
|
|
1177
|
+
fileName = "foo"
|
|
1178
|
+
with self.jobstore_initialized.write_shared_file_stream(
|
|
1179
|
+
fileName, encrypted=True
|
|
1180
|
+
) as f:
|
|
1103
1181
|
f.write(phrase)
|
|
1104
1182
|
with self.jobstore_initialized.read_shared_file_stream(fileName) as f:
|
|
1105
1183
|
self.assertEqual(phrase, f.read())
|
|
@@ -1110,7 +1188,9 @@ class AbstractEncryptedJobStoreTest:
|
|
|
1110
1188
|
with self.jobstore_initialized.read_shared_file_stream(fileName) as f:
|
|
1111
1189
|
self.assertEqual(phrase, f.read())
|
|
1112
1190
|
except AssertionError as e:
|
|
1113
|
-
self.assertEqual(
|
|
1191
|
+
self.assertEqual(
|
|
1192
|
+
"Content is encrypted but no key was provided.", e.args[0]
|
|
1193
|
+
)
|
|
1114
1194
|
else:
|
|
1115
1195
|
self.fail("Read encryption content with encryption off.")
|
|
1116
1196
|
|
|
@@ -1126,21 +1206,21 @@ class FileJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1126
1206
|
shutil.rmtree(self.jobstore_initialized.jobStoreDir)
|
|
1127
1207
|
|
|
1128
1208
|
def _prepareTestFile(self, dirPath, size=None):
|
|
1129
|
-
fileName =
|
|
1209
|
+
fileName = "testfile_%s" % uuid.uuid4()
|
|
1130
1210
|
localFilePath = dirPath + fileName
|
|
1131
|
-
url =
|
|
1211
|
+
url = "file://%s" % localFilePath
|
|
1132
1212
|
if size is None:
|
|
1133
1213
|
return url
|
|
1134
1214
|
else:
|
|
1135
1215
|
content = os.urandom(size)
|
|
1136
|
-
with open(localFilePath,
|
|
1216
|
+
with open(localFilePath, "wb") as writable:
|
|
1137
1217
|
writable.write(content)
|
|
1138
1218
|
|
|
1139
1219
|
return url, hashlib.md5(content).hexdigest()
|
|
1140
1220
|
|
|
1141
1221
|
def _hashTestFile(self, url):
|
|
1142
1222
|
localFilePath = FileJobStore._extract_path_from_url(urlparse.urlparse(url))
|
|
1143
|
-
with open(localFilePath,
|
|
1223
|
+
with open(localFilePath, "rb") as f:
|
|
1144
1224
|
return hashlib.md5(f.read()).hexdigest()
|
|
1145
1225
|
|
|
1146
1226
|
def _createExternalStore(self):
|
|
@@ -1157,7 +1237,9 @@ class FileJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1157
1237
|
job = self.arbitraryJob()
|
|
1158
1238
|
self.jobstore_initialized.assign_job_id(job)
|
|
1159
1239
|
self.jobstore_initialized.create_job(job)
|
|
1160
|
-
fileID = self.jobstore_initialized.write_file(
|
|
1240
|
+
fileID = self.jobstore_initialized.write_file(
|
|
1241
|
+
path, job.jobStoreID, cleanup=True
|
|
1242
|
+
)
|
|
1161
1243
|
self.assertTrue(fileID.endswith(os.path.basename(path)))
|
|
1162
1244
|
finally:
|
|
1163
1245
|
os.unlink(path)
|
|
@@ -1168,12 +1250,19 @@ class FileJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1168
1250
|
original_filestore = None
|
|
1169
1251
|
try:
|
|
1170
1252
|
original_filestore = self._createExternalStore()
|
|
1171
|
-
dir_symlinked_to_original_filestore = f
|
|
1253
|
+
dir_symlinked_to_original_filestore = f"{original_filestore}-am-i-real"
|
|
1172
1254
|
os.symlink(original_filestore, dir_symlinked_to_original_filestore)
|
|
1173
|
-
filejobstore_using_symlink = FileJobStore(
|
|
1174
|
-
|
|
1255
|
+
filejobstore_using_symlink = FileJobStore(
|
|
1256
|
+
dir_symlinked_to_original_filestore, fanOut=2
|
|
1257
|
+
)
|
|
1258
|
+
self.assertEqual(
|
|
1259
|
+
dir_symlinked_to_original_filestore,
|
|
1260
|
+
filejobstore_using_symlink.jobStoreDir,
|
|
1261
|
+
)
|
|
1175
1262
|
finally:
|
|
1176
|
-
if dir_symlinked_to_original_filestore and os.path.exists(
|
|
1263
|
+
if dir_symlinked_to_original_filestore and os.path.exists(
|
|
1264
|
+
dir_symlinked_to_original_filestore
|
|
1265
|
+
):
|
|
1177
1266
|
os.unlink(dir_symlinked_to_original_filestore)
|
|
1178
1267
|
if original_filestore and os.path.exists(original_filestore):
|
|
1179
1268
|
shutil.rmtree(original_filestore)
|
|
@@ -1187,21 +1276,23 @@ class FileJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1187
1276
|
try:
|
|
1188
1277
|
# Grab a temp directory to make files in. Make sure it's on the
|
|
1189
1278
|
# same device as everything else.
|
|
1190
|
-
temp_dir = os.path.abspath(self.namePrefix +
|
|
1279
|
+
temp_dir = os.path.abspath(self.namePrefix + "-import")
|
|
1191
1280
|
os.mkdir(temp_dir)
|
|
1192
|
-
to_import = os.path.join(temp_dir,
|
|
1193
|
-
with open(to_import,
|
|
1194
|
-
f.write(
|
|
1281
|
+
to_import = os.path.join(temp_dir, "import-me")
|
|
1282
|
+
with open(to_import, "w") as f:
|
|
1283
|
+
f.write("test")
|
|
1195
1284
|
|
|
1196
1285
|
# And a temp directory next to the job store to download to
|
|
1197
|
-
download_dir = os.path.abspath(self.namePrefix +
|
|
1286
|
+
download_dir = os.path.abspath(self.namePrefix + "-dl")
|
|
1198
1287
|
os.mkdir(download_dir)
|
|
1199
1288
|
|
|
1200
1289
|
# Import it as a symlink
|
|
1201
|
-
file_id = self.jobstore_initialized.import_file(
|
|
1290
|
+
file_id = self.jobstore_initialized.import_file(
|
|
1291
|
+
"file://" + to_import, symlink=True
|
|
1292
|
+
)
|
|
1202
1293
|
|
|
1203
1294
|
# Take it out as a hard link or copy
|
|
1204
|
-
download_to = os.path.join(download_dir,
|
|
1295
|
+
download_to = os.path.join(download_dir, "downloaded")
|
|
1205
1296
|
self.jobstore_initialized.read_file(file_id, download_to)
|
|
1206
1297
|
|
|
1207
1298
|
# Make sure it isn't a symlink
|
|
@@ -1226,7 +1317,9 @@ class FileJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1226
1317
|
for link_imports in [True, False]:
|
|
1227
1318
|
self.jobstore_initialized.linkImports = link_imports
|
|
1228
1319
|
# Import into job store under test
|
|
1229
|
-
jobStoreFileID = self.jobstore_initialized.import_file(
|
|
1320
|
+
jobStoreFileID = self.jobstore_initialized.import_file(
|
|
1321
|
+
srcUrl, symlink=symlink
|
|
1322
|
+
)
|
|
1230
1323
|
self.assertTrue(isinstance(jobStoreFileID, FileID))
|
|
1231
1324
|
with self.jobstore_initialized.read_file_stream(jobStoreFileID) as f:
|
|
1232
1325
|
# gets abs path
|
|
@@ -1242,16 +1335,44 @@ class FileJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1242
1335
|
os.remove(filename)
|
|
1243
1336
|
os.remove(srcUrl[7:])
|
|
1244
1337
|
|
|
1338
|
+
def test_symlink_read_control(self):
|
|
1339
|
+
"""
|
|
1340
|
+
Test that files are read by symlink when expected
|
|
1341
|
+
"""
|
|
1342
|
+
|
|
1343
|
+
for should_link in (False, True):
|
|
1344
|
+
# Configure a jobstore to symlink out reads or not, as appropriate
|
|
1345
|
+
config = self._createConfig()
|
|
1346
|
+
config.symlink_job_store_reads = should_link
|
|
1347
|
+
store = FileJobStore(
|
|
1348
|
+
self.namePrefix + ("-link" if should_link else "-nolink")
|
|
1349
|
+
)
|
|
1350
|
+
store.initialize(config)
|
|
1351
|
+
|
|
1352
|
+
# Put something in the job store
|
|
1353
|
+
src_url, _ = self._prepareTestFile(self._externalStore(), 1)
|
|
1354
|
+
file_id = store.import_file(src_url, symlink=False)
|
|
1355
|
+
|
|
1356
|
+
# Read it out, accepting a symlink
|
|
1357
|
+
dest_dir = self._createTempDir()
|
|
1358
|
+
dest_path = os.path.join(dest_dir, "file.dat")
|
|
1359
|
+
store.read_file(file_id, dest_path, symlink=True)
|
|
1360
|
+
|
|
1361
|
+
# Make sure we get a symlink exactly when configured to
|
|
1362
|
+
assert os.path.exists(dest_path)
|
|
1363
|
+
assert os.path.islink(dest_path) == should_link
|
|
1364
|
+
|
|
1245
1365
|
|
|
1246
1366
|
@needs_google_project
|
|
1247
1367
|
@needs_google_storage
|
|
1248
1368
|
@pytest.mark.xfail
|
|
1249
1369
|
class GoogleJobStoreTest(AbstractJobStoreTest.Test):
|
|
1250
|
-
projectID = os.getenv(
|
|
1370
|
+
projectID = os.getenv("TOIL_GOOGLE_PROJECTID")
|
|
1251
1371
|
headers = {"x-goog-project-id": projectID}
|
|
1252
1372
|
|
|
1253
1373
|
def _createJobStore(self):
|
|
1254
1374
|
from toil.jobStores.googleJobStore import GoogleJobStore
|
|
1375
|
+
|
|
1255
1376
|
return GoogleJobStore(GoogleJobStoreTest.projectID + ":" + self.namePrefix)
|
|
1256
1377
|
|
|
1257
1378
|
def _corruptJobStore(self):
|
|
@@ -1261,24 +1382,31 @@ class GoogleJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1261
1382
|
|
|
1262
1383
|
def _prepareTestFile(self, bucket, size=None):
|
|
1263
1384
|
from toil.jobStores.googleJobStore import GoogleJobStore
|
|
1264
|
-
|
|
1265
|
-
|
|
1385
|
+
|
|
1386
|
+
fileName = "testfile_%s" % uuid.uuid4()
|
|
1387
|
+
url = f"gs://{bucket.name}/{fileName}"
|
|
1266
1388
|
if size is None:
|
|
1267
1389
|
return url
|
|
1268
|
-
with open(
|
|
1390
|
+
with open("/dev/urandom", "rb") as readable:
|
|
1269
1391
|
contents = str(readable.read(size))
|
|
1270
|
-
GoogleJobStore._write_to_url(
|
|
1392
|
+
GoogleJobStore._write_to_url(
|
|
1393
|
+
BytesIO(bytes(contents, "utf-8")), urlparse.urlparse(url)
|
|
1394
|
+
)
|
|
1271
1395
|
return url, hashlib.md5(contents.encode()).hexdigest()
|
|
1272
1396
|
|
|
1273
1397
|
def _hashTestFile(self, url):
|
|
1274
1398
|
from toil.jobStores.googleJobStore import GoogleJobStore
|
|
1275
|
-
|
|
1399
|
+
|
|
1400
|
+
contents = GoogleJobStore._get_blob_from_url(
|
|
1401
|
+
urlparse.urlparse(url)
|
|
1402
|
+
).download_as_string()
|
|
1276
1403
|
return hashlib.md5(contents).hexdigest()
|
|
1277
1404
|
|
|
1278
1405
|
@google_retry
|
|
1279
1406
|
def _createExternalStore(self):
|
|
1280
1407
|
from google.cloud import storage
|
|
1281
|
-
|
|
1408
|
+
|
|
1409
|
+
bucketName = "import-export-test-" + str(uuid.uuid4())
|
|
1282
1410
|
storageClient = storage.Client()
|
|
1283
1411
|
return storageClient.create_bucket(bucketName)
|
|
1284
1412
|
|
|
@@ -1297,11 +1425,13 @@ class GoogleJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1297
1425
|
class AWSJobStoreTest(AbstractJobStoreTest.Test):
|
|
1298
1426
|
def _createJobStore(self):
|
|
1299
1427
|
from toil.jobStores.aws.jobStore import AWSJobStore
|
|
1428
|
+
|
|
1300
1429
|
partSize = self._partSize()
|
|
1301
|
-
return AWSJobStore(self.awsRegion() +
|
|
1430
|
+
return AWSJobStore(self.awsRegion() + ":" + self.namePrefix, partSize=partSize)
|
|
1302
1431
|
|
|
1303
1432
|
def _corruptJobStore(self):
|
|
1304
1433
|
from toil.jobStores.aws.jobStore import AWSJobStore
|
|
1434
|
+
|
|
1305
1435
|
assert isinstance(self.jobstore_initialized, AWSJobStore) # type hinting
|
|
1306
1436
|
self.jobstore_initialized.destroy()
|
|
1307
1437
|
|
|
@@ -1311,53 +1441,71 @@ class AWSJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1311
1441
|
failed to be created. We simulate a failed jobstore bucket creation by using a bucket in a
|
|
1312
1442
|
different region with the same name.
|
|
1313
1443
|
"""
|
|
1314
|
-
from boto.sdb import connect_to_region
|
|
1315
1444
|
from botocore.exceptions import ClientError
|
|
1316
1445
|
|
|
1317
1446
|
from toil.jobStores.aws.jobStore import BucketLocationConflictException
|
|
1318
1447
|
from toil.lib.aws.session import establish_boto3_session
|
|
1319
1448
|
from toil.lib.aws.utils import retry_s3
|
|
1320
1449
|
|
|
1321
|
-
externalAWSLocation =
|
|
1322
|
-
for testRegion in
|
|
1450
|
+
externalAWSLocation = "us-west-1"
|
|
1451
|
+
for testRegion in "us-east-1", "us-west-2":
|
|
1323
1452
|
# We run this test twice, once with the default s3 server us-east-1 as the test region
|
|
1324
1453
|
# and once with another server (us-west-2). The external server is always us-west-1.
|
|
1325
1454
|
# This incidentally tests that the BucketLocationConflictException is thrown when using
|
|
1326
1455
|
# both the default, and a non-default server.
|
|
1327
1456
|
testJobStoreUUID = str(uuid.uuid4())
|
|
1328
1457
|
# Create the bucket at the external region
|
|
1329
|
-
bucketName =
|
|
1330
|
-
client = establish_boto3_session().client(
|
|
1331
|
-
|
|
1458
|
+
bucketName = "domain-test-" + testJobStoreUUID + "--files"
|
|
1459
|
+
client = establish_boto3_session().client(
|
|
1460
|
+
"s3", region_name=externalAWSLocation
|
|
1461
|
+
)
|
|
1462
|
+
resource = establish_boto3_session().resource(
|
|
1463
|
+
"s3", region_name=externalAWSLocation
|
|
1464
|
+
)
|
|
1332
1465
|
|
|
1333
1466
|
for attempt in retry_s3(delays=(2, 5, 10, 30, 60), timeout=600):
|
|
1334
1467
|
with attempt:
|
|
1335
1468
|
# Create the bucket at the home region
|
|
1336
|
-
client.create_bucket(
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1469
|
+
client.create_bucket(
|
|
1470
|
+
Bucket=bucketName,
|
|
1471
|
+
CreateBucketConfiguration={
|
|
1472
|
+
"LocationConstraint": externalAWSLocation
|
|
1473
|
+
},
|
|
1474
|
+
)
|
|
1475
|
+
|
|
1476
|
+
owner_tag = os.environ.get("TOIL_OWNER_TAG")
|
|
1340
1477
|
if owner_tag:
|
|
1341
1478
|
for attempt in retry_s3(delays=(1, 1, 2, 4, 8, 16), timeout=33):
|
|
1342
1479
|
with attempt:
|
|
1343
1480
|
bucket_tagging = resource.BucketTagging(bucketName)
|
|
1344
|
-
bucket_tagging.put(
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1481
|
+
bucket_tagging.put(
|
|
1482
|
+
Tagging={"TagSet": [{"Key": "Owner", "Value": owner_tag}]}
|
|
1483
|
+
)
|
|
1484
|
+
|
|
1485
|
+
options = Job.Runner.getDefaultOptions(
|
|
1486
|
+
"aws:" + testRegion + ":domain-test-" + testJobStoreUUID
|
|
1487
|
+
)
|
|
1488
|
+
options.logLevel = "DEBUG"
|
|
1348
1489
|
try:
|
|
1349
1490
|
with Toil(options) as toil:
|
|
1350
1491
|
pass
|
|
1351
1492
|
except BucketLocationConflictException:
|
|
1352
1493
|
# Catch the expected BucketLocationConflictException and ensure that the bound
|
|
1353
1494
|
# domains don't exist in SDB.
|
|
1354
|
-
sdb =
|
|
1495
|
+
sdb = establish_boto3_session().client(
|
|
1496
|
+
region_name=self.awsRegion(), service_name="sdb"
|
|
1497
|
+
)
|
|
1355
1498
|
next_token = None
|
|
1356
1499
|
allDomainNames = []
|
|
1357
1500
|
while True:
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1501
|
+
if next_token is None:
|
|
1502
|
+
domains = sdb.list_domains(MaxNumberOfDomains=100)
|
|
1503
|
+
else:
|
|
1504
|
+
domains = sdb.list_domains(
|
|
1505
|
+
MaxNumberOfDomains=100, NextToken=next_token
|
|
1506
|
+
)
|
|
1507
|
+
allDomainNames.extend(domains["DomainNames"])
|
|
1508
|
+
next_token = domains.get("NextToken")
|
|
1361
1509
|
if next_token is None:
|
|
1362
1510
|
break
|
|
1363
1511
|
self.assertFalse([d for d in allDomainNames if testJobStoreUUID in d])
|
|
@@ -1370,7 +1518,10 @@ class AWSJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1370
1518
|
client.delete_bucket(Bucket=bucketName)
|
|
1371
1519
|
except ClientError as e:
|
|
1372
1520
|
# The actual HTTP code of the error is in status.
|
|
1373
|
-
if
|
|
1521
|
+
if (
|
|
1522
|
+
e.response.get("ResponseMetadata", {}).get("HTTPStatusCode")
|
|
1523
|
+
== 404
|
|
1524
|
+
):
|
|
1374
1525
|
# The bucket doesn't exist; maybe a failed delete actually succeeded.
|
|
1375
1526
|
pass
|
|
1376
1527
|
else:
|
|
@@ -1379,26 +1530,29 @@ class AWSJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1379
1530
|
@slow
|
|
1380
1531
|
def testInlinedFiles(self):
|
|
1381
1532
|
from toil.jobStores.aws.jobStore import AWSJobStore
|
|
1533
|
+
|
|
1382
1534
|
jobstore = self.jobstore_initialized
|
|
1383
1535
|
for encrypted in (True, False):
|
|
1384
1536
|
n = AWSJobStore.FileInfo.maxInlinedSize()
|
|
1385
1537
|
sizes = (1, n // 2, n - 1, n, n + 1, 2 * n)
|
|
1386
1538
|
for size in chain(sizes, islice(reversed(sizes), 1)):
|
|
1387
1539
|
s = os.urandom(size)
|
|
1388
|
-
with jobstore.write_shared_file_stream(
|
|
1540
|
+
with jobstore.write_shared_file_stream("foo") as f:
|
|
1389
1541
|
f.write(s)
|
|
1390
|
-
with jobstore.read_shared_file_stream(
|
|
1542
|
+
with jobstore.read_shared_file_stream("foo") as f:
|
|
1391
1543
|
self.assertEqual(s, f.read())
|
|
1392
1544
|
|
|
1393
1545
|
def testOverlargeJob(self):
|
|
1394
1546
|
jobstore = self.jobstore_initialized
|
|
1395
1547
|
jobRequirements = dict(memory=12, cores=34, disk=35, preemptible=True)
|
|
1396
|
-
overlargeJob = JobDescription(
|
|
1397
|
-
|
|
1398
|
-
|
|
1548
|
+
overlargeJob = JobDescription(
|
|
1549
|
+
requirements=jobRequirements,
|
|
1550
|
+
jobName="test-overlarge",
|
|
1551
|
+
unitName="onJobStore",
|
|
1552
|
+
)
|
|
1399
1553
|
|
|
1400
1554
|
# Make the pickled size of the job larger than 256K
|
|
1401
|
-
with open("/dev/urandom",
|
|
1555
|
+
with open("/dev/urandom", "rb") as random:
|
|
1402
1556
|
overlargeJob.jobName = str(random.read(512 * 1024))
|
|
1403
1557
|
jobstore.assign_job_id(overlargeJob)
|
|
1404
1558
|
jobstore.create_job(overlargeJob)
|
|
@@ -1410,33 +1564,39 @@ class AWSJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1410
1564
|
jobstore.delete_job(overlargeJob.jobStoreID)
|
|
1411
1565
|
|
|
1412
1566
|
def testMultiThreadImportFile(self) -> None:
|
|
1413
|
-
"""
|
|
1567
|
+
"""Tests that importFile is thread-safe."""
|
|
1414
1568
|
|
|
1415
1569
|
from concurrent.futures.thread import ThreadPoolExecutor
|
|
1416
1570
|
|
|
1417
1571
|
from toil.lib.threading import cpu_count
|
|
1418
1572
|
|
|
1419
|
-
threads:
|
|
1573
|
+
threads: tuple[int, ...] = (2, cpu_count()) if cpu_count() > 2 else (2,)
|
|
1420
1574
|
num_of_files: int = 5
|
|
1421
1575
|
size: int = 1 << 16 + 1
|
|
1422
1576
|
|
|
1423
1577
|
# The string in otherCls() is arbitrary as long as it returns a class that has access
|
|
1424
1578
|
# to ._externalStore() and ._prepareTestFile()
|
|
1425
|
-
other: AbstractJobStoreTest.Test = AWSJobStoreTest(
|
|
1579
|
+
other: AbstractJobStoreTest.Test = AWSJobStoreTest("testSharedFiles")
|
|
1426
1580
|
store: Any = other._externalStore()
|
|
1427
1581
|
|
|
1428
1582
|
# prepare test files to import
|
|
1429
|
-
logger.debug(
|
|
1583
|
+
logger.debug(
|
|
1584
|
+
f"Preparing {num_of_files} test files for testMultiThreadImportFile()."
|
|
1585
|
+
)
|
|
1430
1586
|
test_files = [other._prepareTestFile(store, size) for _ in range(num_of_files)]
|
|
1431
1587
|
|
|
1432
1588
|
for thread_count in threads:
|
|
1433
|
-
with self.subTest(
|
|
1589
|
+
with self.subTest(
|
|
1590
|
+
f'Testing threaded importFile with "{thread_count}" threads.'
|
|
1591
|
+
):
|
|
1434
1592
|
results = []
|
|
1435
1593
|
|
|
1436
1594
|
with ThreadPoolExecutor(max_workers=thread_count) as executor:
|
|
1437
1595
|
for url, expected_md5 in test_files:
|
|
1438
1596
|
# run jobStore.importFile() asynchronously
|
|
1439
|
-
future = executor.submit(
|
|
1597
|
+
future = executor.submit(
|
|
1598
|
+
self.jobstore_initialized.import_file, url
|
|
1599
|
+
)
|
|
1440
1600
|
results.append((future, expected_md5))
|
|
1441
1601
|
|
|
1442
1602
|
self.assertEqual(len(results), num_of_files)
|
|
@@ -1446,32 +1606,39 @@ class AWSJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1446
1606
|
self.assertIsInstance(file_id, FileID)
|
|
1447
1607
|
|
|
1448
1608
|
with self.jobstore_initialized.read_file_stream(file_id) as f:
|
|
1449
|
-
self.assertEqual(
|
|
1609
|
+
self.assertEqual(
|
|
1610
|
+
hashlib.md5(f.read()).hexdigest(), expected_md5
|
|
1611
|
+
)
|
|
1450
1612
|
|
|
1451
1613
|
def _prepareTestFile(self, bucket, size=None):
|
|
1452
1614
|
from toil.lib.aws.utils import retry_s3
|
|
1453
1615
|
|
|
1454
|
-
file_name =
|
|
1455
|
-
url = f
|
|
1616
|
+
file_name = "testfile_%s" % uuid.uuid4()
|
|
1617
|
+
url = f"s3://{bucket.name}/{file_name}"
|
|
1456
1618
|
if size is None:
|
|
1457
1619
|
return url
|
|
1458
|
-
with open(
|
|
1620
|
+
with open("/dev/urandom", "rb") as readable:
|
|
1459
1621
|
for attempt in retry_s3():
|
|
1460
1622
|
with attempt:
|
|
1461
1623
|
bucket.put_object(Key=file_name, Body=str(readable.read(size)))
|
|
1462
|
-
return
|
|
1624
|
+
return (
|
|
1625
|
+
url,
|
|
1626
|
+
hashlib.md5(bucket.Object(file_name).get().get("Body").read()).hexdigest(),
|
|
1627
|
+
)
|
|
1463
1628
|
|
|
1464
1629
|
def _hashTestFile(self, url: str) -> str:
|
|
1465
1630
|
from toil.jobStores.aws.jobStore import AWSJobStore
|
|
1631
|
+
from toil.lib.aws.utils import get_object_for_url
|
|
1632
|
+
|
|
1466
1633
|
str(AWSJobStore) # to prevent removal of that import
|
|
1467
1634
|
key = get_object_for_url(urlparse.urlparse(url), existing=True)
|
|
1468
|
-
contents = key.get().get(
|
|
1635
|
+
contents = key.get().get("Body").read()
|
|
1469
1636
|
return hashlib.md5(contents).hexdigest()
|
|
1470
1637
|
|
|
1471
1638
|
def _createExternalStore(self):
|
|
1472
1639
|
"""A S3.Bucket instance is returned"""
|
|
1473
1640
|
from toil.jobStores.aws.jobStore import establish_boto3_session
|
|
1474
|
-
from toil.lib.aws.utils import retry_s3
|
|
1641
|
+
from toil.lib.aws.utils import create_s3_bucket, retry_s3
|
|
1475
1642
|
|
|
1476
1643
|
resource = establish_boto3_session().resource(
|
|
1477
1644
|
"s3", region_name=self.awsRegion()
|
|
@@ -1502,6 +1669,7 @@ class AWSJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1502
1669
|
|
|
1503
1670
|
def _batchDeletionSize(self):
|
|
1504
1671
|
from toil.jobStores.aws.jobStore import AWSJobStore
|
|
1672
|
+
|
|
1505
1673
|
return AWSJobStore.itemsPerBatchDelete
|
|
1506
1674
|
|
|
1507
1675
|
|
|
@@ -1509,15 +1677,10 @@ class AWSJobStoreTest(AbstractJobStoreTest.Test):
|
|
|
1509
1677
|
class InvalidAWSJobStoreTest(ToilTest):
|
|
1510
1678
|
def testInvalidJobStoreName(self):
|
|
1511
1679
|
from toil.jobStores.aws.jobStore import AWSJobStore
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
self.assertRaises(ValueError,
|
|
1516
|
-
AWSJobStore,
|
|
1517
|
-
'us-west-2:' + ('a' * 100))
|
|
1518
|
-
self.assertRaises(ValueError,
|
|
1519
|
-
AWSJobStore,
|
|
1520
|
-
'us-west-2:a_b')
|
|
1680
|
+
|
|
1681
|
+
self.assertRaises(ValueError, AWSJobStore, "us-west-2:a--b")
|
|
1682
|
+
self.assertRaises(ValueError, AWSJobStore, "us-west-2:" + ("a" * 100))
|
|
1683
|
+
self.assertRaises(ValueError, AWSJobStore, "us-west-2:a_b")
|
|
1521
1684
|
|
|
1522
1685
|
|
|
1523
1686
|
@needs_aws_s3
|
|
@@ -1528,14 +1691,14 @@ class EncryptedAWSJobStoreTest(AWSJobStoreTest, AbstractEncryptedJobStoreTest.Te
|
|
|
1528
1691
|
|
|
1529
1692
|
|
|
1530
1693
|
class StubHttpRequestHandler(http.server.SimpleHTTPRequestHandler):
|
|
1531
|
-
fileContents =
|
|
1694
|
+
fileContents = "A good programmer looks both ways before crossing a one-way street"
|
|
1532
1695
|
|
|
1533
1696
|
def do_GET(self):
|
|
1534
1697
|
self.send_response(200)
|
|
1535
1698
|
self.send_header("Content-type", "text/plain")
|
|
1536
1699
|
self.send_header("Content-length", len(self.fileContents))
|
|
1537
1700
|
self.end_headers()
|
|
1538
|
-
self.fileContents = self.fileContents.encode(
|
|
1701
|
+
self.fileContents = self.fileContents.encode("utf-8")
|
|
1539
1702
|
self.wfile.write(self.fileContents)
|
|
1540
1703
|
|
|
1541
1704
|
|