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.
Files changed (47) hide show
  1. toil/batchSystems/registry.py +15 -118
  2. toil/common.py +20 -1
  3. toil/cwl/cwltoil.py +80 -37
  4. toil/cwl/utils.py +103 -3
  5. toil/jobStores/abstractJobStore.py +11 -236
  6. toil/jobStores/aws/jobStore.py +2 -1
  7. toil/jobStores/fileJobStore.py +2 -1
  8. toil/jobStores/googleJobStore.py +7 -4
  9. toil/lib/accelerators.py +1 -1
  10. toil/lib/generatedEC2Lists.py +81 -19
  11. toil/lib/misc.py +1 -1
  12. toil/lib/plugins.py +106 -0
  13. toil/lib/url.py +320 -0
  14. toil/options/cwl.py +13 -1
  15. toil/options/runner.py +17 -10
  16. toil/options/wdl.py +12 -1
  17. toil/provisioners/aws/awsProvisioner.py +25 -2
  18. toil/server/app.py +12 -6
  19. toil/server/cli/wes_cwl_runner.py +2 -2
  20. toil/server/wes/abstract_backend.py +21 -43
  21. toil/server/wes/toil_backend.py +2 -2
  22. toil/test/__init__.py +2 -2
  23. toil/test/batchSystems/batchSystemTest.py +2 -9
  24. toil/test/batchSystems/batch_system_plugin_test.py +7 -0
  25. toil/test/cwl/cwlTest.py +181 -8
  26. toil/test/docs/scriptsTest.py +2 -1
  27. toil/test/lib/test_url.py +69 -0
  28. toil/test/lib/url_plugin_test.py +105 -0
  29. toil/test/provisioners/aws/awsProvisionerTest.py +1 -1
  30. toil/test/provisioners/clusterTest.py +15 -2
  31. toil/test/provisioners/gceProvisionerTest.py +1 -1
  32. toil/test/server/serverTest.py +78 -36
  33. toil/test/wdl/md5sum/md5sum-gs.json +1 -1
  34. toil/test/wdl/testfiles/read_file.wdl +18 -0
  35. toil/test/wdl/testfiles/url_to_optional_file.wdl +2 -1
  36. toil/test/wdl/wdltoil_test.py +74 -125
  37. toil/utils/toilSshCluster.py +23 -0
  38. toil/utils/toilUpdateEC2Instances.py +1 -0
  39. toil/version.py +9 -9
  40. toil/wdl/wdltoil.py +182 -314
  41. toil/worker.py +11 -6
  42. {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/METADATA +23 -23
  43. {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/RECORD +47 -42
  44. {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/WHEEL +1 -1
  45. {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/entry_points.txt +0 -0
  46. {toil-8.2.0.dist-info → toil-9.0.0.dist-info}/licenses/LICENSE +0 -0
  47. {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 TestCWLv12 on an ARM instance
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 src/toil/test/cwl/cwlTest.py::TestCWLv12::test_run_conformance",
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=/home/s3am/bin/asadmin",
344
+ "--fileToSort=/etc/passwd",
345
345
  "--sortMemory=0.6G",
346
346
  "--mergeMemory=3.0G",
347
347
  ]
@@ -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
- server_app = create_app(args)
329
+ self.app = create_app(args)
300
330
 
301
- # Fish out the actual Flask
302
- self.app: Flask = server_app.app
303
- self.app.testing = True
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 = rv.json.get("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(rv.json.get("state"), "COMPLETE")
365
- task_logs = rv.json.get("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.data.decode("utf-8"))
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
- "workflow_attachment": [
412
- (BytesIO(self.slow_cwl.encode()), "slow.cwl"),
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
- self.assertTrue(rv.is_json)
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
- self.assertTrue(rv.is_json)
432
- self.assertIn("run_id", rv.json)
433
- self.assertEqual(rv.json.get("run_id"), run_id)
434
- self.assertIn("state", rv.json)
435
- state = rv.json.get("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
- self.assertEqual(rv.status_code, 302)
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.loads(rv.data)
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
- self.assertTrue(rv.is_json)
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
- "workflow_attachment": [
593
- (BytesIO(self.example_cwl.encode()), "example.cwl"),
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
- self.assertTrue(rv.is_json)
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
- self.assertTrue(rv.is_json)
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://broad-public-datasets/NA12878/NA12878.cram.crai"}
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 = "https://httpstat.us/" + http_code
9
+ File? the_file = base_url + http_code
9
10
 
10
11
  output {
11
12
  File? out_file = the_file
@@ -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
- # There are still some bugs with the WDL spec, use a fixed version until
117
- # See comments of https://github.com/openwdl/wdl/pull/669
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/stxue1/wdl.git"
120
- repo_branch = "wdl-1.1.3-fixes"
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
- json_value = '{"url_to_optional_file.http_code": %d}' % code
669
+ httpserver.expect_request(
670
+ "/" + str(code)
671
+ ).respond_with_data(
672
+ "Some data",
673
+ status=code,
674
+ content_type="text/plain"
675
+ )
676
+ base_url = httpserver.url_for("/")
677
+ json_value = '{"url_to_optional_file.http_code": %d, "url_to_optional_file.base_url": "%s"}' % (code, base_url)
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 WDL.Expr.String(pos, parts)
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 apath with the task name and without the ID
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
  )
@@ -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,