toil 8.2.0__py3-none-any.whl → 9.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- toil/batchSystems/registry.py +15 -118
- toil/common.py +20 -1
- toil/cwl/cwltoil.py +80 -37
- toil/cwl/utils.py +103 -3
- toil/jobStores/abstractJobStore.py +11 -236
- toil/jobStores/aws/jobStore.py +2 -1
- toil/jobStores/fileJobStore.py +2 -1
- toil/jobStores/googleJobStore.py +7 -4
- toil/lib/accelerators.py +1 -1
- toil/lib/generatedEC2Lists.py +81 -19
- toil/lib/misc.py +1 -1
- toil/lib/plugins.py +106 -0
- toil/lib/url.py +320 -0
- toil/options/cwl.py +13 -1
- toil/options/runner.py +17 -10
- toil/options/wdl.py +12 -1
- toil/provisioners/aws/awsProvisioner.py +25 -2
- toil/server/app.py +12 -6
- toil/server/cli/wes_cwl_runner.py +2 -2
- toil/server/wes/abstract_backend.py +21 -43
- toil/server/wes/toil_backend.py +2 -2
- toil/test/__init__.py +2 -2
- toil/test/batchSystems/batchSystemTest.py +2 -9
- toil/test/batchSystems/batch_system_plugin_test.py +7 -0
- toil/test/cwl/cwlTest.py +181 -8
- toil/test/docs/scriptsTest.py +2 -1
- toil/test/lib/test_url.py +69 -0
- toil/test/lib/url_plugin_test.py +105 -0
- toil/test/provisioners/aws/awsProvisionerTest.py +1 -1
- toil/test/provisioners/clusterTest.py +15 -2
- toil/test/provisioners/gceProvisionerTest.py +1 -1
- toil/test/server/serverTest.py +78 -36
- toil/test/wdl/md5sum/md5sum-gs.json +1 -1
- toil/test/wdl/testfiles/read_file.wdl +18 -0
- toil/test/wdl/testfiles/url_to_optional_file.wdl +2 -1
- toil/test/wdl/wdltoil_test.py +74 -125
- toil/utils/toilSshCluster.py +23 -0
- toil/utils/toilUpdateEC2Instances.py +1 -0
- toil/version.py +9 -9
- toil/wdl/wdltoil.py +182 -314
- toil/worker.py +11 -6
- {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/METADATA +23 -23
- {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/RECORD +47 -42
- {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/WHEEL +1 -1
- {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/entry_points.txt +0 -0
- {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/licenses/LICENSE +0 -0
- {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/top_level.txt +0 -0
|
@@ -31,6 +31,8 @@ from toil.test import (
|
|
|
31
31
|
slow,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
|
+
from toil.test.cwl.cwlTest import TestCWLv12Conformance
|
|
35
|
+
|
|
34
36
|
log = logging.getLogger(__name__)
|
|
35
37
|
|
|
36
38
|
|
|
@@ -237,6 +239,17 @@ class CWLOnARMTest(AbstractClusterTest):
|
|
|
237
239
|
|
|
238
240
|
@needs_env_var("CI_COMMIT_SHA", "a git commit sha")
|
|
239
241
|
def test_cwl_on_arm(self) -> None:
|
|
242
|
+
# Import the test we want to run remotely, so we know right away if it exists.
|
|
243
|
+
test_class = TestCWLv12Conformance
|
|
244
|
+
test_method = test_class.test_run_conformance
|
|
245
|
+
|
|
246
|
+
# Work out how to describe it as a pytest test spec.
|
|
247
|
+
# __qualname__ gives classname.methodname
|
|
248
|
+
test_name = test_method.__qualname__.replace(".", "::")
|
|
249
|
+
# The module path is the file path under src, with dots.
|
|
250
|
+
test_path = test_class.__module__.replace(".", "/")
|
|
251
|
+
test_spec = f"src/{test_path}.py::{test_name}"
|
|
252
|
+
|
|
240
253
|
# Make a cluster
|
|
241
254
|
self.launchCluster()
|
|
242
255
|
# get the leader so we know the IP address - we don't need to wait since create cluster
|
|
@@ -276,12 +289,12 @@ class CWLOnARMTest(AbstractClusterTest):
|
|
|
276
289
|
]
|
|
277
290
|
)
|
|
278
291
|
|
|
279
|
-
# Runs the
|
|
292
|
+
# Runs the test on an ARM instance
|
|
280
293
|
self.sshUtil(
|
|
281
294
|
[
|
|
282
295
|
"bash",
|
|
283
296
|
"-c",
|
|
284
|
-
f". .{self.venvDir}/bin/activate && cd {self.cwl_test_dir}/toil && pytest --log-cli-level DEBUG -r s
|
|
297
|
+
f". .{self.venvDir}/bin/activate && cd {self.cwl_test_dir}/toil && pytest --log-cli-level DEBUG -r s {test_spec}",
|
|
285
298
|
]
|
|
286
299
|
)
|
|
287
300
|
|
|
@@ -341,7 +341,7 @@ class GCEAutoscaleTestMultipleNodeTypes(AbstractGCEAutoscaleTest):
|
|
|
341
341
|
runCommand = [
|
|
342
342
|
"/home/venv/bin/python",
|
|
343
343
|
"/home/sort.py",
|
|
344
|
-
"--fileToSort=/
|
|
344
|
+
"--fileToSort=/etc/passwd",
|
|
345
345
|
"--sortMemory=0.6G",
|
|
346
346
|
"--mergeMemory=3.0G",
|
|
347
347
|
]
|
toil/test/server/serverTest.py
CHANGED
|
@@ -33,7 +33,7 @@ except ImportError:
|
|
|
33
33
|
# extra wasn't installed. We'll then skip them all.
|
|
34
34
|
pass
|
|
35
35
|
|
|
36
|
-
from toil.test import ToilTest, needs_aws_s3, needs_celery_broker, needs_server
|
|
36
|
+
from toil.test import ToilTest, needs_aws_s3, needs_celery_broker, needs_cwl, needs_server, integrative
|
|
37
37
|
|
|
38
38
|
logger = logging.getLogger(__name__)
|
|
39
39
|
logging.basicConfig(level=logging.INFO)
|
|
@@ -185,6 +185,7 @@ class FileStateStoreURLTest(hidden.AbstractStateStoreTest):
|
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
@needs_aws_s3
|
|
188
|
+
@integrative
|
|
188
189
|
class BucketUsingTest(ToilTest):
|
|
189
190
|
"""
|
|
190
191
|
Base class for tests that need a bucket.
|
|
@@ -268,6 +269,35 @@ class AWSStateStoreTest(hidden.AbstractStateStoreTest, BucketUsingTest):
|
|
|
268
269
|
self.assertEqual(obj.content_length, len("testvalue"))
|
|
269
270
|
|
|
270
271
|
|
|
272
|
+
# Problem: httpx (which the Connexion test client uses the API of)
|
|
273
|
+
# automatically decides whether to send posts in
|
|
274
|
+
# application/x-www-form-urlencoded or multipart/form-data format
|
|
275
|
+
# based on whether they are uploading any files. But the GA4GH WES
|
|
276
|
+
# API run workflow endpoint only accepts multipart/form-data. It takes
|
|
277
|
+
# workflow_attachment as an array of binary strings officially, but this is
|
|
278
|
+
# actually how Swagger takes multiple-file upload fields. See
|
|
279
|
+
# <https://swagger.io/docs/specification/v2_0/file-upload/#multiple-upload>.
|
|
280
|
+
# (httpx doesn't seem to support actually sending multiple files to one field
|
|
281
|
+
# either, but if we send just one file it ends up as the only value in
|
|
282
|
+
# Swagger's array.)
|
|
283
|
+
#
|
|
284
|
+
# The apparently-official workaround for forcing multipart encoding is to
|
|
285
|
+
# construct an empty dict-saped data structure that is truthy, and pass that as
|
|
286
|
+
# your file list. See
|
|
287
|
+
# <https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186>
|
|
288
|
+
class TrueDict(dict):
|
|
289
|
+
"""
|
|
290
|
+
Dict that is truthy even when empty.
|
|
291
|
+
|
|
292
|
+
Used as a workaround to set httpx post request encoding as recommended in
|
|
293
|
+
<https://github.com/encode/httpx/discussions/2399#discussioncomment-3814186>.
|
|
294
|
+
"""
|
|
295
|
+
def __bool__(self) -> bool:
|
|
296
|
+
"""
|
|
297
|
+
Always say the object is truthy.
|
|
298
|
+
"""
|
|
299
|
+
return True
|
|
300
|
+
|
|
271
301
|
@needs_server
|
|
272
302
|
class AbstractToilWESServerTest(ToilTest):
|
|
273
303
|
"""
|
|
@@ -296,11 +326,12 @@ class AbstractToilWESServerTest(ToilTest):
|
|
|
296
326
|
)
|
|
297
327
|
|
|
298
328
|
# Make the FlaskApp
|
|
299
|
-
|
|
329
|
+
self.app = create_app(args)
|
|
300
330
|
|
|
301
|
-
#
|
|
302
|
-
|
|
303
|
-
|
|
331
|
+
# Neither <https://flask.palletsprojects.com/en/stable/testing/> nor
|
|
332
|
+
# <https://connexion.readthedocs.io/en/latest/testing.html#testing>
|
|
333
|
+
# suggests setting a testing flag on the Connexion app or its internal
|
|
334
|
+
# Flask app, so we don't.
|
|
304
335
|
|
|
305
336
|
self.example_cwl = textwrap.dedent(
|
|
306
337
|
"""
|
|
@@ -345,7 +376,6 @@ class AbstractToilWESServerTest(ToilTest):
|
|
|
345
376
|
"""
|
|
346
377
|
rv = client.get(f"/ga4gh/wes/v1/runs/{run_id}")
|
|
347
378
|
self.assertEqual(rv.status_code, 200)
|
|
348
|
-
self.assertTrue(rv.is_json)
|
|
349
379
|
return rv
|
|
350
380
|
|
|
351
381
|
def _check_successful_log(self, client: "FlaskClient", run_id: str) -> None:
|
|
@@ -354,15 +384,16 @@ class AbstractToilWESServerTest(ToilTest):
|
|
|
354
384
|
The workflow should succeed, it should have some tasks, and they should have all succeeded.
|
|
355
385
|
"""
|
|
356
386
|
rv = self._fetch_run_log(client, run_id)
|
|
387
|
+
rv_json = rv.json()
|
|
357
388
|
logger.debug("Log info: %s", rv.json)
|
|
358
|
-
run_log =
|
|
389
|
+
run_log = rv_json.get("run_log")
|
|
359
390
|
self.assertEqual(type(run_log), dict)
|
|
360
391
|
if "exit_code" in run_log:
|
|
361
392
|
# The workflow succeeded if it has an exit code
|
|
362
393
|
self.assertEqual(run_log["exit_code"], 0)
|
|
363
394
|
# The workflow is complete
|
|
364
|
-
self.assertEqual(
|
|
365
|
-
task_logs =
|
|
395
|
+
self.assertEqual(rv_json.get("state"), "COMPLETE")
|
|
396
|
+
task_logs = rv_json.get("task_logs")
|
|
366
397
|
# There are tasks reported
|
|
367
398
|
self.assertEqual(type(task_logs), list)
|
|
368
399
|
self.assertGreater(len(task_logs), 0)
|
|
@@ -375,7 +406,7 @@ class AbstractToilWESServerTest(ToilTest):
|
|
|
375
406
|
"""Report the log for the given workflow run."""
|
|
376
407
|
rv = self._fetch_run_log(client, run_id)
|
|
377
408
|
logger.debug(f"Report log response: {rv.json}")
|
|
378
|
-
run_log = rv.json.get("run_log")
|
|
409
|
+
run_log = rv.json().get("run_log")
|
|
379
410
|
self.assertEqual(type(run_log), dict)
|
|
380
411
|
self.assertEqual(type(run_log.get("stdout")), str)
|
|
381
412
|
self.assertEqual(type(run_log.get("stderr")), str)
|
|
@@ -395,12 +426,13 @@ class AbstractToilWESServerTest(ToilTest):
|
|
|
395
426
|
logger.info("Fetch %s", url)
|
|
396
427
|
rv = client.get(url)
|
|
397
428
|
self.assertEqual(rv.status_code, 200)
|
|
398
|
-
logger.info("Got %s:\n%s", url, rv.
|
|
429
|
+
logger.info("Got %s:\n%s", url, rv.content.decode("utf-8"))
|
|
399
430
|
|
|
400
431
|
def _start_slow_workflow(self, client: "FlaskClient") -> str:
|
|
401
432
|
"""
|
|
402
433
|
Start a slow workflow and return its ID.
|
|
403
434
|
"""
|
|
435
|
+
|
|
404
436
|
rv = client.post(
|
|
405
437
|
"/ga4gh/wes/v1/runs",
|
|
406
438
|
data={
|
|
@@ -408,15 +440,18 @@ class AbstractToilWESServerTest(ToilTest):
|
|
|
408
440
|
"workflow_type": "CWL",
|
|
409
441
|
"workflow_type_version": "v1.0",
|
|
410
442
|
"workflow_params": json.dumps({"delay": "5"}),
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
443
|
+
},
|
|
444
|
+
files={
|
|
445
|
+
"workflow_attachment": (
|
|
446
|
+
"slow.cwl",
|
|
447
|
+
BytesIO(self.slow_cwl.encode()),
|
|
448
|
+
"application/octet-stream",
|
|
449
|
+
),
|
|
414
450
|
},
|
|
415
451
|
)
|
|
416
452
|
# workflow is submitted successfully
|
|
417
453
|
self.assertEqual(rv.status_code, 200)
|
|
418
|
-
|
|
419
|
-
run_id = rv.json.get("run_id")
|
|
454
|
+
run_id = rv.json().get("run_id")
|
|
420
455
|
self.assertIsNotNone(run_id)
|
|
421
456
|
|
|
422
457
|
return run_id
|
|
@@ -428,11 +463,11 @@ class AbstractToilWESServerTest(ToilTest):
|
|
|
428
463
|
|
|
429
464
|
rv = client.get(f"/ga4gh/wes/v1/runs/{run_id}/status")
|
|
430
465
|
self.assertEqual(rv.status_code, 200)
|
|
431
|
-
|
|
432
|
-
self.assertIn("run_id",
|
|
433
|
-
self.assertEqual(
|
|
434
|
-
self.assertIn("state",
|
|
435
|
-
state =
|
|
466
|
+
rv_json = rv.json()
|
|
467
|
+
self.assertIn("run_id", rv_json)
|
|
468
|
+
self.assertEqual(rv_json.get("run_id"), run_id)
|
|
469
|
+
self.assertIn("state", rv_json)
|
|
470
|
+
state = rv_json.get("state")
|
|
436
471
|
self.assertIn(
|
|
437
472
|
state,
|
|
438
473
|
[
|
|
@@ -492,7 +527,10 @@ class ToilWESServerBenchTest(AbstractToilWESServerTest):
|
|
|
492
527
|
"""Test the homepage endpoint."""
|
|
493
528
|
with self.app.test_client() as client:
|
|
494
529
|
rv = client.get("/")
|
|
495
|
-
|
|
530
|
+
# The client will follow the redirect and populate the url on the response
|
|
531
|
+
self.assertEqual(rv.url.path, "/ga4gh/wes/v1/service-info")
|
|
532
|
+
# We see the final 200 OK status code
|
|
533
|
+
self.assertEqual(rv.status_code, 200)
|
|
496
534
|
|
|
497
535
|
def test_health(self) -> None:
|
|
498
536
|
"""Test the health check endpoint."""
|
|
@@ -505,7 +543,7 @@ class ToilWESServerBenchTest(AbstractToilWESServerTest):
|
|
|
505
543
|
with self.app.test_client() as client:
|
|
506
544
|
rv = client.get("/ga4gh/wes/v1/service-info")
|
|
507
545
|
self.assertEqual(rv.status_code, 200)
|
|
508
|
-
service_info = json
|
|
546
|
+
service_info = rv.json()
|
|
509
547
|
|
|
510
548
|
self.assertIn("version", service_info)
|
|
511
549
|
self.assertIn("workflow_type_versions", service_info)
|
|
@@ -519,7 +557,7 @@ class ToilWESServerBenchTest(AbstractToilWESServerTest):
|
|
|
519
557
|
self.assertIn("system_state_counts", service_info)
|
|
520
558
|
self.assertIn("tags", service_info)
|
|
521
559
|
|
|
522
|
-
|
|
560
|
+
@needs_cwl
|
|
523
561
|
class ToilWESServerWorkflowTest(AbstractToilWESServerTest):
|
|
524
562
|
"""
|
|
525
563
|
Tests of the WES server running workflows.
|
|
@@ -548,11 +586,10 @@ class ToilWESServerWorkflowTest(AbstractToilWESServerTest):
|
|
|
548
586
|
{"message": "Hello, world!"} if include_message else {}
|
|
549
587
|
)
|
|
550
588
|
with self.app.test_client() as client:
|
|
551
|
-
rv = client.post("/ga4gh/wes/v1/runs", data=post_data)
|
|
589
|
+
rv = client.post("/ga4gh/wes/v1/runs", data=post_data, files=TrueDict())
|
|
552
590
|
# workflow is submitted successfully
|
|
553
591
|
self.assertEqual(rv.status_code, 200)
|
|
554
|
-
|
|
555
|
-
run_id = rv.json.get("run_id")
|
|
592
|
+
run_id = rv.json().get("run_id")
|
|
556
593
|
self.assertIsNotNone(run_id)
|
|
557
594
|
|
|
558
595
|
# Check status
|
|
@@ -571,11 +608,11 @@ class ToilWESServerWorkflowTest(AbstractToilWESServerTest):
|
|
|
571
608
|
"workflow_type_version": "v1.0",
|
|
572
609
|
"workflow_params": "{}",
|
|
573
610
|
},
|
|
611
|
+
files=TrueDict(),
|
|
574
612
|
)
|
|
575
613
|
self.assertEqual(rv.status_code, 400)
|
|
576
|
-
self.assertTrue(rv.is_json)
|
|
577
614
|
self.assertEqual(
|
|
578
|
-
rv.json.get("msg"),
|
|
615
|
+
rv.json().get("msg"),
|
|
579
616
|
"Relative 'workflow_url' but missing 'workflow_attachment'",
|
|
580
617
|
)
|
|
581
618
|
|
|
@@ -589,20 +626,24 @@ class ToilWESServerWorkflowTest(AbstractToilWESServerTest):
|
|
|
589
626
|
"workflow_type": "CWL",
|
|
590
627
|
"workflow_type_version": "v1.0",
|
|
591
628
|
"workflow_params": json.dumps({"message": "Hello, world!"}),
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
629
|
+
},
|
|
630
|
+
files={
|
|
631
|
+
"workflow_attachment": (
|
|
632
|
+
"example.cwl",
|
|
633
|
+
BytesIO(self.example_cwl.encode()),
|
|
634
|
+
"application/octet-stream",
|
|
635
|
+
),
|
|
595
636
|
},
|
|
596
637
|
)
|
|
597
638
|
# workflow is submitted successfully
|
|
598
639
|
self.assertEqual(rv.status_code, 200)
|
|
599
|
-
|
|
600
|
-
run_id = rv.json.get("run_id")
|
|
640
|
+
run_id = rv.json().get("run_id")
|
|
601
641
|
self.assertIsNotNone(run_id)
|
|
602
642
|
|
|
603
643
|
# Check status
|
|
604
644
|
self._wait_for_success(client, run_id)
|
|
605
645
|
|
|
646
|
+
@integrative
|
|
606
647
|
def test_run_workflow_https_url(self) -> None:
|
|
607
648
|
"""Test run example CWL workflow from the Internet."""
|
|
608
649
|
with self.app.test_client() as client:
|
|
@@ -614,11 +655,11 @@ class ToilWESServerWorkflowTest(AbstractToilWESServerTest):
|
|
|
614
655
|
"workflow_type_version": "v1.2",
|
|
615
656
|
"workflow_params": json.dumps({"message": "Hello, world!"}),
|
|
616
657
|
},
|
|
658
|
+
files=TrueDict(),
|
|
617
659
|
)
|
|
618
660
|
# workflow is submitted successfully
|
|
619
661
|
self.assertEqual(rv.status_code, 200)
|
|
620
|
-
|
|
621
|
-
run_id = rv.json.get("run_id")
|
|
662
|
+
run_id = rv.json().get("run_id")
|
|
622
663
|
self.assertIsNotNone(run_id)
|
|
623
664
|
|
|
624
665
|
# Check status
|
|
@@ -730,6 +771,7 @@ class ToilWESServerWorkflowTest(AbstractToilWESServerTest):
|
|
|
730
771
|
|
|
731
772
|
|
|
732
773
|
@needs_celery_broker
|
|
774
|
+
@integrative
|
|
733
775
|
class ToilWESServerCeleryWorkflowTest(ToilWESServerWorkflowTest):
|
|
734
776
|
"""
|
|
735
777
|
End-to-end workflow-running tests against Celery.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"ga4ghMd5.inputFile": "gs://
|
|
1
|
+
{"ga4ghMd5.inputFile": "gs://gcp-public-data-landsat/LT04/01/003/027/LT04_L1TP_003027_19830202_20170220_01_T1/README.GTF"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
version 1.0
|
|
2
|
+
|
|
3
|
+
# Workflow to read a file from a string path
|
|
4
|
+
|
|
5
|
+
workflow read_file {
|
|
6
|
+
|
|
7
|
+
input {
|
|
8
|
+
String input_string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
Array[String] the_lines = read_lines(input_string)
|
|
12
|
+
|
|
13
|
+
output {
|
|
14
|
+
Array[String] lines = the_lines
|
|
15
|
+
File remade_file = write_lines(the_lines)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
}
|
|
@@ -3,9 +3,10 @@ version 1.0
|
|
|
3
3
|
workflow url_to_optional_file {
|
|
4
4
|
input {
|
|
5
5
|
Int http_code = 404
|
|
6
|
+
String base_url = "https://httpstat.us/"
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
File? the_file =
|
|
9
|
+
File? the_file = base_url + http_code
|
|
9
10
|
|
|
10
11
|
output {
|
|
11
12
|
File? out_file = the_file
|
toil/test/wdl/wdltoil_test.py
CHANGED
|
@@ -13,6 +13,8 @@ from unittest.mock import patch
|
|
|
13
13
|
from uuid import uuid4
|
|
14
14
|
|
|
15
15
|
import pytest
|
|
16
|
+
from pytest_httpserver import HTTPServer
|
|
17
|
+
|
|
16
18
|
import WDL.Error
|
|
17
19
|
import WDL.Expr
|
|
18
20
|
|
|
@@ -23,6 +25,7 @@ from toil.test import (
|
|
|
23
25
|
needs_docker,
|
|
24
26
|
needs_docker_cuda,
|
|
25
27
|
needs_google_storage,
|
|
28
|
+
needs_online,
|
|
26
29
|
needs_singularity_or_docker,
|
|
27
30
|
needs_wdl,
|
|
28
31
|
slow,
|
|
@@ -32,7 +35,6 @@ from toil.wdl.wdltoil import (
|
|
|
32
35
|
WDLSectionJob,
|
|
33
36
|
WDLWorkflowGraph,
|
|
34
37
|
parse_disks,
|
|
35
|
-
remove_common_leading_whitespace,
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
logger = logging.getLogger(__name__)
|
|
@@ -113,11 +115,11 @@ class TestWDLConformance:
|
|
|
113
115
|
|
|
114
116
|
@slow
|
|
115
117
|
def test_unit_tests_v11(self, wdl_conformance_test_repo: Path) -> None:
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
+
# TODO: Using a branch lets Toil commits that formerly passed start to
|
|
119
|
+
# fail CI when the branch moves.
|
|
118
120
|
os.chdir(wdl_conformance_test_repo)
|
|
119
|
-
repo_url = "https://github.com/
|
|
120
|
-
repo_branch = "wdl-1.1
|
|
121
|
+
repo_url = "https://github.com/openwdl/wdl.git"
|
|
122
|
+
repo_branch = "wdl-1.1"
|
|
121
123
|
commands1 = [
|
|
122
124
|
exactPython,
|
|
123
125
|
"setup_unit_tests.py",
|
|
@@ -249,6 +251,7 @@ class TestWDL:
|
|
|
249
251
|
assert os.path.exists(result["ga4ghMd5.value"])
|
|
250
252
|
assert os.path.basename(result["ga4ghMd5.value"]) == "md5sum.txt"
|
|
251
253
|
|
|
254
|
+
@needs_online
|
|
252
255
|
def test_url_to_file(self, tmp_path: Path) -> None:
|
|
253
256
|
"""
|
|
254
257
|
Test if web URL strings can be coerced to usable Files.
|
|
@@ -308,6 +311,59 @@ class TestWDL:
|
|
|
308
311
|
assert isinstance(result["wait.result"], str)
|
|
309
312
|
assert result["wait.result"] == "waited"
|
|
310
313
|
|
|
314
|
+
def test_restart(self, tmp_path: Path) -> None:
|
|
315
|
+
"""
|
|
316
|
+
Test if a WDL workflow can be restarted and finish successfully.
|
|
317
|
+
"""
|
|
318
|
+
with get_data("test/wdl/testfiles/read_file.wdl") as wdl:
|
|
319
|
+
out_dir = tmp_path / "out"
|
|
320
|
+
file_path = tmp_path / "file"
|
|
321
|
+
jobstore_path = tmp_path / "tree"
|
|
322
|
+
command = (
|
|
323
|
+
self.base_command
|
|
324
|
+
+ [
|
|
325
|
+
str(wdl),
|
|
326
|
+
"-o",
|
|
327
|
+
str(out_dir),
|
|
328
|
+
"-i",
|
|
329
|
+
json.dumps({"read_file.input_string": str(file_path)}),
|
|
330
|
+
"--jobStore",
|
|
331
|
+
str(jobstore_path),
|
|
332
|
+
"--retryCount=0"
|
|
333
|
+
]
|
|
334
|
+
)
|
|
335
|
+
with pytest.raises(subprocess.CalledProcessError):
|
|
336
|
+
# The first time we run it, it should fail because it's trying
|
|
337
|
+
# to work on a nonexistent file from a string path.
|
|
338
|
+
result_json = subprocess.check_output(
|
|
339
|
+
command + ["--logCritical"]
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
# Then create the file
|
|
343
|
+
with open(file_path, "w") as f:
|
|
344
|
+
f.write("This is a line\n")
|
|
345
|
+
f.write("This is a different line")
|
|
346
|
+
|
|
347
|
+
# Now it should work
|
|
348
|
+
result_json = subprocess.check_output(
|
|
349
|
+
command + ["--restart"]
|
|
350
|
+
)
|
|
351
|
+
result = json.loads(result_json)
|
|
352
|
+
|
|
353
|
+
assert "read_file.lines" in result
|
|
354
|
+
assert isinstance(result["read_file.lines"], list)
|
|
355
|
+
assert result["read_file.lines"] == [
|
|
356
|
+
"This is a line",
|
|
357
|
+
"This is a different line"
|
|
358
|
+
]
|
|
359
|
+
|
|
360
|
+
# Since we were catching
|
|
361
|
+
# <https://github.com/DataBiosphere/toil/issues/5247> at file
|
|
362
|
+
# export, make sure we actually exported a file.
|
|
363
|
+
assert "read_file.remade_file" in result
|
|
364
|
+
assert isinstance(result["read_file.remade_file"], str)
|
|
365
|
+
assert os.path.exists(result["read_file.remade_file"])
|
|
366
|
+
|
|
311
367
|
@needs_singularity_or_docker
|
|
312
368
|
def test_workflow_file_deletion(self, tmp_path: Path) -> None:
|
|
313
369
|
"""
|
|
@@ -597,7 +653,7 @@ class TestWDL:
|
|
|
597
653
|
!= result_not_cached["random.value_written"]
|
|
598
654
|
)
|
|
599
655
|
|
|
600
|
-
def test_url_to_optional_file(self, tmp_path: Path) -> None:
|
|
656
|
+
def test_url_to_optional_file(self, tmp_path: Path, httpserver: HTTPServer) -> None:
|
|
601
657
|
"""
|
|
602
658
|
Test if missing and error-producing URLs are handled correctly for optional File? values.
|
|
603
659
|
"""
|
|
@@ -610,7 +666,15 @@ class TestWDL:
|
|
|
610
666
|
Return the parsed output.
|
|
611
667
|
"""
|
|
612
668
|
logger.info("Test optional file with HTTP code %s", code)
|
|
613
|
-
|
|
669
|
+
httpserver.expect_request(
|
|
670
|
+
"/" + str(code)
|
|
671
|
+
).respond_with_data(
|
|
672
|
+
"Some data",
|
|
673
|
+
status=code,
|
|
674
|
+
content_type="text/plain"
|
|
675
|
+
)
|
|
676
|
+
base_url = httpserver.url_for("/")
|
|
677
|
+
json_value = '{"url_to_optional_file.http_code": %d, "url_to_optional_file.base_url": "%s"}' % (code, base_url)
|
|
614
678
|
result_json = subprocess.check_output(
|
|
615
679
|
self.base_command
|
|
616
680
|
+ [
|
|
@@ -1055,7 +1119,7 @@ class TestWDLToilBench(unittest.TestCase):
|
|
|
1055
1119
|
assert "decl2" in result[0]
|
|
1056
1120
|
assert "successor" in result[1]
|
|
1057
1121
|
|
|
1058
|
-
def make_string_expr(self, to_parse: str) -> WDL.Expr.String:
|
|
1122
|
+
def make_string_expr(self, to_parse: str, expr_type: type[WDL.Expr.String] = WDL.Expr.String) -> WDL.Expr.String:
|
|
1059
1123
|
"""
|
|
1060
1124
|
Parse pseudo-WDL for testing whitespace removal.
|
|
1061
1125
|
"""
|
|
@@ -1066,122 +1130,7 @@ class TestWDLToilBench(unittest.TestCase):
|
|
|
1066
1130
|
for i in range(1, len(parts), 2):
|
|
1067
1131
|
parts[i] = WDL.Expr.Placeholder(pos, {}, WDL.Expr.Null(pos))
|
|
1068
1132
|
|
|
1069
|
-
return
|
|
1070
|
-
|
|
1071
|
-
def test_remove_common_leading_whitespace(self) -> None:
|
|
1072
|
-
"""
|
|
1073
|
-
Make sure leading whitespace removal works properly.
|
|
1074
|
-
"""
|
|
1075
|
-
|
|
1076
|
-
# For a single line, we remove its leading whitespace
|
|
1077
|
-
expr = self.make_string_expr(" a ~{b} c")
|
|
1078
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1079
|
-
assert len(trimmed.parts) == 3
|
|
1080
|
-
assert trimmed.parts[0] == "a "
|
|
1081
|
-
assert trimmed.parts[2] == " c"
|
|
1082
|
-
|
|
1083
|
-
# Whitespace removed isn't affected by totally blank lines
|
|
1084
|
-
expr = self.make_string_expr(" \n\n a\n ~{stuff}\n b\n\n")
|
|
1085
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1086
|
-
assert len(trimmed.parts) == 3
|
|
1087
|
-
assert trimmed.parts[0] == "\n\na\n"
|
|
1088
|
-
assert trimmed.parts[2] == "\nb\n\n"
|
|
1089
|
-
|
|
1090
|
-
# Unless blank toleration is off
|
|
1091
|
-
expr = self.make_string_expr(" \n\n a\n ~{stuff}\n b\n\n")
|
|
1092
|
-
trimmed = remove_common_leading_whitespace(expr, tolerate_blanks=False)
|
|
1093
|
-
assert len(trimmed.parts) == 3
|
|
1094
|
-
assert trimmed.parts[0] == " \n\n a\n "
|
|
1095
|
-
assert trimmed.parts[2] == "\n b\n\n"
|
|
1096
|
-
|
|
1097
|
-
# Whitespace is still removed if the first line doesn't have it before the newline
|
|
1098
|
-
expr = self.make_string_expr("\n a\n ~{stuff}\n b\n")
|
|
1099
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1100
|
-
assert len(trimmed.parts) == 3
|
|
1101
|
-
assert trimmed.parts[0] == "\na\n"
|
|
1102
|
-
assert trimmed.parts[2] == "\nb\n"
|
|
1103
|
-
|
|
1104
|
-
# Whitespace is not removed if actual content is dedented
|
|
1105
|
-
expr = self.make_string_expr(" \n\n a\n ~{stuff}\nuhoh\n b\n\n")
|
|
1106
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1107
|
-
assert len(trimmed.parts) == 3
|
|
1108
|
-
assert trimmed.parts[0] == " \n\n a\n "
|
|
1109
|
-
assert trimmed.parts[2] == "\nuhoh\n b\n\n"
|
|
1110
|
-
|
|
1111
|
-
# Unless dedents are tolerated
|
|
1112
|
-
expr = self.make_string_expr(" \n\n a\n ~{stuff}\nuhoh\n b\n\n")
|
|
1113
|
-
trimmed = remove_common_leading_whitespace(expr, tolerate_dedents=True)
|
|
1114
|
-
assert len(trimmed.parts) == 3
|
|
1115
|
-
assert trimmed.parts[0] == "\n\na\n"
|
|
1116
|
-
assert trimmed.parts[2] == "\nuhoh\nb\n\n"
|
|
1117
|
-
|
|
1118
|
-
# Whitespace is still removed if all-whitespace lines have less of it
|
|
1119
|
-
expr = self.make_string_expr("\n a\n ~{stuff}\n \n b\n")
|
|
1120
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1121
|
-
assert len(trimmed.parts) == 3
|
|
1122
|
-
assert trimmed.parts[0] == "\na\n"
|
|
1123
|
-
assert trimmed.parts[2] == "\n\nb\n"
|
|
1124
|
-
|
|
1125
|
-
# Unless all-whitespace lines are not tolerated
|
|
1126
|
-
expr = self.make_string_expr("\n a\n ~{stuff}\n \n b\n")
|
|
1127
|
-
trimmed = remove_common_leading_whitespace(expr, tolerate_all_whitespace=False)
|
|
1128
|
-
assert len(trimmed.parts) == 3
|
|
1129
|
-
assert trimmed.parts[0] == "\n a\n "
|
|
1130
|
-
assert trimmed.parts[2] == "\n\n b\n"
|
|
1131
|
-
|
|
1132
|
-
# When mixed tabs and spaces are detected, nothing is changed.
|
|
1133
|
-
expr = self.make_string_expr("\n a\n\t~{stuff}\n b\n")
|
|
1134
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1135
|
-
assert len(trimmed.parts) == 3
|
|
1136
|
-
assert trimmed.parts[0] == "\n a\n\t"
|
|
1137
|
-
assert trimmed.parts[2] == "\n b\n"
|
|
1138
|
-
|
|
1139
|
-
# When mixed tabs and spaces are not in the prefix, whitespace is removed.
|
|
1140
|
-
expr = self.make_string_expr("\n\ta\n\t~{stuff} \n\tb\n")
|
|
1141
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1142
|
-
assert len(trimmed.parts) == 3
|
|
1143
|
-
assert trimmed.parts[0] == "\na\n"
|
|
1144
|
-
assert trimmed.parts[2] == " \nb\n"
|
|
1145
|
-
|
|
1146
|
-
# An empty string works
|
|
1147
|
-
expr = self.make_string_expr("")
|
|
1148
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1149
|
-
assert len(trimmed.parts) == 1
|
|
1150
|
-
assert trimmed.parts[0] == ""
|
|
1151
|
-
|
|
1152
|
-
# A string of only whitespace is preserved as an all-whitespece line
|
|
1153
|
-
expr = self.make_string_expr("\t\t\t")
|
|
1154
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1155
|
-
assert len(trimmed.parts) == 1
|
|
1156
|
-
assert trimmed.parts[0] == "\t\t\t"
|
|
1157
|
-
|
|
1158
|
-
# A string of only whitespace is trimmed when all-whitespace lines are not tolerated
|
|
1159
|
-
expr = self.make_string_expr("\t\t\t")
|
|
1160
|
-
trimmed = remove_common_leading_whitespace(expr, tolerate_all_whitespace=False)
|
|
1161
|
-
assert len(trimmed.parts) == 1
|
|
1162
|
-
assert trimmed.parts[0] == ""
|
|
1163
|
-
|
|
1164
|
-
# An empty expression works
|
|
1165
|
-
expr = WDL.Expr.String(
|
|
1166
|
-
WDL.Error.SourcePosition("nowhere", "nowhere", 0, 0, 0, 0), []
|
|
1167
|
-
)
|
|
1168
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1169
|
-
assert len(trimmed.parts) == 0
|
|
1170
|
-
|
|
1171
|
-
# An expression of only placeholders works
|
|
1172
|
-
expr = self.make_string_expr("~{AAA}")
|
|
1173
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1174
|
-
assert len(trimmed.parts) == 3
|
|
1175
|
-
assert trimmed.parts[0] == ""
|
|
1176
|
-
assert trimmed.parts[2] == ""
|
|
1177
|
-
|
|
1178
|
-
# The command flag is preserved
|
|
1179
|
-
expr = self.make_string_expr(" a ~{b} c")
|
|
1180
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1181
|
-
assert trimmed.command == False
|
|
1182
|
-
expr.command = True
|
|
1183
|
-
trimmed = remove_common_leading_whitespace(expr)
|
|
1184
|
-
assert trimmed.command == True
|
|
1133
|
+
return expr_type(pos, parts)
|
|
1185
1134
|
|
|
1186
1135
|
def test_choose_human_readable_directory(self) -> None:
|
|
1187
1136
|
"""
|
|
@@ -1195,7 +1144,7 @@ class TestWDLToilBench(unittest.TestCase):
|
|
|
1195
1144
|
|
|
1196
1145
|
state: DirectoryNamingStateDict = {}
|
|
1197
1146
|
|
|
1198
|
-
# The first time we should get
|
|
1147
|
+
# The first time we should get a path with the task name and without the ID
|
|
1199
1148
|
first_chosen = choose_human_readable_directory(
|
|
1200
1149
|
"root", "taskname", "111-222-333", state
|
|
1201
1150
|
)
|
toil/utils/toilSshCluster.py
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"""SSH into the toil appliance container running on the leader of the cluster."""
|
|
15
15
|
import argparse
|
|
16
16
|
import logging
|
|
17
|
+
import socket
|
|
17
18
|
import sys
|
|
18
19
|
|
|
19
20
|
from toil.common import parser_with_common_options
|
|
@@ -22,6 +23,22 @@ from toil.statsAndLogging import set_logging_from_options
|
|
|
22
23
|
|
|
23
24
|
logger = logging.getLogger(__name__)
|
|
24
25
|
|
|
26
|
+
def have_ipv6() -> bool:
|
|
27
|
+
"""
|
|
28
|
+
Return True if the IPv6 loopback interface is useable.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
# We sniff for actual IPv6 like in
|
|
32
|
+
# https://github.com/urllib3/urllib3/pull/840/files in urllib3 (which we
|
|
33
|
+
# don't depend on)
|
|
34
|
+
if socket.has_ipv6:
|
|
35
|
+
# Built with IPv6 support
|
|
36
|
+
try:
|
|
37
|
+
socket.socket(socket.AF_INET6).bind(("::1", 0))
|
|
38
|
+
return True
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
return False
|
|
25
42
|
|
|
26
43
|
def main() -> None:
|
|
27
44
|
parser = parser_with_common_options(
|
|
@@ -73,6 +90,12 @@ def main() -> None:
|
|
|
73
90
|
["-L", f"{options.grafana_port}:localhost:3000", "-L", "9090:localhost:9090"]
|
|
74
91
|
)
|
|
75
92
|
|
|
93
|
+
if not have_ipv6():
|
|
94
|
+
# If we try to do SSH port forwarding without any other options, but
|
|
95
|
+
# IPv6 is turned off on the host, we might get complaints that we
|
|
96
|
+
# "Cannot assign requested address" on ports on [::1].
|
|
97
|
+
sshOptions.append("-4")
|
|
98
|
+
|
|
76
99
|
try:
|
|
77
100
|
cluster.getLeader().sshAppliance(
|
|
78
101
|
*command,
|