diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py index e1f08a70..fdd5eb9e 100644 --- a/lean/components/docker/lean_runner.py +++ b/lean/components/docker/lean_runner.py @@ -223,14 +223,7 @@ def get_basic_docker_config(self, self._mount_common_directories(run_options, paths_to_mount, lean_config, data_dir, storage_dir, project_dir, output_dir) - # Update all hosts that need to point to the host's localhost to host.docker.internal so they resolve properly - # TODO: we should remove it or add to config json - for key in ["quant-bridge-server-host"]: - if key not in lean_config: - continue - - if lean_config[key] == "localhost" or lean_config[key] == "127.0.0.1": - lean_config[key] = "host.docker.internal" + lean_config = self._replace_localhost_with_docker_host(lean_config) # Set up modules set_up_common_csharp_options_called = self._setup_installed_packages(run_options, image, lean_config) @@ -285,14 +278,7 @@ def get_basic_docker_config_without_algo(self, self._mount_common_directories(run_options, paths_to_mount, lean_config, data_dir, storage_dir, None, None) - # Update all hosts that need to point to the host's localhost to host.docker.internal so they resolve properly - # TODO: we should remove it or add to config json - for key in ["quant-bridge-server-host"]: - if key not in lean_config: - continue - - if lean_config[key] == "localhost" or lean_config[key] == "127.0.0.1": - lean_config[key] = "host.docker.internal" + lean_config = self._replace_localhost_with_docker_host(lean_config) # Set up modules self._setup_installed_packages(run_options, image, lean_config, target_path) @@ -415,11 +401,15 @@ def _mount_common_directories(self, ("transaction-log", cli_root_dir), ("quant-bridge-symbol-map-file", cli_root_dir / DEFAULT_DATA_DIRECTORY_NAME / "symbol-properties") ] + top_level_and_environment = self._get_top_level_and_selected_environment(lean_config) for key, base_path in files_to_mount: - if key not in lean_config or lean_config[key] == "": + # The same key can be set at the top level and in the selected environment: + # mount it once, with the environment's value taking precedence + configs = [config for config in top_level_and_environment if key in config and config[key] != ""] + if not configs: continue - lean_config_entry = Path(lean_config[key]) + lean_config_entry = Path(configs[-1][key]) local_path = lean_config_entry if lean_config_entry.is_absolute() else base_path / lean_config_entry if not local_path.exists(): local_path.parent.mkdir(parents=True, exist_ok=True) @@ -430,7 +420,33 @@ def _mount_common_directories(self, type="bind", read_only=False)) - lean_config[key] = f"/Files/{key}" + self._logger.debug( + f"LeanRunner._mount_common_directories(): mounting '{local_path}' to '/Files/{key}', " + f"setting '{key}' to '/Files/{key}'") + for config in configs: + config[key] = f"/Files/{key}" + + def _replace_localhost_with_docker_host(self, lean_config: Dict[str, Any]) -> Dict[str, Any]: + """Returns the config with hosts that point to the local machine set to host.docker.internal.""" + # TODO: we should remove it or add to config json + for config in self._get_top_level_and_selected_environment(lean_config): + for key in ["quant-bridge-server-host"]: + if key in config and config[key] in ["localhost", "127.0.0.1"]: + self._logger.debug( + f"LeanRunner._replace_localhost_with_docker_host(): replacing '{key}' value '{config[key]}' " + f"with 'host.docker.internal'") + config[key] = "host.docker.internal" + return lean_config + + @staticmethod + def _get_top_level_and_selected_environment(lean_config: Dict[str, Any]) -> List[Dict[str, Any]]: + """Returns the top level of the Lean config plus the selected environment's entries, if any.""" + top_level_and_environment = [lean_config] + if "environment" in lean_config and "environments" in lean_config: + environment = lean_config["environments"].get(lean_config["environment"]) + if environment is not None: + top_level_and_environment.append(environment) + return top_level_and_environment def _initialize_run_options(self, detach: bool, docker_project_config: Dict[str, Any], debugging_method: Optional[DebuggingMethod]): diff --git a/tests/components/docker/test_lean_runner.py b/tests/components/docker/test_lean_runner.py index 26d009f8..b463bcf3 100644 --- a/tests/components/docker/test_lean_runner.py +++ b/tests/components/docker/test_lean_runner.py @@ -831,3 +831,75 @@ def test_lean_runner_parses_environment_values_containing_equals_from_extra_dock {"environment": ["JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8", "TOKEN=abc=="]}) assert run_options["environment"] == {"JAVA_TOOL_OPTIONS": "-Dfile.encoding=UTF-8", "TOKEN": "abc=="} + + +def _run_with_environment_config(lean_config: dict): + create_fake_lean_cli_directory() + + docker_manager = mock.Mock() + docker_manager.run_image.return_value = True + + lean_runner = create_lean_runner(docker_manager) + lean_runner.run_lean(lean_config, + "lean-cli", + Path.cwd() / "Python Project" / "main.py", + Path.cwd() / "output", + ENGINE_IMAGE, + None, + False, + False) + + docker_manager.run_image.assert_called_once() + return docker_manager.run_image.call_args + + +@pytest.mark.parametrize("host", ["localhost", "127.0.0.1"]) +def test_run_lean_rewrites_localhost_quant_bridge_host_in_selected_environment(host: str) -> None: + lean_config = { + "environment": "lean-cli", + "environments": {"lean-cli": {"quant-bridge-server-host": host}} + } + + _run_with_environment_config(lean_config) + + assert lean_config["environments"]["lean-cli"]["quant-bridge-server-host"] == "host.docker.internal" + + +def test_run_lean_mounts_quant_bridge_symbol_map_file_from_selected_environment() -> None: + lean_config = { + "environment": "lean-cli", + "environments": {"lean-cli": {"quant-bridge-symbol-map-file": "quant-bridge-symbol-map.json"}} + } + + args, kwargs = _run_with_environment_config(lean_config) + + from lean.container import container + cli_root_dir = container.lean_config_manager.get_cli_root_directory() + expected_source = cli_root_dir / DEFAULT_DATA_DIRECTORY_NAME / "symbol-properties" / "quant-bridge-symbol-map.json" + + assert any([ + Path(mount["Source"]) == expected_source and + mount["Target"] == "/Files/quant-bridge-symbol-map-file" + for mount in kwargs["mounts"] + ]) + assert lean_config["environments"]["lean-cli"]["quant-bridge-symbol-map-file"] == "/Files/quant-bridge-symbol-map-file" + + +def test_run_lean_mounts_quant_bridge_symbol_map_file_once_when_set_in_root_and_environment() -> None: + lean_config = { + "environment": "lean-cli", + "quant-bridge-symbol-map-file": "root-symbol-map.json", + "environments": {"lean-cli": {"quant-bridge-symbol-map-file": "env-symbol-map.json"}} + } + + args, kwargs = _run_with_environment_config(lean_config) + + from lean.container import container + cli_root_dir = container.lean_config_manager.get_cli_root_directory() + expected_source = cli_root_dir / DEFAULT_DATA_DIRECTORY_NAME / "symbol-properties" / "env-symbol-map.json" + + targets = [mount["Target"] for mount in kwargs["mounts"]] + assert targets.count("/Files/quant-bridge-symbol-map-file") == 1 + assert any(Path(mount["Source"]) == expected_source for mount in kwargs["mounts"]) + assert lean_config["quant-bridge-symbol-map-file"] == "/Files/quant-bridge-symbol-map-file" + assert lean_config["environments"]["lean-cli"]["quant-bridge-symbol-map-file"] == "/Files/quant-bridge-symbol-map-file"