Coverage for mlos_bench/mlos_bench/tests/launcher_parse_args_test.py: 98%
80 statements
« prev ^ index » next coverage.py v7.5.1, created at 2024-05-06 00:35 +0000
« 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"""
6Unit tests to check the Launcher CLI arg parsing
7See Also: test_load_cli_config_examples.py
8"""
10import os
11import sys
12from getpass import getuser
13from typing import List
15import pytest
17from mlos_bench.launcher import Launcher
18from mlos_bench.optimizers import OneShotOptimizer, MlosCoreOptimizer
19from mlos_bench.os_environ import environ
20from mlos_bench.config.schemas import ConfigSchema
21from mlos_bench.util import path_join
22from mlos_bench.schedulers import SyncScheduler
23from mlos_bench.services.types import (
24 SupportsAuth,
25 SupportsConfigLoading,
26 SupportsFileShareOps,
27 SupportsLocalExec,
28 SupportsRemoteExec,
29)
30from mlos_bench.tests import check_class_name
32if sys.version_info < (3, 10):
33 from importlib_resources import files
34else:
35 from importlib.resources import files
37# pylint: disable=redefined-outer-name
40@pytest.fixture
41def config_paths() -> List[str]:
42 """
43 Returns a list of config paths.
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 ]
56def test_launcher_args_parse_1(config_paths: List[str]) -> None:
57 """
58 Test that using multiple --globals arguments works and that multiple space
59 separated options to --config-paths works.
60 Check $var expansion and Environment loading.
61 """
62 # The VSCode pytest wrapper actually starts in a different directory before
63 # changing into the code directory, but doesn't update the PWD environment
64 # variable so we use a separate variable.
65 # See global_test_config.jsonc for more details.
66 environ["CUSTOM_PATH_FROM_ENV"] = os.getcwd()
67 if sys.platform == 'win32':
68 # Some env tweaks for platform compatibility.
69 environ['USER'] = environ['USERNAME']
71 # This is part of the minimal required args by the Launcher.
72 env_conf_path = 'environments/mock/mock_env.jsonc'
73 cli_args = '--config-paths ' + ' '.join(config_paths) + \
74 ' --service services/remote/mock/mock_auth_service.jsonc' + \
75 ' --service services/remote/mock/mock_remote_exec_service.jsonc' + \
76 ' --scheduler schedulers/sync_scheduler.jsonc' + \
77 f' --environment {env_conf_path}' + \
78 ' --globals globals/global_test_config.jsonc' + \
79 ' --globals globals/global_test_extra_config.jsonc' \
80 ' --test_global_value_2 from-args'
81 launcher = Launcher(description=__name__, argv=cli_args.split())
82 # Check that the parent service
83 assert isinstance(launcher.service, SupportsAuth)
84 assert isinstance(launcher.service, SupportsConfigLoading)
85 assert isinstance(launcher.service, SupportsLocalExec)
86 assert isinstance(launcher.service, SupportsRemoteExec)
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 the second --globals file is loaded.
93 assert launcher.global_config['test_global_value'] == 'from-file'
94 # Check overriding values in a file from the command line.
95 assert launcher.global_config['test_global_value_2'] == 'from-args'
96 # Check that we can expand a $var in a config file that references an environment variable.
97 assert path_join(launcher.global_config["pathVarWithEnvVarRef"], abs_path=True) \
98 == path_join(os.getcwd(), "foo", abs_path=True)
99 assert launcher.global_config["varWithEnvVarRef"] == f'user:{getuser()}'
100 assert launcher.teardown
101 # Check that the environment that got loaded looks to be of the right type.
102 env_config = launcher.config_loader.load_config(env_conf_path, ConfigSchema.ENVIRONMENT)
103 assert check_class_name(launcher.environment, env_config['class'])
104 # Check that the optimizer looks right.
105 assert isinstance(launcher.optimizer, OneShotOptimizer)
106 # Check that the optimizer got initialized with defaults.
107 assert launcher.optimizer.tunable_params.is_defaults()
108 assert launcher.optimizer.max_iterations == 1 # value for OneShotOptimizer
109 # Check that we pick up the right scheduler config:
110 assert isinstance(launcher.scheduler, SyncScheduler)
111 assert launcher.scheduler._trial_config_repeat_count == 3 # pylint: disable=protected-access
112 assert launcher.scheduler._max_trials == -1 # pylint: disable=protected-access
115def test_launcher_args_parse_2(config_paths: List[str]) -> None:
116 """
117 Test multiple --config-path instances, --config file vs --arg, --var=val
118 overrides, $var templates, option args, --random-init, etc.
119 """
120 # The VSCode pytest wrapper actually starts in a different directory before
121 # changing into the code directory, but doesn't update the PWD environment
122 # variable so we use a separate variable.
123 # See global_test_config.jsonc for more details.
124 environ["CUSTOM_PATH_FROM_ENV"] = os.getcwd()
125 if sys.platform == 'win32':
126 # Some env tweaks for platform compatibility.
127 environ['USER'] = environ['USERNAME']
129 config_file = 'cli/test-cli-config.jsonc'
130 globals_file = 'globals/global_test_config.jsonc'
131 cli_args = ' '.join([f"--config-path {config_path}" for config_path in config_paths]) + \
132 f' --config {config_file}' + \
133 ' --service services/remote/mock/mock_auth_service.jsonc' + \
134 ' --service services/remote/mock/mock_remote_exec_service.jsonc' + \
135 f' --globals {globals_file}' + \
136 ' --experiment_id MockeryExperiment' + \
137 ' --no-teardown' + \
138 ' --random-init' + \
139 ' --random-seed 1234' + \
140 ' --trial-config-repeat-count 5' + \
141 ' --max_trials 200'
142 launcher = Launcher(description=__name__, argv=cli_args.split())
143 # Check that the parent service
144 assert isinstance(launcher.service, SupportsAuth)
145 assert isinstance(launcher.service, SupportsConfigLoading)
146 assert isinstance(launcher.service, SupportsFileShareOps)
147 assert isinstance(launcher.service, SupportsLocalExec)
148 assert isinstance(launcher.service, SupportsRemoteExec)
149 # Check that the --globals file is loaded and $var expansion is handled
150 # using the value provided on the CLI.
151 assert launcher.global_config['experiment_id'] == 'MockeryExperiment'
152 assert launcher.global_config['testVmName'] == 'MockeryExperiment-vm'
153 # Check that secondary expansion also works.
154 assert launcher.global_config['testVnetName'] == 'MockeryExperiment-vm-vnet'
155 # Check that we can expand a $var in a config file that references an environment variable.
156 assert path_join(launcher.global_config["pathVarWithEnvVarRef"], abs_path=True) \
157 == path_join(os.getcwd(), "foo", abs_path=True)
158 assert launcher.global_config["varWithEnvVarRef"] == f'user:{getuser()}'
159 assert not launcher.teardown
161 config = launcher.config_loader.load_config(config_file, ConfigSchema.CLI)
162 assert launcher.config_loader.config_paths == [path_join(path, abs_path=True) for path in config_paths + config['config_path']]
164 # Check that the environment that got loaded looks to be of the right type.
165 env_config_file = config['environment']
166 env_config = launcher.config_loader.load_config(env_config_file, ConfigSchema.ENVIRONMENT)
167 assert check_class_name(launcher.environment, env_config['class'])
169 # Check that the optimizer looks right.
170 assert isinstance(launcher.optimizer, MlosCoreOptimizer)
171 opt_config_file = config['optimizer']
172 opt_config = launcher.config_loader.load_config(opt_config_file, ConfigSchema.OPTIMIZER)
173 globals_file_config = launcher.config_loader.load_config(globals_file, ConfigSchema.GLOBALS)
174 # The actual global_config gets overwritten as a part of processing, so to test
175 # this we read the original value out of the source files.
176 orig_max_iters = globals_file_config.get('max_suggestions', opt_config.get('config', {}).get('max_suggestions', 100))
177 assert launcher.optimizer.max_iterations \
178 == orig_max_iters \
179 == launcher.global_config['max_suggestions']
181 # Check that the optimizer got initialized with random values instead of the defaults.
182 # Note: the environment doesn't get updated until suggest() is called to
183 # return these values in run.py.
184 assert not launcher.optimizer.tunable_params.is_defaults()
186 # TODO: Add a check that this flows through and replaces other seed config
187 # values through the stack.
188 # See Also: #495
190 # Check that CLI parameter overrides JSON config:
191 assert isinstance(launcher.scheduler, SyncScheduler)
192 assert launcher.scheduler._trial_config_repeat_count == 5 # pylint: disable=protected-access
193 assert launcher.scheduler._max_trials == 200 # pylint: disable=protected-access
195 # Check that the value from the file is overridden by the CLI arg.
196 assert config['random_seed'] == 42
197 # TODO: This isn't actually respected yet because the `--random-init` only
198 # applies to a temporary Optimizer used to populate the initial values via
199 # random sampling.
200 # assert launcher.optimizer.seed == 1234
203if __name__ == '__main__':
204 pytest.main([__file__, "-n1"])