Coverage for mlos_bench/mlos_bench/tests/launcher_parse_args_test.py: 99%

103 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-20 00:44 +0000

1# 

2# Copyright (c) Microsoft Corporation. 

3# Licensed under the MIT License. 

4# 

5""" 

6Unit tests to check the Launcher CLI arg parsing 

7See Also: test_load_cli_config_examples.py 

8""" 

9 

10import os 

11import sys 

12from getpass import getuser 

13from typing import List 

14 

15import pytest 

16 

17from mlos_bench.config.schemas import ConfigSchema 

18from mlos_bench.launcher import Launcher 

19from mlos_bench.optimizers import MlosCoreOptimizer, OneShotOptimizer 

20from mlos_bench.os_environ import environ 

21from mlos_bench.schedulers import SyncScheduler 

22from mlos_bench.services.types import ( 

23 SupportsAuth, 

24 SupportsConfigLoading, 

25 SupportsFileShareOps, 

26 SupportsLocalExec, 

27 SupportsRemoteExec, 

28) 

29from mlos_bench.tests import check_class_name 

30from mlos_bench.util import path_join 

31 

32if sys.version_info < (3, 10): 

33 from importlib_resources import files 

34else: 

35 from importlib.resources import files 

36 

37# pylint: disable=redefined-outer-name 

38 

39 

40@pytest.fixture 

41def config_paths() -> List[str]: 

42 """ 

43 Returns a list of config paths. 

44 

45 Returns 

46 ------- 

47 List[str] 

48 """ 

49 return [ 

50 path_join(os.getcwd(), abs_path=True), 

51 str(files("mlos_bench.config")), 

52 str(files("mlos_bench.tests.config")), 

53 ] 

54 

55 

56# This is part of the minimal required args by the Launcher. 

57ENV_CONF_PATH = "environments/mock/mock_env.jsonc" 

58 

59 

60def _get_launcher(desc: str, cli_args: str) -> Launcher: 

61 # The VSCode pytest wrapper actually starts in a different directory before 

62 # changing into the code directory, but doesn't update the PWD environment 

63 # variable so we use a separate variable. 

64 # See global_test_config.jsonc for more details. 

65 environ["CUSTOM_PATH_FROM_ENV"] = os.getcwd() 

66 if sys.platform == "win32": 

67 # Some env tweaks for platform compatibility. 

68 environ["USER"] = environ["USERNAME"] 

69 launcher = Launcher(description=desc, argv=cli_args.split()) 

70 # Check the basic parent service 

71 assert isinstance(launcher.service, SupportsConfigLoading) # built-in 

72 assert isinstance(launcher.service, SupportsLocalExec) # built-in 

73 return launcher 

74 

75 

76def test_launcher_args_parse_defaults(config_paths: List[str]) -> None: 

77 """Test that we get the defaults we expect when using minimal config arg 

78 examples. 

79 """ 

80 cli_args = ( 

81 "--config-paths " 

82 + " ".join(config_paths) 

83 + f" --environment {ENV_CONF_PATH}" 

84 + " --globals globals/global_test_config.jsonc" 

85 ) 

86 launcher = _get_launcher(__name__, cli_args) 

87 # Check that the first --globals file is loaded and $var expansion is handled. 

88 assert launcher.global_config["experiment_id"] == "MockExperiment" 

89 assert launcher.global_config["testVmName"] == "MockExperiment-vm" 

90 # Check that secondary expansion also works. 

91 assert launcher.global_config["testVnetName"] == "MockExperiment-vm-vnet" 

92 # Check that we can expand a $var in a config file that references an environment variable. 

93 assert path_join(launcher.global_config["pathVarWithEnvVarRef"], abs_path=True) == path_join( 

94 os.getcwd(), "foo", abs_path=True 

95 ) 

96 assert launcher.global_config["varWithEnvVarRef"] == f"user:{getuser()}" 

97 assert launcher.teardown # defaults 

98 # Check that the environment that got loaded looks to be of the right type. 

99 env_config = launcher.config_loader.load_config(ENV_CONF_PATH, ConfigSchema.ENVIRONMENT) 

100 assert env_config["class"] == "mlos_bench.environments.mock_env.MockEnv" 

101 assert check_class_name(launcher.environment, env_config["class"]) 

102 # Check that the optimizer looks right. 

103 assert isinstance(launcher.optimizer, OneShotOptimizer) 

104 # Check that the optimizer got initialized with defaults. 

105 assert launcher.optimizer.tunable_params.is_defaults() 

