Coverage for src/ensembl/utils/plugin.py: 95%
59 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-05 15:47 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-05 15:47 +0000
1# See the NOTICE file distributed with this work for additional information
2# regarding copyright ownership.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15"""Ensembl's pytest plugin with useful unit testing hooks and fixtures."""
17from __future__ import annotations
19from difflib import unified_diff
20import os
21from pathlib import Path
22import re
23from typing import Callable, Generator
25import pytest
26from pytest import Config, FixtureRequest, Parser
28from ensembl.utils import StrPath
29from ensembl.utils.database import UnitTestDB
32def pytest_addoption(parser: Parser) -> None:
33 """Registers argparse-style options for Ensembl's unit testing.
35 `Pytest initialisation hook
36 <https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_addoption>`_.
38 Args:
39 parser: Parser for command line arguments and ini-file values.
41 """
42 # Add the Ensembl unitary test parameters to pytest parser
43 group = parser.getgroup("Ensembl unit testing")
44 group.addoption(
45 "--server",
46 action="store",
47 metavar="URL",
48 dest="server",
49 required=False,
50 default=os.getenv("DB_HOST", "sqlite:///"),
51 help="Server URL where to create the test database(s)",
52 )
53 group.addoption(
54 "--keep-dbs",
55 action="store_true",
56 dest="keep_dbs",
57 required=False,
58 help="Do not remove the test databases (default: False)",
59 )
62def pytest_report_header(config: Config) -> str:
63 """Presents extra information in the report header.
65 Args:
66 config: Access to configuration values, pluginmanager and plugin hooks.
68 """
69 # Show server information, masking the password value
70 server = config.getoption("server")
71 server = re.sub(r"(//[^/]+:).*(@)", r"\1xxxxxx\2", server)
72 return f"server: {server}"
75@pytest.fixture(name="data_dir", scope="module")
76def local_data_dir(request: FixtureRequest) -> Path:
77 """Returns the path to the test data folder matching the test's name.
79 Args:
80 request: Fixture that provides information of the requesting test function.
82 """
83 return Path(request.module.__file__).with_suffix("")
86@pytest.fixture(name="assert_files")
87def fixture_assert_files() -> Callable[[StrPath, StrPath], None]:
88 """Returns a function that asserts if two text files are equal, or prints their differences."""
90 def _assert_files(result_path: StrPath, expected_path: StrPath) -> None:
91 """Asserts if two files are equal, or prints their differences.
93 Args:
94 result_path: Path to results (test-made) file.
95 expected_path: Path to expected file.
97 """
98 with open(result_path, "r") as result_fh:
99 results = result_fh.readlines()
100 with open(expected_path, "r") as expected_fh:
101 expected = expected_fh.readlines()
102 files_diff = list(
103 unified_diff(
104 results,
105 expected,
106 fromfile=f"Test-made file {Path(result_path).name}",
107 tofile=f"Expected file {Path(expected_path).name}",
108 )
109 )
110 assert_message = f"Test-made and expected files differ\n{' '.join(files_diff)}"
111 assert len(files_diff) == 0, assert_message
113 return _assert_files
116@pytest.fixture(name="db_factory", scope="module")
117def fixture_db_factory(request: FixtureRequest, data_dir: Path) -> Generator[Callable, None, None]:
118 """Yields a unit test database factory.
120 Args:
121 request: Fixture that provides information of the requesting test function.
122 data_dir: Fixture that provides the path to the test data folder matching the test's name.
124 """
125 created: dict[str, UnitTestDB] = {}
126 server_url = request.config.getoption("server")
128 def _db_factory(src: StrPath | None, name: str | None = None) -> UnitTestDB:
129 """Returns a unit test database.
131 Args:
132 src: Directory path where the test database schema and content files are located, if any.
133 name: Name to give to the new database. See `UnitTestDB` for more information.
135 """
136 if src is not None: 136 ↛ 143line 136 didn't jump to line 143 because the condition on line 136 was always true
137 src_path = Path(src)
138 if not src_path.is_absolute():
139 src_path = data_dir / src_path
140 db_key = name if name else src_path.name
141 dump_dir: Path | None = src_path if src_path.exists() else None
142 else:
143 db_key = name if name else "dbkey"
144 dump_dir = None
145 return created.setdefault(db_key, UnitTestDB(server_url, dump_dir=dump_dir, name=name))
147 yield _db_factory
148 # Drop all unit test databases unless the user has requested to keep them
149 if not request.config.getoption("keep_dbs"): 149 ↛ exitline 149 didn't return from function 'fixture_db_factory' because the condition on line 149 was always true
150 for test_db in created.values():
151 test_db.drop()
154@pytest.fixture(scope="module")
155def test_dbs(request: FixtureRequest, db_factory: Callable) -> dict[str, UnitTestDB]:
156 """Returns a dictionary of unit test databases with the database name as key.
158 Requires a list of dictionaries, each with keys `src` (mandatory) and `name` (optional), passed via
159 `request.param`. See `db_factory()` for details about each key's value. This fixture is a wrapper of
160 `db_factory()` intended to be used via indirect parametrization, for example::
162 @pytest.mark.parametrize(
163 "test_dbs", [[{"src": "master"}, {"src": "master", "name": "master2"}]], indirect=True
164 )
165 def test_method(..., test_dbs: dict[str, UnitTestDB], ...):
168 Args:
169 request: Fixture that provides information of the requesting test function.
170 db_factory: Fixture that provides a unit test database factory.
172 """
173 databases = {}
174 for argument in request.param:
175 src = Path(argument["src"])
176 name = argument.get("name", None)
177 key = name if name else src.name
178 databases[key] = db_factory(src=src, name=name)
179 return databases