total-perspective-vortex 3.2.1__tar.gz → 3.3.1__tar.gz
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.
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/PKG-INFO +8 -4
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/README.md +3 -3
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/pyproject.toml +13 -1
- total_perspective_vortex-3.3.1/tests/test_dryrunner_resource_pool.py +60 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_entity.py +47 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_explain.py +75 -0
- total_perspective_vortex-3.3.1/tests/test_helpers.py +350 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_context.py +1 -1
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_params_specific.py +4 -1
- total_perspective_vortex-3.3.1/tests/test_mapper_resource_pool_integration.py +300 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_resource_requirements.py +21 -5
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_rules.py +59 -0
- total_perspective_vortex-3.3.1/tests/test_mapper_weighted_choice.py +59 -0
- total_perspective_vortex-3.3.1/tests/test_resource_pool.py +899 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_scenarios.py +2 -2
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_shell.py +71 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/PKG-INFO +8 -4
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/SOURCES.txt +5 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/requires.txt +5 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/dryrunner.py +15 -1
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/dumper.py +2 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/shell.py +8 -1
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/test/mock_galaxy.py +19 -8
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/core/entities.py +92 -2
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/core/explain.py +1 -0
- total_perspective_vortex-3.3.1/tpv/core/helpers.py +268 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/core/loader.py +5 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/core/mapper.py +210 -23
- total_perspective_vortex-3.3.1/tpv/core/resource_pool.py +393 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/core/resource_requirements.py +6 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/rules/gateway.py +27 -14
- total_perspective_vortex-3.2.1/tests/test_helpers.py +0 -59
- total_perspective_vortex-3.2.1/tpv/core/helpers.py +0 -170
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/LICENSE +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/setup.cfg +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/setup.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_dump.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_basic.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_destinations.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_inheritance.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_merge_multiple.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_rank.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_resubmit.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_role.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_sample.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_user.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/dependency_links.txt +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/entry_points.txt +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/total_perspective_vortex.egg-info/top_level.txt +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/__init__.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/__init__.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/formatter.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/linter.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/mypychecker.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/test/__init__.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/commands/type_check_template.j2 +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/core/__init__.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/core/evaluator.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/core/util.py +0 -0
- {total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tpv/rules/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: total-perspective-vortex
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.1
|
|
4
4
|
Summary: A library for routing entities (jobs, users or groups) to destinations in Galaxy
|
|
5
5
|
Author: Galaxy Contributors
|
|
6
6
|
Maintainer-email: Galaxy Committers <galaxy-committers@lists.galaxyproject.org>
|
|
@@ -22,10 +22,14 @@ Requires-Dist: types-requests
|
|
|
22
22
|
Provides-Extra: cli
|
|
23
23
|
Requires-Dist: total-perspective-vortex; extra == "cli"
|
|
24
24
|
Requires-Dist: galaxy; extra == "cli"
|
|
25
|
+
Provides-Extra: resource-pools
|
|
26
|
+
Requires-Dist: redis>=5.3.0; extra == "resource-pools"
|
|
25
27
|
Provides-Extra: test
|
|
26
28
|
Requires-Dist: total-perspective-vortex[cli]; extra == "test"
|
|
29
|
+
Requires-Dist: total-perspective-vortex[resource-pools]; extra == "test"
|
|
27
30
|
Requires-Dist: pytest; extra == "test"
|
|
28
31
|
Requires-Dist: responses; extra == "test"
|
|
32
|
+
Requires-Dist: fakeredis[lua]; extra == "test"
|
|
29
33
|
Requires-Dist: tox>=2.9.1; extra == "test"
|
|
30
34
|
Requires-Dist: coverage>=4.4.1; extra == "test"
|
|
31
35
|
Provides-Extra: dev
|
|
@@ -59,12 +63,12 @@ GTN](https://training.galaxyproject.org/training-material/topics/admin/tutorials
|
|
|
59
63
|
### Installing latest released version
|
|
60
64
|
|
|
61
65
|
Once configured in the Galaxy application, TPV will be automatically installed
|
|
62
|
-
in the Galaxy environment. If you would like to manually install it
|
|
63
|
-
can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
66
|
+
in the Galaxy environment. If you would like to manually install it for independent
|
|
67
|
+
CLI use instead, you can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
64
68
|
the following command:
|
|
65
69
|
|
|
66
70
|
```python
|
|
67
|
-
pip install total-perspective-vortex
|
|
71
|
+
pip install "total-perspective-vortex[cli]"
|
|
68
72
|
```
|
|
69
73
|
|
|
70
74
|
|
|
@@ -23,12 +23,12 @@ GTN](https://training.galaxyproject.org/training-material/topics/admin/tutorials
|
|
|
23
23
|
### Installing latest released version
|
|
24
24
|
|
|
25
25
|
Once configured in the Galaxy application, TPV will be automatically installed
|
|
26
|
-
in the Galaxy environment. If you would like to manually install it
|
|
27
|
-
can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
26
|
+
in the Galaxy environment. If you would like to manually install it for independent
|
|
27
|
+
CLI use instead, you can do so from [PyPI](https://pypi.org/project/total-perspective-vortex/) with
|
|
28
28
|
the following command:
|
|
29
29
|
|
|
30
30
|
```python
|
|
31
|
-
pip install total-perspective-vortex
|
|
31
|
+
pip install "total-perspective-vortex[cli]"
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
|
|
@@ -14,7 +14,7 @@ authors = [
|
|
|
14
14
|
maintainers = [
|
|
15
15
|
{name = "Galaxy Committers", email = "galaxy-committers@lists.galaxyproject.org"}
|
|
16
16
|
]
|
|
17
|
-
version = "3.
|
|
17
|
+
version = "3.3.1"
|
|
18
18
|
dependencies = [
|
|
19
19
|
"pydantic>=2",
|
|
20
20
|
"cachetools>=3.1.0",
|
|
@@ -33,10 +33,19 @@ cli = [
|
|
|
33
33
|
"total-perspective-vortex",
|
|
34
34
|
"galaxy"
|
|
35
35
|
]
|
|
36
|
+
# Required by the Valkey-backed per-user resource pools (see docs: Per-user resource pools).
|
|
37
|
+
# redis-py speaks the Valkey wire protocol.
|
|
38
|
+
resource-pools = [
|
|
39
|
+
"redis>=5.3.0",
|
|
40
|
+
]
|
|
36
41
|
test = [
|
|
37
42
|
"total-perspective-vortex[cli]",
|
|
43
|
+
"total-perspective-vortex[resource-pools]",
|
|
38
44
|
"pytest",
|
|
39
45
|
"responses",
|
|
46
|
+
# Lets the resource-pool store contract test run the real Valkey adapter (Lua script)
|
|
47
|
+
# without a live server. This is required: the production Lua path must be tested.
|
|
48
|
+
"fakeredis[lua]",
|
|
40
49
|
"tox>=2.9.1",
|
|
41
50
|
"coverage>=4.4.1",
|
|
42
51
|
]
|
|
@@ -56,6 +65,9 @@ Documentation = "https://total-perspective-vortex.readthedocs.io"
|
|
|
56
65
|
[tool.setuptools]
|
|
57
66
|
include-package-data = true
|
|
58
67
|
|
|
68
|
+
[tool.setuptools.packages.find]
|
|
69
|
+
include = ["tpv*"]
|
|
70
|
+
|
|
59
71
|
[tool.setuptools.package-data]
|
|
60
72
|
"tpv" = ["**/*.j2"]
|
|
61
73
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
from tpv.commands.dryrunner import TPVDryRunner
|
|
5
|
+
from tpv.commands.test import mock_galaxy
|
|
6
|
+
from tpv.core.resource_pool import InMemoryAllocationStore
|
|
7
|
+
from tpv.rules import gateway
|
|
8
|
+
|
|
9
|
+
FIXTURES = os.path.join(os.path.dirname(__file__), "fixtures")
|
|
10
|
+
POOL_CONFIG = os.path.join(FIXTURES, "mapping-resource-pool.yml")
|
|
11
|
+
DEAD_STORE_OVERLAY = os.path.join(FIXTURES, "mapping-resource-pool-dead-store.yml")
|
|
12
|
+
JOB_CONF = os.path.join(FIXTURES, "job_conf_dry_run.yml")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestMockJob(unittest.TestCase):
|
|
16
|
+
def test_jobs_get_distinct_default_ids(self):
|
|
17
|
+
# Pool admission keys the ledger by job id, so a mock job must always have one.
|
|
18
|
+
a, b = mock_galaxy.Job(), mock_galaxy.Job()
|
|
19
|
+
self.assertIsInstance(a.id, int)
|
|
20
|
+
self.assertNotEqual(a.id, b.id)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestDryRunResourcePools(unittest.TestCase):
|
|
24
|
+
def _run(self, tool, user, confs, explain=False):
|
|
25
|
+
runner = TPVDryRunner.from_params(job_conf=JOB_CONF, tool_id=tool, user_email=user, tpv_confs=confs)
|
|
26
|
+
return runner.run(explain=explain)
|
|
27
|
+
|
|
28
|
+
def test_dry_run_maps_a_pooled_config(self):
|
|
29
|
+
destination, _ = self._run("bigtool", "arthur@vortex.org", [POOL_CONFIG])
|
|
30
|
+
self.assertEqual(destination.id, "local")
|
|
31
|
+
|
|
32
|
+
def test_dry_run_never_touches_the_configured_store(self):
|
|
33
|
+
# The configured store is a Valkey nothing listens on. If the dry run consulted it, the
|
|
34
|
+
# job would be deferred fail-closed (or the run would error); an admin's dry run must
|
|
35
|
+
# neither depend on nor write to production accounting.
|
|
36
|
+
destination, _ = self._run("bigtool", "arthur@vortex.org", [POOL_CONFIG, DEAD_STORE_OVERLAY])
|
|
37
|
+
self.assertEqual(destination.id, "local")
|
|
38
|
+
store = gateway.ACTIVE_DESTINATION_MAPPERS["tpv_dispatcher"].resource_pools.store
|
|
39
|
+
self.assertIsInstance(store, InMemoryAllocationStore)
|
|
40
|
+
|
|
41
|
+
def test_dry_run_still_reports_pools_without_a_store(self):
|
|
42
|
+
# Swapping in an in-memory store must not hide the misconfiguration the mapper rejects.
|
|
43
|
+
no_store = os.path.join(FIXTURES, "mapping-resource-pool-no-store.yml")
|
|
44
|
+
with self.assertRaisesRegex(ValueError, "resource_pool_store"):
|
|
45
|
+
self._run("bigtool", "arthur@vortex.org", [no_store])
|
|
46
|
+
|
|
47
|
+
def test_explain_reports_which_pools_govern_the_job_and_why(self):
|
|
48
|
+
_, collector = self._run("bigtool", "arthur@vortex.org", [POOL_CONFIG], explain=True)
|
|
49
|
+
trace = collector.render()
|
|
50
|
+
self.assertIn("Resource Pools", trace)
|
|
51
|
+
# bigtool is 64 cores against a 32-core budget: governed by 'default', classified oversize.
|
|
52
|
+
self.assertIn("default", trace)
|
|
53
|
+
self.assertIn("oversize", trace.lower())
|
|
54
|
+
self.assertIn("32", trace)
|
|
55
|
+
|
|
56
|
+
def test_explain_reports_a_permanently_rejected_request(self):
|
|
57
|
+
# hugetool (200 cores) exceeds the oversize ceiling and can never be scheduled.
|
|
58
|
+
destination, collector = self._run("hugetool", "arthur@vortex.org", [POOL_CONFIG], explain=True)
|
|
59
|
+
self.assertIsNone(destination)
|
|
60
|
+
self.assertIn("can never be scheduled", collector.render())
|
|
@@ -11,6 +11,53 @@ from tpv.rules import gateway
|
|
|
11
11
|
|
|
12
12
|
class TestEntity(unittest.TestCase):
|
|
13
13
|
|
|
14
|
+
def test_rule_context_uses_local_copy_and_keeps_self_alias(self):
|
|
15
|
+
loader = TPVConfigLoader(
|
|
16
|
+
{
|
|
17
|
+
"tools": {
|
|
18
|
+
"test": {
|
|
19
|
+
"cores": 1,
|
|
20
|
+
"params": {},
|
|
21
|
+
"rules": [
|
|
22
|
+
{
|
|
23
|
+
"if": "self is entity and entity.cores == 1",
|
|
24
|
+
"execute": "entity.params['local_change'] = 'preserved'",
|
|
25
|
+
},
|
|
26
|
+
{"if": True, "cores": 4},
|
|
27
|
+
{
|
|
28
|
+
"if": "self is entity and entity.cores == 4",
|
|
29
|
+
"execute": "entity.mem = 8",
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
original = loader.config.tools["test"]
|
|
37
|
+
context = {"entity": original, "self": original}
|
|
38
|
+
evaluated = original.evaluate_rules(context)
|
|
39
|
+
self.assertEqual(evaluated.cores, 4)
|
|
40
|
+
self.assertEqual(evaluated.mem, 8)
|
|
41
|
+
self.assertEqual(evaluated.params, {"local_change": "preserved"})
|
|
42
|
+
self.assertEqual(original.cores, 1)
|
|
43
|
+
self.assertIsNone(original.mem)
|
|
44
|
+
self.assertEqual(original.params, {})
|
|
45
|
+
self.assertIs(context["entity"], evaluated)
|
|
46
|
+
self.assertIs(context["self"], evaluated)
|
|
47
|
+
self.assertIsNot(evaluated, original)
|
|
48
|
+
|
|
49
|
+
def test_rule_context_initialized_when_no_rules_match(self):
|
|
50
|
+
for rules in ([], [{"if": False, "cores": 4}]):
|
|
51
|
+
with self.subTest(rules=rules):
|
|
52
|
+
loader = TPVConfigLoader({"tools": {"test": {"cores": 1, "rules": rules}}})
|
|
53
|
+
original = loader.config.tools["test"]
|
|
54
|
+
context = {}
|
|
55
|
+
evaluated = original.evaluate_rules(context)
|
|
56
|
+
self.assertIs(context["entity"], evaluated)
|
|
57
|
+
self.assertIs(context["self"], evaluated)
|
|
58
|
+
self.assertIsNot(evaluated, original)
|
|
59
|
+
self.assertEqual(evaluated.cores, 1)
|
|
60
|
+
|
|
14
61
|
@staticmethod
|
|
15
62
|
def _map_to_destination(app, job, tool, user, referrer=None):
|
|
16
63
|
tpv_config = os.path.join(os.path.dirname(__file__), "fixtures/mapping-rule-argument-based.yml")
|
|
@@ -3,11 +3,14 @@ import unittest
|
|
|
3
3
|
from unittest.mock import MagicMock
|
|
4
4
|
|
|
5
5
|
import yaml
|
|
6
|
+
from galaxy.jobs import JobDestination
|
|
6
7
|
from galaxy.jobs.mapper import JobMappingException
|
|
7
8
|
|
|
8
9
|
from tpv.commands.dryrunner import TPVDryRunner
|
|
10
|
+
from tpv.commands.test import mock_galaxy
|
|
9
11
|
from tpv.core.entities import SchedulingTags
|
|
10
12
|
from tpv.core.explain import ExplainCollector, ExplainPhase
|
|
13
|
+
from tpv.rules import gateway
|
|
11
14
|
|
|
12
15
|
|
|
13
16
|
class TestExplainCollectorUnit(unittest.TestCase):
|
|
@@ -239,6 +242,20 @@ class TestDryRunExplain(unittest.TestCase):
|
|
|
239
242
|
self.assertIn("Destination Ranking", trace)
|
|
240
243
|
self.assertIn("Final Result", trace)
|
|
241
244
|
|
|
245
|
+
def test_dry_run_explain_unconfigured_tool_without_a_default(self):
|
|
246
|
+
"""A tool with no entity in a config that sets no default_inherits still maps, via the
|
|
247
|
+
entity TPV builds from the Galaxy tool itself -- and the trace names that entity, which is
|
|
248
|
+
how an admin learns the tool got injected defaults rather than configured ones."""
|
|
249
|
+
dry_runner = TPVDryRunner.from_params(
|
|
250
|
+
job_conf=self._fixture_path("job_conf_dry_run.yml"),
|
|
251
|
+
tool_id="a_tool_nobody_configured",
|
|
252
|
+
user_email="fairycake@vortex.org",
|
|
253
|
+
tpv_confs=[self._fixture_path("mapping-inheritance-no-default.yml")],
|
|
254
|
+
)
|
|
255
|
+
destination, collector = dry_runner.run(explain=True)
|
|
256
|
+
self.assertIsNotNone(destination)
|
|
257
|
+
self.assertIn("matched entity 'tool_provided_resources_a_tool_nobody_configured'", collector.render())
|
|
258
|
+
|
|
242
259
|
def test_dry_run_explain_shows_matched_entities(self):
|
|
243
260
|
"""The trace should show which tool and user entities matched."""
|
|
244
261
|
dry_runner = TPVDryRunner.from_params(
|
|
@@ -496,3 +513,61 @@ class TestDryRunExplain(unittest.TestCase):
|
|
|
496
513
|
tpv_confs=[self._fixture_path("mapping-basic.yml")],
|
|
497
514
|
)
|
|
498
515
|
self.assertIsNone(runner.tool)
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
class TestGatewayExplainOnFailure(unittest.TestCase):
|
|
519
|
+
"""Tests for the tpv_explain_on_failure gateway config option."""
|
|
520
|
+
|
|
521
|
+
@staticmethod
|
|
522
|
+
def _fixture_path(name):
|
|
523
|
+
return os.path.join(os.path.dirname(__file__), f"fixtures/{name}")
|
|
524
|
+
|
|
525
|
+
def _call_gateway(self, tool_id, referrer=None, explain_collector=None):
|
|
526
|
+
gateway.ACTIVE_DESTINATION_MAPPERS = {}
|
|
527
|
+
app = mock_galaxy.App(job_conf=self._fixture_path("job_conf.yml"))
|
|
528
|
+
tool = mock_galaxy.Tool(tool_id)
|
|
529
|
+
job = mock_galaxy.Job()
|
|
530
|
+
user = mock_galaxy.User("gargravarr", "fairycake@vortex.org")
|
|
531
|
+
return gateway.map_tool_to_destination(
|
|
532
|
+
app,
|
|
533
|
+
job,
|
|
534
|
+
tool,
|
|
535
|
+
user,
|
|
536
|
+
referrer=referrer,
|
|
537
|
+
tpv_config_files=[self._fixture_path("mapping-basic.yml")],
|
|
538
|
+
explain_collector=explain_collector,
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
def test_no_log_when_param_not_set(self):
|
|
542
|
+
"""No warning is logged on failure when tpv_explain_on_failure is not configured."""
|
|
543
|
+
with self.assertNoLogs("tpv.rules.gateway", level="WARNING"):
|
|
544
|
+
with self.assertRaises(JobMappingException):
|
|
545
|
+
self._call_gateway("unschedulable_tool")
|
|
546
|
+
|
|
547
|
+
def test_logs_trace_on_mapping_failure(self):
|
|
548
|
+
"""A WARNING containing the scheduling trace is logged when tpv_explain_on_failure=true and mapping fails."""
|
|
549
|
+
referrer = JobDestination(id="tpv_dispatcher", params={"tpv_explain_on_failure": True})
|
|
550
|
+
with self.assertLogs("tpv.rules.gateway", level="WARNING") as cm:
|
|
551
|
+
with self.assertRaises(JobMappingException):
|
|
552
|
+
self._call_gateway("unschedulable_tool", referrer=referrer)
|
|
553
|
+
self.assertEqual(len(cm.records), 1)
|
|
554
|
+
trace = cm.records[0].getMessage()
|
|
555
|
+
self.assertIn("TPV SCHEDULING DECISION TRACE", trace)
|
|
556
|
+
self.assertIn("REJECTED", trace)
|
|
557
|
+
self.assertIn("tag mismatch", trace)
|
|
558
|
+
self.assertIn("No destinations", trace)
|
|
559
|
+
|
|
560
|
+
def test_no_log_on_successful_mapping(self):
|
|
561
|
+
"""No warning is logged when tpv_explain_on_failure=true but mapping succeeds."""
|
|
562
|
+
referrer = JobDestination(id="tpv_dispatcher", params={"tpv_explain_on_failure": True})
|
|
563
|
+
with self.assertNoLogs("tpv.rules.gateway", level="WARNING"):
|
|
564
|
+
destination = self._call_gateway("bwa", referrer=referrer)
|
|
565
|
+
self.assertIsNotNone(destination)
|
|
566
|
+
|
|
567
|
+
def test_explicit_collector_not_logged_by_gateway(self):
|
|
568
|
+
"""When an explicit explain_collector is passed (dry-run path), the gateway does not log on failure."""
|
|
569
|
+
collector = ExplainCollector()
|
|
570
|
+
with self.assertNoLogs("tpv.rules.gateway", level="WARNING"):
|
|
571
|
+
with self.assertRaises(JobMappingException):
|
|
572
|
+
self._call_gateway("unschedulable_tool", explain_collector=collector)
|
|
573
|
+
self.assertTrue(len(collector.steps) > 0)
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"""Unit tests module for the helper functions"""
|
|
2
|
+
|
|
3
|
+
import unittest
|
|
4
|
+
from types import SimpleNamespace
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from tpv.commands.test import mock_galaxy
|
|
8
|
+
from tpv.core.helpers import (
|
|
9
|
+
concurrent_job_count_for_tool,
|
|
10
|
+
get_dataset_attributes,
|
|
11
|
+
get_input_dataset,
|
|
12
|
+
get_input_datasets,
|
|
13
|
+
get_input_size,
|
|
14
|
+
input_size,
|
|
15
|
+
job_args_match,
|
|
16
|
+
tool_version_eq,
|
|
17
|
+
tool_version_gte,
|
|
18
|
+
weighted_choice,
|
|
19
|
+
weighted_random_sampling,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TestHelpers(unittest.TestCase):
|
|
24
|
+
"""Tests for helper functions"""
|
|
25
|
+
|
|
26
|
+
def test_get_dataset_attributes(self):
|
|
27
|
+
"""Test that the function returns a dictionary with the correct attributes"""
|
|
28
|
+
job = mock_galaxy.Job()
|
|
29
|
+
dataset = mock_galaxy.Dataset("test.txt", file_size=7 * 1024**3, object_store_id="files1")
|
|
30
|
+
job.add_input_dataset(mock_galaxy.DatasetAssociation("test", dataset))
|
|
31
|
+
dataset_attributes = get_dataset_attributes(job.input_datasets)
|
|
32
|
+
expected_result = {dataset.id: {"object_store_id": "files1", "size": 7 * 1024**3}}
|
|
33
|
+
self.assertEqual(dataset_attributes, expected_result)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _job_with_multiple_data_param():
|
|
37
|
+
"""A job with a `multiple="true"` data param `inputs` holding two datasets.
|
|
38
|
+
|
|
39
|
+
Galaxy records the first dataset twice: once as `inputs` (the copy-metadata alias for
|
|
40
|
+
the first element) and once as `inputs1`.
|
|
41
|
+
"""
|
|
42
|
+
job = mock_galaxy.Job()
|
|
43
|
+
first = mock_galaxy.DatasetAssociation(
|
|
44
|
+
"first", mock_galaxy.Dataset("first.txt", file_size=3 * 1024**3), extension="txt"
|
|
45
|
+
)
|
|
46
|
+
second = mock_galaxy.DatasetAssociation(
|
|
47
|
+
"second", mock_galaxy.Dataset("second.txt", file_size=5 * 1024**3), extension="txt"
|
|
48
|
+
)
|
|
49
|
+
job.add_input_dataset(first, name="inputs")
|
|
50
|
+
job.add_input_dataset(first, name="inputs1")
|
|
51
|
+
job.add_input_dataset(second, name="inputs2")
|
|
52
|
+
return job
|
|
53
|
+
|
|
54
|
+
def test_get_input_datasets_deduplicates_multiple_data_param_alias(self):
|
|
55
|
+
"""The `name`/`name1` alias of a multiple data param must not yield the dataset twice"""
|
|
56
|
+
job = self._job_with_multiple_data_param()
|
|
57
|
+
datasets = get_input_datasets(job, "inputs")
|
|
58
|
+
self.assertEqual([dataset.name for dataset in datasets], ["first", "second"])
|
|
59
|
+
|
|
60
|
+
def test_get_input_datasets_matches_collection_element_names(self):
|
|
61
|
+
"""A collection param is recorded as `name1`..`nameN`, with no unsuffixed entry"""
|
|
62
|
+
job = mock_galaxy.Job()
|
|
63
|
+
for index in range(1, 4):
|
|
64
|
+
job.add_input_dataset(
|
|
65
|
+
mock_galaxy.DatasetAssociation(
|
|
66
|
+
f"element{index}", mock_galaxy.Dataset(f"element{index}.txt", file_size=1 * 1024**3)
|
|
67
|
+
),
|
|
68
|
+
name=f"inputs{index}",
|
|
69
|
+
)
|
|
70
|
+
datasets = get_input_datasets(job, "inputs")
|
|
71
|
+
self.assertEqual([dataset.name for dataset in datasets], ["element1", "element2", "element3"])
|
|
72
|
+
|
|
73
|
+
def test_get_input_datasets_ignores_other_params(self):
|
|
74
|
+
"""Only datasets recorded for the requested param are returned"""
|
|
75
|
+
job = self._job_with_multiple_data_param()
|
|
76
|
+
job.add_input_dataset(
|
|
77
|
+
mock_galaxy.DatasetAssociation("ref", mock_galaxy.Dataset("ref.txt", file_size=9 * 1024**3)),
|
|
78
|
+
name="reference",
|
|
79
|
+
)
|
|
80
|
+
self.assertEqual([dataset.name for dataset in get_input_datasets(job, "reference")], ["ref"])
|
|
81
|
+
self.assertEqual([dataset.name for dataset in get_input_datasets(job, "inputs")], ["first", "second"])
|
|
82
|
+
self.assertEqual([dataset.name for dataset in get_input_datasets(job)], ["first", "second", "ref"])
|
|
83
|
+
|
|
84
|
+
def test_get_input_datasets_skips_unset_optional_params(self):
|
|
85
|
+
"""An unset optional data param is recorded with no dataset at all"""
|
|
86
|
+
job = mock_galaxy.Job()
|
|
87
|
+
job.input_datasets.append(mock_galaxy.JobToInputDatasetAssociation("inputs", None))
|
|
88
|
+
self.assertEqual(get_input_datasets(job, "inputs"), [])
|
|
89
|
+
self.assertIsNone(get_input_dataset(job, "inputs"))
|
|
90
|
+
self.assertEqual(get_input_size(job, "inputs"), 0)
|
|
91
|
+
|
|
92
|
+
def test_get_input_dataset_returns_first_match(self):
|
|
93
|
+
job = self._job_with_multiple_data_param()
|
|
94
|
+
dataset = get_input_dataset(job, "inputs")
|
|
95
|
+
self.assertEqual(dataset.name, "first")
|
|
96
|
+
self.assertIsNone(get_input_dataset(job, "nonexistent"))
|
|
97
|
+
|
|
98
|
+
def test_get_input_size_totals_all_inputs_by_default(self):
|
|
99
|
+
job = self._job_with_multiple_data_param()
|
|
100
|
+
self.assertEqual(get_input_size(job), 8)
|
|
101
|
+
# the existing input_size helper deduplicates the same way
|
|
102
|
+
self.assertEqual(input_size(job), 8)
|
|
103
|
+
|
|
104
|
+
def test_get_input_size_skips_inputs_whose_dataset_is_gone(self):
|
|
105
|
+
# An input association can outlive its dataset (deleted or purged). It must contribute
|
|
106
|
+
# nothing rather than raise mid-scheduling.
|
|
107
|
+
job = self._job_with_multiple_data_param()
|
|
108
|
+
job.add_input_dataset(mock_galaxy.DatasetAssociation("gone", None))
|
|
109
|
+
self.assertEqual(get_input_size(job), 8)
|
|
110
|
+
|
|
111
|
+
def test_get_input_size_by_param_name(self):
|
|
112
|
+
job = self._job_with_multiple_data_param()
|
|
113
|
+
job.add_input_dataset(
|
|
114
|
+
mock_galaxy.DatasetAssociation("ref", mock_galaxy.Dataset("ref.txt", file_size=9 * 1024**3)),
|
|
115
|
+
name="reference",
|
|
116
|
+
)
|
|
117
|
+
self.assertEqual(get_input_size(job, "inputs"), 8)
|
|
118
|
+
self.assertEqual(get_input_size(job, "reference"), 9)
|
|
119
|
+
self.assertEqual(get_input_size(job), 17)
|
|
120
|
+
|
|
121
|
+
def test_get_input_size_adjusts_compressed_inputs(self):
|
|
122
|
+
job = mock_galaxy.Job()
|
|
123
|
+
job.add_input_dataset(
|
|
124
|
+
mock_galaxy.DatasetAssociation(
|
|
125
|
+
"compressed",
|
|
126
|
+
mock_galaxy.Dataset("compressed.fastq.gz", file_size=2 * 1024**3),
|
|
127
|
+
extension="fastqsanger.gz",
|
|
128
|
+
),
|
|
129
|
+
name="inputs1",
|
|
130
|
+
)
|
|
131
|
+
job.add_input_dataset(
|
|
132
|
+
mock_galaxy.DatasetAssociation(
|
|
133
|
+
"uncompressed",
|
|
134
|
+
mock_galaxy.Dataset("uncompressed.fastq", file_size=4 * 1024**3),
|
|
135
|
+
extension="fastqsanger",
|
|
136
|
+
),
|
|
137
|
+
name="inputs2",
|
|
138
|
+
)
|
|
139
|
+
self.assertAlmostEqual(get_input_size(job, "inputs"), 2 * 3.4 + 4)
|
|
140
|
+
self.assertAlmostEqual(get_input_size(job, "inputs", compression_factor=2), 2 * 2 + 4)
|
|
141
|
+
self.assertEqual(get_input_size(job, "inputs", estimate_uncompressed_size=False), 6)
|
|
142
|
+
|
|
143
|
+
def test_weighted_random_sampling_without_weights_uses_unweighted_sampling(self):
|
|
144
|
+
"""When no destination defines params.weight, use unweighted random sampling."""
|
|
145
|
+
destinations = [
|
|
146
|
+
SimpleNamespace(id="dest_a", params={}),
|
|
147
|
+
SimpleNamespace(id="dest_b", params=None),
|
|
148
|
+
SimpleNamespace(id="dest_c", params={"foo": "bar"}),
|
|
149
|
+
]
|
|
150
|
+
sampled_destinations = [destinations[2], destinations[0], destinations[1]]
|
|
151
|
+
|
|
152
|
+
with patch("tpv.core.helpers.random.sample", return_value=sampled_destinations) as sample_mock:
|
|
153
|
+
with patch("tpv.core.helpers.random.choices") as choices_mock:
|
|
154
|
+
result = weighted_random_sampling(destinations)
|
|
155
|
+
|
|
156
|
+
self.assertEqual(result, sampled_destinations)
|
|
157
|
+
sample_mock.assert_called_once_with(destinations, k=3)
|
|
158
|
+
choices_mock.assert_not_called()
|
|
159
|
+
|
|
160
|
+
def test_weighted_random_sampling_with_weights_uses_weighted_choices(self):
|
|
161
|
+
"""When any destination defines params.weight, use weighted random choices."""
|
|
162
|
+
destinations = [
|
|
163
|
+
SimpleNamespace(id="dest_a", params={"weight": 5}),
|
|
164
|
+
SimpleNamespace(id="dest_b", params={}),
|
|
165
|
+
SimpleNamespace(id="dest_c", params=None),
|
|
166
|
+
]
|
|
167
|
+
sampled_destinations = [destinations[0], destinations[0], destinations[2]]
|
|
168
|
+
|
|
169
|
+
with patch("tpv.core.helpers.random.choices", return_value=sampled_destinations) as choices_mock:
|
|
170
|
+
with patch("tpv.core.helpers.random.sample") as sample_mock:
|
|
171
|
+
result = weighted_random_sampling(destinations)
|
|
172
|
+
|
|
173
|
+
self.assertEqual(result, sampled_destinations)
|
|
174
|
+
choices_mock.assert_called_once_with(destinations, weights=[5, 1, 1], k=3)
|
|
175
|
+
sample_mock.assert_not_called()
|
|
176
|
+
|
|
177
|
+
def test_weighted_choice_without_weights_uses_unweighted_choice(self):
|
|
178
|
+
"""When no item defines weight, use unweighted random choice."""
|
|
179
|
+
items = [
|
|
180
|
+
{"value": "/fast/jobs"},
|
|
181
|
+
{"value": "/slow/jobs"},
|
|
182
|
+
{"value": "/backup/jobs", "foo": "bar"},
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
with patch("tpv.core.helpers.random.sample", return_value=[items[1]]) as sample_mock:
|
|
186
|
+
with patch("tpv.core.helpers.random.choices") as choices_mock:
|
|
187
|
+
result = weighted_choice(items)
|
|
188
|
+
|
|
189
|
+
self.assertEqual(result, items[1])
|
|
190
|
+
sample_mock.assert_called_once_with(items, k=1)
|
|
191
|
+
choices_mock.assert_not_called()
|
|
192
|
+
|
|
193
|
+
def test_weighted_choice_with_weights_uses_weighted_choices(self):
|
|
194
|
+
"""When any item defines weight, use weighted random choices."""
|
|
195
|
+
items = [
|
|
196
|
+
{"value": "/fast/jobs", "weight": 3},
|
|
197
|
+
{"value": "/slow/jobs"},
|
|
198
|
+
{"value": "/backup/jobs"},
|
|
199
|
+
]
|
|
200
|
+
with patch("tpv.core.helpers.random.choices", return_value=[items[0]]) as choices_mock:
|
|
201
|
+
with patch("tpv.core.helpers.random.sample") as sample_mock:
|
|
202
|
+
result = weighted_choice(items)
|
|
203
|
+
|
|
204
|
+
self.assertEqual(result, items[0])
|
|
205
|
+
choices_mock.assert_called_once_with(items, weights=[3, 1, 1], k=1)
|
|
206
|
+
sample_mock.assert_not_called()
|
|
207
|
+
|
|
208
|
+
def test_weighted_choice_missing_weight_defaults_to_one(self):
|
|
209
|
+
"""Items without a weight key should default to weight 1."""
|
|
210
|
+
items = [
|
|
211
|
+
{"value": "/a", "weight": 5},
|
|
212
|
+
{"value": "/b"},
|
|
213
|
+
{"value": "/c"},
|
|
214
|
+
]
|
|
215
|
+
with patch("tpv.core.helpers.random.choices", return_value=[items[1]]) as choices_mock:
|
|
216
|
+
result = weighted_choice(items)
|
|
217
|
+
|
|
218
|
+
self.assertEqual(result, items[1])
|
|
219
|
+
choices_mock.assert_called_once_with(items, weights=[5, 1, 1], k=1)
|
|
220
|
+
|
|
221
|
+
def test_weighted_choice_zero_weights_falls_back_to_unweighted(self):
|
|
222
|
+
"""If all weights are zero or negative, fall back to unweighted selection."""
|
|
223
|
+
items = [
|
|
224
|
+
{"value": "/drained/jobs", "weight": 0},
|
|
225
|
+
{"value": "/also-drained/jobs", "weight": -1},
|
|
226
|
+
]
|
|
227
|
+
with patch("tpv.core.helpers.random.sample", return_value=[items[0]]) as sample_mock:
|
|
228
|
+
with patch("tpv.core.helpers.random.choices") as choices_mock:
|
|
229
|
+
result = weighted_choice(items)
|
|
230
|
+
|
|
231
|
+
self.assertEqual(result, items[0])
|
|
232
|
+
sample_mock.assert_called_once_with(items, k=1)
|
|
233
|
+
choices_mock.assert_not_called()
|
|
234
|
+
|
|
235
|
+
def test_weighted_choice_negative_weight_is_clamped_to_zero(self):
|
|
236
|
+
"""A negative weight is clamped to 0 while positive weights are preserved."""
|
|
237
|
+
items = [
|
|
238
|
+
{"value": "/a", "weight": 5},
|
|
239
|
+
{"value": "/b", "weight": -3},
|
|
240
|
+
]
|
|
241
|
+
with patch("tpv.core.helpers.random.choices", return_value=[items[0]]) as choices_mock:
|
|
242
|
+
with patch("tpv.core.helpers.random.sample") as sample_mock:
|
|
243
|
+
weighted_choice(items)
|
|
244
|
+
|
|
245
|
+
choices_mock.assert_called_once_with(items, weights=[5, 0], k=1)
|
|
246
|
+
sample_mock.assert_not_called()
|
|
247
|
+
|
|
248
|
+
def test_weighted_choice_default_weight_keys_use_unweighted(self):
|
|
249
|
+
"""If every item has weight: 1 (the default), use unweighted choice."""
|
|
250
|
+
items = [
|
|
251
|
+
{"value": "/a", "weight": 1},
|
|
252
|
+
{"value": "/b", "weight": 1},
|
|
253
|
+
]
|
|
254
|
+
with patch("tpv.core.helpers.random.sample", return_value=[items[0]]) as sample_mock:
|
|
255
|
+
with patch("tpv.core.helpers.random.choices") as choices_mock:
|
|
256
|
+
weighted_choice(items)
|
|
257
|
+
|
|
258
|
+
sample_mock.assert_called_once_with(items, k=1)
|
|
259
|
+
choices_mock.assert_not_called()
|
|
260
|
+
|
|
261
|
+
def test_weighted_choice_empty_list_raises_value_error(self):
|
|
262
|
+
"""An empty list should raise ValueError."""
|
|
263
|
+
with self.assertRaises(ValueError):
|
|
264
|
+
weighted_choice([])
|
|
265
|
+
|
|
266
|
+
def test_weighted_choice_returns_selected_item(self):
|
|
267
|
+
"""The helper should return the selected item, not a derived string value."""
|
|
268
|
+
items = [
|
|
269
|
+
{"value": "/primary/jobs", "weight": 10},
|
|
270
|
+
{"value": "/secondary/jobs", "weight": 1},
|
|
271
|
+
]
|
|
272
|
+
with patch("tpv.core.helpers.random.choices", return_value=[items[1]]):
|
|
273
|
+
result = weighted_choice(items)
|
|
274
|
+
|
|
275
|
+
self.assertIsInstance(result, dict)
|
|
276
|
+
self.assertEqual(result, items[1])
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class TestJobArgsMatch(unittest.TestCase):
|
|
280
|
+
"""job_args_match decides whether a rule fires on a job's parameters. Its only test so far is
|
|
281
|
+
the positive case, which a helper that always returned True would pass."""
|
|
282
|
+
|
|
283
|
+
def setUp(self):
|
|
284
|
+
self.app = mock_galaxy.App(create_model=True)
|
|
285
|
+
self.job = mock_galaxy.Job()
|
|
286
|
+
self.job.param_values = {
|
|
287
|
+
"input_opts": {"db_selector": "db", "tabs_to_spaces": False},
|
|
288
|
+
"files": [{"name": "reads.fq"}],
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
def test_matches_only_when_every_given_value_matches(self):
|
|
292
|
+
self.assertTrue(job_args_match(self.job, self.app, {"input_opts": {"db_selector": "db"}}))
|
|
293
|
+
self.assertFalse(job_args_match(self.job, self.app, {"input_opts": {"db_selector": "other"}}))
|
|
294
|
+
# one matching and one differing value is not a match
|
|
295
|
+
self.assertFalse(
|
|
296
|
+
job_args_match(self.job, self.app, {"input_opts": {"db_selector": "db", "tabs_to_spaces": True}})
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
def test_a_parameter_the_job_does_not_have_does_not_match(self):
|
|
300
|
+
self.assertFalse(job_args_match(self.job, self.app, {"input_opts": {"no_such_option": 1}}))
|
|
301
|
+
self.assertFalse(job_args_match(self.job, self.app, {"no_such_section": {"x": 1}}))
|
|
302
|
+
# a nested dict in args where the job's parameter is a scalar is a mismatch, not an error
|
|
303
|
+
self.assertFalse(job_args_match(self.job, self.app, {"files": {"name": "reads.fq"}}))
|
|
304
|
+
|
|
305
|
+
def test_nothing_to_match_against_never_matches(self):
|
|
306
|
+
for args in (None, {}, "input_opts", ["input_opts"]):
|
|
307
|
+
with self.subTest(args=args):
|
|
308
|
+
self.assertFalse(job_args_match(self.job, self.app, args))
|
|
309
|
+
|
|
310
|
+
def test_a_list_valued_parameter_is_compared_as_a_whole(self):
|
|
311
|
+
# Repeats and multi-selects are lists. The list is the value; keys inside its items are
|
|
312
|
+
# not a path into it.
|
|
313
|
+
self.assertTrue(job_args_match(self.job, self.app, {"files": [{"name": "reads.fq"}]}))
|
|
314
|
+
self.assertFalse(job_args_match(self.job, self.app, {"files": [{"name": "other.fq"}]}))
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class TestConcurrentJobCount(unittest.TestCase):
|
|
318
|
+
def test_plain_tool_ids_match_exactly_and_do_not_count_prefix_collisions(self):
|
|
319
|
+
# Tool Shed ids are matched by prefix so every installed version is counted together.
|
|
320
|
+
# A plain id must not get that treatment: "bwa" must not count "bwa_mem" jobs.
|
|
321
|
+
app = mock_galaxy.App(create_model=True)
|
|
322
|
+
session = app.model.context
|
|
323
|
+
user = app.model.User(username="arthur", email="arthur@vortex.org", password="x")
|
|
324
|
+
session.add(user)
|
|
325
|
+
session.flush()
|
|
326
|
+
for tool_id in ("bwa", "bwa", "bwa_mem"):
|
|
327
|
+
job = app.model.Job()
|
|
328
|
+
job.user = user
|
|
329
|
+
job.tool_id = tool_id
|
|
330
|
+
job.state = "running"
|
|
331
|
+
session.add(job)
|
|
332
|
+
session.flush()
|
|
333
|
+
|
|
334
|
+
self.assertEqual(concurrent_job_count_for_tool(app, mock_galaxy.Tool("bwa")), 2)
|
|
335
|
+
self.assertEqual(concurrent_job_count_for_tool(app, mock_galaxy.Tool("bwa_mem")), 1)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
class TestToolVersionComparison(unittest.TestCase):
|
|
339
|
+
def test_versions_are_compared_as_versions_not_strings(self):
|
|
340
|
+
tool = mock_galaxy.Tool("t", version="1.2.0")
|
|
341
|
+
self.assertTrue(tool_version_eq(tool, "1.2")) # PEP 440: 1.2.0 == 1.2
|
|
342
|
+
self.assertTrue(tool_version_eq(tool, "1.2.0"))
|
|
343
|
+
self.assertFalse(tool_version_eq(tool, "1.3"))
|
|
344
|
+
self.assertTrue(tool_version_gte(tool, "1.10") is False) # not a string compare: "1.2" < "1.10"
|
|
345
|
+
|
|
346
|
+
def test_comparison_is_unknown_not_false_when_a_version_is_missing(self):
|
|
347
|
+
# A rule can tell "no version to compare" (None) from "compared and did not match" (False).
|
|
348
|
+
unversioned = mock_galaxy.Tool("t")
|
|
349
|
+
self.assertIsNone(tool_version_eq(unversioned, "1.0"))
|
|
350
|
+
self.assertIsNone(tool_version_gte(mock_galaxy.Tool("t", version="1.0"), None))
|
{total_perspective_vortex-3.2.1 → total_perspective_vortex-3.3.1}/tests/test_mapper_context.py
RENAMED
|
@@ -44,7 +44,7 @@ class TestMapperContext(unittest.TestCase):
|
|
|
44
44
|
self.assertEqual(destination.params["native_spec"], "--mem 15 --cores 5 --gpus 4")
|
|
45
45
|
|
|
46
46
|
def test_context_variable_overridden_in_rule(self):
|
|
47
|
-
# test that job will not fail with
|
|
47
|
+
# test that job will not fail with 40GiB input size because large_input_size has been set to 60
|
|
48
48
|
tool = mock_galaxy.Tool("bwa")
|
|
49
49
|
user = mock_galaxy.User("gargravarr", "fairycake@vortex.org")
|
|
50
50
|
datasets = [mock_galaxy.DatasetAssociation("test", mock_galaxy.Dataset("test.txt", file_size=40 * 1024**3))]
|