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

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.""" 

16 

17from __future__ import annotations 

18 

19from difflib import unified_diff 

20import os 

21from pathlib import Path 

22import re 

23from typing import Callable, Generator 

24 

25import pytest 

26from pytest import Config, FixtureRequest, Parser 

27 

28from ensembl.utils import StrPath 

29from ensembl.utils.database import UnitTestDB 

30 

31 

32def pytest_addoption(parser: Parser) -> None: 

33 """Registers argparse-style options for Ensembl's unit testing. 

34 

35 `Pytest initialisation hook 

36 <https://docs.pytest.org/en/latest/reference.html#_pytest.hookspec.pytest_addoption>`_. 

37 

38 Args: 

39 parser: Parser for command line arguments and ini-file values. 

40 

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 ) 

60 

61 

62def pytest_report_header(config: Config) -> str: 

63 """Presents extra information in the report header. 

64 

65 Args: 

66 config: Access to configuration values, pluginmanager and plugin hooks. 

67 

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}" 

73 

74 

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. 

78 

79 Args: 

80 request: Fixture that provides information of the requesting test function. 

81 

82 """ 

83 return Path(request.module.__file__).with_suffix("") 

84 

85 

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.""" 

89 

90 def _assert_files(result_path: StrPath, expected_path: StrPath) -> None: 

91 """Asserts if two files are equal, or prints their differences. 

92 

93 Args: 

94 result_path: Path to results (test-made) file. 

95 expected_path: Path to expected file. 

96 

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 

112 

113 return _assert_files 

114 

115 

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. 

119 

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. 

123 

124 """ 

125 created: dict[str, UnitTestDB] = {} 

126 server_url = request.config.getoption("server") 

127 

128 def _db_factory(src: StrPath | None, name: str | None = None) -> UnitTestDB: 

129 """Returns a unit test database. 

130 

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. 

134 

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)) 

146 

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() 

152 

153 

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. 

157 

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:: 

161 

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], ...): 

166 

167 

168 Args: 

169 request: Fixture that provides information of the requesting test function. 

170 db_factory: Fixture that provides a unit test database factory. 

171 

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