106 assert launcher.optimizer.max_suggestions == 1 # value for OneShotOptimizer 

107 # Check that we pick up the right scheduler config: 

108 assert isinstance(launcher.scheduler, SyncScheduler) 

109 assert launcher.scheduler.trial_config_repeat_count == 1 # default 

110 assert launcher.scheduler.max_trials == -1 # default 

111 

112 

113def test_launcher_args_parse_1(config_paths: List[str]) -> None: 

114 """ 

115 Test that using multiple --globals arguments works and that multiple space separated 

116 options to --config-paths works. 

117 

118 Check $var expansion and Environment loading. 

119 """ 

120 # Here we have multiple paths following --config-paths and --service. 

121 cli_args = ( 

122 "--config-paths " 

123 + " ".join(config_paths) 

124 + " --service services/remote/mock/mock_auth_service.jsonc" 

125 + " services/remote/mock/mock_remote_exec_service.jsonc" 

126 + " --scheduler schedulers/sync_scheduler.jsonc" 

127 + f" --environment {ENV_CONF_PATH}" 

128 + " --globals globals/global_test_config.jsonc" 

129 + " --globals globals/global_test_extra_config.jsonc" 

130 " --test_global_value_2 from-args" 

131 ) 

132 launcher = _get_launcher(__name__, cli_args) 

133 # Check some additional features of the the parent service 

134 assert isinstance(launcher.service, SupportsAuth) # from --service 

135 assert isinstance(launcher.service, SupportsRemoteExec) # from --service 

136 # Check that the first --globals file is loaded and $var expansion is handled. 

137 assert launcher.global_config["experiment_id"] == "MockExperiment" 

138 assert launcher.global_config["testVmName"] == "MockExperiment-vm" 

139 # Check that secondary expansion also works. 

140 assert launcher.global_config["testVnetName"] == "MockExperiment-vm-vnet" 

141 # Check that the second --globals file is loaded. 

142 assert launcher.global_config["test_global_value"] == "from-file" 

143 # Check overriding values in a file from the command line. 

144 assert launcher.global_config["test_global_value_2"] == "from-args" 

145 # Check that we can expand a $var in a config file that references an environment variable. 

146 assert path_join(launcher.global_config["pathVarWithEnvVarRef"], abs_path=True) == path_join( 

147 os.getcwd(), "foo", abs_path=True 

148 ) 

149 assert launcher.global_config["varWithEnvVarRef"] == f"user:{getuser()}" 

150 assert launcher.teardown 

151 # Check that the environment that got loaded looks to be of the right type. 

152 env_config = launcher.config_loader.load_config(ENV_CONF_PATH, ConfigSchema.ENVIRONMENT) 

153 assert env_config["class"] == "mlos_bench.environments.mock_env.MockEnv" 

154 # Check that the optimizer looks right. 

155 assert isinstance(launcher.optimizer, OneShotOptimizer) 

156 # Check that the optimizer got initialized with defaults. 

157 assert launcher.optimizer.tunable_params.is_defaults() 

158 assert launcher.optimizer.max_suggestions == 1 # value for OneShotOptimizer 

159 # Check that we pick up the right scheduler config: 

160 assert isinstance(launcher.scheduler, SyncScheduler) 

161 assert ( 

162 launcher.scheduler.trial_config_repeat_count == 3 

163 ) # from the custom sync_scheduler.jsonc config 

164 assert launcher.scheduler.max_trials == -1 

165 

166 

167def test_launcher_args_parse_2(config_paths: List[str]) -> None: 

168 """Test multiple --config-path instances, --config file vs --arg, --var=val 

169 overrides, $var templates, option args, --random-init, etc. 

170 """ 

171 config_file = "cli/test-cli-config.jsonc" 

172 globals_file = "globals/global_test_config.jsonc" 

173 # Here we have multiple --config-path and --service args, each with their own path. 

174 cli_args = ( 

175 " ".join([f"--config-path {config_path}" for config_path in config_paths]) 

176 + f" --config {config_file}" 

177 + " --service services/remote/mock/mock_auth_service.jsonc" 

178 + " --service services/remote/mock/mock_remote_exec_service.jsonc" 

179 + f" --globals {globals_file}" 

180 + " --experiment_id MockeryExperiment" 

181 + " --no-teardown" 

182 + " --random-init" 

183 + " --random-seed 1234" 

184 + " --trial-config-repeat-count 5" 

185 + " --max_trials 200" 

186 ) 

187 launcher = _get_launcher(__name__, cli_args) 

188 # Check some additional features of the the parent service 

