From 0296f461d77e9ada1cf1eab6a2e1fe2bbb8939ef Mon Sep 17 00:00:00 2001 From: Corey Oordt Date: Mon, 18 Nov 2024 08:30:28 -0600 Subject: [PATCH] Add test suite for CLI and enhance TODO tags Introduced a comprehensive test suite for the CLI functionality using `pytest` and `unittest.mock.patch` to ensure robustness. Enhanced TODO tags with issue numbers for improved tracking and organization. --- project_forge/cli.py | 5 +- project_forge/configurations/pattern.py | 2 +- project_forge/core/urls.py | 4 +- project_forge/settings.py | 2 +- tests/test_cli.py | 96 +++++++++++++++++++ tests/test_configurations/test_composition.py | 9 +- 6 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 tests/test_cli.py diff --git a/project_forge/cli.py b/project_forge/cli.py index fb45938..15ffdfb 100644 --- a/project_forge/cli.py +++ b/project_forge/cli.py @@ -73,9 +73,10 @@ def build( initial_context: dict[str, Any] = {} if data_file: - initial_context |= parse_file(data_file) + values = parse_file(data_file) + initial_context |= values or {} if data: initial_context |= dict(data) - print(type(output_dir)) + build_project(composition, output_dir=output_dir, use_defaults=use_defaults, initial_context=initial_context) diff --git a/project_forge/configurations/pattern.py b/project_forge/configurations/pattern.py index 344c1b7..970b928 100644 --- a/project_forge/configurations/pattern.py +++ b/project_forge/configurations/pattern.py @@ -107,7 +107,7 @@ class Question(BaseModel): ), ) - # TODO: How to do a basic regex or other string pattern validation? + # TODO[#6]: How to do a basic regex or other string pattern validation? validator: str = Field( default="", diff --git a/project_forge/core/urls.py b/project_forge/core/urls.py index 832b201..d777bd2 100644 --- a/project_forge/core/urls.py +++ b/project_forge/core/urls.py @@ -108,7 +108,7 @@ def parse_git_path(path: str) -> PathInfo: return {**default, **match_dict, **internal_path} # type: ignore[typeddict-item] -# TODO: Add abbreviation support such as `gh:` and `gl:` +# TODO[#4]: Add abbreviation support such as `gh:` and `gl:` def parse_git_url(git_url: str) -> ParsedURL: @@ -141,7 +141,7 @@ def parse_git_url(git_url: str) -> ParsedURL: else: url = ParsedURL(protocol="file", full_path=bits.path.rstrip("/")) - # TODO: parse_git_path won't work against a file:// scheme. The owner/repo_name will be in different places + # TODO[#5]: parse_git_path won't work against a file:// scheme. The owner/repo_name will be in different places parsed_path = parse_git_path(url.full_path) url.owner = parsed_path.get("owner") url.repo_name = parsed_path.get("repo_name") diff --git a/project_forge/settings.py b/project_forge/settings.py index b918735..e656b3b 100644 --- a/project_forge/settings.py +++ b/project_forge/settings.py @@ -27,5 +27,5 @@ class Settings(BaseSettings): def get_settings(config_file: Path = DEFAULT_CONFIG_FILE) -> Settings: """Return the settings.""" - # TODO: Implement settings management + # TODO[#3]: Implement settings management return Settings() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 0000000..e3c629d --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,96 @@ +from pathlib import Path +from unittest.mock import patch + +import pytest +from project_forge.cli import build +from click.testing import CliRunner + + +@pytest.fixture +def runner(): + """The CLI runner.""" + return CliRunner() + + +@pytest.fixture +def composition_path(tmp_path: Path): + """The path to a composition file.""" + path = tmp_path / "composition.yaml" + path.touch(exist_ok=True) + return path + + +class TestBuildCommand: + """ + Tests for the cli.build command. + + Doesn't test the implementation, just the CLI interface. + """ + + @patch("project_forge.commands.build.build_project") + def test_use_defaults_passed_to_build_project(self, mock_build_project, runner: CliRunner, composition_path: Path): + """The `use-defaults` flag is correctly passed to the `build_project` function.""" + + result = runner.invoke(build, [str(composition_path), "--use-defaults"]) + + if result.exit_code != 0: + print(result.output) + + assert result.exit_code == 0 + mock_build_project.assert_called_once_with( + composition_path, output_dir=Path.cwd(), use_defaults=True, initial_context={} + ) + + @patch("project_forge.commands.build.build_project") + def test_custom_output_dir_passed_to_build_project( + self, mock_build_project, runner: CliRunner, composition_path: Path, tmp_path: Path + ): + """The `output-dir` option is correctly passed to the `build_project` function.`""" + output_dir = tmp_path / "custom-dir" + output_dir.mkdir(parents=True, exist_ok=True) + + result = runner.invoke(build, [str(composition_path), "--output-dir", str(output_dir)]) + + if result.exit_code != 0: + print(result.output) + + assert result.exit_code == 0 + mock_build_project.assert_called_once_with( + composition_path, output_dir=output_dir, use_defaults=False, initial_context={} + ) + + @patch("project_forge.commands.build.build_project") + @patch("project_forge.cli.parse_file") + def test_data_file_adds_to_initial_context( + self, mock_parse_file, mock_build_project, runner: CliRunner, composition_path: Path, tmp_path: Path + ): + """The parsed results of a data file are added to the initial context.""" + data_file_path = tmp_path / "data.yaml" + data_file_path.touch(exist_ok=True) + mock_parse_file.return_value = {"key": "value"} + + result = runner.invoke(build, [str(composition_path), "--data-file", str(data_file_path)]) + + if result.exit_code != 0: + print("OUTPUT:", result.output, result) + + assert result.exit_code == 0 + + mock_parse_file.assert_called_once_with(data_file_path) + mock_build_project.assert_called_once_with( + composition_path, output_dir=Path.cwd(), use_defaults=False, initial_context={"key": "value"} + ) + + @patch("project_forge.commands.build.build_project") + def test_data_options_adds_to_initial_context(self, mock_build_project, runner: CliRunner, composition_path: Path): + """Data options are added to the initial context.""" + + result = runner.invoke(build, [str(composition_path), "-d", "key", "value"]) + + if result.exit_code != 0: + print(result.output) + + assert result.exit_code == 0 + mock_build_project.assert_called_once_with( + composition_path, output_dir=Path.cwd(), use_defaults=False, initial_context={"key": "value"} + ) diff --git a/tests/test_configurations/test_composition.py b/tests/test_configurations/test_composition.py index 0b3a2b3..30746a7 100644 --- a/tests/test_configurations/test_composition.py +++ b/tests/test_configurations/test_composition.py @@ -124,8 +124,7 @@ def test_reads_and_converts_a_pattern_file(self, fixtures_dir: Path): assert pattern.template_location.resolve() == fixtures_dir / "mkdocs" / "{{ repo_name }}" -# TODO: Composition scenarios to test -# - patterns with optional questions -# - patterns with optional questions that are answered in a previous pattern -# - patterns with files/directories named with mapped keys. They should properly get mapped to the combined template structure -# - patterns with mixed file types +# TODO[#7]: Write composition test scenario patterns with optional questions +# TODO[#8]: Write composition test scenario patterns with optional questions that are answered in a previous pattern +# TODO[#9]: Write composition test scenario patterns with files/directories named with mapped keys. They should properly get mapped to the combined template structure +# TODO[#10]: Write composition test scenario patterns with mixed file types