From 995a8f621be82c32d97d8be9eb68126530ede6dc Mon Sep 17 00:00:00 2001 From: mvdbeek Date: Mon, 4 May 2026 16:26:24 +0200 Subject: [PATCH] Add integration test for tool source storage and LazyToolBox End-to-end test that boots Galaxy through the composite store + LazyToolBox path and verifies tools are discoverable via /api/tools, covering the wiring exercised by all of the preceding commits. --- test/integration/test_tool_source_storage.py | 113 +++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 test/integration/test_tool_source_storage.py diff --git a/test/integration/test_tool_source_storage.py b/test/integration/test_tool_source_storage.py new file mode 100644 index 00000000000..8a2623beb67 --- /dev/null +++ b/test/integration/test_tool_source_storage.py @@ -0,0 +1,113 @@ +"""Integration tests for tool source storage backends. + +These tests configure Galaxy to use different tool source storage backends +and verify that the toolbox boots and serves tools through the API. +Dataset uploads / job execution are exercised by Galaxy's general +integration suite — these tests focus only on the tool source storage +plumbing. +""" + +import os +import tempfile + +from galaxy_test.driver import integration_util + + +class BaseToolSourceStorageIntegrationTestCase(integration_util.IntegrationTestCase): + """Base class for tool source storage integration tests.""" + + framework_tool_and_types = True + + def _test_api_tools_list(self): + response = self._get("tools") + self._assert_status_code_is(response, 200) + tools = response.json() + assert len(tools) > 0, "Expected at least one tool to be loaded" + + def _test_api_tools_show(self, tool_id: str = "cat1"): + response = self._get(f"tools/{tool_id}") + self._assert_status_code_is(response, 200) + tool_info = response.json() + assert tool_info["id"] == tool_id + + +class TestDatabaseToolSourceStorage(BaseToolSourceStorageIntegrationTestCase): + """Integration tests with database tool source storage backend.""" + + @classmethod + def handle_galaxy_config_kwds(cls, config): + super().handle_galaxy_config_kwds(config) + config["tool_source_store"] = "database" + + def test_api_tools_list(self): + self._test_api_tools_list() + + def test_api_tools_show(self): + self._test_api_tools_show() + + def test_default_store_is_database_backend(self): + from galaxy.tool_source_store.database import DatabaseToolSourceStore + + assert isinstance(self._app.tool_source_store, DatabaseToolSourceStore) + + +class TestCompositeToolSourceStorage(BaseToolSourceStorageIntegrationTestCase): + """Galaxy boots with a default DB store + a per-conf read-only sqlite store. + + Verifies the composite wiring: a tool_conf carrying ``store="cvmfs_main"`` + plus ``use_lazy_toolbox: true`` causes ``build_tool_source_store`` to wrap + the default backend in a composite store. + """ + + _sqlite_path: str + _conf_path: str + _tmpdir: str + + @classmethod + def handle_galaxy_config_kwds(cls, config): + super().handle_galaxy_config_kwds(config) + cls._tmpdir = tempfile.mkdtemp(prefix="composite_tss_") + cls._sqlite_path = os.path.join(cls._tmpdir, "sources.sqlite") + + from galaxy.tool_source_store.sqlalchemy import SqlAlchemyToolSourceStore + + SqlAlchemyToolSourceStore(path=cls._sqlite_path).count() + + cls._conf_path = os.path.join(cls._tmpdir, "extra_tool_conf.xml") + with open(cls._conf_path, "w") as f: + f.write('\n\n') + + config["tool_source_store"] = "database" + config["use_lazy_toolbox"] = True + existing_confs = config.get("tool_config_file") or "config/tool_conf.xml.sample" + if isinstance(existing_confs, str): + config["tool_config_file"] = f"{existing_confs},{cls._conf_path}" + else: + config["tool_config_file"] = list(existing_confs) + [cls._conf_path] + config["tool_source_stores"] = { + "cvmfs_main": { + "backend": "sqlalchemy", + "path": cls._sqlite_path, + "read_only": True, + } + } + + def test_composite_store_is_wired(self): + # The boot path must produce a CompositeToolSourceStore when a + # tool_conf opts into a named per-conf store and use_lazy_toolbox + # is enabled. Verifying the live app's store directly is more + # robust than relying on /api/tools, which depends on whether the + # store was populated in advance. + from galaxy.tool_source_store.composite import CompositeToolSourceStore + + assert isinstance(self._app.tool_source_store, CompositeToolSourceStore) + + def test_api_tools_list_populated_via_bootstrap(self): + # With use_lazy_toolbox=true and an empty store, LazyToolBox + # auto-bootstraps from the configured tool confs on first boot; + # /api/tools must therefore return a non-empty tool list. + response = self._get("tools") + self._assert_status_code_is(response, 200) + tools = response.json() + assert isinstance(tools, list) + assert len(tools) > 0, "Bootstrap should have populated the store with framework tools"