189 assert isinstance(launcher.service, SupportsAuth) # from --service 

190 assert isinstance(launcher.service, SupportsFileShareOps) # from --config 

191 assert isinstance(launcher.service, SupportsRemoteExec) # from --service 

192 # Check that the --globals file is loaded and $var expansion is handled 

193 # using the value provided on the CLI. 

194 assert launcher.global_config["experiment_id"] == "MockeryExperiment" 

195 assert launcher.global_config["testVmName"] == "MockeryExperiment-vm" 

196 # Check that secondary expansion also works. 

197 assert launcher.global_config["testVnetName"] == "MockeryExperiment-vm-vnet" 

198 # Check that we can expand a $var in a config file that references an environment variable. 

199 assert path_join(launcher.global_config["pathVarWithEnvVarRef"], abs_path=True) == path_join( 

200 os.getcwd(), "foo", abs_path=True 

201 ) 

202 assert launcher.global_config["varWithEnvVarRef"] == f"user:{getuser()}" 

203 assert not launcher.teardown 

204 

205 config = launcher.config_loader.load_config(config_file, ConfigSchema.CLI) 

206 assert launcher.config_loader.config_paths == [ 

207 path_join(path, abs_path=True) for path in config_paths + config["config_path"] 

208 ] 

209 

210 # Check that the environment that got loaded looks to be of the right type. 

211 env_config_file = config["environment"] 

212 env_config = launcher.config_loader.load_config(env_config_file, ConfigSchema.ENVIRONMENT) 

213 assert check_class_name(launcher.environment, env_config["class"]) 

214 

215 # Check that the optimizer looks right. 

216 assert isinstance(launcher.optimizer, MlosCoreOptimizer) 

217 opt_config_file = config["optimizer"] 

218 opt_config = launcher.config_loader.load_config(opt_config_file, ConfigSchema.OPTIMIZER) 

219 globals_file_config = launcher.config_loader.load_config(globals_file, ConfigSchema.GLOBALS) 

220 # The actual global_config gets overwritten as a part of processing, so to test 

221 # this we read the original value out of the source files. 

222 orig_max_iters = globals_file_config.get( 

223 "max_suggestions", opt_config.get("config", {}).get("max_suggestions", 100) 

224 ) 

225 assert ( 

226 launcher.optimizer.max_suggestions 

227 == orig_max_iters 

228 == launcher.global_config["max_suggestions"] 

229 ) 

230 

231 # Check that the optimizer got initialized with random values instead of the defaults. 

232 # Note: the environment doesn't get updated until suggest() is called to 

233 # return these values in run.py. 

234 assert not launcher.optimizer.tunable_params.is_defaults() 

235 

236 # TODO: Add a check that this flows through and replaces other seed config 

237 # values through the stack. 

238 # See Also: #495 

239 

240 # Check that CLI parameter overrides JSON config: 

241 assert isinstance(launcher.scheduler, SyncScheduler) 

242 assert launcher.scheduler.trial_config_repeat_count == 5 # from cli args 

243 assert launcher.scheduler.max_trials == 200 

244 

245 # Check that the value from the file is overridden by the CLI arg. 

246 assert config["random_seed"] == 42 

247 # TODO: This isn't actually respected yet because the `--random-init` only 

248 # applies to a temporary Optimizer used to populate the initial values via 

249 # random sampling. 

250 # assert launcher.optimizer.seed == 1234 

251 

252 

253def test_launcher_args_parse_3(config_paths: List[str]) -> None: 

254 """Check that cli file values take precedence over other values.""" 

255 config_file = "cli/test-cli-config.jsonc" 

256 globals_file = "globals/global_test_config.jsonc" 

257 # Here we don't override values in test-cli-config with cli args but ensure that 

258 # those take precedence over other config files. 

259 cli_args = ( 

260 " ".join([f"--config-path {config_path}" for config_path in config_paths]) 

261 + f" --config {config_file}" 

262 + f" --globals {globals_file}" 

263 + " --max-suggestions 10" # check for - to _ conversion too 

264 ) 

265 launcher = _get_launcher(__name__, cli_args) 

266 

267 assert launcher.optimizer.max_suggestions == 10 # from CLI args 

268 

269 # Check that CLI file parameter overrides JSON config: 

270 assert isinstance(launcher.scheduler, SyncScheduler) 

271 # from test-cli-config.jsonc (should override scheduler config file) 

272 assert launcher.scheduler.trial_config_repeat_count == 2 

273 

274 

275if __name__ == "__main__": 

276 pytest.main([__file__, "-n0"])