Coverage for mlos_bench/mlos_bench/tests/conftest.py: 100%

44 statements  

« prev     ^ index     » next       coverage.py v7.5.1, created at 2024-05-06 00:35 +0000

1# 

2# Copyright (c) Microsoft Corporation. 

3# Licensed under the MIT License. 

4# 

5""" 

6Common fixtures for mock TunableGroups and Environment objects. 

7""" 

8 

9from typing import Any, Generator, List 

10 

11import os 

12 

13from fasteners import InterProcessLock, InterProcessReaderWriterLock 

14from pytest_docker.plugin import get_docker_services, Services as DockerServices 

15 

16import pytest 

17 

18from mlos_bench.environments.mock_env import MockEnv 

19from mlos_bench.tunables.tunable_groups import TunableGroups 

20 

21from mlos_bench.tests import SEED, tunable_groups_fixtures 

22 

23# pylint: disable=redefined-outer-name 

24# -- Ignore pylint complaints about pytest references to 

25# `tunable_groups` fixture as both a function and a parameter. 

26 

27# Expose some of those as local names so they can be picked up as fixtures by pytest. 

28tunable_groups_config = tunable_groups_fixtures.tunable_groups_config 

29tunable_groups = tunable_groups_fixtures.tunable_groups 

30mixed_numerics_tunable_groups = tunable_groups_fixtures.mixed_numerics_tunable_groups 

31covariant_group = tunable_groups_fixtures.covariant_group 

32 

33 

34@pytest.fixture 

35def mock_env(tunable_groups: TunableGroups) -> MockEnv: 

36 """ 

37 Test fixture for MockEnv. 

38 """ 

39 return MockEnv( 

40 name="Test Env", 

41 config={ 

42 "tunable_params": ["provision", "boot", "kernel"], 

43 "seed": SEED, 

44 "range": [60, 120], 

45 "metrics": ["score"], 

46 }, 

47 tunables=tunable_groups 

48 ) 

49 

50 

51@pytest.fixture 

52def mock_env_no_noise(tunable_groups: TunableGroups) -> MockEnv: 

53 """ 

54 Test fixture for MockEnv. 

55 """ 

56 return MockEnv( 

57 name="Test Env No Noise", 

58 config={ 

59 "tunable_params": ["provision", "boot", "kernel"], 

60 "range": [60, 120], 

61 "metrics": ["score", "other_score"], 

62 }, 

63 tunables=tunable_groups 

64 ) 

65 

66 

67# Fixtures to configure the pytest-docker plugin. 

68 

69 

70@pytest.fixture(scope="session") 

71def docker_compose_file(pytestconfig: pytest.Config) -> List[str]: 

72 """ 

73 Returns the path to the docker-compose file. 

74 

75 Parameters 

76 ---------- 

77 pytestconfig : pytest.Config 

78 

79 Returns 

80 ------- 

81 str 

82 Path to the docker-compose file. 

83 """ 

84 _ = pytestconfig # unused 

85 return [ 

86 os.path.join(os.path.dirname(__file__), "services", "remote", "ssh", "docker-compose.yml"), 

87 # Add additional configs as necessary here. 

88 ] 

89 

90 

91@pytest.fixture(scope="session") 

92def docker_compose_project_name(short_testrun_uid: str) -> str: 

93 """ 

94 Returns the name of the docker-compose project. 

95 

96 Returns 

97 ------- 

98 str 

99 Name of the docker-compose project. 

100 """ 

101 # Use the xdist testrun UID to ensure that the docker-compose project name 

102 # is unique across sessions, but shared amongst workers. 

103 return f"mlos_bench-test-{short_testrun_uid}" 

104 

105 

106@pytest.fixture(scope="session") 

107def docker_services_lock(shared_temp_dir: str, short_testrun_uid: str) -> InterProcessReaderWriterLock: 

108 """ 

109 Gets a pytest session lock for xdist workers to mark when they're using the 

110 docker services. 

111 

112 Yields 

113 ------ 

114 A lock to ensure that setup/teardown operations don't happen while a 

115 worker is using the docker services. 

116 """ 

117 return InterProcessReaderWriterLock(f"{shared_temp_dir}/pytest_docker_services-{short_testrun_uid}.lock") 

118 

119 

120@pytest.fixture(scope="session") 

121def docker_setup_teardown_lock(shared_temp_dir: str, short_testrun_uid: str) -> InterProcessLock: 

122 """ 

123 Gets a pytest session lock between xdist workers for the docker 

124 setup/teardown operations. 

125 

126 Yields 

127 ------ 

128 A lock to ensure that only one worker is doing setup/teardown at a time. 

129 """ 

130 return InterProcessLock(f"{shared_temp_dir}/pytest_docker_services-setup-teardown-{short_testrun_uid}.lock") 

131 

132 

133@pytest.fixture(scope="session") 

134def locked_docker_services( 

135 docker_compose_command: Any, 

136 docker_compose_file: Any, 

137 docker_compose_project_name: Any, 

138 docker_setup: Any, 

139 docker_cleanup: Any, 

140 docker_setup_teardown_lock: InterProcessLock, 

141 docker_services_lock: InterProcessReaderWriterLock, 

142) -> Generator[DockerServices, Any, None]: 

143 """ 

144 A locked version of the docker_services fixture to implement xdist single instance locking. 

145 """ 

146 # pylint: disable=too-many-arguments 

147 # Mark the services as in use with the reader lock. 

148 docker_services_lock.acquire_read_lock() 

149 # Acquire the setup lock to prevent multiple setup operations at once. 

150 docker_setup_teardown_lock.acquire() 

151 # This "with get_docker_services(...)"" pattern is in the default fixture. 

152 # We call it instead of docker_services() to avoid pytest complaints about 

153 # calling fixtures directly. 

154 with get_docker_services( 

155 docker_compose_command, 

156 docker_compose_file, 

157 docker_compose_project_name, 

158 docker_setup, 

159 docker_cleanup, 

160 ) as docker_services: 

161 # Release the setup/tear down lock in order to let the setup operation 

162 # continue for other workers (should be a no-op at this point). 

163 docker_setup_teardown_lock.release() 

164 # Yield the services so that tests within this worker can use them. 

165 yield docker_services 

166 # Now tests that use those services get to run on this worker... 

167 # Once the tests are done, release the read lock that marks the services as in use. 

168 docker_services_lock.release_read_lock() 

169 # Now as we prepare to execute the cleanup code on context exit we need 

170 # to acquire the setup/teardown lock again. 

171 # First we attempt to get the write lock so that we wait for other 

172 # readers to finish and guard against a lock inversion possibility. 

173 docker_services_lock.acquire_write_lock() 

174 # Next, acquire the setup/teardown lock 

175 # First one here is the one to do actual work, everyone else is basically a no-op. 

176 # Upon context exit, we should execute the docker_cleanup code. 

177 # And try to get the setup/tear down lock again. 

178 docker_setup_teardown_lock.acquire() 

179 # Finally, after the docker_cleanup code has finished, remove both locks. 

180 docker_setup_teardown_lock.release() 

181 docker_services_lock.release_write_lock()