diff --git a/living_documentation_generator/action_inputs.py b/living_documentation_generator/action_inputs.py
index 7cd0007..26df0d7 100644
--- a/living_documentation_generator/action_inputs.py
+++ b/living_documentation_generator/action_inputs.py
@@ -102,7 +102,7 @@ def get_repositories() -> list[ConfigRepository]:
sys.exit(1)
except TypeError:
- logger.error("Type error parsing input JSON repositories: `%s.`", repositories_json)
+ logger.error("Type error parsing input JSON repositories: %s.", repositories_json)
sys.exit(1)
return repositories
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index 96e5eb3..8060f2d 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -152,7 +152,7 @@ def _fetch_github_issues(self) -> dict[str, list[Issue]]:
issues[repository_id] = self.__safe_call(repository.get_issues)(state=ISSUE_STATE_ALL)
amount_of_issues_per_repo = len(list(issues[repository_id]))
logger.debug(
- "Fetched `%s` repository issues (%s)`.",
+ "Fetched `%i` repository issues (%s)`.",
amount_of_issues_per_repo,
repository.full_name,
)
@@ -170,13 +170,13 @@ def _fetch_github_issues(self) -> dict[str, list[Issue]]:
# Accumulate the count of issues
total_issues_number += amount_of_issues_per_repo
logger.info(
- "Fetching repository GitHub issues - fetched `%s` repository issues (%s).",
+ "Fetching repository GitHub issues - fetched `%i` repository issues (%s).",
amount_of_issues_per_repo,
repository.full_name,
)
logger.info(
- "Fetching repository GitHub issues - loaded `%s` repository issues in total.",
+ "Fetching repository GitHub issues - loaded `%i` repository issues in total.",
total_issues_number,
)
return issues
@@ -213,7 +213,7 @@ def _fetch_github_project_issues(self) -> dict[str, list[ProjectIssue]]:
if projects:
logger.info(
- "Fetching GitHub project data - for repository `%s` found `%s` project/s.",
+ "Fetching GitHub project data - for repository `%s` found `%i` project/s.",
repository.full_name,
len(projects),
)
@@ -278,8 +278,8 @@ def _consolidate_issues_data(
for project_issue in project_issues[key]:
consolidated_issue.update_with_project_data(project_issue.project_status)
- logging.info(
- "Issue and project data consolidation - consolidated `%s` repository issues with extra project data.",
+ logger.info(
+ "Issue and project data consolidation - consolidated `%i` repository issues with extra project data.",
len(consolidated_issues),
)
return consolidated_issues
@@ -651,6 +651,7 @@ def _generate_index_directory_path(repository_id: Optional[str], topic: Optional
Generates a directory path based on if structured output is required.
@param repository_id: The repository id.
+ @param topic: The topic used for grouping issues.
@return: The generated directory path.
"""
output_path: str = ActionInputs.get_output_directory()
diff --git a/tests/conftest.py b/tests/conftest.py
index dc989bd..eb344ee 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-
+import datetime
import time
import pytest
from github import Github
@@ -21,7 +21,11 @@
from github.RateLimit import RateLimit
from github.Repository import Repository
+from living_documentation_generator.generator import LivingDocumentationGenerator
+from living_documentation_generator.model.config_repository import ConfigRepository
+from living_documentation_generator.model.consolidated_issue import ConsolidatedIssue
from living_documentation_generator.model.github_project import GithubProject
+from living_documentation_generator.model.project_status import ProjectStatus
from living_documentation_generator.utils.github_rate_limiter import GithubRateLimiter
@@ -76,3 +80,83 @@ def repository_setup(mocker):
repository.full_name = "test_owner/test_repo"
return repository
+
+
+@pytest.fixture
+def load_all_templates_setup(mocker):
+ mock_load_all_templates = mocker.patch.object(
+ LivingDocumentationGenerator,
+ "_load_all_templates",
+ return_value=(
+ "Issue Page Template",
+ "Index Page Template",
+ "Root Level Page Template",
+ "Org Level Template",
+ "Repo Page Template",
+ "Data Level Template",
+ ),
+ )
+
+ return mock_load_all_templates
+
+
+@pytest.fixture
+def generator(mocker):
+ mock_github_class = mocker.patch("living_documentation_generator.generator.Github")
+ mock_github_instance = mock_github_class.return_value
+
+ mock_rate_limit = mocker.Mock()
+ mock_rate_limit.remaining = 5000
+ mock_rate_limit.reset = datetime.datetime.now() + datetime.timedelta(minutes=10)
+
+ mock_github_instance.get_rate_limit.return_value = mocker.Mock(core=mock_rate_limit)
+ mock_github_instance.get_repo.return_value = mocker.Mock()
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_github_token", return_value="FakeGithubToken"
+ )
+ return LivingDocumentationGenerator()
+
+
+@pytest.fixture
+def config_repository(mocker):
+ config_repository = mocker.Mock(spec=ConfigRepository)
+ config_repository.organization_name = "test_org"
+ config_repository.repository_name = "test_repo"
+ config_repository.labels = []
+ config_repository.projects_title_filter = []
+
+ return config_repository
+
+
+@pytest.fixture
+def consolidated_issue(mocker):
+ consolidated_issue = mocker.Mock(spec=ConsolidatedIssue)
+ consolidated_issue.repository_id = "TestOrg/TestRepo"
+ consolidated_issue.organization_name = "TestOrg"
+ consolidated_issue.repository_name = "TestRepo"
+ consolidated_issue.number = 42
+ consolidated_issue.title = "Sample Issue"
+ consolidated_issue.state = "OPEN"
+ consolidated_issue.html_url = "https://github.com/TestOrg/TestRepo/issues/42"
+ consolidated_issue.created_at = "2024-01-01T00:00:00Z"
+ consolidated_issue.updated_at = "2024-01-02T00:00:00Z"
+ consolidated_issue.closed_at = None
+ consolidated_issue.labels = ["bug", "urgent"]
+ consolidated_issue.body = "This is the issue content."
+ consolidated_issue.linked_to_project = False
+ consolidated_issue.topics = ["documentationTopic"]
+
+ return consolidated_issue
+
+
+@pytest.fixture
+def project_status(mocker):
+ project_status = mocker.Mock(spec=ProjectStatus)
+ project_status.project_title = "Test Project"
+ project_status.status = "In Progress"
+ project_status.priority = "High"
+ project_status.size = "Large"
+ project_status.moscow = "Must Have"
+
+ return project_status
diff --git a/tests/test_action_inputs.py b/tests/test_action_inputs.py
index 2239a8e..aa34ba6 100644
--- a/tests/test_action_inputs.py
+++ b/tests/test_action_inputs.py
@@ -14,15 +14,73 @@
# limitations under the License.
#
import json
+import os
from living_documentation_generator.action_inputs import ActionInputs
from living_documentation_generator.model.config_repository import ConfigRepository
+# Check Action Inputs default values
+
+
+def test_project_state_mining_default():
+ # Arrange
+ os.environ.pop("INPUT_PROJECT_STATE_MINING", None)
+
+ # Act
+ actual = ActionInputs.get_is_project_state_mining_enabled()
+
+ # Assert
+ assert not actual
+
+
+def test_verbose_logging_default():
+ # Act
+ actual = os.getenv("INPUT_VERBOSE_LOGGING", "false").lower() == "true"
+
+ # Assert
+ assert not actual
+
+
+def test_output_path_default():
+ # Arrange
+ os.environ.pop("INPUT_OUTPUT_PATH", None)
+ expected = os.path.abspath("./output")
+
+ # Act
+ actual = ActionInputs.get_output_directory()
+
+ # Assert
+ assert expected == actual
+
+
+def test_structured_output_default():
+ # Arrange
+ os.environ.pop("INPUT_STRUCTURED_OUTPUT", None)
+
+ # Act
+ actual = ActionInputs.get_is_structured_output_enabled()
+
+ # Assert
+ assert not actual
+
+
+def test_group_output_by_topics_default():
+ # Arrange
+ os.environ.pop("INPUT_GROUP_OUTPUT_BY_TOPICS", None)
+
+ # Act
+ actual = ActionInputs.get_is_grouping_by_topics_enabled()
+
+ # Assert
+ assert not actual
+
+
# get_repositories
def test_get_repositories_correct_behaviour(mocker):
+ # Arrange
repositories_json = [
{
"organization-name": "organizationABC",
@@ -41,8 +99,10 @@ def test_get_repositories_correct_behaviour(mocker):
"living_documentation_generator.action_inputs.get_action_input", return_value=json.dumps(repositories_json)
)
+ # Act
actual = ActionInputs.get_repositories()
+ # Assert
assert 2 == len(actual)
assert isinstance(actual[0], ConfigRepository)
assert "organizationABC" == actual[0].organization_name
@@ -56,95 +116,100 @@ def test_get_repositories_correct_behaviour(mocker):
assert ["wanted_project"] == actual[1].projects_title_filter
-# FixMe: For some reason this test is called 11 times. Please help me to understand the reason.
-# def test_get_repositories_error_parsing_json_from_json_string(mocker):
-# mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
-# mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="not a JSON string")
-# mock_exit = mocker.patch("sys.exit")
-#
-# ActionInputs.get_repositories()
-#
-# mock_log_error.assert_called_once_with("Error parsing input JSON repositories: `%s.`", mocker.ANY, exc_info=True)
-# mock_exit.assert_called_once_with(1)
-
-
def test_get_repositories_default_value_as_json(mocker):
+ # Arrange
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="[]")
mock_exit = mocker.patch("sys.exit")
+ # Act
actual = ActionInputs.get_repositories()
- assert actual == []
+ # Assert
+ assert [] == actual
mock_exit.assert_not_called()
mock_log_error.assert_not_called()
def test_get_repositories_empty_object_as_input(mocker):
+ # Arrange
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="{}")
mock_exit = mocker.patch("sys.exit")
+ # Act
actual = ActionInputs.get_repositories()
- assert actual == []
+ # Assert
+ assert [] == actual
mock_exit.assert_not_called()
mock_log_error.assert_not_called()
def test_get_repositories_error_with_loading_repository_json(mocker):
+ # Arrange
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="[{}]")
mocker.patch.object(ConfigRepository, "load_from_json", return_value=False)
mock_exit = mocker.patch("sys.exit")
+ # Act
ActionInputs.get_repositories()
+ # Assert
mock_exit.assert_not_called()
mock_log_error.assert_called_once_with("Failed to load repository from JSON: %s.", {})
def test_get_repositories_number_instead_of_json(mocker):
+ # Arrange
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value=1)
mock_exit = mocker.patch("sys.exit")
+ # Act
ActionInputs.get_repositories()
+ # Assert
mock_exit.assert_called_once_with(1)
- mock_log_error.assert_called_once_with("Type error parsing input JSON repositories: `%s.`", mocker.ANY)
+ mock_log_error.assert_called_once_with("Type error parsing input JSON repositories: %s.", mocker.ANY)
def test_get_repositories_empty_string_as_input(mocker):
+ # Arrange
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="")
mock_exit = mocker.patch("sys.exit")
+ # Act
actual = ActionInputs.get_repositories()
- assert actual == []
+ # Assert
+ assert [] == actual
mock_exit.assert_called_once()
mock_log_error.assert_called_once_with("Error parsing JSON repositories: %s.", mocker.ANY, exc_info=True)
def test_get_repositories_invalid_string_as_input(mocker):
+ # Arrange
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
- mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="string")
+ mocker.patch("living_documentation_generator.action_inputs.get_action_input", return_value="not a JSON string")
mock_exit = mocker.patch("sys.exit")
+ # Act
actual = ActionInputs.get_repositories()
- assert actual == []
- mock_exit.assert_called_once()
+ # Assert
+ assert [] == actual
mock_log_error.assert_called_once_with("Error parsing JSON repositories: %s.", mocker.ANY, exc_info=True)
+ mock_exit.assert_called_once_with(1)
# validate_inputs
def test_validate_inputs_correct_behaviour(mocker):
- mock_log_debug = mocker.patch("living_documentation_generator.action_inputs.logger.debug")
- mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
+ # Arrange
repositories_json = [
{
"organization-name": "organizationABC",
@@ -153,39 +218,52 @@ def test_validate_inputs_correct_behaviour(mocker):
"projects-title-filter": [],
}
]
+ mock_log_debug = mocker.patch("living_documentation_generator.action_inputs.logger.debug")
+ mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
+
mocker.patch(
"living_documentation_generator.action_inputs.ActionInputs.get_repositories", return_value=repositories_json
)
mock_exit = mocker.patch("sys.exit")
+ # Act
ActionInputs.validate_inputs("./output")
+ # Assert
mock_exit.assert_not_called()
mock_log_debug.assert_called_once_with("Action inputs validation successfully completed.")
mock_log_error.assert_not_called()
def test_validate_inputs_error_output_path_as_empty_string(mocker):
+ # Arrange
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
mock_exit = mocker.patch("sys.exit")
+ # Act
ActionInputs.validate_inputs("")
+ # Assert
mock_exit.assert_called_once_with(1)
mock_log_error.assert_called_once_with("INPUT_OUTPUT_PATH can not be an empty string.")
def test_validate_inputs_error_output_path_as_project_directory(mocker):
+ # Arrange
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
mock_exit = mocker.patch("sys.exit")
+ # Act
ActionInputs.validate_inputs("./templates/template_subfolder")
+ # Assert
mock_exit.assert_called_once_with(1)
mock_log_error.assert_called_once_with("INPUT_OUTPUT_PATH cannot be chosen as a part of any project folder.")
def test_validate_inputs_absolute_output_path_with_relative_project_directories(mocker):
+ # Arrange
+ absolute_out_path = "/root/project/dir1/subfolder"
mock_log_error = mocker.patch("living_documentation_generator.action_inputs.logger.error")
mock_exit = mocker.patch("sys.exit")
mocker.patch(
@@ -193,9 +271,10 @@ def test_validate_inputs_absolute_output_path_with_relative_project_directories(
return_value=["project/dir1", "project/dir2"],
)
mocker.patch("os.path.abspath", side_effect=lambda path: f"/root/{path}" if not path.startswith("/") else path)
- absolute_out_path = "/root/project/dir1/subfolder"
+ # Act
ActionInputs.validate_inputs(absolute_out_path)
+ # Assert
mock_exit.assert_called_once_with(1)
mock_log_error.assert_called_once_with("INPUT_OUTPUT_PATH cannot be chosen as a part of any project folder.")
diff --git a/tests/test_generator.py b/tests/test_generator.py
new file mode 100644
index 0000000..ea22845
--- /dev/null
+++ b/tests/test_generator.py
@@ -0,0 +1,1204 @@
+#
+# 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 datetime import datetime
+
+from github.Issue import Issue
+
+from living_documentation_generator.generator import LivingDocumentationGenerator
+from living_documentation_generator.model.consolidated_issue import ConsolidatedIssue
+from living_documentation_generator.model.project_issue import ProjectIssue
+
+
+# generate
+
+
+def test_generate_correct_behaviour(mocker, generator):
+ # Arrange
+ mock_clean_output_directory = mocker.patch.object(generator, "_clean_output_directory")
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+ mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
+
+ mock_issue = mocker.Mock()
+ project_issue_mock = mocker.Mock()
+ consolidated_issue_mock = mocker.Mock()
+
+ mock_fetch_github_issues = mocker.patch.object(
+ generator, "_fetch_github_issues", return_value={"test_org/test_repo": [mock_issue]}
+ )
+ mock_fetch_github_project_issues = mocker.patch.object(
+ generator, "_fetch_github_project_issues", return_value={"test_org/test_repo#1": [project_issue_mock]}
+ )
+ mock_consolidate_issues_data = mocker.patch.object(
+ generator, "_consolidate_issues_data", return_value={"test_org/test_repo#1": consolidated_issue_mock}
+ )
+ mock_generate_markdown_pages = mocker.patch.object(generator, "_generate_markdown_pages")
+
+ # Act
+ generator.generate()
+
+ # Assert
+ mock_clean_output_directory.assert_called_once()
+ mock_fetch_github_issues.assert_called_once()
+ mock_fetch_github_project_issues.assert_called_once()
+ mock_consolidate_issues_data.assert_called_once_with(
+ {"test_org/test_repo": [mock_issue]}, {"test_org/test_repo#1": [project_issue_mock]}
+ )
+ mock_generate_markdown_pages.assert_called_once_with({"test_org/test_repo#1": consolidated_issue_mock})
+ mock_logger_debug.assert_called_once_with("Output directory cleaned.")
+ mock_logger_info.assert_has_calls(
+ [
+ mocker.call("Fetching repository GitHub issues - started."),
+ mocker.call("Fetching repository GitHub issues - finished."),
+ mocker.call("Fetching GitHub project data - started."),
+ mocker.call("Fetching GitHub project data - finished."),
+ mocker.call("Issue and project data consolidation - started."),
+ mocker.call("Issue and project data consolidation - finished."),
+ mocker.call("Markdown page generation - started."),
+ mocker.call("Markdown page generation - finished."),
+ ],
+ any_order=True,
+ )
+
+
+# _clean_output_directory
+
+
+def test_clean_output_directory_correct_behaviour(mocker, generator):
+ # Arrange
+ mock_output_path = "/test/output/path"
+ mock_get_output_directory = mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value=mock_output_path
+ )
+ mock_exists = mocker.patch("os.path.exists", return_value=True)
+ mock_rmtree = mocker.patch("shutil.rmtree")
+ mock_makedirs = mocker.patch("os.makedirs")
+
+ # Act
+ generator._clean_output_directory()
+
+ # Assert
+ mock_get_output_directory.assert_called_once()
+ mock_exists.assert_called_once_with(mock_output_path)
+ mock_rmtree.assert_called_once_with(mock_output_path)
+ mock_makedirs.assert_called_once_with(mock_output_path)
+
+
+# _fetch_github_issues
+
+
+def test_fetch_github_issues_no_query_labels(mocker, generator, config_repository):
+ # Arrange
+ config_repository.query_labels = []
+ repo = mocker.Mock()
+ repo.full_name = "test_org/test_repo"
+ issue1 = mocker.Mock()
+ issue2 = mocker.Mock()
+ issue3 = mocker.Mock()
+
+ expected_issues = {"test_org/test_repo": [issue1, issue2, issue3]}
+ mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ mock_get_repo.return_value = repo
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository]
+ )
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+ mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
+ mock_get_issues = mocker.patch.object(repo, "get_issues", return_value=[issue1, issue2, issue3])
+
+ # Act
+ actual = generator._fetch_github_issues()
+
+ # Assert
+ assert expected_issues == actual
+ assert 1 == len(actual)
+ assert 3 == len(actual["test_org/test_repo"])
+ mock_get_repo.assert_called_once_with("test_org/test_repo")
+ mock_get_issues.assert_called_once_with(state="all")
+ mock_logger_info.assert_has_calls(
+ [
+ mocker.call("Fetching repository GitHub issues - from `%s`.", "test_org/test_repo"),
+ mocker.call(
+ "Fetching repository GitHub issues - fetched `%i` repository issues (%s).", 3, "test_org/test_repo"
+ ),
+ mocker.call("Fetching repository GitHub issues - loaded `%i` repository issues in total.", 3),
+ ],
+ any_order=True,
+ )
+ mock_logger_debug.assert_has_calls(
+ [
+ mocker.call("Fetching all issues in the repository"),
+ mocker.call("Fetched `%i` repository issues (%s)`.", 3, "test_org/test_repo"),
+ ],
+ any_order=True,
+ )
+
+
+def test_fetch_github_issues_with_given_query_labels(mocker, generator, config_repository):
+ # Arrange
+ config_repository.query_labels = ["bug", "enhancement"]
+ repo = mocker.Mock()
+ repo.full_name = "test_org/test_repo"
+ issue1 = mocker.Mock()
+ issue2 = mocker.Mock()
+
+ expected_issues = {"test_org/test_repo": [issue1, issue2]}
+ mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ mock_get_repo.return_value = repo
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository]
+ )
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+ mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
+ mock_get_issues = mocker.patch.object(repo, "get_issues", side_effect=[[issue1], [issue2]])
+
+ # Act
+ actual = generator._fetch_github_issues()
+
+ # Assert
+ assert expected_issues == actual
+ assert 1 == len(actual)
+ mock_get_repo.assert_called_once_with("test_org/test_repo")
+ mock_get_issues.assert_any_call(state="all", labels=["bug"])
+ mock_get_issues.assert_any_call(state="all", labels=["enhancement"])
+ mock_logger_info.assert_has_calls(
+ [
+ mocker.call("Fetching repository GitHub issues - from `%s`.", "test_org/test_repo"),
+ mocker.call(
+ "Fetching repository GitHub issues - fetched `%i` repository issues (%s).", 2, "test_org/test_repo"
+ ),
+ mocker.call("Fetching repository GitHub issues - loaded `%i` repository issues in total.", 2),
+ ],
+ any_order=True,
+ )
+ mock_logger_debug.assert_has_calls(
+ [
+ mocker.call("Labels to be fetched from: %s.", config_repository.query_labels),
+ mocker.call("Fetching issues with label `%s`.", "bug"),
+ mocker.call("Fetching issues with label `%s`.", "enhancement"),
+ ],
+ any_order=True,
+ )
+
+
+def test_fetch_github_issues_repository_none(mocker, generator, config_repository):
+ # Arrange
+ mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ mock_get_repo.return_value = None
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository]
+ )
+
+ # Act
+ actual = generator._fetch_github_issues()
+
+ # Assert
+ assert {} == actual
+ mock_get_repo.assert_called_once_with("test_org/test_repo")
+
+
+# _fetch_github_project_issues
+
+
+def test_fetch_github_project_issues_correct_behaviour(mocker, generator):
+ # Arrange
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+ mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
+
+ repository_1 = mocker.Mock()
+ repository_1.organization_name = "OrgA"
+ repository_1.repository_name = "RepoA"
+ repository_1.projects_title_filter = ""
+
+ repository_2 = mocker.Mock()
+ repository_2.organization_name = "OrgA"
+ repository_2.repository_name = "RepoB"
+ repository_2.projects_title_filter = "ProjectB"
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_repositories",
+ return_value=[repository_1, repository_2],
+ )
+
+ mock_github_projects_instance = mocker.patch.object(
+ generator, "_LivingDocumentationGenerator__github_projects_instance"
+ )
+
+ repo_a = mocker.Mock()
+ repo_a.full_name = "OrgA/RepoA"
+ repo_b = mocker.Mock()
+ repo_b.full_name = "OrgA/RepoB"
+ generator._LivingDocumentationGenerator__github_instance.get_repo.side_effect = [repo_a, repo_b]
+
+ project_a = mocker.Mock(title="Project A")
+ project_b = mocker.Mock(title="Project B")
+ mock_github_projects_instance.get_repository_projects.side_effect = [[project_a], [project_b]]
+
+ project_status_1 = mocker.Mock()
+ project_status_1.status = "In Progress"
+
+ project_status_2 = mocker.Mock()
+ project_status_2.status = "Done"
+
+ project_issue_1 = mocker.Mock(spec=ProjectIssue)
+ project_issue_1.organization_name = "OrgA"
+ project_issue_1.repository_name = "RepoA"
+ project_issue_1.number = 1
+ project_issue_1.project_status = project_status_1
+
+ # By creating two same Project Issues (same unique issue key) that has different project statuses
+ # we test the situation where one issue is linked to more projects (need of keeping all project statuses)
+ project_issue_2 = mocker.Mock(spec=ProjectIssue)
+ project_issue_2.organization_name = "OrgA"
+ project_issue_2.repository_name = "RepoA"
+ project_issue_2.number = 1
+ project_issue_2.project_status = project_status_2
+
+ mock_github_projects_instance.get_project_issues.side_effect = [[project_issue_1], [project_issue_2]]
+
+ mock_make_issue_key = mocker.patch(
+ "living_documentation_generator.generator.make_issue_key",
+ side_effect=lambda org, repo, num: f"{org}/{repo}#{num}",
+ )
+
+ # Act
+ actual = generator._fetch_github_project_issues()
+
+ # Assert
+ assert mock_make_issue_key.call_count == 2
+ assert len(actual) == 1
+ assert "OrgA/RepoA#1" in actual
+ assert actual["OrgA/RepoA#1"] == [project_issue_1, project_issue_2]
+
+ generator._LivingDocumentationGenerator__github_instance.get_repo.assert_any_call("OrgA/RepoA")
+ generator._LivingDocumentationGenerator__github_instance.get_repo.assert_any_call("OrgA/RepoB")
+ mock_github_projects_instance.get_repository_projects.assert_any_call(repository=repo_a, projects_title_filter="")
+ mock_github_projects_instance.get_repository_projects.assert_any_call(
+ repository=repo_b, projects_title_filter="ProjectB"
+ )
+ mock_github_projects_instance.get_project_issues.assert_any_call(project=project_a)
+ mock_github_projects_instance.get_project_issues.assert_any_call(project=project_b)
+ mock_logger_info.assert_has_calls(
+ [
+ mocker.call("Fetching GitHub project data - for repository `%s` found `%i` project/s.", "OrgA/RepoA", 1),
+ mocker.call("Fetching GitHub project data - fetching project data from `%s`.", "Project A"),
+ mocker.call("Fetching GitHub project data - successfully fetched project data from `%s`.", "Project A"),
+ mocker.call("Fetching GitHub project data - for repository `%s` found `%i` project/s.", "OrgA/RepoB", 1),
+ mocker.call("Fetching GitHub project data - fetching project data from `%s`.", "Project B"),
+ mocker.call("Fetching GitHub project data - successfully fetched project data from `%s`.", "Project B"),
+ ],
+ any_order=True,
+ )
+ mock_logger_debug.assert_has_calls(
+ [
+ mocker.call("Project data mining allowed."),
+ mocker.call("Filtering projects: %s. If filter is empty, fetching all.", ""),
+ mocker.call("Filtering projects: %s. If filter is empty, fetching all.", "ProjectB"),
+ mocker.call("Fetching GitHub project data - looking for repository `%s` projects.", "OrgA/RepoA"),
+ mocker.call("Fetching GitHub project data - looking for repository `%s` projects.", "OrgA/RepoB"),
+ ],
+ any_order=True,
+ )
+
+
+def test_fetch_github_project_issues_project_mining_disabled(mocker, generator):
+ # Arrange
+ mock_get_project_mining_enabled = mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False
+ )
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+
+ # Act
+ actual = generator._fetch_github_project_issues()
+
+ # Assert
+ assert {} == actual
+ mock_get_project_mining_enabled.assert_called_once()
+ mock_logger_info.assert_called_once_with("Fetching GitHub project data - project mining is not allowed.")
+
+
+def test_fetch_github_project_issues_no_repositories(mocker, generator, config_repository):
+ # Arrange
+ mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ mock_get_repo.return_value = None
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository]
+ )
+
+ # Act
+ actual = generator._fetch_github_project_issues()
+
+ # Assert
+ assert {} == actual
+ mock_get_repo.assert_called_once_with("test_org/test_repo")
+
+
+def test_fetch_github_project_issues_with_no_projects(mocker, generator, config_repository):
+ # Arrange
+ mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ repo_a = mocker.Mock()
+ repo_a.full_name = "test_org/test_repo"
+ mock_get_repo.return_value = repo_a
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository]
+ )
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+ mock_get_repository_projects = mocker.patch.object(
+ generator._LivingDocumentationGenerator__github_projects_instance, "get_repository_projects", return_value=[]
+ )
+
+ # Act
+ actual = generator._fetch_github_project_issues()
+
+ # Assert
+ assert {} == actual
+ mock_get_repo.assert_called_once_with("test_org/test_repo")
+ mock_get_repository_projects.assert_called_once_with(repository=repo_a, projects_title_filter=[])
+ mock_logger_info.assert_called_once_with(
+ "Fetching GitHub project data - no project data found for repository `%s`.", "test_org/test_repo"
+ )
+
+
+# _consolidate_issues_data
+
+
+def test_consolidate_issues_data_correct_behaviour(mocker, generator):
+ # Arrange
+ consolidated_issue_mock_1 = mocker.Mock(spec=ConsolidatedIssue)
+ consolidated_issue_mock_2 = mocker.Mock(spec=ConsolidatedIssue)
+ repository_issues = {"TestOrg/TestRepo": [mocker.Mock(spec=Issue, number=1), mocker.Mock(spec=Issue, number=2)]}
+ project_issues = {
+ "TestOrg/TestRepo#1": [mocker.Mock(spec=ProjectIssue, project_status="In Progress")],
+ "TestOrg/TestRepo#2": [mocker.Mock(spec=ProjectIssue, project_status="Done")],
+ }
+
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+ mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
+ mock_make_issue_key = mocker.patch(
+ "living_documentation_generator.generator.make_issue_key",
+ side_effect=lambda org, repo, num: f"{org}/{repo}#{num}",
+ )
+ mock_consolidated_issue_class = mocker.patch(
+ "living_documentation_generator.generator.ConsolidatedIssue",
+ side_effect=[consolidated_issue_mock_1, consolidated_issue_mock_2],
+ )
+
+ # Act
+ actual = generator._consolidate_issues_data(repository_issues, project_issues)
+
+ # Assert
+ assert 2 == len(actual)
+ assert 2 == mock_consolidated_issue_class.call_count
+ assert 2 == mock_make_issue_key.call_count
+ assert actual["TestOrg/TestRepo#1"] == consolidated_issue_mock_1
+ assert actual["TestOrg/TestRepo#2"] == consolidated_issue_mock_2
+ consolidated_issue_mock_1.update_with_project_data.assert_called_once_with("In Progress")
+ consolidated_issue_mock_2.update_with_project_data.assert_called_once_with("Done")
+ mock_logger_info.assert_called_once_with(
+ "Issue and project data consolidation - consolidated `%i` repository issues with extra project data.", 2
+ )
+ mock_logger_debug.assert_called_once_with("Updating consolidated issue structure with project data.")
+
+
+# _generate_markdown_pages
+
+
+def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabled(
+ mocker, generator, consolidated_issue, load_all_templates_setup
+):
+ # Arrange
+ mock_load_all_templates = load_all_templates_setup
+ topics = {"documentationTopic"}
+ issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mock_generate_root_level_index_page = mocker.patch(
+ "living_documentation_generator.generator.generate_root_level_index_page"
+ )
+ mock_generate_structured_index_pages = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_structured_index_pages"
+ )
+ mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+ mock_generate_md_issue_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_md_issue_page")
+
+ # Act
+ generator._generate_markdown_pages(issues)
+
+ # Assert
+ assert 2 == mock_generate_md_issue_page.call_count
+ mock_load_all_templates.assert_called_once()
+ mock_generate_md_issue_page.assert_any_call("Issue Page Template", consolidated_issue)
+ mock_generate_root_level_index_page.assert_called_once_with("Root Level Page Template", "/base/output")
+ mock_generate_structured_index_pages.assert_called_once_with(
+ "Data Level Template", "Repo Page Template", "Org Level Template", topics, issues
+ )
+ mock_generate_index_page.assert_not_called()
+ mock_logger_info.assert_called_once_with("Markdown page generation - generated `%i` issue pages.", 2)
+
+
+def test_generate_markdown_pages_with_structured_output_enabled_and_topic_grouping_disabled(
+ mocker, generator, consolidated_issue, load_all_templates_setup
+):
+ # Arrange
+ topics = {"documentationTopic", "FETopic"}
+ consolidated_issue.topics = ["documentationTopic", "FETopic"]
+ issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue, "issue_3": consolidated_issue}
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mock_generate_md_issue_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_md_issue_page")
+ mock_generate_root_level_index_page = mocker.patch(
+ "living_documentation_generator.generator.generate_root_level_index_page"
+ )
+ mock_generate_structured_index_pages = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_structured_index_pages"
+ )
+ mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+
+ # Act
+ generator._generate_markdown_pages(issues)
+
+ # Assert
+ assert 3 == mock_generate_md_issue_page.call_count
+ load_all_templates_setup.assert_called_once()
+ mock_generate_md_issue_page.assert_any_call("Issue Page Template", consolidated_issue)
+ mock_generate_root_level_index_page.assert_called_once_with("Root Level Page Template", "/base/output")
+ mock_generate_structured_index_pages.assert_called_once_with(
+ "Data Level Template", "Repo Page Template", "Org Level Template", topics, issues
+ )
+ mock_logger_info.assert_called_once_with("Markdown page generation - generated `%i` issue pages.", 3)
+
+
+def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disabled(
+ mocker, generator, consolidated_issue, load_all_templates_setup
+):
+ # Arrange
+ issues = {"issue_1": consolidated_issue}
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mock_generate_md_issue_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_md_issue_page")
+ mock_generate_root_level_index_page = mocker.patch(
+ "living_documentation_generator.generator.generate_root_level_index_page"
+ )
+ mock_generate_structured_index_pages = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_structured_index_pages"
+ )
+ mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+
+ # Act
+ generator._generate_markdown_pages(issues)
+
+ # Assert
+ load_all_templates_setup.assert_called_once()
+ assert mock_generate_md_issue_page.call_count == 1
+ mock_generate_md_issue_page.assert_any_call("Issue Page Template", consolidated_issue)
+ mock_generate_root_level_index_page.assert_not_called()
+ mock_generate_structured_index_pages.assert_not_called()
+ mock_generate_index_page.assert_called_once_with("Index Page Template", list(issues.values()))
+ mock_logger_info.assert_any_call("Markdown page generation - generated `%i` issue pages.", 1)
+ mock_logger_info.assert_any_call("Markdown page generation - generated `_index.md`")
+
+
+def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_output_disabled(
+ mocker, generator, consolidated_issue, load_all_templates_setup
+):
+ # Arrange
+ consolidated_issue.topics = ["documentationTopic", "FETopic"]
+ issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mock_generate_md_issue_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_md_issue_page")
+ mock_generate_root_level_index_page = mocker.patch(
+ "living_documentation_generator.generator.generate_root_level_index_page"
+ )
+ mock_generate_structured_index_pages = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_structured_index_pages"
+ )
+ mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
+
+ # Act
+ generator._generate_markdown_pages(issues)
+
+ # Assert
+ assert 2 == mock_generate_md_issue_page.call_count
+ load_all_templates_setup.assert_called_once()
+ mock_generate_md_issue_page.assert_any_call("Issue Page Template", consolidated_issue)
+ mock_generate_root_level_index_page.assert_called_once_with("Root Level Page Template", "/base/output")
+ mock_generate_structured_index_pages.assert_not_called()
+ mock_generate_index_page.assert_any_call(
+ "Data Level Template", list(issues.values()), grouping_topic="documentationTopic"
+ )
+ mock_generate_index_page.assert_any_call("Data Level Template", list(issues.values()), grouping_topic="FETopic")
+
+
+# _generate_md_issue_page
+
+
+def test_generate_md_issue_page(mocker, generator, consolidated_issue):
+ # Arrange
+ issue_page_template = "Title: {title}\nDate: {date}\nSummary:\n{issue_summary_table}\nContent:\n{issue_content}"
+ consolidated_issue.generate_directory_path = mocker.Mock(return_value=["/base/output/org/repo/issues"])
+ consolidated_issue.generate_page_filename = mocker.Mock(return_value="issue_42.md")
+ expected_date = datetime.now().strftime("%Y-%m-%d")
+ expected_content = f"Title: Sample Issue\nDate: {expected_date}\nSummary:\nGenerated Issue Summary Table\nContent:\nThis is the issue content."
+
+ mock_generate_issue_summary_table = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_issue_summary_table", return_value="Generated Issue Summary Table"
+ )
+ mock_makedirs = mocker.patch("os.makedirs")
+ mock_open = mocker.patch("builtins.open", mocker.mock_open())
+
+ # Act
+ generator._generate_md_issue_page(issue_page_template, consolidated_issue)
+
+ # Assert
+ mock_generate_issue_summary_table.assert_called_once_with(consolidated_issue)
+ mock_makedirs.assert_called_once_with("/base/output/org/repo/issues", exist_ok=True)
+ mock_open.assert_called_once_with("/base/output/org/repo/issues/issue_42.md", "w", encoding="utf-8")
+ mock_open().write.assert_called_once_with(expected_content)
+
+
+# _generate_structured_index_pages
+
+
+def test_generate_structured_index_pages_with_topic_grouping_enabled(mocker, generator, consolidated_issue):
+ # Arrange
+ index_data_level_template = "Data Level Template"
+ index_repo_level_template = "Repo Level Template"
+ index_org_level_template = "Org Level Template"
+ topics = ["documentationTopic"]
+ consolidated_issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
+ mock_generate_sub_level_index_page = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_sub_level_index_page"
+ )
+ mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
+ mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
+ mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
+
+ # Act
+ generator._generate_structured_index_pages(
+ index_data_level_template,
+ index_repo_level_template,
+ index_org_level_template,
+ topics,
+ consolidated_issues,
+ )
+
+ # Assert
+ mock_generate_sub_level_index_page.assert_any_call(index_org_level_template, "org", "TestOrg/TestRepo")
+ mock_generate_sub_level_index_page.assert_any_call(index_repo_level_template, "repo", "TestOrg/TestRepo")
+ mock_generate_index_page.assert_called_once_with(
+ index_data_level_template, [consolidated_issue, consolidated_issue], "TestOrg/TestRepo", "documentationTopic"
+ )
+ mock_logger_info.assert_called_once_with(
+ "Markdown page generation - generated `_index.md` pages for %s.", "TestOrg/TestRepo"
+ )
+ mock_logger_debug.assert_any_call("Generated organization level `_index.md` for %s.", "TestOrg")
+ mock_logger_debug.assert_any_call("Generated repository level _index.md` for repository: %s.", "TestRepo")
+ mock_logger_debug.assert_any_call(
+ "Generated data level `_index.md` with topic: %s for %s.", "documentationTopic", "TestOrg/TestRepo"
+ )
+
+
+def test_generate_structured_index_pages_with_topic_grouping_disabled(mocker, generator, consolidated_issue):
+ # Arrange
+ index_data_level_template = "Data Level Template"
+ index_repo_level_template = "Repo Level Template"
+ index_org_level_template = "Org Level Template"
+ topics = ["documentationTopic"]
+ consolidated_issue.repository_id = "TestOrg/TestRepo"
+ consolidated_issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False
+ )
+ mock_generate_sub_level_index_page = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_sub_level_index_page"
+ )
+ mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
+ mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
+
+ # Act
+ generator._generate_structured_index_pages(
+ index_data_level_template,
+ index_repo_level_template,
+ index_org_level_template,
+ topics,
+ consolidated_issues,
+ )
+
+ # Assert
+ mock_generate_sub_level_index_page.assert_called_once_with(index_org_level_template, "org", "TestOrg/TestRepo")
+ mock_generate_index_page.assert_called_once_with(
+ index_data_level_template, [consolidated_issue, consolidated_issue], "TestOrg/TestRepo"
+ )
+ mock_logger_debug.assert_any_call("Generated organization level `_index.md` for %s.", "TestOrg")
+ mock_logger_debug.assert_any_call("Generated data level `_index.md` for %s", "TestOrg/TestRepo")
+
+
+# _generate_index_page
+TABLE_HEADER_WITH_PROJECT_DATA = """
+| Organization name | Repository name | Issue 'Number - Title' |Linked to project | Project status | Issue URL |
+|-------------------|-----------------|------------------------|------------------|----------------|-----------|
+"""
+
+
+def test_generate_index_page_with_all_features_enabled(mocker, generator, consolidated_issue, project_status):
+ # Arrange
+ issue_index_page_template = "Date: {date}\nIssues:\n{issue_overview_table}\nData Level: {data_level_name}"
+ consolidated_issue.linked_to_project = True
+ consolidated_issue.project_issue_statuses = [project_status]
+ consolidated_issues = [consolidated_issue, consolidated_issue]
+
+ repository_id = "TestOrg/TestRepo"
+ grouping_topic = "documentationTopic"
+
+ expected_date = datetime.now().strftime("%Y-%m-%d")
+ expected_issue_line = "| TestOrg | TestRepo | [#42 - Sample Issue](features#sample-issue) | 🟢 | In Progress |GitHub link |\n"
+ expected_issue_table = TABLE_HEADER_WITH_PROJECT_DATA + expected_issue_line + expected_issue_line
+ expected_data_level_name = "documentationTopic"
+ expected_index_page_content = (
+ f"Date: {expected_date}\nIssues:\n{expected_issue_table}\nData Level: {expected_data_level_name}"
+ )
+ expected_output_path = "/base/output/org/repo/topic/_index.md"
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True
+ )
+ mock_generate_index_directory_path = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_index_directory_path", return_value="/base/output/org/repo/topic"
+ )
+ mock_open = mocker.patch("builtins.open", mocker.mock_open())
+ mocker.patch("os.makedirs")
+
+ # Act
+ generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, grouping_topic)
+
+ # Assert
+ mock_generate_index_directory_path.assert_called_once_with(repository_id, grouping_topic)
+ mock_open.assert_called_once_with(expected_output_path, "w", encoding="utf-8")
+ mock_open().write.assert_called_once_with(expected_index_page_content)
+
+
+def test_generate_index_page_with_topic_grouping_disabled_structured_output_project_mining_enabled(
+ mocker, generator, consolidated_issue, project_status
+):
+ # Arrange
+ issue_index_page_template = "Date: {date}\nIssues:\n{issue_overview_table}\nData Level: {data_level_name}"
+ consolidated_issue.linked_to_project = True
+ consolidated_issue.project_issue_statuses = [project_status]
+ consolidated_issues = [consolidated_issue, consolidated_issue]
+
+ repository_id = "TestOrg/TestRepo"
+ grouping_topic = None
+
+ expected_date = datetime.now().strftime("%Y-%m-%d")
+ expected_issue_line = "| TestOrg | TestRepo | [#42 - Sample Issue](features#sample-issue) | 🟢 | In Progress |GitHub link |\n"
+ expected_issue_table = TABLE_HEADER_WITH_PROJECT_DATA + expected_issue_line + expected_issue_line
+ expected_data_level_name = "TestRepo"
+ expected_index_page_content = (
+ f"Date: {expected_date}\nIssues:\n{expected_issue_table}\nData Level: {expected_data_level_name}"
+ )
+ expected_output_path = "/base/output/org/repo/_index.md"
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True
+ )
+ mock_generate_index_directory_path = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_index_directory_path", return_value="/base/output/org/repo"
+ )
+ mock_open = mocker.patch("builtins.open", mocker.mock_open())
+ mocker.patch("os.makedirs")
+
+ # Act
+ generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, grouping_topic)
+
+ # Assert
+ mock_generate_index_directory_path.assert_called_once_with(repository_id, grouping_topic)
+ mock_open.assert_called_once_with(expected_output_path, "w", encoding="utf-8")
+ mock_open().write.assert_called_once_with(expected_index_page_content)
+
+
+def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_project_mining_enabled(
+ mocker, generator, consolidated_issue, project_status
+):
+ # Arrange
+ issue_index_page_template = "Date: {date}\nIssues:\n{issue_overview_table}\n"
+ consolidated_issue.project_issue_statuses = [project_status]
+ consolidated_issues = [consolidated_issue, consolidated_issue]
+
+ repository_id = None
+ grouping_topic = None
+
+ expected_date = datetime.now().strftime("%Y-%m-%d")
+ expected_issue_line = "| TestOrg | TestRepo | [#42 - Sample Issue](features#sample-issue) | 🔴 | In Progress |GitHub link |\n"
+ expected_issue_table = TABLE_HEADER_WITH_PROJECT_DATA + expected_issue_line + expected_issue_line
+ expected_index_page_content = f"Date: {expected_date}\nIssues:\n{expected_issue_table}\n"
+ expected_output_path = "/base/output/_index.md"
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False
+ )
+ mock_generate_index_directory_path = mocker.patch.object(
+ LivingDocumentationGenerator, "_generate_index_directory_path", return_value="/base/output"
+ )
+ mock_open = mocker.patch("builtins.open", mocker.mock_open())
+ mocker.patch("os.makedirs")
+
+ # Act
+ generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, grouping_topic)
+
+ # Assert
+ mock_generate_index_directory_path.assert_called_once_with(repository_id, grouping_topic)
+ mock_open.assert_called_once_with(expected_output_path, "w", encoding="utf-8")
+ mock_open().write.assert_called_once_with(expected_index_page_content)
+
+
+# _generate_sub_level_index_page
+
+
+def test_generate_sub_level_index_page_for_org_level(mocker, generator):
+ # Arrange
+ index_template = "Organization: {organization_name}, Date: {date}"
+ level = "org"
+ repository_id = "TestOrg/TestRepo"
+ expected_replacement_content = f"Organization: TestOrg, Date: {datetime.now().strftime('%Y-%m-%d')}"
+ expected_output_path = "/base/output/TestOrg/_index.md"
+
+ mock_get_output_directory = mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mock_open = mocker.patch("builtins.open", mocker.mock_open())
+ mocker.patch("os.makedirs")
+
+ # Act
+ generator._generate_sub_level_index_page(index_template, level, repository_id)
+
+ # Assert
+ mock_get_output_directory.assert_called_once()
+ mock_open.assert_called_once_with(expected_output_path, "w", encoding="utf-8")
+ mock_open().write.assert_called_once_with(expected_replacement_content)
+
+
+def test_generate_sub_level_index_page_for_repo_level(mocker, generator):
+ # Arrange
+ index_template = "Repository: {repository_name}, Date: {date}"
+ level = "repo"
+ repository_id = "TestOrg/TestRepo"
+ expected_replacement_content = f"Repository: TestRepo, Date: {datetime.now().strftime('%Y-%m-%d')}"
+ expected_output_path = "/base/output/TestOrg/TestRepo/_index.md"
+
+ mock_get_output_directory = mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mock_open = mocker.patch("builtins.open", mocker.mock_open())
+ mocker.patch("os.makedirs")
+
+ # Act
+ generator._generate_sub_level_index_page(index_template, level, repository_id)
+
+ # Assert
+ mock_get_output_directory.assert_called_once()
+ mock_open.assert_called_once_with(expected_output_path, "w", encoding="utf-8")
+ mock_open().write.assert_called_once_with(expected_replacement_content)
+
+
+# _generate_markdown_line
+
+
+def test_generate_markdown_line_with_project_state_mining_enabled_linked_to_project_true_symbol(
+ mocker, consolidated_issue, project_status
+):
+ # Arrange
+ consolidated_issue.linked_to_project = True
+ consolidated_issue.project_issue_statuses = [project_status, project_status]
+ expected_md_issue_line = (
+ "| TestOrg | TestRepo | [#42 - Sample Issue](features#sample-issue) | 🟢 | In Progress, In Progress |"
+ "GitHub link |\n"
+ )
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+
+ # Assert
+ assert expected_md_issue_line == actual
+
+
+def test_generate_markdown_line_with_project_state_mining_enabled_linked_to_project_false_symbol(
+ mocker, consolidated_issue, project_status
+):
+ # Arrange
+ consolidated_issue.project_issue_statuses = [project_status]
+ expected_md_issue_line = (
+ "| TestOrg | TestRepo | [#42 - Sample Issue](features#sample-issue) | 🔴 | In Progress |"
+ "GitHub link |\n"
+ )
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+
+ # Assert
+ assert expected_md_issue_line == actual
+
+
+def test_generate_markdown_line_with_project_state_mining_disabled(mocker, consolidated_issue, project_status):
+ # Arrange
+ consolidated_issue.project_issue_statuses = [project_status]
+ expected_md_issue_line = (
+ "| TestOrg | TestRepo | [#42 - Sample Issue](features#sample-issue) | OPEN |"
+ "GitHub link |\n"
+ )
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False
+ )
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+
+ # Assert
+ assert expected_md_issue_line == actual
+
+
+# _generate_issue_summary_table
+
+
+def test_generate_issue_summary_table_without_project_state_mining(mocker, generator, consolidated_issue):
+ # Arrange
+ expected_issue_info = (
+ "| Attribute | Content |\n"
+ "|---|---|\n"
+ "| Organization name | TestOrg |\n"
+ "| Repository name | TestRepo |\n"
+ "| Issue number | 42 |\n"
+ "| State | open |\n"
+ "| Issue URL | GitHub link |\n"
+ "| Created at | 2024-01-01T00:00:00Z |\n"
+ "| Updated at | 2024-01-02T00:00:00Z |\n"
+ "| Closed at | None |\n"
+ "| Labels | bug, urgent |\n"
+ )
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False
+ )
+
+ actual = generator._generate_issue_summary_table(consolidated_issue)
+
+ assert expected_issue_info == actual
+
+
+def test_generate_issue_summary_table_with_project_state_mining_and_multiple_project_statuses(
+ mocker, consolidated_issue, project_status
+):
+ # Arrange
+ consolidated_issue.linked_to_project = True
+ consolidated_issue.project_issue_statuses = [project_status, project_status]
+ expected_issue_info = (
+ "| Attribute | Content |\n"
+ "|---|---|\n"
+ "| Organization name | TestOrg |\n"
+ "| Repository name | TestRepo |\n"
+ "| Issue number | 42 |\n"
+ "| State | open |\n"
+ "| Issue URL | GitHub link |\n"
+ "| Created at | 2024-01-01T00:00:00Z |\n"
+ "| Updated at | 2024-01-02T00:00:00Z |\n"
+ "| Closed at | None |\n"
+ "| Labels | bug, urgent |\n"
+ "| Project title | Test Project |\n"
+ "| Status | In Progress |\n"
+ "| Priority | High |\n"
+ "| Size | Large |\n"
+ "| MoSCoW | Must Have |\n"
+ "| Project title | Test Project |\n"
+ "| Status | In Progress |\n"
+ "| Priority | High |\n"
+ "| Size | Large |\n"
+ "| MoSCoW | Must Have |\n"
+ )
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_issue_summary_table(consolidated_issue)
+
+ # Assert
+ assert expected_issue_info == actual
+
+
+def test_generate_issue_summary_table_with_project_state_mining_but_no_linked_project(
+ mocker, consolidated_issue, project_status
+):
+ # Arrange
+ consolidated_issue.linked_to_project = False
+ expected_issue_info = (
+ "| Attribute | Content |\n"
+ "|---|---|\n"
+ "| Organization name | TestOrg |\n"
+ "| Repository name | TestRepo |\n"
+ "| Issue number | 42 |\n"
+ "| State | open |\n"
+ "| Issue URL | GitHub link |\n"
+ "| Created at | 2024-01-01T00:00:00Z |\n"
+ "| Updated at | 2024-01-02T00:00:00Z |\n"
+ "| Closed at | None |\n"
+ "| Labels | bug, urgent |\n"
+ "| Linked to project | 🔴 |\n"
+ )
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_issue_summary_table(consolidated_issue)
+
+ assert expected_issue_info == actual
+
+
+# _generate_index_directory_path
+
+
+def test_generate_index_directory_path_with_structured_output_grouped_by_topics(mocker):
+ # Arrange
+ repository_id = "org123/repo456"
+ topic = "documentation"
+ expected_path = "/base/output/org123/repo456/documentation"
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
+ mocker.patch("os.makedirs")
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+
+ # Assert
+ assert expected_path == actual
+
+
+def test_generate_index_directory_path_with_structured_output_not_grouped_by_topics(mocker):
+ # Arrange
+ repository_id = "org123/repo456"
+ topic = None
+ expected_path = "/base/output/org123/repo456"
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False
+ )
+ mocker.patch("os.makedirs")
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+
+ # Assert
+ assert expected_path == actual
+
+
+def test_generate_index_directory_path_with_only_grouping_by_topic_no_structured_output(mocker):
+ # Arrange
+ repository_id = "org123/repo456"
+ topic = "documentation"
+ expected_path = "/base/output/documentation"
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
+ mocker.patch("os.makedirs")
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+
+ assert expected_path == actual
+
+
+def test_generate_index_directory_path_with_no_structured_output_and_no_grouping_by_topics(mocker):
+ # Arrange
+ repository_id = None
+ topic = None
+ expected_path = "/base/output"
+
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_structured_output_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False
+ )
+ mocker.patch("os.makedirs")
+
+ # Act
+ actual = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+
+ # Assert
+ assert expected_path == actual
+
+
+# _load_all_templates
+
+
+def test_load_all_templates_loads_correctly(mocker):
+ # Arrange
+ expected_templates = (
+ "Issue Page Template Content",
+ "Index Page Template Content",
+ "Root Level Template Content",
+ "Organization Level Template Content",
+ "Repository Level Template Content",
+ "Data Level Template Content",
+ )
+
+ load_template_mock = mocker.patch("living_documentation_generator.generator.load_template")
+ load_template_mock.side_effect = [
+ "Issue Page Template Content",
+ "Index Page Template Content",
+ "Root Level Template Content",
+ "Organization Level Template Content",
+ "Repository Level Template Content",
+ "Data Level Template Content",
+ ]
+
+ # Act
+ actual = LivingDocumentationGenerator._load_all_templates()
+
+ assert actual == expected_templates
+ assert load_template_mock.call_count == 6
+
+
+def test_load_all_templates_loads_just_some_templates(mocker):
+ # Arrange
+ expected_templates = (
+ None,
+ None,
+ None,
+ None,
+ None,
+ "Data Level Template Content",
+ )
+
+ load_template_mock = mocker.patch("living_documentation_generator.generator.load_template")
+ load_template_mock.side_effect = [
+ None,
+ None,
+ None,
+ None,
+ None,
+ "Data Level Template Content",
+ ]
+
+ # Act
+ actual = LivingDocumentationGenerator._load_all_templates()
+
+ # Assert
+ assert actual == expected_templates
+ assert load_template_mock.call_count == 6
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..a292ab5
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,46 @@
+#
+# Copyright 2024 ABSA Group Limited
+#
+# 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 os
+
+from living_documentation_generator.generator import LivingDocumentationGenerator
+from main import run
+
+
+# run
+
+
+def test_run_correct_behaviour(mocker):
+ # Arrange
+ mock_log_info = mocker.patch("logging.getLogger").return_value.info
+ mock_get_action_input = mocker.patch("main.get_action_input")
+ mock_get_action_input.side_effect = lambda first_arg, **kwargs: (
+ "./user/output/path" if first_arg == "OUTPUT_PATH" else None
+ )
+ mocker.patch("main.ActionInputs.get_output_directory", return_value="./user/output/path")
+ mocker.patch.dict(os.environ, {"INPUT_GITHUB_TOKEN": "fake_token"})
+ mocker.patch.object(LivingDocumentationGenerator, "generate")
+
+ # Act
+ run()
+
+ # Assert
+ mock_log_info.assert_has_calls(
+ [
+ mocker.call("Starting Living Documentation generation."),
+ mocker.call("Living Documentation generation - output path set to `%s`.", "./user/output/path"),
+ mocker.call("Living Documentation generation completed."),
+ ]
+ )