diff --git a/README.md b/README.md index b0e921a3..2ee13602 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The following features are currently planned to be implemented (in order of prio - [x] Cloud synchronization - [x] Cloud backtesting - [ ] **First beta release** -- [ ] Local data downloading +- [x] Local data downloading - [ ] Local optimization - [ ] Local backtest visualization - [ ] Local live trading @@ -53,10 +53,11 @@ The Lean CLI supports multiple workflows. The examples below serve as a starting A locally-focused workflow (local development, local execution) with the CLI may look like this: 1. `cd` into the Lean CLI project. -2. Run `lean create-project "RSI Strategy"` to create a new project with some basic code to get you started. -3. Work on your strategy in `./RSI Strategy`. -4. Run `lean research "RSI Strategy"` to launch a Jupyter Lab session to work on research notebooks. -5. Run a backtest with `lean backtest "RSI Strategy"`. This runs your backtest in a Docker container containing the same packages as the ones used on QuantConnect.com, but with your own data. +2. Download some data with the [`lean toolbox`](#lean-toolbox) command. +3. Run `lean create-project "RSI Strategy"` to create a new project with some basic code to get you started. +4. Work on your strategy in `./RSI Strategy`. +5. Run `lean research "RSI Strategy"` to launch a Jupyter Lab session to work on research notebooks. +6. Run a backtest with `lean backtest "RSI Strategy"`. This runs your backtest in a Docker container containing the same packages as the ones used on QuantConnect.com, but with your own local data. A cloud-focused workflow (local development, cloud execution) with the CLI may look like this: 1. `cd` into the Lean CLI project. @@ -80,6 +81,7 @@ A cloud-focused workflow (local development, cloud execution) with the CLI may l - [`lean login`](#lean-login) - [`lean logout`](#lean-logout) - [`lean research`](#lean-research) +- [`lean toolbox`](#lean-toolbox) ### `lean backtest` @@ -98,9 +100,9 @@ Usage: lean backtest [OPTIONS] PROJECT Options: --output PATH Directory to store results in (defaults to PROJECT/backtests/TIMESTAMP) + --debug [pycharm|ptvsd|mono] Enable a certain debugging method (see --help for more information) --update Pull the selected LEAN engine version before running the backtest --version TEXT The LEAN engine version to run (defaults to the latest installed version) - --debug [pycharm|ptvsd|mono] Enable a certain debugging method (see --help for more information) --help Show this message and exit. -c, --config FILE The Lean configuration file that should be used (defaults to the nearest lean.json) --verbose Enable debug logging @@ -319,7 +321,7 @@ Usage: lean research [OPTIONS] PROJECT Run a Jupyter Lab environment locally using Docker. Options: - --port INTEGER The port to run Jupyter Lab on [default: 8888] + --port INTEGER The port to run Jupyter Lab on (defaults to 8888) --update Pull the selected research environment version before starting it --version TEXT The version of the research environment version to run (defaults to the latest installed version) --help Show this message and exit. @@ -328,6 +330,36 @@ Options: ``` _See code: [lean/commands/research.py](lean/commands/research.py)_ + +### `lean toolbox` + +Download, convert or generate data using one of the tools in Lean's ToolBox using Docker. + +``` +Usage: lean toolbox [OPTIONS] + + Download, convert or generate data using one of the tools in Lean's ToolBox using Docker. + + All options are passed to the toolbox. + Go to the following url to see the available apps and their supported options: + https://github.com/QuantConnect/Lean/blob/master/ToolBox/README.md + + If a --source-dir or --source-meta-dir option is given, its value will be mounted as a volume in the Docker container. + The --destination-dir option should be omitted, it'll automatically be set by the CLI. + + Example usage: + $ lean toolbox --app=YahooDownloader --tickers=SPY,AAPL --resolution=Daily --from-date=19980102-00:00:00 --to-date=20210107-00:00:00 + +Options: + --toolbox-help Pass the --help flag to the ToolBox + --update Pull the selected LEAN engine version before running the ToolBox + --version TEXT The LEAN engine version to run the ToolBox in (defaults to the latest installed version) + --help Show this message and exit. + -c, --config FILE The Lean configuration file that should be used (defaults to the nearest lean.json) + --verbose Enable debug logging +``` + +_See code: [lean/commands/toolbox.py](lean/commands/toolbox.py)_ ## Local debugging diff --git a/lean/commands/__init__.py b/lean/commands/__init__.py index aa70cc75..1cb348f6 100644 --- a/lean/commands/__init__.py +++ b/lean/commands/__init__.py @@ -22,6 +22,7 @@ from lean.commands.login import login from lean.commands.logout import logout from lean.commands.research import research +from lean.commands.toolbox import toolbox @click.group() @@ -33,11 +34,13 @@ def lean() -> None: pass +lean.add_command(config) +lean.add_command(cloud) + lean.add_command(login) lean.add_command(logout) -lean.add_command(config) lean.add_command(init) lean.add_command(create_project) lean.add_command(backtest) lean.add_command(research) -lean.add_command(cloud) +lean.add_command(toolbox) diff --git a/lean/commands/backtest.py b/lean/commands/backtest.py index d118998a..0acd7b0f 100644 --- a/lean/commands/backtest.py +++ b/lean/commands/backtest.py @@ -28,15 +28,18 @@ @click.option("--output", type=PathParameter(exists=False), help="Directory to store results in (defaults to PROJECT/backtests/TIMESTAMP)") -@click.option("--update", is_flag=True, help="Pull the selected LEAN engine version before running the backtest") +@click.option("--debug", + type=click.Choice(["pycharm", "ptvsd", "mono"], case_sensitive=False), + help="Enable a certain debugging method (see --help for more information)") +@click.option("--update", + is_flag=True, + default=False, + help="Pull the selected LEAN engine version before running the backtest") @click.option("--version", type=str, default="latest", help="The LEAN engine version to run (defaults to the latest installed version)") -@click.option("--debug", - type=click.Choice(["pycharm", "ptvsd", "mono"], case_sensitive=False), - help="Enable a certain debugging method (see --help for more information)") -def backtest(project: Path, output: Optional[Path], update: bool, version: str, debug: Optional[str]) -> None: +def backtest(project: Path, output: Optional[Path], debug: Optional[str], update: bool, version: str) -> None: """Backtest a project locally using Docker. \b diff --git a/lean/commands/research.py b/lean/commands/research.py index a17404c6..c10b981c 100644 --- a/lean/commands/research.py +++ b/lean/commands/research.py @@ -23,8 +23,11 @@ @click.command(cls=LeanCommand, requires_cli_project=True) @click.argument("project", type=PathParameter(exists=True, file_okay=False, dir_okay=True)) -@click.option("--port", type=int, default=8888, show_default=True, help="The port to run Jupyter Lab on") -@click.option("--update", is_flag=True, help="Pull the selected research environment version before starting it") +@click.option("--port", type=int, default=8888, help="The port to run Jupyter Lab on (defaults to 8888)") +@click.option("--update", + is_flag=True, + default=False, + help="Pull the selected research environment version before starting it") @click.option("--version", type=str, default="latest", @@ -71,4 +74,4 @@ def research(project: Path, port: int, update: bool, version: str) -> None: if update: docker_manager.pull_image(RESEARCH_IMAGE, version) - docker_manager.run_image(RESEARCH_IMAGE, version, None, False, **run_options) + docker_manager.run_image(RESEARCH_IMAGE, version, **run_options) diff --git a/lean/commands/toolbox.py b/lean/commands/toolbox.py new file mode 100644 index 00000000..836332f4 --- /dev/null +++ b/lean/commands/toolbox.py @@ -0,0 +1,110 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import itertools +from pathlib import Path + +import click + +from lean.click import LeanCommand +from lean.constants import ENGINE_IMAGE +from lean.container import container + + +@click.command(cls=LeanCommand, + requires_cli_project=True, + context_settings={"ignore_unknown_options": True, "allow_extra_args": True}) +@click.option("--toolbox-help", is_flag=True, default=False, help="Pass the --help flag to the ToolBox") +@click.option("--update", + is_flag=True, + default=False, + help="Pull the selected LEAN engine version before running the ToolBox") +@click.option("--version", + type=str, + default="latest", + help="The LEAN engine version to run the ToolBox in (defaults to the latest installed version)") +@click.pass_context +def toolbox(context: click.Context, toolbox_help: bool, update: bool, version: str) -> None: + """Download, convert or generate data using one of the tools in Lean's ToolBox using Docker. + + \b + All options are passed to the toolbox. + Go to the following url to see the available apps and their supported options: + https://github.com/QuantConnect/Lean/blob/master/ToolBox/README.md + + \b + If a --source-dir or --source-meta-dir option is given, its value will be mounted as a volume in the Docker container. + The --destination-dir option should be omitted, it'll automatically be set by the CLI. + + \b + Example usage: + $ lean toolbox --app=YahooDownloader --tickers=SPY,AAPL --resolution=Daily --from-date=19980102-00:00:00 --to-date=20210107-00:00:00 + """ + args = list(itertools.chain(*[arg.split("=") for arg in context.args])) + if len(args) % 2 != 0: + raise RuntimeError("Invalid options given") + extra_options = {args[i]: args[i + 1] for i in range(0, len(args), 2)} + + lean_config_manager = container.lean_config_manager() + data_dir = lean_config_manager.get_data_directory() + lean_config_path = lean_config_manager.get_lean_config_path() + + run_options = { + "entrypoint": ["mono", "QuantConnect.ToolBox.exe", "--destination-dir", "/Lean/Data"], + "volumes": { + str(data_dir): { + "bind": "/Lean/Data", + "mode": "rw" + } + } + } + + if toolbox_help: + run_options["entrypoint"].append("--help") + + for key, value in extra_options.items(): + # --destination-dir is automatically set by the CLI based on the data folder in the Lean config file + if key == "--destination-dir": + raise RuntimeError( + f"Please configure the 'data-folder' in '{lean_config_path}' instead of setting --destination-dir") + + # --source-dir and --source-meta-dir specify directories which should be mounted into the container + if key == "--source-dir" or key == "--source-meta-dir": + path = Path(value).expanduser().resolve() + + if not path.is_dir(): + raise RuntimeError(f"The given value for '{key}' is not an existing directory") + + run_options["volumes"][str(path)] = { + "bind": f"/Lean/Launcher/bin/Debug/{key.lstrip('--')}", + "mode": "ro" + } + + run_options["entrypoint"].extend([key, key.lstrip("--")]) + continue + + run_options["entrypoint"].extend([key, value]) + + docker_manager = container.docker_manager() + + if version != "latest": + if not docker_manager.tag_exists(ENGINE_IMAGE, version): + raise RuntimeError("The specified version does not exist") + + if update: + docker_manager.pull_image(ENGINE_IMAGE, version) + + success = docker_manager.run_image(ENGINE_IMAGE, version, **run_options) + + if not success: + raise RuntimeError("Something went wrong while running the ToolBox") diff --git a/lean/components/config/lean_config_manager.py b/lean/components/config/lean_config_manager.py index d050be3c..6298168f 100644 --- a/lean/components/config/lean_config_manager.py +++ b/lean/components/config/lean_config_manager.py @@ -81,7 +81,7 @@ def get_data_directory(self) -> Path: :return: the path to the data directory as it is configured in the Lean config """ - config = self._read_lean_config() + config = self.get_lean_config() return self.get_cli_root_directory() / config["data-folder"] def clean_lean_config(self, config: str) -> str: @@ -139,7 +139,7 @@ def get_complete_lean_config(self, :param algorithm_file: the path to the algorithm that will be ran :param debugging_method: the debugging method to use, or None to disable debugging """ - config = self._read_lean_config() + config = self.get_lean_config() config["environment"] = environment config["close-automatically"] = True @@ -169,7 +169,7 @@ def get_complete_lean_config(self, return config - def _read_lean_config(self) -> Dict[str, Any]: + def get_lean_config(self) -> Dict[str, Any]: """Reads the Lean config into a dict. :return: a dict containing the contents of the Lean config file diff --git a/lean/components/docker/docker_manager.py b/lean/components/docker/docker_manager.py index 866f13b9..3d6af92a 100644 --- a/lean/components/docker/docker_manager.py +++ b/lean/components/docker/docker_manager.py @@ -17,7 +17,6 @@ import sys import threading import types -from typing import Optional import docker import requests @@ -48,7 +47,7 @@ def pull_image(self, image: str, tag: str) -> None: # Since the pull command is the same on Windows, Linux and macOS we can safely use a system call os.system(f"docker image pull {image}:{tag}") - def run_image(self, image: str, tag: str, command: Optional[str], quiet: bool = False, **kwargs) -> bool: + def run_image(self, image: str, tag: str, **kwargs) -> bool: """Runs a Docker image. If the image is not available yet it will be pulled first. See https://docker-py.readthedocs.io/en/stable/containers.html for all the supported kwargs. @@ -58,8 +57,6 @@ def run_image(self, image: str, tag: str, command: Optional[str], quiet: bool = :param image: the name of the image to run :param tag: the image's tag to run - :param command: the command to run - :param quiet: whether the logs of the image should be printed to stdout :param kwargs: the kwargs to forward to docker.containers.run :return: True if the command in the container exited successfully, False if not """ @@ -71,7 +68,7 @@ def run_image(self, image: str, tag: str, command: Optional[str], quiet: bool = docker_client = self._get_docker_client() kwargs["detach"] = True - container = docker_client.containers.run(f"{image}:{tag}", command, **kwargs) + container = docker_client.containers.run(f"{image}:{tag}", None, **kwargs) # Kill the container on Ctrl+C def signal_handler(sig: signal.Signals, frame: types.FrameType) -> None: @@ -96,8 +93,7 @@ def print_logs() -> None: on_run() on_run_called = True - if not quiet: - self._logger.info(line.decode("utf-8").strip()) + self._logger.info(line.decode("utf-8").strip()) thread = threading.Thread(target=print_logs) thread.daemon = True diff --git a/lean/components/docker/lean_runner.py b/lean/components/docker/lean_runner.py index 54879837..da06f688 100644 --- a/lean/components/docker/lean_runner.py +++ b/lean/components/docker/lean_runner.py @@ -20,8 +20,8 @@ from docker.types import Mount from lean.components.config.lean_config_manager import LeanConfigManager -from lean.components.docker.docker_manager import DockerManager from lean.components.docker.csharp_compiler import CSharpCompiler +from lean.components.docker.docker_manager import DockerManager from lean.components.util.logger import Logger from lean.constants import ENGINE_IMAGE from lean.models.config import DebuggingMethod @@ -128,7 +128,10 @@ def run_lean(self, source=str(csharp_dll_dir / f"LeanCLI.{extension}"), type="bind")) - command = "--data-folder /Data --results-destination-folder /Results --config /Lean/Launcher/config.json" + run_options["entrypoint"] = ["mono", "QuantConnect.Lean.Launcher.exe", + "--data-folder", "/Data", + "--results-destination-folder", "/Results", + "--config", "/Lean/Launcher/config.json"] # Set up PTVSD debugging if debugging_method == DebuggingMethod.PTVSD: @@ -137,19 +140,16 @@ def run_lean(self, # Set up Mono debugging if debugging_method == DebuggingMethod.Mono: run_options["ports"]["55555"] = "55555" - run_options["entrypoint"] = "mono" - - command = " ".join([ - "--debug", - "--debugger-agent=transport=dt_socket,server=y,address=0.0.0.0:55555,suspend=y", - "QuantConnect.Lean.Launcher.exe", - command - ]) + run_options["entrypoint"] = ["mono", + "--debug", + "--debugger-agent=transport=dt_socket,server=y,address=0.0.0.0:55555,suspend=y", + "QuantConnect.Lean.Launcher.exe", + *run_options["entrypoint"][2:]] self._logger.info("Docker container starting, attach to Mono debugger at localhost:55555 to begin") # Run the engine and log the result - success = self._docker_manager.run_image(ENGINE_IMAGE, version, command, quiet=False, **run_options) + success = self._docker_manager.run_image(ENGINE_IMAGE, version, **run_options) cli_root_dir = self._lean_config_manager.get_cli_root_directory() relative_project_dir = project_dir.relative_to(cli_root_dir) diff --git a/tests/commands/test_research.py b/tests/commands/test_research.py index 0d9c901d..b734beee 100644 --- a/tests/commands/test_research.py +++ b/tests/commands/test_research.py @@ -161,7 +161,7 @@ def test_research_opens_browser_when_container_started(open) -> None: open.assert_called_once_with("http://localhost:8888/") -def test_backtest_forces_update_when_update_option_given() -> None: +def test_research_forces_update_when_update_option_given() -> None: create_fake_lean_cli_project() docker_manager = mock.Mock() diff --git a/tests/commands/test_toolbox.py b/tests/commands/test_toolbox.py new file mode 100644 index 00000000..8b8c297f --- /dev/null +++ b/tests/commands/test_toolbox.py @@ -0,0 +1,222 @@ +# QUANTCONNECT.COM - Democratizing Finance, Empowering Individuals. +# Lean CLI v1.0. Copyright 2021 QuantConnect Corporation. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pathlib import Path +from unittest import mock + +import pytest +from click.testing import CliRunner +from dependency_injector import providers + +from lean.commands import lean +from lean.container import container +from tests.test_helpers import create_fake_lean_cli_project + + +def test_toolbox_runs_engine_container() -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox"]) + + assert result.exit_code == 0 + + docker_manager.run_image.assert_called_once() + args, kwargs = docker_manager.run_image.call_args + + assert args[0] == "quantconnect/lean" + assert args[1] == "latest" + + +def test_toolbox_adds_destination_dir_pointing_to_data_directory_to_entrypoint() -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox"]) + + assert result.exit_code == 0 + + docker_manager.run_image.assert_called_once() + args, kwargs = docker_manager.run_image.call_args + + assert "--destination-dir" in kwargs["entrypoint"] + assert str(Path.cwd() / "data") in kwargs["volumes"] + + destination_dir = kwargs["entrypoint"][kwargs["entrypoint"].index("--destination-dir") + 1] + assert kwargs["volumes"][str(Path.cwd() / "data")]["bind"] == destination_dir + + +def test_toolbox_adds_help_to_entrypoint_when_toolbox_help_option_given() -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox", "--toolbox-help"]) + + assert result.exit_code == 0 + + docker_manager.run_image.assert_called_once() + args, kwargs = docker_manager.run_image.call_args + + assert "--help" in kwargs["entrypoint"] + + +def test_toolbox_adds_extra_options_to_entrypoint() -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox", "--option1=value1", "--option2", "value2"]) + + assert result.exit_code == 0 + + docker_manager.run_image.assert_called_once() + args, kwargs = docker_manager.run_image.call_args + + entrypoint = " ".join(kwargs["entrypoint"]) + assert "--option1 value1" in entrypoint + assert "--option2 value2" in entrypoint + + +def test_toolbox_aborts_when_destination_dir_given() -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox", "--destination-dir", "data"]) + + assert result.exit_code != 0 + + docker_manager.run_image.assert_not_called() + + +@pytest.mark.parametrize("option", ["--source-dir", "--source-meta-dir"]) +def test_toolbox_mounts_directory_as_volume_when_directory_option_given(option) -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + path = Path.cwd() / "data-directory" + path.mkdir() + + result = CliRunner().invoke(lean, ["toolbox", option, "data-directory"]) + + assert result.exit_code == 0 + + docker_manager.run_image.assert_called_once() + args, kwargs = docker_manager.run_image.call_args + + assert option in kwargs["entrypoint"] + + assert str(path) in kwargs["volumes"] + assert kwargs["volumes"][str(path)]["bind"].endswith(option.lstrip("--")) + + +@pytest.mark.parametrize("option", ["--source-dir", "--source-meta-dir"]) +def test_toolbox_aborts_when_directory_option_given_with_non_existent_directory(option) -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox", option, "fake-directory"]) + + assert result.exit_code != 0 + + docker_manager.run_image.assert_not_called() + + +@pytest.mark.parametrize("option", ["--source-dir", "--source-meta-dir"]) +def test_toolbox_aborts_when_directory_option_given_with_file(option) -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + (Path.cwd() / "file.txt").touch() + + result = CliRunner().invoke(lean, ["toolbox", option, "file.txt"]) + + assert result.exit_code != 0 + + docker_manager.run_image.assert_not_called() + + +def test_toolbox_forces_update_when_update_option_given() -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox", "--update"]) + + assert result.exit_code == 0 + + docker_manager.pull_image.assert_called_once_with("quantconnect/lean", "latest") + docker_manager.run_image.assert_called_once() + + +def test_toolbox_runs_custom_version() -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox", "--version", "3"]) + + assert result.exit_code == 0 + + docker_manager.run_image.assert_called_once() + args, kwargs = docker_manager.run_image.call_args + + assert args[0] == "quantconnect/lean" + assert args[1] == "3" + + +def test_toolbox_aborts_when_version_invalid() -> None: + create_fake_lean_cli_project() + + docker_manager = mock.Mock() + docker_manager.tag_exists.return_value = False + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox", "--version", "3"]) + + assert result.exit_code != 0 + + docker_manager.run_lean.assert_not_called() + + +def test_toolbox_aborts_when_run_image_fails() -> None: + create_fake_lean_cli_project() + + def run_image(*args, **kwargs) -> None: + raise RuntimeError("Oops") + + docker_manager = mock.Mock() + docker_manager.run_image.side_effect = run_image + container.docker_manager.override(providers.Object(docker_manager)) + + result = CliRunner().invoke(lean, ["toolbox"]) + + assert result.exit_code != 0 + + docker_manager.run_image.assert_called_once() diff --git a/tests/components/config/test_lean_config_manager.py b/tests/components/config/test_lean_config_manager.py index b454d7e2..a3060022 100644 --- a/tests/components/config/test_lean_config_manager.py +++ b/tests/components/config/test_lean_config_manager.py @@ -316,3 +316,12 @@ def test_get_complete_lean_config_sets_parameters() -> None: "key2": "value2", "key3": "value3" } + + +def test_get_lean_config_returns_contents_of_lean_config_file_as_dict() -> None: + create_fake_lean_cli_project() + + manager = LeanConfigManager(mock.Mock(), ProjectConfigManager()) + config = manager.get_lean_config() + + assert config == {"data-folder": "data"} diff --git a/tests/components/docker/test_lean_runner.py b/tests/components/docker/test_lean_runner.py index 241f631d..210ef03f 100644 --- a/tests/components/docker/test_lean_runner.py +++ b/tests/components/docker/test_lean_runner.py @@ -312,7 +312,7 @@ def test_run_lean_exposes_55555_when_debugging_with_mono() -> None: assert kwargs["ports"]["55555"] == "55555" -def test_run_lean_sets_correct_command_when_debugging_with_mono() -> None: +def test_run_lean_sets_correct_entrypoint_when_debugging_with_mono() -> None: create_fake_lean_cli_project() docker_manager = mock.Mock() @@ -329,8 +329,8 @@ def test_run_lean_sets_correct_command_when_debugging_with_mono() -> None: docker_manager.run_image.assert_called_once() args, kwargs = docker_manager.run_image.call_args - assert args[2].startswith("--debug") - assert kwargs["entrypoint"] == "mono" + assert kwargs["entrypoint"][0] == "mono" + assert "--debug" in kwargs["entrypoint"] def test_run_lean_raises_when_run_image_fails() -> None: