From bf8b45def29db93a338c472cbc0fb3f13b3c755e Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 4 Nov 2024 12:30:56 +0100
Subject: [PATCH 01/24] Introducing `TOPIC` term as a replacement for `LABEL`.
---
living_documentation_generator/generator.py | 30 +++++++++----------
.../model/config_repository.py | 10 +++----
.../model/consolidated_issue.py | 6 ++--
tests/model/test_config_repository.py | 6 ++--
tests/test_action_inputs.py | 8 ++---
5 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index 93f515a..6fd2c63 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -88,7 +88,7 @@ def generate(self) -> None:
self._clean_output_directory()
logger.debug("Output directory cleaned.")
- # Data mine GitHub issues with defined labels from all repositories
+ # Data mine GitHub issues with defined topics from all repositories
logger.info("Fetching repository GitHub issues - started.")
repository_issues: dict[str, list[Issue]] = self._fetch_github_issues()
# Note: got dict of list of issues for each repository (key is repository id)
@@ -127,8 +127,8 @@ def _clean_output_directory() -> None:
def _fetch_github_issues(self) -> dict[str, list[Issue]]:
"""
- Fetch GitHub repository issues using the GitHub library. Only issues with correct labels are fetched,
- if no labels are defined in the configuration, all repository issues are fetched.
+ Fetch GitHub repository issues using the GitHub library. Only issues with correct topic are fetched,
+ if no topics are defined in the configuration, all repository issues are fetched.
@return: A dictionary containing repository issue objects with unique key.
"""
@@ -145,8 +145,8 @@ def _fetch_github_issues(self) -> dict[str, list[Issue]]:
logger.info("Fetching repository GitHub issues - from `%s`.", repository.full_name)
- # If the query labels are not defined, fetch all issues from the repository
- if not config_repository.query_labels:
+ # If the query topics are not defined, fetch all issues from the repository
+ if not config_repository.topics:
logger.debug("Fetching all issues in the repository")
issues[repository_id] = self.__safe_call(repository.get_issues)(state=ISSUE_STATE_ALL)
amount_of_issues_per_repo = len(list(issues[repository_id]))
@@ -156,13 +156,13 @@ def _fetch_github_issues(self) -> dict[str, list[Issue]]:
repository.full_name,
)
else:
- # Fetch only issues with required labels from configuration
+ # Fetch only issues with required topics from configuration
issues[repository_id] = []
- logger.debug("Labels to be fetched from: %s.", config_repository.query_labels)
- for label in config_repository.query_labels:
- logger.debug("Fetching issues with label `%s`.", label)
+ logger.debug("Topics to be fetched from: %s.", config_repository.topics)
+ for topic in config_repository.topics:
+ logger.debug("Fetching issues with topic `%s`.", topic)
issues[repository_id].extend(
- self.__safe_call(repository.get_issues)(state=ISSUE_STATE_ALL, labels=[label])
+ self.__safe_call(repository.get_issues)(state=ISSUE_STATE_ALL, labels=[topic])
)
amount_of_issues_per_repo = len(issues[repository_id])
@@ -543,9 +543,9 @@ def _generate_issue_summary_table(consolidated_issue: ConsolidatedIssue) -> str:
@param consolidated_issue: The ConsolidatedIssue object containing the issue data.
@return: The string representation of the issue info in a table format.
"""
- # Join issue labels into one string
- labels = consolidated_issue.labels
- labels = ", ".join(labels) if labels else None
+ # Join issue topics into one string
+ topics = consolidated_issue.topics
+ topics = ", ".join(topics) if topics else None
# Format issue URL as a Markdown link
issue_url = consolidated_issue.html_url
@@ -561,7 +561,7 @@ def _generate_issue_summary_table(consolidated_issue: ConsolidatedIssue) -> str:
"Created at",
"Updated at",
"Closed at",
- "Labels",
+ "Topics",
]
# Define the values for the issue summary table
@@ -574,7 +574,7 @@ def _generate_issue_summary_table(consolidated_issue: ConsolidatedIssue) -> str:
consolidated_issue.created_at,
consolidated_issue.updated_at,
consolidated_issue.closed_at,
- labels,
+ topics,
]
# Update the summary table, based on the project data mining situation
diff --git a/living_documentation_generator/model/config_repository.py b/living_documentation_generator/model/config_repository.py
index 32073ca..5b92558 100644
--- a/living_documentation_generator/model/config_repository.py
+++ b/living_documentation_generator/model/config_repository.py
@@ -33,7 +33,7 @@ class ConfigRepository:
def __init__(self):
self.__organization_name: str = ""
self.__repository_name: str = ""
- self.__query_labels: list[Optional[str]] = [None]
+ self.__topics: list[Optional[str]] = [None]
self.__projects_title_filter: list[Optional[str]] = [None]
@property
@@ -47,9 +47,9 @@ def repository_name(self) -> str:
return self.__repository_name
@property
- def query_labels(self) -> list[str]:
- """Getter of the query labels."""
- return self.__query_labels
+ def topics(self) -> list[str]:
+ """Getter of the topics."""
+ return self.__topics
@property
def projects_title_filter(self) -> list[str]:
@@ -66,7 +66,7 @@ def load_from_json(self, repository_json: dict) -> bool:
try:
self.__organization_name = repository_json["organization-name"]
self.__repository_name = repository_json["repository-name"]
- self.__query_labels = repository_json["query-labels"]
+ self.__topics = repository_json["topics"]
self.__projects_title_filter = repository_json["projects-title-filter"]
return True
except KeyError as e:
diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_generator/model/consolidated_issue.py
index a4e099d..c9416a0 100644
--- a/living_documentation_generator/model/consolidated_issue.py
+++ b/living_documentation_generator/model/consolidated_issue.py
@@ -108,10 +108,10 @@ def body(self) -> str:
return self.__issue.body if self.__issue else ""
@property
- def labels(self) -> list[str]:
- """Getter of the issue labels."""
+ def topics(self) -> list[str]:
+ """Getter of the topics."""
if self.__issue:
- return [label.name for label in self.__issue.labels]
+ return [topic.name for topic in self.__issue.labels]
return []
# Project properties
diff --git a/tests/model/test_config_repository.py b/tests/model/test_config_repository.py
index ae1e026..291ba59 100644
--- a/tests/model/test_config_repository.py
+++ b/tests/model/test_config_repository.py
@@ -21,13 +21,13 @@ def test_load_from_json_with_valid_input_loads_correctly():
config_repository = ConfigRepository()
organization_name = "organizationABC"
repository_name = "repositoryABC"
- query_labels = ["feature", "bug"]
+ topics = ["DocumentedUserStory", "DocumentedFeature"]
projects_title_filter = ["project1"]
other_value = "other-value"
repository_json = {
"organization-name": organization_name,
"repository-name": repository_name,
- "query-labels": query_labels,
+ "topics": topics,
"projects-title-filter": projects_title_filter,
"other-field": other_value,
}
@@ -37,7 +37,7 @@ def test_load_from_json_with_valid_input_loads_correctly():
assert actual
assert organization_name == config_repository.organization_name
assert repository_name == config_repository.repository_name
- assert query_labels == config_repository.query_labels
+ assert topics == config_repository.topics
assert projects_title_filter == config_repository.projects_title_filter
diff --git a/tests/test_action_inputs.py b/tests/test_action_inputs.py
index 2239a8e..bc36f35 100644
--- a/tests/test_action_inputs.py
+++ b/tests/test_action_inputs.py
@@ -27,13 +27,13 @@ def test_get_repositories_correct_behaviour(mocker):
{
"organization-name": "organizationABC",
"repository-name": "repositoryABC",
- "query-labels": ["feature"],
+ "topics": ["DocumentedFeature"],
"projects-title-filter": [],
},
{
"organization-name": "organizationXYZ",
"repository-name": "repositoryXYZ",
- "query-labels": ["bug"],
+ "topics": ["DocumentedUserStory"],
"projects-title-filter": ["wanted_project"],
},
]
@@ -47,12 +47,12 @@ def test_get_repositories_correct_behaviour(mocker):
assert isinstance(actual[0], ConfigRepository)
assert "organizationABC" == actual[0].organization_name
assert "repositoryABC" == actual[0].repository_name
- assert ["feature"] == actual[0].query_labels
+ assert ["DocumentedFeature"] == actual[0].topics
assert [] == actual[0].projects_title_filter
assert isinstance(actual[1], ConfigRepository)
assert "organizationXYZ" == actual[1].organization_name
assert "repositoryXYZ" == actual[1].repository_name
- assert ["bug"] == actual[1].query_labels
+ assert ["DocumentedUserStory"] == actual[1].topics
assert ["wanted_project"] == actual[1].projects_title_filter
From 164128c4200df441794cb9fedc8a8a8d02a4bcf6 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 4 Nov 2024 12:45:07 +0100
Subject: [PATCH 02/24] Introducing new action input `GROUP_OUTPUT_BY_TOPICS`.
---
action.yml | 4 ++++
living_documentation_generator/action_inputs.py | 11 ++++++++++-
living_documentation_generator/utils/constants.py | 1 +
3 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/action.yml b/action.yml
index e484f03..d1bd9b8 100644
--- a/action.yml
+++ b/action.yml
@@ -23,6 +23,10 @@ inputs:
description: 'Enable or disable structured output.'
required: false
default: 'false'
+ group-output-by-topics:
+ description: 'Enable or disable grouping tickets by topics in the summary index.md file.'
+ required: false
+ default: 'false'
outputs:
output-path:
description: 'Path to the generated living documentation files'
diff --git a/living_documentation_generator/action_inputs.py b/living_documentation_generator/action_inputs.py
index 8765329..7cd0007 100644
--- a/living_documentation_generator/action_inputs.py
+++ b/living_documentation_generator/action_inputs.py
@@ -30,8 +30,9 @@
GITHUB_TOKEN,
PROJECT_STATE_MINING,
REPOSITORIES,
- OUTPUT_PATH,
+ GROUP_OUTPUT_BY_TOPICS,
STRUCTURED_OUTPUT,
+ OUTPUT_PATH,
DEFAULT_OUTPUT_PATH,
)
@@ -60,6 +61,14 @@ def get_is_project_state_mining_enabled() -> bool:
"""
return get_action_input(PROJECT_STATE_MINING, "false").lower() == "true"
+ @staticmethod
+ def get_is_grouping_by_topics_enabled() -> bool:
+ """
+ Getter of the switch, that will group the tickets in the index.md file by topics.
+ @return: True if grouping by topics is enabled, False otherwise.
+ """
+ return get_action_input(GROUP_OUTPUT_BY_TOPICS, "false").lower() == "true"
+
@staticmethod
def get_is_structured_output_enabled() -> bool:
"""
diff --git a/living_documentation_generator/utils/constants.py b/living_documentation_generator/utils/constants.py
index fff60d6..e7d13ca 100644
--- a/living_documentation_generator/utils/constants.py
+++ b/living_documentation_generator/utils/constants.py
@@ -25,6 +25,7 @@
OUTPUT_PATH = "OUTPUT_PATH"
DEFAULT_OUTPUT_PATH = "./output"
STRUCTURED_OUTPUT = "STRUCTURED_OUTPUT"
+GROUP_OUTPUT_BY_TOPICS = "GROUP_OUTPUT_BY_TOPICS"
# GitHub API constants
ISSUES_PER_PAGE_LIMIT = 100
From 720e4ee00ed4d35726e99890ac0784ea62395c4e Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 4 Nov 2024 13:14:30 +0100
Subject: [PATCH 03/24] Revert "Introducing `TOPIC` term as a replacement for
`LABEL`."
This reverts commit bf8b45def29db93a338c472cbc0fb3f13b3c755e.
---
living_documentation_generator/generator.py | 30 +++++++++----------
.../model/config_repository.py | 10 +++----
.../model/consolidated_issue.py | 6 ++--
tests/model/test_config_repository.py | 6 ++--
tests/test_action_inputs.py | 8 ++---
5 files changed, 30 insertions(+), 30 deletions(-)
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index 6fd2c63..93f515a 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -88,7 +88,7 @@ def generate(self) -> None:
self._clean_output_directory()
logger.debug("Output directory cleaned.")
- # Data mine GitHub issues with defined topics from all repositories
+ # Data mine GitHub issues with defined labels from all repositories
logger.info("Fetching repository GitHub issues - started.")
repository_issues: dict[str, list[Issue]] = self._fetch_github_issues()
# Note: got dict of list of issues for each repository (key is repository id)
@@ -127,8 +127,8 @@ def _clean_output_directory() -> None:
def _fetch_github_issues(self) -> dict[str, list[Issue]]:
"""
- Fetch GitHub repository issues using the GitHub library. Only issues with correct topic are fetched,
- if no topics are defined in the configuration, all repository issues are fetched.
+ Fetch GitHub repository issues using the GitHub library. Only issues with correct labels are fetched,
+ if no labels are defined in the configuration, all repository issues are fetched.
@return: A dictionary containing repository issue objects with unique key.
"""
@@ -145,8 +145,8 @@ def _fetch_github_issues(self) -> dict[str, list[Issue]]:
logger.info("Fetching repository GitHub issues - from `%s`.", repository.full_name)
- # If the query topics are not defined, fetch all issues from the repository
- if not config_repository.topics:
+ # If the query labels are not defined, fetch all issues from the repository
+ if not config_repository.query_labels:
logger.debug("Fetching all issues in the repository")
issues[repository_id] = self.__safe_call(repository.get_issues)(state=ISSUE_STATE_ALL)
amount_of_issues_per_repo = len(list(issues[repository_id]))
@@ -156,13 +156,13 @@ def _fetch_github_issues(self) -> dict[str, list[Issue]]:
repository.full_name,
)
else:
- # Fetch only issues with required topics from configuration
+ # Fetch only issues with required labels from configuration
issues[repository_id] = []
- logger.debug("Topics to be fetched from: %s.", config_repository.topics)
- for topic in config_repository.topics:
- logger.debug("Fetching issues with topic `%s`.", topic)
+ logger.debug("Labels to be fetched from: %s.", config_repository.query_labels)
+ for label in config_repository.query_labels:
+ logger.debug("Fetching issues with label `%s`.", label)
issues[repository_id].extend(
- self.__safe_call(repository.get_issues)(state=ISSUE_STATE_ALL, labels=[topic])
+ self.__safe_call(repository.get_issues)(state=ISSUE_STATE_ALL, labels=[label])
)
amount_of_issues_per_repo = len(issues[repository_id])
@@ -543,9 +543,9 @@ def _generate_issue_summary_table(consolidated_issue: ConsolidatedIssue) -> str:
@param consolidated_issue: The ConsolidatedIssue object containing the issue data.
@return: The string representation of the issue info in a table format.
"""
- # Join issue topics into one string
- topics = consolidated_issue.topics
- topics = ", ".join(topics) if topics else None
+ # Join issue labels into one string
+ labels = consolidated_issue.labels
+ labels = ", ".join(labels) if labels else None
# Format issue URL as a Markdown link
issue_url = consolidated_issue.html_url
@@ -561,7 +561,7 @@ def _generate_issue_summary_table(consolidated_issue: ConsolidatedIssue) -> str:
"Created at",
"Updated at",
"Closed at",
- "Topics",
+ "Labels",
]
# Define the values for the issue summary table
@@ -574,7 +574,7 @@ def _generate_issue_summary_table(consolidated_issue: ConsolidatedIssue) -> str:
consolidated_issue.created_at,
consolidated_issue.updated_at,
consolidated_issue.closed_at,
- topics,
+ labels,
]
# Update the summary table, based on the project data mining situation
diff --git a/living_documentation_generator/model/config_repository.py b/living_documentation_generator/model/config_repository.py
index 5b92558..32073ca 100644
--- a/living_documentation_generator/model/config_repository.py
+++ b/living_documentation_generator/model/config_repository.py
@@ -33,7 +33,7 @@ class ConfigRepository:
def __init__(self):
self.__organization_name: str = ""
self.__repository_name: str = ""
- self.__topics: list[Optional[str]] = [None]
+ self.__query_labels: list[Optional[str]] = [None]
self.__projects_title_filter: list[Optional[str]] = [None]
@property
@@ -47,9 +47,9 @@ def repository_name(self) -> str:
return self.__repository_name
@property
- def topics(self) -> list[str]:
- """Getter of the topics."""
- return self.__topics
+ def query_labels(self) -> list[str]:
+ """Getter of the query labels."""
+ return self.__query_labels
@property
def projects_title_filter(self) -> list[str]:
@@ -66,7 +66,7 @@ def load_from_json(self, repository_json: dict) -> bool:
try:
self.__organization_name = repository_json["organization-name"]
self.__repository_name = repository_json["repository-name"]
- self.__topics = repository_json["topics"]
+ self.__query_labels = repository_json["query-labels"]
self.__projects_title_filter = repository_json["projects-title-filter"]
return True
except KeyError as e:
diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_generator/model/consolidated_issue.py
index c9416a0..a4e099d 100644
--- a/living_documentation_generator/model/consolidated_issue.py
+++ b/living_documentation_generator/model/consolidated_issue.py
@@ -108,10 +108,10 @@ def body(self) -> str:
return self.__issue.body if self.__issue else ""
@property
- def topics(self) -> list[str]:
- """Getter of the topics."""
+ def labels(self) -> list[str]:
+ """Getter of the issue labels."""
if self.__issue:
- return [topic.name for topic in self.__issue.labels]
+ return [label.name for label in self.__issue.labels]
return []
# Project properties
diff --git a/tests/model/test_config_repository.py b/tests/model/test_config_repository.py
index 291ba59..ae1e026 100644
--- a/tests/model/test_config_repository.py
+++ b/tests/model/test_config_repository.py
@@ -21,13 +21,13 @@ def test_load_from_json_with_valid_input_loads_correctly():
config_repository = ConfigRepository()
organization_name = "organizationABC"
repository_name = "repositoryABC"
- topics = ["DocumentedUserStory", "DocumentedFeature"]
+ query_labels = ["feature", "bug"]
projects_title_filter = ["project1"]
other_value = "other-value"
repository_json = {
"organization-name": organization_name,
"repository-name": repository_name,
- "topics": topics,
+ "query-labels": query_labels,
"projects-title-filter": projects_title_filter,
"other-field": other_value,
}
@@ -37,7 +37,7 @@ def test_load_from_json_with_valid_input_loads_correctly():
assert actual
assert organization_name == config_repository.organization_name
assert repository_name == config_repository.repository_name
- assert topics == config_repository.topics
+ assert query_labels == config_repository.query_labels
assert projects_title_filter == config_repository.projects_title_filter
diff --git a/tests/test_action_inputs.py b/tests/test_action_inputs.py
index bc36f35..2239a8e 100644
--- a/tests/test_action_inputs.py
+++ b/tests/test_action_inputs.py
@@ -27,13 +27,13 @@ def test_get_repositories_correct_behaviour(mocker):
{
"organization-name": "organizationABC",
"repository-name": "repositoryABC",
- "topics": ["DocumentedFeature"],
+ "query-labels": ["feature"],
"projects-title-filter": [],
},
{
"organization-name": "organizationXYZ",
"repository-name": "repositoryXYZ",
- "topics": ["DocumentedUserStory"],
+ "query-labels": ["bug"],
"projects-title-filter": ["wanted_project"],
},
]
@@ -47,12 +47,12 @@ def test_get_repositories_correct_behaviour(mocker):
assert isinstance(actual[0], ConfigRepository)
assert "organizationABC" == actual[0].organization_name
assert "repositoryABC" == actual[0].repository_name
- assert ["DocumentedFeature"] == actual[0].topics
+ assert ["feature"] == actual[0].query_labels
assert [] == actual[0].projects_title_filter
assert isinstance(actual[1], ConfigRepository)
assert "organizationXYZ" == actual[1].organization_name
assert "repositoryXYZ" == actual[1].repository_name
- assert ["DocumentedUserStory"] == actual[1].topics
+ assert ["bug"] == actual[1].query_labels
assert ["wanted_project"] == actual[1].projects_title_filter
From 358f62ec0e5ef7d067ed3ad823520c7f030f303d Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Tue, 5 Nov 2024 09:40:13 +0100
Subject: [PATCH 04/24] Implementing logic, for having Topic structured output.
---
living_documentation_generator/generator.py | 7 +++--
.../model/consolidated_issue.py | 30 +++++++++++++++++++
2 files changed, 34 insertions(+), 3 deletions(-)
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index 93f515a..c5978ef 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -380,7 +380,8 @@ def _generate_md_issue_page(self, issue_page_template: str, consolidated_issue:
issue_md_page_content = issue_page_template.format(**replacements)
# Create a directory structure path for the issue page
- page_directory_path = self._generate_directory_path(consolidated_issue.repository_id)
+ page_directory_path = consolidated_issue.generate_directory_path(issue_table)
+ os.makedirs(page_directory_path, exist_ok=True)
# Save the single issue Markdown page
page_filename = consolidated_issue.generate_page_filename()
@@ -464,7 +465,7 @@ def _generate_index_page(
# Generate a directory structure path for the index page
# Note: repository_id is used only, if the structured output is generated
- index_directory_path = self._generate_directory_path(repository_id)
+ index_directory_path = self._generate_index_directory_path(repository_id)
# Create an index page file
with open(os.path.join(index_directory_path, "_index.md"), "w", encoding="utf-8") as f:
@@ -617,7 +618,7 @@ def _generate_issue_summary_table(consolidated_issue: ConsolidatedIssue) -> str:
return issue_info
@staticmethod
- def _generate_directory_path(repository_id: Optional[str]) -> str:
+ def _generate_index_directory_path(repository_id: Optional[str]) -> str:
"""
Generates a directory path based on if structured output is required.
diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_generator/model/consolidated_issue.py
index a4e099d..f9342b5 100644
--- a/living_documentation_generator/model/consolidated_issue.py
+++ b/living_documentation_generator/model/consolidated_issue.py
@@ -18,10 +18,13 @@
This module contains a data container for Consolidated Issue, which holds all the essential logic.
"""
import logging
+import os
+import re
from typing import Optional
from github.Issue import Issue
+from living_documentation_generator.action_inputs import ActionInputs
from living_documentation_generator.utils.utils import sanitize_filename
from living_documentation_generator.model.project_status import ProjectStatus
@@ -161,3 +164,30 @@ def generate_page_filename(self) -> str:
return f"{self.number}.md"
return page_filename
+
+ def generate_directory_path(self, issue_table: str) -> str:
+ output_path = ActionInputs.get_output_directory()
+
+ # If structured output is enabled, create a directory path based on the repository
+ if ActionInputs.get_is_structured_output_enabled() and self.repository_id:
+ organization_name, repository_name = self.repository_id.split("/")
+ output_path = os.path.join(output_path, organization_name, repository_name)
+
+ # If grouping by topics is enabled, create a directory path based on the issue topic
+ if ActionInputs.get_is_grouping_by_topics_enabled():
+ # Extract labels from the issue table
+ labels = re.findall(r"\| Labels \| (.*?) \|", issue_table)
+ if labels:
+ labels = labels[0].split(", ")
+
+ # Check for labels ending with "Topic" and generate a directory path based on it
+ for label in labels:
+ if label.endswith("Topic"):
+ topic_path = os.path.join(output_path, label)
+ return topic_path
+
+ # If no label ends with "Topic", create a "noTopic" issue directory path
+ no_topic_path = os.path.join(output_path, "noTopic")
+ return no_topic_path
+
+ return output_path
From 5fa1088eed49411dc4459164fb137e3dea5eafc5 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Tue, 5 Nov 2024 10:46:56 +0100
Subject: [PATCH 05/24] Logic for Topics without structured output.
---
living_documentation_generator/generator.py | 25 +++++++++++++++----
.../model/consolidated_issue.py | 8 ++++++
2 files changed, 28 insertions(+), 5 deletions(-)
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index c5978ef..78264fd 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -290,6 +290,7 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
@param issues: A dictionary containing all consolidated issues.
"""
+ output_path = ActionInputs.get_output_directory()
issue_page_detail_template = None
index_page_template = None
index_root_level_page = None
@@ -342,7 +343,13 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
if ActionInputs.get_is_structured_output_enabled():
self._generate_structured_index_pages(index_repo_level_template, index_org_level_template, issues)
- output_path = ActionInputs.get_output_directory()
+ with open(os.path.join(output_path, "_index.md"), "w", encoding="utf-8") as f:
+ f.write(index_root_level_page)
+ elif ActionInputs.get_is_grouping_by_topics_enabled():
+ issues = list(issues.values())
+ topics = set([issue.topic for issue in issues])
+ for topic in topics:
+ self._generate_index_page(index_page_template, issues, topic=topic)
with open(os.path.join(output_path, "_index.md"), "w", encoding="utf-8") as f:
f.write(index_root_level_page)
else:
@@ -430,7 +437,7 @@ def _generate_structured_index_pages(
logger.info("Markdown page generation - generated `_index.md` pages for %s.", repository_id)
def _generate_index_page(
- self, issue_index_page_template: str, consolidated_issues: list[ConsolidatedIssue], repository_id: str = None
+ self, issue_index_page_template: str, consolidated_issues: list[ConsolidatedIssue], repository_id: str = None, topic: str = None
) -> None:
"""
Generates an index page with a summary of all issues and save it to the output directory.
@@ -438,6 +445,7 @@ def _generate_index_page(
@param issue_index_page_template: The template string for generating the index markdown page.
@param consolidated_issues: A dictionary containing all consolidated issues.
@param repository_id: The repository id used if the structured output is generated.
+ @param topic: The topic used if the grouping issues by topics is enabled.
@return: None
"""
# Initializing the issue table header based on the project mining state
@@ -449,7 +457,11 @@ def _generate_index_page(
# Create an issue summary table for every issue
for consolidated_issue in consolidated_issues:
- issue_table += self._generate_markdown_line(consolidated_issue)
+ if ActionInputs.get_is_grouping_by_topics_enabled():
+ if topic == consolidated_issue.topic:
+ issue_table += self._generate_markdown_line(consolidated_issue)
+ else:
+ issue_table += self._generate_markdown_line(consolidated_issue)
# Prepare issues replacement for the index page
replacement = {
@@ -465,7 +477,7 @@ def _generate_index_page(
# Generate a directory structure path for the index page
# Note: repository_id is used only, if the structured output is generated
- index_directory_path = self._generate_index_directory_path(repository_id)
+ index_directory_path = self._generate_index_directory_path(repository_id, topic)
# Create an index page file
with open(os.path.join(index_directory_path, "_index.md"), "w", encoding="utf-8") as f:
@@ -618,7 +630,7 @@ def _generate_issue_summary_table(consolidated_issue: ConsolidatedIssue) -> str:
return issue_info
@staticmethod
- def _generate_index_directory_path(repository_id: Optional[str]) -> str:
+ def _generate_index_directory_path(repository_id: Optional[str], topic: Optional[str]) -> str:
"""
Generates a directory path based on if structured output is required.
@@ -631,6 +643,9 @@ def _generate_index_directory_path(repository_id: Optional[str]) -> str:
organization_name, repository_name = repository_id.split("/")
output_path = os.path.join(output_path, organization_name, repository_name)
+ if ActionInputs.get_is_grouping_by_topics_enabled():
+ output_path = os.path.join(output_path, topic)
+
os.makedirs(output_path, exist_ok=True)
return output_path
diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_generator/model/consolidated_issue.py
index f9342b5..94366eb 100644
--- a/living_documentation_generator/model/consolidated_issue.py
+++ b/living_documentation_generator/model/consolidated_issue.py
@@ -47,6 +47,7 @@ def __init__(self, repository_id: str, repository_issue: Issue = None):
parts = repository_id.split("/")
self.__organization_name: str = parts[0] if len(parts) == 2 else ""
self.__repository_name: str = parts[1] if len(parts) == 2 else ""
+ self.__topic: str = ""
# Extra project data (optionally provided from GithubProjects class)
self.__linked_to_project: bool = False
@@ -75,6 +76,11 @@ def repository_name(self) -> str:
"""Getter of the repository name where the issue was fetched from."""
return self.__repository_name
+ @property
+ def topic(self) -> str:
+ """Getter of the issue topic."""
+ return self.__topic
+
@property
def title(self) -> str:
"""Getter of the issue title."""
@@ -183,10 +189,12 @@ def generate_directory_path(self, issue_table: str) -> str:
# Check for labels ending with "Topic" and generate a directory path based on it
for label in labels:
if label.endswith("Topic"):
+ self.__topic = label
topic_path = os.path.join(output_path, label)
return topic_path
# If no label ends with "Topic", create a "noTopic" issue directory path
+ self.__topic = "noTopic"
no_topic_path = os.path.join(output_path, "noTopic")
return no_topic_path
From 592fce13b5b014d564517fbff11505a57663bb3e Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Tue, 5 Nov 2024 12:21:50 +0100
Subject: [PATCH 06/24] Logic for grouping issues by Topics.
---
living_documentation_generator/generator.py | 115 +++++++++++++-----
living_documentation_generator/utils/utils.py | 12 ++
....md => _index_data_level_page_template.md} | 2 +-
templates/_index_no_struct_page_template.md | 2 +-
templates/_index_org_level_page_template.md | 2 +-
templates/_index_repo_page_template.md | 7 ++
6 files changed, 104 insertions(+), 36 deletions(-)
rename templates/{_index_repo_level_page_template.md => _index_data_level_page_template.md} (96%)
create mode 100644 templates/_index_repo_page_template.md
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index 78264fd..7291bb0 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -35,7 +35,7 @@
from living_documentation_generator.model.project_issue import ProjectIssue
from living_documentation_generator.utils.decorators import safe_call_decorator
from living_documentation_generator.utils.github_rate_limiter import GithubRateLimiter
-from living_documentation_generator.utils.utils import make_issue_key
+from living_documentation_generator.utils.utils import make_issue_key, generate_root_level_index_page
from living_documentation_generator.utils.constants import (
ISSUES_PER_PAGE_LIMIT,
ISSUE_STATE_ALL,
@@ -67,9 +67,10 @@ class LivingDocumentationGenerator:
INDEX_ORG_LEVEL_TEMPLATE_FILE = os.path.join(
PROJECT_ROOT, os.pardir, "templates", "_index_org_level_page_template.md"
)
- INDEX_REPO_LEVEL_TEMPLATE_FILE = os.path.join(
- PROJECT_ROOT, os.pardir, "templates", "_index_repo_level_page_template.md"
+ INDEX_DATA_LEVEL_TEMPLATE_FILE = os.path.join(
+ PROJECT_ROOT, os.pardir, "templates", "_index_data_level_page_template.md"
)
+ INDEX_TOPIC_PAGE_TEMPLATE_FILE = os.path.join(PROJECT_ROOT, os.pardir, "templates", "_index_repo_page_template.md")
def __init__(self):
github_token = ActionInputs.get_github_token()
@@ -290,12 +291,15 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
@param issues: A dictionary containing all consolidated issues.
"""
+ is_structured_output = ActionInputs.get_is_structured_output_enabled()
+ is_grouping_by_topics = ActionInputs.get_is_grouping_by_topics_enabled()
output_path = ActionInputs.get_output_directory()
issue_page_detail_template = None
index_page_template = None
index_root_level_page = None
index_org_level_template = None
- index_repo_level_template = None
+ index_repo_page_template = None
+ index_data_level_template = None
# Load the template files for generating the Markdown pages
try:
@@ -327,31 +331,43 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
)
try:
- with open(LivingDocumentationGenerator.INDEX_REPO_LEVEL_TEMPLATE_FILE, "r", encoding="utf-8") as f:
- index_repo_level_template = f.read()
+ with open(LivingDocumentationGenerator.INDEX_TOPIC_PAGE_TEMPLATE_FILE, "r", encoding="utf-8") as f:
+ index_repo_page_template = f.read()
except IOError:
logger.error(
"Structured index page template file for repository level was not successfully loaded.", exc_info=True
)
+ try:
+ with open(LivingDocumentationGenerator.INDEX_DATA_LEVEL_TEMPLATE_FILE, "r", encoding="utf-8") as f:
+ index_data_level_template = f.read()
+ except IOError:
+ logger.error(
+ "Structured index page template file for data level was not successfully loaded.", exc_info=True
+ )
+
# Generate a markdown page for every issue
for consolidated_issue in issues.values():
self._generate_md_issue_page(issue_page_detail_template, consolidated_issue)
logger.info("Markdown page generation - generated `%s` issue pages.", len(issues))
- # Generate an index page with a summary table about all issues
- if ActionInputs.get_is_structured_output_enabled():
- self._generate_structured_index_pages(index_repo_level_template, index_org_level_template, issues)
+ # Generate all structure of the index pages
+ if is_structured_output:
+ generate_root_level_index_page(index_root_level_page, output_path)
+ self._generate_structured_index_pages(
+ index_data_level_template, index_repo_page_template, index_org_level_template, issues
+ )
- with open(os.path.join(output_path, "_index.md"), "w", encoding="utf-8") as f:
- f.write(index_root_level_page)
- elif ActionInputs.get_is_grouping_by_topics_enabled():
+ # Generate an index page with a summary table about all issues grouped by topics
+ elif is_grouping_by_topics:
issues = list(issues.values())
topics = set([issue.topic for issue in issues])
+ generate_root_level_index_page(index_root_level_page, output_path)
+
for topic in topics:
self._generate_index_page(index_page_template, issues, topic=topic)
- with open(os.path.join(output_path, "_index.md"), "w", encoding="utf-8") as f:
- f.write(index_root_level_page)
+
+ # Generate an index page with a summary table about all issues
else:
issues = list(issues.values())
self._generate_index_page(index_page_template, issues)
@@ -399,6 +415,7 @@ def _generate_md_issue_page(self, issue_page_template: str, consolidated_issue:
def _generate_structured_index_pages(
self,
+ index_data_level_template: str,
index_repo_level_template: str,
index_org_level_template: str,
consolidated_issues: dict[str, ConsolidatedIssue],
@@ -406,6 +423,7 @@ def _generate_structured_index_pages(
"""
Generates a set of index pages due to a structured output feature.
+ @param index_data_level_template: The template string for generating the data level index markdown page.
@param index_repo_level_template: The template string for generating the repository level index markdown page.
@param index_org_level_template: The template string for generating the organization level index markdown page.
@param consolidated_issues: A dictionary containing all consolidated issues.
@@ -421,23 +439,44 @@ def _generate_structured_index_pages(
# Generate an index page for each repository
for repository_id, issues in issues_by_repository.items():
- organization_name, _ = repository_id.split("/")
+ organization_name, repository_name = repository_id.split("/")
- self._generate_org_level_index_page(index_org_level_template, organization_name)
+ self._generate_sub_level_index_page(index_org_level_template, "org", repository_id)
logger.debug(
- "Generated organization level `_index.md` for %s as a cause of structured output feature.",
+ "Generated organization level `_index.md` for %s.",
organization_name,
)
- self._generate_index_page(index_repo_level_template, issues, repository_id)
- logger.debug(
- "Generated repository level `_index.md` for %s as a cause of structured output feature.", repository_id
- )
+ if ActionInputs.get_is_grouping_by_topics_enabled():
+ self._generate_sub_level_index_page(index_repo_level_template, "repo", repository_id)
+ logger.debug(
+ "Generated repository level _index.md` for repository: %s.",
+ repository_name,
+ )
+
+ topics = set([issue.topic for issue in issues])
+ for topic in topics:
+ self._generate_index_page(index_data_level_template, issues, repository_id, topic)
+ logger.debug(
+ "Generated data level `_index.md` with topic: %s for %s.",
+ topic,
+ repository_id,
+ )
+ else:
+ self._generate_index_page(index_data_level_template, issues, repository_id)
+ logger.debug(
+ "Generated repository level `_index.md` for %s",
+ repository_id,
+ )
logger.info("Markdown page generation - generated `_index.md` pages for %s.", repository_id)
def _generate_index_page(
- self, issue_index_page_template: str, consolidated_issues: list[ConsolidatedIssue], repository_id: str = None, topic: str = None
+ self,
+ issue_index_page_template: str,
+ consolidated_issues: list[ConsolidatedIssue],
+ repository_id: str = None,
+ topic: str = None,
) -> None:
"""
Generates an index page with a summary of all issues and save it to the output directory.
@@ -466,7 +505,7 @@ def _generate_index_page(
# Prepare issues replacement for the index page
replacement = {
"date": datetime.now().strftime("%Y-%m-%d"),
- "issue-overview-table": issue_table,
+ "issue_overview_table": issue_table,
}
if ActionInputs.get_is_structured_output_enabled():
@@ -484,27 +523,37 @@ def _generate_index_page(
f.write(index_page)
@staticmethod
- def _generate_org_level_index_page(index_org_level_template: str, organization_name: str) -> None:
+ def _generate_sub_level_index_page(index_template: str, level: str, repository_id: str) -> None:
"""
- Generates an organization level index page and save it.
+ Generates an index page for the structured output based on the level.
- @param index_org_level_template: The template string for generating the organization level index markdown page.
- @param organization_name: The name of the organization.
+ @param index_template: The template string for generating the index markdown page.
+ @param level: The level of the index page. Enum for "org" or "repo".
+ @param repository_id: The repository id of a repository that stores the issues.
@return: None
"""
- # Prepare issues replacement for the index page
+ index_level_dir = ""
replacement = {
"date": datetime.now().strftime("%Y-%m-%d"),
- "organization_name": organization_name,
}
+ # Set correct behaviour based on the level
+ if level == "org":
+ organization_name = repository_id.split("/")[0]
+ index_level_dir = organization_name
+ replacement["organization_name"] = organization_name
+ elif level == "repo":
+ repository_name = repository_id.split("/")[1]
+ index_level_dir = repository_id
+ replacement["repository_name"] = repository_name
+
# Replace the issue placeholders in the index template
- org_level_index_page = index_org_level_template.format(**replacement)
+ sub_level_index_page = index_template.format(**replacement)
# Create a sub index page file
- output_path = os.path.join(ActionInputs.get_output_directory(), organization_name)
+ output_path = os.path.join(ActionInputs.get_output_directory(), index_level_dir)
with open(os.path.join(output_path, "_index.md"), "w", encoding="utf-8") as f:
- f.write(org_level_index_page)
+ f.write(sub_level_index_page)
@staticmethod
def _generate_markdown_line(consolidated_issue: ConsolidatedIssue) -> str:
@@ -643,7 +692,7 @@ def _generate_index_directory_path(repository_id: Optional[str], topic: Optional
organization_name, repository_name = repository_id.split("/")
output_path = os.path.join(output_path, organization_name, repository_name)
- if ActionInputs.get_is_grouping_by_topics_enabled():
+ if ActionInputs.get_is_grouping_by_topics_enabled() and topic:
output_path = os.path.join(output_path, topic)
os.makedirs(output_path, exist_ok=True)
diff --git a/living_documentation_generator/utils/utils.py b/living_documentation_generator/utils/utils.py
index bb78cf3..a205861 100644
--- a/living_documentation_generator/utils/utils.py
+++ b/living_documentation_generator/utils/utils.py
@@ -101,6 +101,18 @@ def get_all_project_directories(path: str = ".") -> list[str]:
return [os.path.join(path, d) for d in os.listdir(path) if os.path.isdir(os.path.join(path, d))]
+def generate_root_level_index_page(index_root_level_page: str, output_path: str) -> None:
+ """
+ Generate the root level index page for the output living documentation.
+
+ @param index_root_level_page: The content of the root level index page.
+ @param output_path: The path to the output directory.
+ @return: None
+ """
+ with open(os.path.join(output_path, "_index.md"), "w", encoding="utf-8") as f:
+ f.write(index_root_level_page)
+
+
# GitHub action utils
def get_action_input(name: str, default: Optional[str] = None) -> str:
"""
diff --git a/templates/_index_repo_level_page_template.md b/templates/_index_data_level_page_template.md
similarity index 96%
rename from templates/_index_repo_level_page_template.md
rename to templates/_index_data_level_page_template.md
index 69e2d83..9a3bd88 100644
--- a/templates/_index_repo_level_page_template.md
+++ b/templates/_index_data_level_page_template.md
@@ -14,6 +14,6 @@ Our project is designed with a myriad of features to ensure seamless user experi
-{issue-overview-table}
+{issue_overview_table}
\ No newline at end of file
diff --git a/templates/_index_no_struct_page_template.md b/templates/_index_no_struct_page_template.md
index a0d2a70..e6550b0 100644
--- a/templates/_index_no_struct_page_template.md
+++ b/templates/_index_no_struct_page_template.md
@@ -16,6 +16,6 @@ Our project is designed with a myriad of features to ensure seamless user experi
-{issue-overview-table}
+{issue_overview_table}
diff --git a/templates/_index_org_level_page_template.md b/templates/_index_org_level_page_template.md
index 35280c2..647056a 100644
--- a/templates/_index_org_level_page_template.md
+++ b/templates/_index_org_level_page_template.md
@@ -4,4 +4,4 @@ date: {date}
weight: 0
---
-This section displays the living documentation for all repositories within the organization: {organization_name} in a structured output.
\ No newline at end of file
+This section displays the living documentation for all repositories within the organization: **{organization_name}** in a structured output.
\ No newline at end of file
diff --git a/templates/_index_repo_page_template.md b/templates/_index_repo_page_template.md
new file mode 100644
index 0000000..00f2512
--- /dev/null
+++ b/templates/_index_repo_page_template.md
@@ -0,0 +1,7 @@
+---
+title: "{repository_name}"
+date: {date}
+weight: 0
+---
+
+This section displays the living documentation for all topics within the repository: **{repository_name}** in a structured output.
\ No newline at end of file
From 311c1893d27617de851adeae46cb8c38eef3a3d5 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Tue, 5 Nov 2024 14:50:40 +0100
Subject: [PATCH 07/24] Final touches.
---
living_documentation_generator/generator.py | 8 +++++---
templates/_index_data_level_page_template.md | 6 +++---
2 files changed, 8 insertions(+), 6 deletions(-)
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index 7291bb0..fe49dfe 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -365,7 +365,7 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
generate_root_level_index_page(index_root_level_page, output_path)
for topic in topics:
- self._generate_index_page(index_page_template, issues, topic=topic)
+ self._generate_index_page(index_data_level_template, issues, topic=topic)
# Generate an index page with a summary table about all issues
else:
@@ -508,8 +508,10 @@ def _generate_index_page(
"issue_overview_table": issue_table,
}
- if ActionInputs.get_is_structured_output_enabled():
- replacement["repository_name"] = repository_id.split("/")[1]
+ if ActionInputs.get_is_grouping_by_topics_enabled():
+ replacement["data_level_name"] = topic
+ elif ActionInputs.get_is_structured_output_enabled():
+ replacement["data_level_name"] = repository_id.split("/")[1]
# Replace the issue placeholders in the index template
index_page = issue_index_page_template.format(**replacement)
diff --git a/templates/_index_data_level_page_template.md b/templates/_index_data_level_page_template.md
index 9a3bd88..a41456c 100644
--- a/templates/_index_data_level_page_template.md
+++ b/templates/_index_data_level_page_template.md
@@ -1,10 +1,10 @@
---
-title: "{repository_name}"
+title: "{data_level_name}"
date: {date}
weight: 0
---
-This section displays all the information about mined features for the repository: {repository_name}.
+This section displays all the information about mined features for **{data_level_name}**.
Feature Summary page
@@ -16,4 +16,4 @@ Our project is designed with a myriad of features to ensure seamless user experi
{issue_overview_table}
-
\ No newline at end of file
+
From 59bf373c3b21e8d69ced8c71a2f25e4681903abf Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Tue, 5 Nov 2024 15:10:00 +0100
Subject: [PATCH 08/24] Update of README.md file with new feature of grouping
issues by topics.
---
README.md | 62 ++++++++++++++++++++++++++++++++++++++++++++++---------
1 file changed, 52 insertions(+), 10 deletions(-)
diff --git a/README.md b/README.md
index 8174efc..1a52544 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,7 @@
- [Data Mining from GitHub Projects](#data-mining-from-github-projects)
- [Living Documentation Page Generation](#living-documentation-page-generation)
- [Structured Output](#structured-output)
+ - [Grouped Output by Issue Topics](#grouped-output-by-issue-topics)
- [Contribution Guidelines](#contribution-guidelines)
- [License Information](#license-information)
- [Contact or Support Information](#contact-or-support-information)
@@ -114,12 +115,15 @@ See the full example of action step definition (in example are used non-default
# project state mining feature de/activation
project-state-mining: true
-
- # project verbose (debug) logging feature de/activation
- verbose-logging: true
# structured output feature de/activation
structured-output: true
+
+ # group output by topics feature de/activation
+ group-output-by-topics: true
+
+ # project verbose (debug) logging feature de/activation
+ verbose-logging: true
```
## Action Configuration
@@ -214,22 +218,31 @@ Configure the action by customizing the following parameters based on your needs
project-state-mining: true
```
-- **verbose-logging** (optional, `default: false`)
- - **Description**: Enables or disables verbose (debug) logging.
+- **structured-output** (optional, `default: false`)
+ - **Description**: Enables or disables structured output.
- **Usage**: Set to true to activate.
- **Example**:
```yaml
with:
- verbose-logging: true
+ structured-output: true
```
-
-- **structured-output** (optional, `default: false`)
- - **Description**: Enables or disables structured output.
+
+- **group-output-by-topics** (optional, `default: false`)
+ - **Description**: Enable or disable grouping tickets by topics in the summary index.md file.
- **Usage**: Set to true to activate.
- **Example**:
```yaml
with:
- structured-output: true
+ group-output-by-topics: true
+ ```
+
+- **verbose-logging** (optional, `default: false`)
+ - **Description**: Enables or disables verbose (debug) logging.
+ - **Usage**: Set to true to activate.
+ - **Example**:
+ ```yaml
+ with:
+ verbose-logging: true
```
## Action Outputs
@@ -394,6 +407,8 @@ export INPUT_REPOSITORIES='[
]'
export INPUT_OUTPUT_PATH="/output/directory/path
export INPUT_PROJECT_STATE_MINING="true"
+export INPUT_STRUCTURED_OUTPUT="true"
+export INPUT_GROUP_OUTPUT_BY_TOPICS="true"
export INPUT_VERBOSE_LOGGING="true"
```
@@ -601,13 +616,40 @@ This feature allows you to generate structured output for the living documentati
|-- issue md page 1
|-- issue md page 2
|-- _index.md
+ |-- _index.md
|- org 2
|--repo 1
|-- issue md page 1
|-- _index.md
|--repo 2
+ ...
+ |-- _index.md
+ |- _index.md
+ ```
+
+### Grouped Output by Issue Topics
+
+The feature allows you to generate output grouped by issue topics. This feature is useful when you want to group issues by specific topics or themes.
+
+To gain a better understanding of the term "Topic", refer to the [Labels](#labels) section.
+
+- **Default Behavior**: By default, the action generates all the documentation in a single directory.
+- **Non-default Example**: Use the grouped output feature to organize the generated documentation by issue topics.
+ - `group-output-by-topics: true` activates the grouped output feature.
```
+ output
+ |- topic 1
+ |-- issue md page 1
+ |-- issue md page 2
+ |-- _index.md
+ |- topic 2
+ |-- issue md page 1
+ |-- _index.md
+ |- _index.md
+ ```
+
---
+
## Contribution Guidelines
We welcome contributions to the Living Documentation Generator! Whether you're fixing bugs, improving documentation, or proposing new features, your help is appreciated.
From 656c21e570aae2ce5ee222525c17fbce5807db11 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Tue, 5 Nov 2024 15:40:12 +0100
Subject: [PATCH 09/24] Pylint touches.
---
living_documentation_generator/generator.py | 107 +++++++++---------
.../model/consolidated_issue.py | 16 ++-
living_documentation_generator/utils/utils.py | 16 +++
3 files changed, 80 insertions(+), 59 deletions(-)
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index fe49dfe..3653427 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -35,7 +35,7 @@
from living_documentation_generator.model.project_issue import ProjectIssue
from living_documentation_generator.utils.decorators import safe_call_decorator
from living_documentation_generator.utils.github_rate_limiter import GithubRateLimiter
-from living_documentation_generator.utils.utils import make_issue_key, generate_root_level_index_page
+from living_documentation_generator.utils.utils import make_issue_key, generate_root_level_index_page, load_template
from living_documentation_generator.utils.constants import (
ISSUES_PER_PAGE_LIMIT,
ISSUE_STATE_ALL,
@@ -294,57 +294,16 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
is_structured_output = ActionInputs.get_is_structured_output_enabled()
is_grouping_by_topics = ActionInputs.get_is_grouping_by_topics_enabled()
output_path = ActionInputs.get_output_directory()
- issue_page_detail_template = None
- index_page_template = None
- index_root_level_page = None
- index_org_level_template = None
- index_repo_page_template = None
- index_data_level_template = None
# Load the template files for generating the Markdown pages
- try:
- with open(LivingDocumentationGenerator.ISSUE_PAGE_TEMPLATE_FILE, "r", encoding="utf-8") as f:
- issue_page_detail_template = f.read()
- except IOError:
- logger.error("Issue page template file was not successfully loaded.", exc_info=True)
-
- try:
- with open(LivingDocumentationGenerator.INDEX_NO_STRUCT_TEMPLATE_FILE, "r", encoding="utf-8") as f:
- index_page_template = f.read()
- except IOError:
- logger.error("Index page template file was not successfully loaded.", exc_info=True)
-
- try:
- with open(LivingDocumentationGenerator.INDEX_ROOT_LEVEL_TEMPLATE_FILE, "r", encoding="utf-8") as f:
- index_root_level_page = f.read()
- except IOError:
- logger.error(
- "Structured index page template file for root level was not successfully loaded.", exc_info=True
- )
-
- try:
- with open(LivingDocumentationGenerator.INDEX_ORG_LEVEL_TEMPLATE_FILE, "r", encoding="utf-8") as f:
- index_org_level_template = f.read()
- except IOError:
- logger.error(
- "Structured index page template file for organization level was not successfully loaded.", exc_info=True
- )
-
- try:
- with open(LivingDocumentationGenerator.INDEX_TOPIC_PAGE_TEMPLATE_FILE, "r", encoding="utf-8") as f:
- index_repo_page_template = f.read()
- except IOError:
- logger.error(
- "Structured index page template file for repository level was not successfully loaded.", exc_info=True
- )
-
- try:
- with open(LivingDocumentationGenerator.INDEX_DATA_LEVEL_TEMPLATE_FILE, "r", encoding="utf-8") as f:
- index_data_level_template = f.read()
- except IOError:
- logger.error(
- "Structured index page template file for data level was not successfully loaded.", exc_info=True
- )
+ (
+ issue_page_detail_template,
+ index_page_template,
+ index_root_level_page,
+ index_org_level_template,
+ index_repo_page_template,
+ index_data_level_template,
+ ) = self._load_all_templates()
# Generate a markdown page for every issue
for consolidated_issue in issues.values():
@@ -361,7 +320,7 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
# Generate an index page with a summary table about all issues grouped by topics
elif is_grouping_by_topics:
issues = list(issues.values())
- topics = set([issue.topic for issue in issues])
+ topics = {issue.topic for issue in issues}
generate_root_level_index_page(index_root_level_page, output_path)
for topic in topics:
@@ -447,6 +406,7 @@ def _generate_structured_index_pages(
organization_name,
)
+ # Generate an index pages for the documentation based on the grouped issues by topics
if ActionInputs.get_is_grouping_by_topics_enabled():
self._generate_sub_level_index_page(index_repo_level_template, "repo", repository_id)
logger.debug(
@@ -454,7 +414,7 @@ def _generate_structured_index_pages(
repository_name,
)
- topics = set([issue.topic for issue in issues])
+ topics = {issue.topic for issue in issues}
for topic in topics:
self._generate_index_page(index_data_level_template, issues, repository_id, topic)
logger.debug(
@@ -465,7 +425,7 @@ def _generate_structured_index_pages(
else:
self._generate_index_page(index_data_level_template, issues, repository_id)
logger.debug(
- "Generated repository level `_index.md` for %s",
+ "Generated data level `_index.md` for %s",
repository_id,
)
@@ -700,3 +660,44 @@ def _generate_index_directory_path(repository_id: Optional[str], topic: Optional
os.makedirs(output_path, exist_ok=True)
return output_path
+
+ @staticmethod
+ def _load_all_templates() -> tuple[str, ...]:
+ """
+ Load all template files for generating the Markdown pages.
+
+ @return: A tuple containing all loaded template files.
+ """
+ issue_page_detail_template = load_template(
+ LivingDocumentationGenerator.ISSUE_PAGE_TEMPLATE_FILE,
+ "Issue page template file was not successfully loaded.",
+ )
+ index_page_template = load_template(
+ LivingDocumentationGenerator.INDEX_NO_STRUCT_TEMPLATE_FILE,
+ "Index page template file was not successfully loaded.",
+ )
+ index_root_level_page = load_template(
+ LivingDocumentationGenerator.INDEX_ROOT_LEVEL_TEMPLATE_FILE,
+ "Structured index page template file for root level was not successfully loaded.",
+ )
+ index_org_level_template = load_template(
+ LivingDocumentationGenerator.INDEX_ORG_LEVEL_TEMPLATE_FILE,
+ "Structured index page template file for organization level was not successfully loaded.",
+ )
+ index_repo_page_template = load_template(
+ LivingDocumentationGenerator.INDEX_TOPIC_PAGE_TEMPLATE_FILE,
+ "Structured index page template file for repository level was not successfully loaded.",
+ )
+ index_data_level_template = load_template(
+ LivingDocumentationGenerator.INDEX_DATA_LEVEL_TEMPLATE_FILE,
+ "Structured index page template file for data level was not successfully loaded.",
+ )
+
+ return (
+ issue_page_detail_template,
+ index_page_template,
+ index_root_level_page,
+ index_org_level_template,
+ index_repo_page_template,
+ index_data_level_template,
+ )
diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_generator/model/consolidated_issue.py
index 94366eb..b63696b 100644
--- a/living_documentation_generator/model/consolidated_issue.py
+++ b/living_documentation_generator/model/consolidated_issue.py
@@ -42,11 +42,7 @@ def __init__(self, repository_id: str, repository_issue: Issue = None):
# save issue from repository (got from GitHub library & keep connection to repository for lazy loading)
# Warning: several issue properties requires additional API calls - use wisely to keep low API usage
self.__issue: Issue = repository_issue
-
self.__repository_id: str = repository_id
- parts = repository_id.split("/")
- self.__organization_name: str = parts[0] if len(parts) == 2 else ""
- self.__repository_name: str = parts[1] if len(parts) == 2 else ""
self.__topic: str = ""
# Extra project data (optionally provided from GithubProjects class)
@@ -69,12 +65,14 @@ def repository_id(self) -> str:
@property
def organization_name(self) -> str:
"""Getter of the organization where the issue was fetched from."""
- return self.__organization_name
+ parts = self.__repository_id.split("/")
+ return parts[0] if len(parts) == 2 else ""
@property
def repository_name(self) -> str:
"""Getter of the repository name where the issue was fetched from."""
- return self.__repository_name
+ parts = self.__repository_id.split("/")
+ return parts[1] if len(parts) == 2 else ""
@property
def topic(self) -> str:
@@ -172,6 +170,12 @@ def generate_page_filename(self) -> str:
return page_filename
def generate_directory_path(self, issue_table: str) -> str:
+ """
+ Generate a directory path based on enabled features.
+
+ @param issue_table: The consolidated issue summary table.
+ @return: The generated directory path.
+ """
output_path = ActionInputs.get_output_directory()
# If structured output is enabled, create a directory path based on the repository
diff --git a/living_documentation_generator/utils/utils.py b/living_documentation_generator/utils/utils.py
index a205861..b7551e6 100644
--- a/living_documentation_generator/utils/utils.py
+++ b/living_documentation_generator/utils/utils.py
@@ -113,6 +113,22 @@ def generate_root_level_index_page(index_root_level_page: str, output_path: str)
f.write(index_root_level_page)
+def load_template(file_path: str, error_message: str) -> Optional[str]:
+ """
+ Load the content of the template file.
+
+ @param file_path: The path to the template file.
+ @param error_message: The error message to log if the file cannot be read.
+ @return: The content of the template file or None if the file cannot be read.
+ """
+ try:
+ with open(file_path, "r", encoding="utf-8") as f:
+ return f.read()
+ except IOError:
+ logger.error(error_message, exc_info=True)
+ return None
+
+
# GitHub action utils
def get_action_input(name: str, default: Optional[str] = None) -> str:
"""
From f62aa4ca4606f6e42dc9651590ca584e306e3f16 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 11 Nov 2024 09:42:02 +0100
Subject: [PATCH 10/24] Review comments added for README.md
---
README.md | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/README.md b/README.md
index 1a52544..8a43a49 100644
--- a/README.md
+++ b/README.md
@@ -27,7 +27,7 @@
- [Data Mining from GitHub Projects](#data-mining-from-github-projects)
- [Living Documentation Page Generation](#living-documentation-page-generation)
- [Structured Output](#structured-output)
- - [Grouped Output by Issue Topics](#grouped-output-by-issue-topics)
+ - [Output Grouped by Topics](#output-grouped-by-topics)
- [Contribution Guidelines](#contribution-guidelines)
- [License Information](#license-information)
- [Contact or Support Information](#contact-or-support-information)
@@ -334,7 +334,7 @@ To enhance clarity, the following label groups define and categorize each Docume
- **Topic**:
- **{Name}Topic:** Designates the main focus area or theme relevant to the ticket, assigned by the editor for consistency across related documentation.
- Examples: `ReportingTopic`, `UserManagementTopic`, `SecurityTopic`.
- - **noTopic:** Indicates that the ticket does not align with a specific topic, based on the editor's discretion.
+ - **NoTopic:** Indicates that the ticket does not align with a specific topic, based on the editor's discretion.
- **Type**:
- **DocumentedUserStory:** Describes a user-centric functionality or process, highlighting its purpose and value.
- Encompasses multiple features, capturing the broader goal from a user perspective.
@@ -627,14 +627,14 @@ This feature allows you to generate structured output for the living documentati
|- _index.md
```
-### Grouped Output by Issue Topics
+### Output Grouped by Topics
-The feature allows you to generate output grouped by issue topics. This feature is useful when you want to group issues by specific topics or themes.
+The feature allows you to generate grouped output by topics. This feature is useful when you want to group tickets by specific topics or themes.
To gain a better understanding of the term "Topic", refer to the [Labels](#labels) section.
- **Default Behavior**: By default, the action generates all the documentation in a single directory.
-- **Non-default Example**: Use the grouped output feature to organize the generated documentation by issue topics.
+- **Non-default Example**: Use the grouped output feature to organize the generated documentation by topics.
- `group-output-by-topics: true` activates the grouped output feature.
```
output
From b6088b4c6cdc910837caa5ce05ebd8a465a5d338 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 11 Nov 2024 10:26:08 +0100
Subject: [PATCH 11/24] Review comments added for consolidated_issue.py.
---
.../model/consolidated_issue.py | 26 +++++++++++++------
1 file changed, 18 insertions(+), 8 deletions(-)
diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_generator/model/consolidated_issue.py
index b63696b..11c2295 100644
--- a/living_documentation_generator/model/consolidated_issue.py
+++ b/living_documentation_generator/model/consolidated_issue.py
@@ -190,16 +190,26 @@ def generate_directory_path(self, issue_table: str) -> str:
if labels:
labels = labels[0].split(", ")
- # Check for labels ending with "Topic" and generate a directory path based on it
- for label in labels:
- if label.endswith("Topic"):
- self.__topic = label
- topic_path = os.path.join(output_path, label)
- return topic_path
+ # Check for all labels ending with "Topic"
+ topic_labels = [label for label in labels if label.endswith("Topic")]
+ if len(topic_labels) > 1:
+ logger.debug(
+ "Multiple Topic labels found for Issue #%s: %s (%s): %s",
+ self.number,
+ self.title,
+ self.repository_id,
+ ", ".join(topic_labels),
+ )
+
+ # Generate a directory path based on a Topic label
+ for topic_label in topic_labels:
+ self.__topic = topic_label
+ topic_path = os.path.join(output_path, topic_label)
+ return topic_path
# If no label ends with "Topic", create a "noTopic" issue directory path
- self.__topic = "noTopic"
- no_topic_path = os.path.join(output_path, "noTopic")
+ self.__topic = "NoTopic"
+ no_topic_path = os.path.join(output_path, "NoTopic")
return no_topic_path
return output_path
From ecaffe065a29c8fa8b513a4a98a1007c88af45b8 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 11 Nov 2024 10:48:57 +0100
Subject: [PATCH 12/24] Logic for handling more than one Topic assigned to an
Issue.
---
living_documentation_generator/generator.py | 15 ++++++-----
.../model/consolidated_issue.py | 26 ++++++++++++-------
2 files changed, 24 insertions(+), 17 deletions(-)
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index 3653427..d593ccd 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -362,15 +362,16 @@ def _generate_md_issue_page(self, issue_page_template: str, consolidated_issue:
issue_md_page_content = issue_page_template.format(**replacements)
# Create a directory structure path for the issue page
- page_directory_path = consolidated_issue.generate_directory_path(issue_table)
- os.makedirs(page_directory_path, exist_ok=True)
+ page_directory_paths: list[str] = consolidated_issue.generate_directory_path(issue_table)
+ for page_directory_path in page_directory_paths:
+ os.makedirs(page_directory_path, exist_ok=True)
- # Save the single issue Markdown page
- page_filename = consolidated_issue.generate_page_filename()
- with open(os.path.join(page_directory_path, page_filename), "w", encoding="utf-8") as f:
- f.write(issue_md_page_content)
+ # Save the single issue Markdown page
+ page_filename = consolidated_issue.generate_page_filename()
+ with open(os.path.join(page_directory_path, page_filename), "w", encoding="utf-8") as f:
+ f.write(issue_md_page_content)
- logger.debug("Generated Markdown page: %s.", page_filename)
+ logger.debug("Generated Markdown page: %s.", page_filename)
def _generate_structured_index_pages(
self,
diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_generator/model/consolidated_issue.py
index 11c2295..0d92a16 100644
--- a/living_documentation_generator/model/consolidated_issue.py
+++ b/living_documentation_generator/model/consolidated_issue.py
@@ -169,12 +169,13 @@ def generate_page_filename(self) -> str:
return page_filename
- def generate_directory_path(self, issue_table: str) -> str:
+ def generate_directory_path(self, issue_table: str) -> list[str]:
"""
- Generate a directory path based on enabled features.
+ Generate a list of directory paths based on enabled features.
+ An issue can be placed in multiple directories if it is associated with more than one topic.
@param issue_table: The consolidated issue summary table.
- @return: The generated directory path.
+ @return: The list of generated directory paths.
"""
output_path = ActionInputs.get_output_directory()
@@ -185,6 +186,8 @@ def generate_directory_path(self, issue_table: str) -> str:
# If grouping by topics is enabled, create a directory path based on the issue topic
if ActionInputs.get_is_grouping_by_topics_enabled():
+ topic_paths = []
+
# Extract labels from the issue table
labels = re.findall(r"\| Labels \| (.*?) \|", issue_table)
if labels:
@@ -192,6 +195,13 @@ def generate_directory_path(self, issue_table: str) -> str:
# Check for all labels ending with "Topic"
topic_labels = [label for label in labels if label.endswith("Topic")]
+
+ # If no label ends with "Topic", create a "NoTopic" issue directory path
+ if not topic_labels:
+ self.__topic = "NoTopic"
+ no_topic_path = os.path.join(output_path, "NoTopic")
+ return [no_topic_path]
+
if len(topic_labels) > 1:
logger.debug(
"Multiple Topic labels found for Issue #%s: %s (%s): %s",
@@ -205,11 +215,7 @@ def generate_directory_path(self, issue_table: str) -> str:
for topic_label in topic_labels:
self.__topic = topic_label
topic_path = os.path.join(output_path, topic_label)
- return topic_path
-
- # If no label ends with "Topic", create a "noTopic" issue directory path
- self.__topic = "NoTopic"
- no_topic_path = os.path.join(output_path, "NoTopic")
- return no_topic_path
+ topic_paths.append(topic_path)
+ return topic_paths
- return output_path
+ return [output_path]
From dadffcca7e64eeb3c4168be38a99a649e58b6f4c Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 11 Nov 2024 11:27:06 +0100
Subject: [PATCH 13/24] Test for added new logic.
---
tests/model/test_consolidated_issue.py | 81 ++++++++++++++++++++++++++
1 file changed, 81 insertions(+)
diff --git a/tests/model/test_consolidated_issue.py b/tests/model/test_consolidated_issue.py
index 2cd7724..5d1fc4a 100644
--- a/tests/model/test_consolidated_issue.py
+++ b/tests/model/test_consolidated_issue.py
@@ -45,3 +45,84 @@ def test_generate_page_filename_with_none_title(mocker):
1,
exc_info=True,
)
+
+
+# generate_directory_path
+
+
+def test_generate_directory_path_structured_output_disabled_grouping_by_topics_disabled(mocker):
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False)
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False)
+ mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
+ consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
+
+ actual = consolidated_issue.generate_directory_path("issue table")
+
+ assert ["/base/output/path"] == actual
+
+
+def test_generate_directory_path_structured_output_enabled_grouping_by_topics_disabled(mocker):
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True)
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False)
+ mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
+ consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
+
+ actual = consolidated_issue.generate_directory_path("issue table")
+
+ assert ["/base/output/path/organization/repository"] == actual
+
+
+def test_generate_directory_path_structured_output_disabled_grouping_by_topics_enabled_two_issue_topics(mocker):
+ mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug")
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False)
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True)
+ mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
+ consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
+
+ actual = consolidated_issue.generate_directory_path("| Labels | feature, BETopic, FETopic |")
+
+ assert ["/base/output/path/BETopic", "/base/output/path/FETopic"] == actual
+ mock_log_debug.assert_called_once_with(
+ "Multiple Topic labels found for Issue #%s: %s (%s): %s",
+ 1,
+ "Issue Title",
+ "organization/repository",
+ "BETopic, FETopic",
+ )
+
+
+def test_generate_directory_path_structured_output_disabled_grouping_by_topics_enabled_no_issue_topics(mocker):
+ mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug")
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False)
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True)
+ mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
+ consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
+
+ actual = consolidated_issue.generate_directory_path("| Labels | feature, bug |")
+
+ assert ["/base/output/path/NoTopic"] == actual
+ mock_log_debug.assert_not_called()
+
+
+def test_generate_directory_path_structured_output_enabled_grouping_by_topics_enabled_one_issue_topic(mocker):
+ mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug")
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True)
+ mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True)
+ mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
+ consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
+
+ actual = consolidated_issue.generate_directory_path("| Labels | feature, BETopic, FETopic |")
+
+ assert ["/base/output/path/organization/repository/BETopic", "/base/output/path/organization/repository/FETopic"] == actual
+ mock_log_debug.assert_called_once_with(
+ "Multiple Topic labels found for Issue #%s: %s (%s): %s",
+ 1,
+ "Issue Title",
+ "organization/repository",
+ "BETopic, FETopic",
+ )
From b622d5d07c93b5ffd0225b214e18d703b6805899 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 11 Nov 2024 11:29:05 +0100
Subject: [PATCH 14/24] Black formatting.
---
tests/model/test_consolidated_issue.py | 84 +++++++++++++++++++-------
1 file changed, 62 insertions(+), 22 deletions(-)
diff --git a/tests/model/test_consolidated_issue.py b/tests/model/test_consolidated_issue.py
index 5d1fc4a..599cbb9 100644
--- a/tests/model/test_consolidated_issue.py
+++ b/tests/model/test_consolidated_issue.py
@@ -51,9 +51,17 @@ def test_generate_page_filename_with_none_title(mocker):
def test_generate_directory_path_structured_output_disabled_grouping_by_topics_disabled(mocker):
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False)
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False)
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_output_directory",
+ return_value="/base/output/path",
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled",
+ return_value=False,
+ )
mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
@@ -63,9 +71,17 @@ def test_generate_directory_path_structured_output_disabled_grouping_by_topics_d
def test_generate_directory_path_structured_output_enabled_grouping_by_topics_disabled(mocker):
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True)
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=False)
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_output_directory",
+ return_value="/base/output/path",
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled",
+ return_value=False,
+ )
mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
@@ -76,9 +92,16 @@ def test_generate_directory_path_structured_output_enabled_grouping_by_topics_di
def test_generate_directory_path_structured_output_disabled_grouping_by_topics_enabled_two_issue_topics(mocker):
mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug")
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False)
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True)
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_output_directory",
+ return_value="/base/output/path",
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
@@ -86,19 +109,26 @@ def test_generate_directory_path_structured_output_disabled_grouping_by_topics_e
assert ["/base/output/path/BETopic", "/base/output/path/FETopic"] == actual
mock_log_debug.assert_called_once_with(
- "Multiple Topic labels found for Issue #%s: %s (%s): %s",
- 1,
- "Issue Title",
- "organization/repository",
- "BETopic, FETopic",
- )
+ "Multiple Topic labels found for Issue #%s: %s (%s): %s",
+ 1,
+ "Issue Title",
+ "organization/repository",
+ "BETopic, FETopic",
+ )
def test_generate_directory_path_structured_output_disabled_grouping_by_topics_enabled_no_issue_topics(mocker):
mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug")
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False)
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True)
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_output_directory",
+ return_value="/base/output/path",
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=False
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
@@ -110,15 +140,25 @@ def test_generate_directory_path_structured_output_disabled_grouping_by_topics_e
def test_generate_directory_path_structured_output_enabled_grouping_by_topics_enabled_one_issue_topic(mocker):
mock_log_debug = mocker.patch("living_documentation_generator.model.consolidated_issue.logger.debug")
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_output_directory", return_value="/base/output/path")
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True)
- mocker.patch("living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True)
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_output_directory",
+ return_value="/base/output/path",
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_structured_output_enabled", return_value=True
+ )
+ mocker.patch(
+ "living_documentation_generator.action_inputs.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
+ )
mock_issue = Issue(None, None, {"number": 1, "title": "Issue Title"}, completed=True)
consolidated_issue = ConsolidatedIssue("organization/repository", mock_issue)
actual = consolidated_issue.generate_directory_path("| Labels | feature, BETopic, FETopic |")
- assert ["/base/output/path/organization/repository/BETopic", "/base/output/path/organization/repository/FETopic"] == actual
+ assert [
+ "/base/output/path/organization/repository/BETopic",
+ "/base/output/path/organization/repository/FETopic",
+ ] == actual
mock_log_debug.assert_called_once_with(
"Multiple Topic labels found for Issue #%s: %s (%s): %s",
1,
From 87e39665dde358c346a18af61b6321f696c93ffe Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 11 Nov 2024 12:24:45 +0100
Subject: [PATCH 15/24] UnitTests using Pytest for main.py
---
tests/test_main.py | 40 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 40 insertions(+)
create mode 100644 tests/test_main.py
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..4f17a10
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,40 @@
+#
+# 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):
+ 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")
+
+ run()
+
+ expected_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.")
+ ]
+ mock_log_info.assert_has_calls(expected_calls)
From 3c650598d7d307668931f3489935aee0174954c6 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Wed, 13 Nov 2024 11:08:09 +0100
Subject: [PATCH 16/24] Unit tests using pytest for generator.py.
---
tests/conftest.py | 71 +++-
tests/test_generator.py | 747 ++++++++++++++++++++++++++++++++++++++++
tests/test_main.py | 6 +-
3 files changed, 821 insertions(+), 3 deletions(-)
create mode 100644 tests/test_generator.py
diff --git a/tests/conftest.py b/tests/conftest.py
index dc989bd..a6eed5e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,7 +13,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-
import time
import pytest
from github import Github
@@ -21,7 +20,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 +79,69 @@ 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):
+ 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_generator.py b/tests/test_generator.py
new file mode 100644
index 0000000..e3efd39
--- /dev/null
+++ b/tests/test_generator.py
@@ -0,0 +1,747 @@
+#
+# 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 living_documentation_generator.generator import LivingDocumentationGenerator
+
+TABLE_HEADER_WITH_PROJECT_DATA = """
+| Organization name | Repository name | Issue 'Number - Title' |Linked to project | Project status | Issue URL |
+|-------------------|-----------------|------------------------|------------------|----------------|-----------|
+"""
+
+
+# _clean_output_directory
+
+
+def test_clean_output_directory(mocker):
+ 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")
+
+ LivingDocumentationGenerator._clean_output_directory()
+
+ 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
+
+
+# _fetch_github_project_issues
+
+
+def test_fetch_github_project_issues_mining_disabled(mocker, generator):
+ 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")
+
+ actual = generator._fetch_github_project_issues()
+
+ mock_get_project_mining_enabled.assert_called_once()
+ mock_logger_info.assert_called_once_with("Fetching GitHub project data - project mining is not allowed.")
+ assert {} == actual
+
+
+# _generate_markdown_pages
+
+
+def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabled(mocker, generator, consolidated_issue, load_all_templates_setup):
+ # Arrange
+ 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_load_all_templates = load_all_templates_setup
+ 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")
+
+ issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+
+ # Act
+ generator._generate_markdown_pages(issues)
+
+ # Assert
+ mock_load_all_templates.assert_called_once()
+ assert mock_generate_md_issue_page.call_count == 2
+ 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", 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
+ 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_load_all_templates = load_all_templates_setup
+ 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")
+
+ issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue, "issue_3": consolidated_issue}
+
+ # Act
+ generator._generate_markdown_pages(issues)
+
+ # Assert
+ mock_load_all_templates.assert_called_once()
+ assert mock_generate_md_issue_page.call_count == 3
+ 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", issues)
+ mock_generate_index_page.assert_not_called()
+ 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
+ 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_load_all_templates = load_all_templates_setup
+ 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")
+
+ issues = {"issue_1": consolidated_issue}
+
+ # Act
+ generator._generate_markdown_pages(issues)
+
+ # Assert
+ mock_load_all_templates.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):
+ 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_load_all_templates = load_all_templates_setup
+ 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")
+
+ consolidated_issue.topics = ["documentationTopic, FETopic"]
+ issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+
+ generator._generate_markdown_pages(issues)
+
+ # Assertions
+ mock_load_all_templates.assert_called_once()
+ assert mock_generate_md_issue_page.call_count == 2
+ 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_called_once_with("Data Level Template", list(issues.values()), topic="documentationTopic")
+
+
+# _generate_md_issue_page
+
+
+def test_generate_md_issue_page(mocker, generator, consolidated_issue):
+ # Arrange
+ 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())
+
+ 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")
+
+ # Act
+ generator._generate_md_issue_page(issue_page_template, consolidated_issue)
+
+ # Assert
+ 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.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
+ 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")
+
+ index_data_level_template = "Data Level Template"
+ index_repo_level_template = "Repo Level Template"
+ index_org_level_template = "Org Level Template"
+ consolidated_issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+
+ # Act
+ generator._generate_structured_index_pages(
+ index_data_level_template,
+ index_repo_level_template,
+ index_org_level_template,
+ 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
+ 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")
+
+ index_data_level_template = "Data Level Template"
+ index_repo_level_template = "Repo Level Template"
+ index_org_level_template = "Org Level Template"
+ consolidated_issue.repository_id = "TestOrg/TestRepo"
+ consolidated_issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+
+ # Act
+ generator._generate_structured_index_pages(
+ index_data_level_template,
+ index_repo_level_template,
+ index_org_level_template,
+ 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
+
+
+def test_generate_index_page_with_all_features_enabled(mocker, generator, consolidated_issue, project_status):
+ # Arrange
+ 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")
+
+ 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"
+ 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"
+
+ # Act
+ generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, topic)
+
+ # Assert
+ mock_generate_index_directory_path.assert_called_once_with(repository_id, 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
+ 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")
+
+ 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"
+ 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"
+
+ # Act
+ generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, topic)
+
+ # Assert
+ mock_generate_index_directory_path.assert_called_once_with(repository_id, 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
+):
+ 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")
+
+ 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
+ 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"
+
+ generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, topic)
+
+ mock_generate_index_directory_path.assert_called_once_with(repository_id, 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):
+ 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")
+
+ 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"
+
+ LivingDocumentationGenerator._generate_sub_level_index_page(index_template, level, repository_id)
+
+ 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):
+ 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")
+
+ 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"
+
+ LivingDocumentationGenerator._generate_sub_level_index_page(index_template, level, repository_id)
+
+ 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_true(
+ mocker, consolidated_issue, project_status
+):
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+
+ 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"
+ )
+
+ actual_md_issue_line = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+
+ assert expected_md_issue_line == actual_md_issue_line
+
+
+def test_generate_markdown_line_with_project_state_mining_enabled_linked_false(
+ mocker, consolidated_issue, project_status
+):
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+
+ consolidated_issue.project_issue_statuses = [project_status]
+
+ expected_md_issue_line = (
+ "| TestOrg | TestRepo | [#42 - Sample Issue](features#sample-issue) | 🔴 | In Progress |"
+ "GitHub link |\n"
+ )
+
+ actual_md_issue_line = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+
+ assert expected_md_issue_line == actual_md_issue_line
+
+
+def test_generate_markdown_line_with_project_state_mining_disabled(mocker, consolidated_issue, project_status):
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False
+ )
+
+ consolidated_issue.project_issue_statuses = [project_status]
+
+ expected_md_issue_line = (
+ "| TestOrg | TestRepo | [#42 - Sample Issue](features#sample-issue) | OPEN |"
+ "GitHub link |\n"
+ )
+
+ actual_md_issue_line = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+
+ assert expected_md_issue_line == actual_md_issue_line
+
+
+# _generate_issue_summary_table
+
+
+def test_generate_issue_summary_table_without_project_state_mining(mocker, consolidated_issue):
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=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"
+ )
+
+ actual_issue_info = LivingDocumentationGenerator._generate_issue_summary_table(consolidated_issue)
+
+ assert expected_issue_info == actual_issue_info
+
+
+def test_generate_issue_summary_table_with_project_state_mining_and_multiple_project_statuses(
+ mocker, consolidated_issue, project_status
+):
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+
+ 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"
+ )
+
+ actual_issue_info = LivingDocumentationGenerator._generate_issue_summary_table(consolidated_issue)
+
+ assert expected_issue_info == actual_issue_info
+
+
+def test_generate_issue_summary_table_with_project_state_mining_but_no_linked_project(
+ mocker, consolidated_issue, project_status
+):
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
+
+ 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"
+ )
+
+ actual_issue_info = LivingDocumentationGenerator._generate_issue_summary_table(consolidated_issue)
+
+ assert expected_issue_info == actual_issue_info
+
+
+# _generate_index_directory_path
+
+
+def test_generate_index_directory_path_with_structured_output_grouped_by_topics(mocker):
+ 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")
+
+ repository_id = "org123/repo456"
+ topic = "documentation"
+ expected_path = "/base/output/org123/repo456/documentation"
+
+ actual_path = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+
+ assert expected_path == actual_path
+
+
+def test_generate_index_directory_path_with_structured_output_not_grouped_by_topics(mocker):
+ 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")
+
+ repository_id = "org123/repo456"
+ topic = None
+ expected_path = "/base/output/org123/repo456"
+
+ actual_path = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+
+ assert expected_path == actual_path
+
+
+def test_generate_index_directory_path_with_only_grouping_by_topic_no_structured_output(mocker):
+ 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")
+
+ repository_id = "org123/repo456"
+ topic = "documentation"
+ expected_path = "/base/output/documentation"
+
+ actual_path = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+
+ assert expected_path == actual_path
+
+
+def test_generate_index_directory_path_with_no_structured_output_and_no_grouping_by_topics(mocker):
+ 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")
+
+ repository_id = None
+ topic = None
+ expected_path = "/base/output"
+
+ actual_path = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+
+ assert expected_path == actual_path
+
+
+# _load_all_templates
+
+
+def test_load_all_templates_loads_correctly(mocker):
+ 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",
+ ]
+
+ 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",
+ )
+
+ actual_templates = LivingDocumentationGenerator._load_all_templates()
+
+ assert actual_templates == expected_templates
+ assert load_template_mock.call_count == 6
+
+
+def test_load_all_templates_loads_just_some_templates(mocker):
+ 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",
+ ]
+
+ expected_templates = (
+ None,
+ None,
+ None,
+ None,
+ None,
+ "Data Level Template Content",
+ )
+
+ actual_templates = LivingDocumentationGenerator._load_all_templates()
+
+ assert actual_templates == expected_templates
+ assert load_template_mock.call_count == 6
diff --git a/tests/test_main.py b/tests/test_main.py
index 4f17a10..c15bcca 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -25,7 +25,9 @@
def test_run_correct_behaviour(mocker):
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
+ 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")
@@ -35,6 +37,6 @@ def test_run_correct_behaviour(mocker):
expected_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.")
+ mocker.call("Living Documentation generation completed."),
]
mock_log_info.assert_has_calls(expected_calls)
From 44adc7c54686285ace764f7df158e851f198c996 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Wed, 13 Nov 2024 11:15:32 +0100
Subject: [PATCH 17/24] Updated logic for having more than one topic linked to
issue.
---
living_documentation_generator/generator.py | 44 ++++++++++---------
.../model/consolidated_issue.py | 12 ++---
2 files changed, 30 insertions(+), 26 deletions(-)
diff --git a/living_documentation_generator/generator.py b/living_documentation_generator/generator.py
index d593ccd..96e5eb3 100644
--- a/living_documentation_generator/generator.py
+++ b/living_documentation_generator/generator.py
@@ -291,6 +291,7 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
@param issues: A dictionary containing all consolidated issues.
"""
+ topics = set()
is_structured_output = ActionInputs.get_is_structured_output_enabled()
is_grouping_by_topics = ActionInputs.get_is_grouping_by_topics_enabled()
output_path = ActionInputs.get_output_directory()
@@ -308,23 +309,24 @@ def _generate_markdown_pages(self, issues: dict[str, ConsolidatedIssue]) -> None
# Generate a markdown page for every issue
for consolidated_issue in issues.values():
self._generate_md_issue_page(issue_page_detail_template, consolidated_issue)
- logger.info("Markdown page generation - generated `%s` issue pages.", len(issues))
+ for topic in consolidated_issue.topics:
+ topics.add(topic)
+ logger.info("Markdown page generation - generated `%i` issue pages.", len(issues))
# Generate all structure of the index pages
if is_structured_output:
generate_root_level_index_page(index_root_level_page, output_path)
self._generate_structured_index_pages(
- index_data_level_template, index_repo_page_template, index_org_level_template, issues
+ index_data_level_template, index_repo_page_template, index_org_level_template, topics, issues
)
# Generate an index page with a summary table about all issues grouped by topics
elif is_grouping_by_topics:
issues = list(issues.values())
- topics = {issue.topic for issue in issues}
generate_root_level_index_page(index_root_level_page, output_path)
for topic in topics:
- self._generate_index_page(index_data_level_template, issues, topic=topic)
+ self._generate_index_page(index_data_level_template, issues, grouping_topic=topic)
# Generate an index page with a summary table about all issues
else:
@@ -378,6 +380,7 @@ def _generate_structured_index_pages(
index_data_level_template: str,
index_repo_level_template: str,
index_org_level_template: str,
+ topics: set[str],
consolidated_issues: dict[str, ConsolidatedIssue],
) -> None:
"""
@@ -386,6 +389,7 @@ def _generate_structured_index_pages(
@param index_data_level_template: The template string for generating the data level index markdown page.
@param index_repo_level_template: The template string for generating the repository level index markdown page.
@param index_org_level_template: The template string for generating the organization level index markdown page.
+ @param topics: A set of topics used for grouping issues.
@param consolidated_issues: A dictionary containing all consolidated issues.
@return: None
"""
@@ -415,7 +419,6 @@ def _generate_structured_index_pages(
repository_name,
)
- topics = {issue.topic for issue in issues}
for topic in topics:
self._generate_index_page(index_data_level_template, issues, repository_id, topic)
logger.debug(
@@ -437,7 +440,7 @@ def _generate_index_page(
issue_index_page_template: str,
consolidated_issues: list[ConsolidatedIssue],
repository_id: str = None,
- topic: str = None,
+ grouping_topic: str = None,
) -> None:
"""
Generates an index page with a summary of all issues and save it to the output directory.
@@ -445,7 +448,7 @@ def _generate_index_page(
@param issue_index_page_template: The template string for generating the index markdown page.
@param consolidated_issues: A dictionary containing all consolidated issues.
@param repository_id: The repository id used if the structured output is generated.
- @param topic: The topic used if the grouping issues by topics is enabled.
+ @param grouping_topic: The topic used if the grouping issues by topics is enabled.
@return: None
"""
# Initializing the issue table header based on the project mining state
@@ -458,8 +461,9 @@ def _generate_index_page(
# Create an issue summary table for every issue
for consolidated_issue in consolidated_issues:
if ActionInputs.get_is_grouping_by_topics_enabled():
- if topic == consolidated_issue.topic:
- issue_table += self._generate_markdown_line(consolidated_issue)
+ for topic in consolidated_issue.topics:
+ if grouping_topic == topic:
+ issue_table += self._generate_markdown_line(consolidated_issue)
else:
issue_table += self._generate_markdown_line(consolidated_issue)
@@ -470,16 +474,16 @@ def _generate_index_page(
}
if ActionInputs.get_is_grouping_by_topics_enabled():
- replacement["data_level_name"] = topic
+ replacement["data_level_name"] = grouping_topic
elif ActionInputs.get_is_structured_output_enabled():
replacement["data_level_name"] = repository_id.split("/")[1]
# Replace the issue placeholders in the index template
- index_page = issue_index_page_template.format(**replacement)
+ index_page: str = issue_index_page_template.format(**replacement)
# Generate a directory structure path for the index page
# Note: repository_id is used only, if the structured output is generated
- index_directory_path = self._generate_index_directory_path(repository_id, topic)
+ index_directory_path: str = self._generate_index_directory_path(repository_id, grouping_topic)
# Create an index page file
with open(os.path.join(index_directory_path, "_index.md"), "w", encoding="utf-8") as f:
@@ -649,7 +653,7 @@ def _generate_index_directory_path(repository_id: Optional[str], topic: Optional
@param repository_id: The repository id.
@return: The generated directory path.
"""
- output_path = ActionInputs.get_output_directory()
+ output_path: str = ActionInputs.get_output_directory()
if ActionInputs.get_is_structured_output_enabled() and repository_id:
organization_name, repository_name = repository_id.split("/")
@@ -663,33 +667,33 @@ def _generate_index_directory_path(repository_id: Optional[str], topic: Optional
return output_path
@staticmethod
- def _load_all_templates() -> tuple[str, ...]:
+ def _load_all_templates() -> tuple[Optional[str], ...]:
"""
Load all template files for generating the Markdown pages.
@return: A tuple containing all loaded template files.
"""
- issue_page_detail_template = load_template(
+ issue_page_detail_template: Optional[str] = load_template(
LivingDocumentationGenerator.ISSUE_PAGE_TEMPLATE_FILE,
"Issue page template file was not successfully loaded.",
)
- index_page_template = load_template(
+ index_page_template: Optional[str] = load_template(
LivingDocumentationGenerator.INDEX_NO_STRUCT_TEMPLATE_FILE,
"Index page template file was not successfully loaded.",
)
- index_root_level_page = load_template(
+ index_root_level_page: Optional[str] = load_template(
LivingDocumentationGenerator.INDEX_ROOT_LEVEL_TEMPLATE_FILE,
"Structured index page template file for root level was not successfully loaded.",
)
- index_org_level_template = load_template(
+ index_org_level_template: Optional[str] = load_template(
LivingDocumentationGenerator.INDEX_ORG_LEVEL_TEMPLATE_FILE,
"Structured index page template file for organization level was not successfully loaded.",
)
- index_repo_page_template = load_template(
+ index_repo_page_template: Optional[str] = load_template(
LivingDocumentationGenerator.INDEX_TOPIC_PAGE_TEMPLATE_FILE,
"Structured index page template file for repository level was not successfully loaded.",
)
- index_data_level_template = load_template(
+ index_data_level_template: Optional[str] = load_template(
LivingDocumentationGenerator.INDEX_DATA_LEVEL_TEMPLATE_FILE,
"Structured index page template file for data level was not successfully loaded.",
)
diff --git a/living_documentation_generator/model/consolidated_issue.py b/living_documentation_generator/model/consolidated_issue.py
index 0d92a16..d5a5592 100644
--- a/living_documentation_generator/model/consolidated_issue.py
+++ b/living_documentation_generator/model/consolidated_issue.py
@@ -43,7 +43,7 @@ def __init__(self, repository_id: str, repository_issue: Issue = None):
# Warning: several issue properties requires additional API calls - use wisely to keep low API usage
self.__issue: Issue = repository_issue
self.__repository_id: str = repository_id
- self.__topic: str = ""
+ self.__topics: list = []
# Extra project data (optionally provided from GithubProjects class)
self.__linked_to_project: bool = False
@@ -75,9 +75,9 @@ def repository_name(self) -> str:
return parts[1] if len(parts) == 2 else ""
@property
- def topic(self) -> str:
- """Getter of the issue topic."""
- return self.__topic
+ def topics(self) -> list:
+ """Getter of the issue topics."""
+ return self.__topics
@property
def title(self) -> str:
@@ -198,7 +198,7 @@ def generate_directory_path(self, issue_table: str) -> list[str]:
# If no label ends with "Topic", create a "NoTopic" issue directory path
if not topic_labels:
- self.__topic = "NoTopic"
+ self.__topics = ["NoTopic"]
no_topic_path = os.path.join(output_path, "NoTopic")
return [no_topic_path]
@@ -213,7 +213,7 @@ def generate_directory_path(self, issue_table: str) -> list[str]:
# Generate a directory path based on a Topic label
for topic_label in topic_labels:
- self.__topic = topic_label
+ self.__topics.append(topic_label)
topic_path = os.path.join(output_path, topic_label)
topic_paths.append(topic_path)
return topic_paths
From e34a969d79e50202abe4b532bb977fb8b355d3d6 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Wed, 13 Nov 2024 15:01:43 +0100
Subject: [PATCH 18/24] All unit tests for generator script.
---
living_documentation_generator/generator.py | 13 +-
tests/conftest.py | 31 +-
tests/test_generator.py | 531 ++++++++++++++++++--
3 files changed, 509 insertions(+), 66 deletions(-)
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 a6eed5e..eb344ee 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -13,6 +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
@@ -83,20 +84,34 @@ def repository_setup(mocker):
@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"
- ))
+ 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"
)
diff --git a/tests/test_generator.py b/tests/test_generator.py
index e3efd39..57d3ec2 100644
--- a/tests/test_generator.py
+++ b/tests/test_generator.py
@@ -13,12 +13,65 @@
#
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
-TABLE_HEADER_WITH_PROJECT_DATA = """
-| Organization name | Repository name | Issue 'Number - Title' |Linked to project | Project status | Issue URL |
-|-------------------|-----------------|------------------------|------------------|----------------|-----------|
-"""
+
+def test_generate(mocker, generator):
+ # Arrange
+ mock_clean_output_directory = mocker.patch.object(generator, "_clean_output_directory")
+
+ issue_mock = mocker.Mock()
+ project_issue_mock = mocker.Mock()
+
+ mock_fetch_github_issues = mocker.patch.object(
+ generator, "_fetch_github_issues", return_value={"test_org/test_repo": [issue_mock]}
+ )
+ mock_fetch_github_project_issues = mocker.patch.object(
+ generator, "_fetch_github_project_issues", return_value={"test_org/test_repo#1": [project_issue_mock]}
+ )
+
+ consolidated_issue_mock = mocker.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")
+
+ 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()
+
+ # 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": [issue_mock]}, {"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
@@ -44,9 +97,225 @@ def test_clean_output_directory(mocker):
# _fetch_github_issues
+def test_fetch_github_issues_no_query_labels(mocker, generator, config_repository):
+ # Arrange
+ config_repository.query_labels = []
+ 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_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ repo = mocker.Mock()
+ repo.full_name = "test_org/test_repo"
+ mock_get_repo.return_value = repo
+
+ issue1 = mocker.Mock()
+ issue2 = mocker.Mock()
+ issue3 = mocker.Mock()
+ mock_get_issues = mocker.patch.object(repo, "get_issues", return_value=[issue1, issue2, issue3])
+ expected_issues = {"test_org/test_repo": [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"]
+ 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_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ repo = mocker.Mock()
+ repo.full_name = "test_org/test_repo"
+ mock_get_repo.return_value = repo
+
+ issue1 = mocker.Mock()
+ issue2 = mocker.Mock()
+
+ # Use side_effect to return different issues for each label
+ mock_get_issues = mocker.patch.object(repo, "get_issues", side_effect=[[issue1], [issue2]])
+ expected_issues = {"test_org/test_repo": [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
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository]
+ )
+ mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ mock_get_repo.return_value = None
+
+ # 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
+
+ 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_mining_disabled(mocker, generator):
mock_get_project_mining_enabled = mocker.patch(
"living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False
@@ -60,22 +329,121 @@ def test_fetch_github_project_issues_mining_disabled(mocker, generator):
assert {} == actual
+def test_fetch_github_project_issues_no_repositories(mocker, generator, config_repository):
+ 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_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
+ mock_get_repo.return_value = None
+
+ actual = generator._fetch_github_project_issues()
+
+ assert {} == actual
+ mock_get_repo.assert_called_once_with("test_org/test_repo")
+
+
+def test_fetch_github_project_issues_no_projects(mocker, generator, config_repository):
+ 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_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
+
+ mock_get_repository_projects = mocker.patch.object(
+ generator._LivingDocumentationGenerator__github_projects_instance, "get_repository_projects", return_value=[]
+ )
+
+ actual = generator._fetch_github_project_issues()
+
+ 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(mocker, generator):
+ # Arrange
+ 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}",
+ )
+
+ consolidated_issue_mock_1 = mocker.Mock(spec=ConsolidatedIssue)
+ consolidated_issue_mock_2 = mocker.Mock(spec=ConsolidatedIssue)
+ mock_consolidated_issue_class = mocker.patch(
+ "living_documentation_generator.generator.ConsolidatedIssue",
+ side_effect=[consolidated_issue_mock_1, consolidated_issue_mock_2],
+ )
+
+ 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")],
+ }
+
+ # 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):
+def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabled(
+ mocker, generator, consolidated_issue, load_all_templates_setup
+):
# Arrange
- 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")
+ 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_load_all_templates = load_all_templates_setup
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_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")
+ topics = {"documentationTopic"}
issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
# Act
@@ -86,24 +454,40 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabl
assert mock_generate_md_issue_page.call_count == 2
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", issues)
+ 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):
+def test_generate_markdown_pages_with_structured_output_enabled_and_topic_grouping_disabled(
+ mocker, generator, consolidated_issue, load_all_templates_setup
+):
# Arrange
- 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")
+ 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_load_all_templates = load_all_templates_setup
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_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")
+ topics = {"documentationTopic", "FETopic"}
+ consolidated_issue.topics = ["documentationTopic", "FETopic"]
issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue, "issue_3": consolidated_issue}
# Act
@@ -114,21 +498,35 @@ def test_generate_markdown_pages_with_structured_output_enabled_and_topic_groupi
assert mock_generate_md_issue_page.call_count == 3
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", issues)
+ 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.", 3)
-def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disabled(mocker, generator, consolidated_issue, load_all_templates_setup):
+def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disabled(
+ mocker, generator, consolidated_issue, load_all_templates_setup
+):
# Arrange
- 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")
+ 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_load_all_templates = load_all_templates_setup
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_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")
@@ -148,18 +546,30 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disab
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):
- 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")
+def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_output_disabled(
+ mocker, generator, consolidated_issue, load_all_templates_setup
+):
+ 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_load_all_templates = load_all_templates_setup
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_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")
- consolidated_issue.topics = ["documentationTopic, FETopic"]
+ consolidated_issue.topics = ["documentationTopic", "FETopic"]
issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
generator._generate_markdown_pages(issues)
@@ -170,7 +580,10 @@ def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_outp
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_called_once_with("Data Level Template", list(issues.values()), topic="documentationTopic")
+ 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
@@ -178,7 +591,9 @@ def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_outp
def test_generate_md_issue_page(mocker, generator, consolidated_issue):
# Arrange
- mock_generate_issue_summary_table = mocker.patch.object(LivingDocumentationGenerator, "_generate_issue_summary_table", return_value="Generated Issue Summary Table")
+ 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())
@@ -191,9 +606,7 @@ def test_generate_md_issue_page(mocker, generator, consolidated_issue):
# Assert
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."
- )
+ 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.assert_called_once_with(consolidated_issue)
mock_makedirs.assert_called_once_with("/base/output/org/repo/issues", exist_ok=True)
@@ -204,9 +617,7 @@ def test_generate_md_issue_page(mocker, generator, consolidated_issue):
# _generate_structured_index_pages
-def test_generate_structured_index_pages_with_topic_grouping_enabled(
- mocker, generator, consolidated_issue
-):
+def test_generate_structured_index_pages_with_topic_grouping_enabled(mocker, generator, consolidated_issue):
# Arrange
mocker.patch(
"living_documentation_generator.generator.ActionInputs.get_is_grouping_by_topics_enabled", return_value=True
@@ -222,6 +633,7 @@ def test_generate_structured_index_pages_with_topic_grouping_enabled(
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}
# Act
@@ -229,6 +641,7 @@ def test_generate_structured_index_pages_with_topic_grouping_enabled(
index_data_level_template,
index_repo_level_template,
index_org_level_template,
+ topics,
consolidated_issues,
)
@@ -243,20 +656,27 @@ def test_generate_structured_index_pages_with_topic_grouping_enabled(
)
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")
+ 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
- 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_grouping_by_topics_enabled", return_value=False
+ )
- mock_generate_sub_level_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_sub_level_index_page")
+ 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")
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}
@@ -265,17 +685,24 @@ def test_generate_structured_index_pages_with_topic_grouping_disabled(mocker, ge
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_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):
@@ -302,7 +729,7 @@ def test_generate_index_page_with_all_features_enabled(mocker, generator, consol
consolidated_issues = [consolidated_issue, consolidated_issue]
repository_id = "TestOrg/TestRepo"
- topic = "documentationTopic"
+ 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"
@@ -314,10 +741,10 @@ def test_generate_index_page_with_all_features_enabled(mocker, generator, consol
expected_output_path = "/base/output/org/repo/topic/_index.md"
# Act
- generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, topic)
+ 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, topic)
+ 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)
@@ -348,7 +775,7 @@ def test_generate_index_page_with_topic_grouping_disabled_structured_output_proj
consolidated_issues = [consolidated_issue, consolidated_issue]
repository_id = "TestOrg/TestRepo"
- topic = 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"
@@ -360,10 +787,10 @@ def test_generate_index_page_with_topic_grouping_disabled_structured_output_proj
expected_output_path = "/base/output/org/repo/_index.md"
# Act
- generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, topic)
+ 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, topic)
+ 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)
@@ -392,7 +819,7 @@ def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_
consolidated_issues = [consolidated_issue, consolidated_issue]
repository_id = None
- topic = 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"
@@ -400,9 +827,9 @@ def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_
expected_index_page_content = f"Date: {expected_date}\nIssues:\n{expected_issue_table}\n"
expected_output_path = "/base/output/_index.md"
- generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, topic)
+ generator._generate_index_page(issue_index_page_template, consolidated_issues, repository_id, grouping_topic)
- mock_generate_index_directory_path.assert_called_once_with(repository_id, topic)
+ 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)
From 811becff322fbc7daaabc66910de0781bead83d2 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Wed, 13 Nov 2024 15:49:46 +0100
Subject: [PATCH 19/24] Design touches for unit tests.
---
pyproject.toml | 2 +-
tests/test_generator.py | 528 +++++++++++++++++++++-------------------
tests/test_main.py | 16 +-
3 files changed, 289 insertions(+), 257 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 7cf6438..9020592 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[tool.black]
line-length = 120
target-version = ['py311']
-force-exclude = '''test'''
+#force-exclude = '''test'''
[tool.coverage.run]
omit = ["tests/*"]
diff --git a/tests/test_generator.py b/tests/test_generator.py
index 57d3ec2..0b17af5 100644
--- a/tests/test_generator.py
+++ b/tests/test_generator.py
@@ -23,12 +23,15 @@
# generate
-def test_generate(mocker, generator):
+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")
issue_mock = 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": [issue_mock]}
@@ -36,16 +39,11 @@ def test_generate(mocker, generator):
mock_fetch_github_project_issues = mocker.patch.object(
generator, "_fetch_github_project_issues", return_value={"test_org/test_repo#1": [project_issue_mock]}
)
-
- consolidated_issue_mock = mocker.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")
- 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()
@@ -57,7 +55,6 @@ def test_generate(mocker, generator):
{"test_org/test_repo": [issue_mock]}, {"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(
[
@@ -77,7 +74,8 @@ def test_generate(mocker, generator):
# _clean_output_directory
-def test_clean_output_directory(mocker):
+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
@@ -86,8 +84,10 @@ def test_clean_output_directory(mocker):
mock_rmtree = mocker.patch("shutil.rmtree")
mock_makedirs = mocker.patch("os.makedirs")
- LivingDocumentationGenerator._clean_output_directory()
+ # 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)
@@ -100,22 +100,22 @@ def test_clean_output_directory(mocker):
def test_fetch_github_issues_no_query_labels(mocker, generator, config_repository):
# Arrange
config_repository.query_labels = []
- 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_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
repo = mocker.Mock()
repo.full_name = "test_org/test_repo"
- mock_get_repo.return_value = repo
-
issue1 = mocker.Mock()
issue2 = mocker.Mock()
issue3 = mocker.Mock()
- mock_get_issues = mocker.patch.object(repo, "get_issues", return_value=[issue1, issue2, issue3])
+
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()
@@ -148,23 +148,21 @@ def test_fetch_github_issues_no_query_labels(mocker, generator, config_repositor
def test_fetch_github_issues_with_given_query_labels(mocker, generator, config_repository):
# Arrange
config_repository.query_labels = ["bug", "enhancement"]
- 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_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
repo = mocker.Mock()
repo.full_name = "test_org/test_repo"
- mock_get_repo.return_value = repo
-
issue1 = mocker.Mock()
issue2 = mocker.Mock()
- # Use side_effect to return different issues for each label
- mock_get_issues = mocker.patch.object(repo, "get_issues", side_effect=[[issue1], [issue2]])
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()
@@ -197,11 +195,11 @@ def test_fetch_github_issues_with_given_query_labels(mocker, generator, config_r
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]
)
- mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
- mock_get_repo.return_value = None
# Act
actual = generator._fetch_github_issues()
@@ -316,36 +314,49 @@ def test_fetch_github_project_issues_correct_behaviour(mocker, generator):
)
-def test_fetch_github_project_issues_mining_disabled(mocker, generator):
+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.")
- assert {} == actual
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]
)
- mock_get_repo = generator._LivingDocumentationGenerator__github_instance.get_repo
- mock_get_repo.return_value = None
+ # 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_no_projects(mocker, generator, config_repository):
+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
)
@@ -353,19 +364,15 @@ def test_fetch_github_project_issues_no_projects(mocker, generator, config_repos
"living_documentation_generator.generator.ActionInputs.get_repositories", return_value=[config_repository]
)
mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
-
- 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
-
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 actual == {}
+ # 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(
@@ -376,28 +383,27 @@ def test_fetch_github_project_issues_no_projects(mocker, generator, config_repos
# _consolidate_issues_data
-def test_consolidate_issues_data(mocker, generator):
+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}",
)
-
- consolidated_issue_mock_1 = mocker.Mock(spec=ConsolidatedIssue)
- consolidated_issue_mock_2 = mocker.Mock(spec=ConsolidatedIssue)
mock_consolidated_issue_class = mocker.patch(
"living_documentation_generator.generator.ConsolidatedIssue",
side_effect=[consolidated_issue_mock_1, consolidated_issue_mock_2],
)
- 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")],
- }
-
# Act
actual = generator._consolidate_issues_data(repository_issues, project_issues)
@@ -422,6 +428,10 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabl
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
)
@@ -431,9 +441,6 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabl
mocker.patch(
"living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
)
-
- mock_load_all_templates = load_all_templates_setup
- 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"
)
@@ -442,16 +449,14 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_enabl
)
mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
-
- topics = {"documentationTopic"}
- issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
+ 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()
- assert mock_generate_md_issue_page.call_count == 2
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(
@@ -465,6 +470,10 @@ def test_generate_markdown_pages_with_structured_output_enabled_and_topic_groupi
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
)
@@ -474,8 +483,6 @@ def test_generate_markdown_pages_with_structured_output_enabled_and_topic_groupi
mocker.patch(
"living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
)
-
- mock_load_all_templates = load_all_templates_setup
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"
@@ -486,22 +493,17 @@ def test_generate_markdown_pages_with_structured_output_enabled_and_topic_groupi
mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
- topics = {"documentationTopic", "FETopic"}
- consolidated_issue.topics = ["documentationTopic", "FETopic"]
- issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue, "issue_3": consolidated_issue}
-
# Act
generator._generate_markdown_pages(issues)
# Assert
- mock_load_all_templates.assert_called_once()
- assert mock_generate_md_issue_page.call_count == 3
+ 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_generate_index_page.assert_not_called()
mock_logger_info.assert_called_once_with("Markdown page generation - generated `%i` issue pages.", 3)
@@ -509,6 +511,8 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disab
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
)
@@ -518,8 +522,6 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disab
mocker.patch(
"living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
)
-
- mock_load_all_templates = load_all_templates_setup
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"
@@ -530,13 +532,11 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disab
mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
- issues = {"issue_1": consolidated_issue}
-
# Act
generator._generate_markdown_pages(issues)
# Assert
- mock_load_all_templates.assert_called_once()
+ 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()
@@ -549,6 +549,10 @@ def test_generate_markdown_pages_with_structured_output_and_topic_grouping_disab
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
)
@@ -558,8 +562,6 @@ def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_outp
mocker.patch(
"living_documentation_generator.generator.ActionInputs.get_output_directory", return_value="/base/output"
)
-
- mock_load_all_templates = load_all_templates_setup
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"
@@ -569,14 +571,12 @@ def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_outp
)
mock_generate_index_page = mocker.patch.object(LivingDocumentationGenerator, "_generate_index_page")
- consolidated_issue.topics = ["documentationTopic", "FETopic"]
- issues = {"issue_1": consolidated_issue, "issue_2": consolidated_issue}
-
+ # Act
generator._generate_markdown_pages(issues)
- # Assertions
- mock_load_all_templates.assert_called_once()
- assert mock_generate_md_issue_page.call_count == 2
+ # 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()
@@ -591,23 +591,22 @@ def test_generate_markdown_pages_with_topic_grouping_enabled_and_structured_outp
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())
- 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")
-
# Act
generator._generate_md_issue_page(issue_page_template, consolidated_issue)
# Assert
- 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.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")
@@ -619,10 +618,15 @@ def test_generate_md_issue_page(mocker, generator, consolidated_issue):
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"
)
@@ -630,12 +634,6 @@ def test_generate_structured_index_pages_with_topic_grouping_enabled(mocker, gen
mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
- 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}
-
# Act
generator._generate_structured_index_pages(
index_data_level_template,
@@ -663,23 +661,22 @@ def test_generate_structured_index_pages_with_topic_grouping_enabled(mocker, gen
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")
- 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}
-
# Act
generator._generate_structured_index_pages(
index_data_level_template,
@@ -707,22 +704,6 @@ def test_generate_structured_index_pages_with_topic_grouping_disabled(mocker, ge
def test_generate_index_page_with_all_features_enabled(mocker, generator, consolidated_issue, project_status):
# Arrange
- 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")
-
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]
@@ -740,35 +721,34 @@ def test_generate_index_page_with_all_features_enabled(mocker, generator, consol
)
expected_output_path = "/base/output/org/repo/topic/_index.md"
- # 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
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
+ "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"
+ 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]
@@ -786,6 +766,21 @@ def test_generate_index_page_with_topic_grouping_disabled_structured_output_proj
)
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)
@@ -798,6 +793,20 @@ def test_generate_index_page_with_topic_grouping_disabled_structured_output_proj
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
)
@@ -807,28 +816,16 @@ def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_
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")
- 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"
-
+ # 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)
@@ -837,43 +834,47 @@ def test_generate_index_page_with_topic_grouping_and_structured_output_disabled_
# _generate_sub_level_index_page
-def test_generate_sub_level_index_page_for_org_level(mocker):
- 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")
-
+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"
- LivingDocumentationGenerator._generate_sub_level_index_page(index_template, level, repository_id)
-
- 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):
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"
- LivingDocumentationGenerator._generate_sub_level_index_page(index_template, level, repository_id)
+ 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)
@@ -882,69 +883,73 @@ def test_generate_sub_level_index_page_for_repo_level(mocker):
# _generate_markdown_line
-def test_generate_markdown_line_with_project_state_mining_enabled_linked_true(
+def test_generate_markdown_line_with_project_state_mining_enabled_linked_to_project_true_symbol(
mocker, consolidated_issue, project_status
):
- mocker.patch(
- "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
- )
-
+ # 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"
)
- actual_md_issue_line = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
- assert expected_md_issue_line == actual_md_issue_line
+ # 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_false(
+def test_generate_markdown_line_with_project_state_mining_enabled_linked_to_project_false_symbol(
mocker, consolidated_issue, project_status
):
- mocker.patch(
- "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
- )
-
+ # 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"
)
- actual_md_issue_line = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
- assert expected_md_issue_line == actual_md_issue_line
+ # 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):
- mocker.patch(
- "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False
- )
+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"
)
- actual_md_issue_line = LivingDocumentationGenerator._generate_markdown_line(consolidated_issue)
+ 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 expected_md_issue_line == actual_md_issue_line
+ # Assert
+ assert expected_md_issue_line == actual
# _generate_issue_summary_table
-def test_generate_issue_summary_table_without_project_state_mining(mocker, consolidated_issue):
- mocker.patch(
- "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False
- )
+def test_generate_issue_summary_table_without_project_state_mining(mocker, generator, consolidated_issue):
+ # Arrange
expected_issue_info = (
"| Attribute | Content |\n"
"|---|---|\n"
@@ -959,21 +964,21 @@ def test_generate_issue_summary_table_without_project_state_mining(mocker, conso
"| Labels | bug, urgent |\n"
)
- actual_issue_info = LivingDocumentationGenerator._generate_issue_summary_table(consolidated_issue)
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=False
+ )
- assert expected_issue_info == actual_issue_info
+ 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
):
- mocker.patch(
- "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
- )
-
+ # Arrange
consolidated_issue.linked_to_project = True
consolidated_issue.project_issue_statuses = [project_status, project_status]
-
expected_issue_info = (
"| Attribute | Content |\n"
"|---|---|\n"
@@ -998,20 +1003,22 @@ def test_generate_issue_summary_table_with_project_state_mining_and_multiple_pro
"| MoSCoW | Must Have |\n"
)
- actual_issue_info = LivingDocumentationGenerator._generate_issue_summary_table(consolidated_issue)
+ mocker.patch(
+ "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
+ )
- assert expected_issue_info == actual_issue_info
+ # 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
):
- mocker.patch(
- "living_documentation_generator.generator.ActionInputs.get_is_project_state_mining_enabled", return_value=True
- )
-
+ # Arrange
consolidated_issue.linked_to_project = False
-
expected_issue_info = (
"| Attribute | Content |\n"
"|---|---|\n"
@@ -1027,15 +1034,25 @@ def test_generate_issue_summary_table_with_project_state_mining_but_no_linked_pr
"| Linked to project | 🔴 |\n"
)
- actual_issue_info = LivingDocumentationGenerator._generate_issue_summary_table(consolidated_issue)
+ 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_issue_info
+ 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"
)
@@ -1047,16 +1064,19 @@ def test_generate_index_directory_path_with_structured_output_grouped_by_topics(
)
mocker.patch("os.makedirs")
- repository_id = "org123/repo456"
- topic = "documentation"
- expected_path = "/base/output/org123/repo456/documentation"
-
- actual_path = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+ # Act
+ actual = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
- assert expected_path == actual_path
+ # 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"
)
@@ -1068,16 +1088,19 @@ def test_generate_index_directory_path_with_structured_output_not_grouped_by_top
)
mocker.patch("os.makedirs")
- repository_id = "org123/repo456"
- topic = None
- expected_path = "/base/output/org123/repo456"
-
- actual_path = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+ # Act
+ actual = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
- assert expected_path == actual_path
+ # 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"
)
@@ -1089,16 +1112,18 @@ def test_generate_index_directory_path_with_only_grouping_by_topic_no_structured
)
mocker.patch("os.makedirs")
- repository_id = "org123/repo456"
- topic = "documentation"
- expected_path = "/base/output/documentation"
-
- actual_path = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+ # Act
+ actual = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
- assert expected_path == actual_path
+ 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"
)
@@ -1110,65 +1135,68 @@ def test_generate_index_directory_path_with_no_structured_output_and_no_grouping
)
mocker.patch("os.makedirs")
- repository_id = None
- topic = None
- expected_path = "/base/output"
-
- actual_path = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
+ # Act
+ actual = LivingDocumentationGenerator._generate_index_directory_path(repository_id, topic)
- assert expected_path == actual_path
+ # Assert
+ assert expected_path == actual
# _load_all_templates
def test_load_all_templates_loads_correctly(mocker):
- load_template_mock = mocker.patch("living_documentation_generator.generator.load_template")
- load_template_mock.side_effect = [
+ # 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",
- ]
+ )
- expected_templates = (
+ 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",
- )
+ ]
- actual_templates = LivingDocumentationGenerator._load_all_templates()
+ # Act
+ actual = LivingDocumentationGenerator._load_all_templates()
- assert actual_templates == expected_templates
+ assert actual == expected_templates
assert load_template_mock.call_count == 6
def test_load_all_templates_loads_just_some_templates(mocker):
- load_template_mock = mocker.patch("living_documentation_generator.generator.load_template")
- load_template_mock.side_effect = [
+ # Arrange
+ expected_templates = (
None,
None,
None,
None,
None,
"Data Level Template Content",
- ]
+ )
- expected_templates = (
+ 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",
- )
+ ]
- actual_templates = LivingDocumentationGenerator._load_all_templates()
+ # Act
+ actual = LivingDocumentationGenerator._load_all_templates()
- assert actual_templates == expected_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
index c15bcca..a292ab5 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -23,6 +23,7 @@
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: (
@@ -32,11 +33,14 @@ def test_run_correct_behaviour(mocker):
mocker.patch.dict(os.environ, {"INPUT_GITHUB_TOKEN": "fake_token"})
mocker.patch.object(LivingDocumentationGenerator, "generate")
+ # Act
run()
- expected_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."),
- ]
- mock_log_info.assert_has_calls(expected_calls)
+ # 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."),
+ ]
+ )
From 48def05b2bbf88c2aa0bc2f5d5f586bb0da12db0 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 18 Nov 2024 09:55:17 +0100
Subject: [PATCH 20/24] Comment suggestions implemented.
---
pyproject.toml | 2 +-
tests/test_generator.py | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/pyproject.toml b/pyproject.toml
index 9020592..7cf6438 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,7 @@
[tool.black]
line-length = 120
target-version = ['py311']
-#force-exclude = '''test'''
+force-exclude = '''test'''
[tool.coverage.run]
omit = ["tests/*"]
diff --git a/tests/test_generator.py b/tests/test_generator.py
index 0b17af5..41c0eeb 100644
--- a/tests/test_generator.py
+++ b/tests/test_generator.py
@@ -29,12 +29,12 @@ def test_generate_correct_behaviour(mocker, generator):
mock_logger_info = mocker.patch("living_documentation_generator.generator.logger.info")
mock_logger_debug = mocker.patch("living_documentation_generator.generator.logger.debug")
- issue_mock = mocker.Mock()
+ 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": [issue_mock]}
+ 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]}
@@ -52,7 +52,7 @@ def test_generate_correct_behaviour(mocker, generator):
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": [issue_mock]}, {"test_org/test_repo#1": [project_issue_mock]}
+ {"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.")
From 0a0405e2ac6cc98b8987a9e131fec640a7c11151 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 18 Nov 2024 10:36:31 +0100
Subject: [PATCH 21/24] Tests for correct default value at Action Inputs.
---
tests/test_generator.py | 58 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 58 insertions(+)
diff --git a/tests/test_generator.py b/tests/test_generator.py
index 41c0eeb..ca01e37 100644
--- a/tests/test_generator.py
+++ b/tests/test_generator.py
@@ -11,15 +11,73 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+import os
from datetime import datetime
from github.Issue import Issue
+from living_documentation_generator.action_inputs import ActionInputs
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
+# 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
+
+
# generate
From 6f90396b97b21fe95c9cc57796a1d1c8711479d6 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 18 Nov 2024 10:45:02 +0100
Subject: [PATCH 22/24] Design change.
---
tests/test_action_inputs.py | 57 ++++++++++++++++++++++++++++++++++++
tests/test_generator.py | 58 -------------------------------------
2 files changed, 57 insertions(+), 58 deletions(-)
diff --git a/tests/test_action_inputs.py b/tests/test_action_inputs.py
index 2239a8e..fe7f65a 100644
--- a/tests/test_action_inputs.py
+++ b/tests/test_action_inputs.py
@@ -14,11 +14,68 @@
# 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
diff --git a/tests/test_generator.py b/tests/test_generator.py
index ca01e37..41c0eeb 100644
--- a/tests/test_generator.py
+++ b/tests/test_generator.py
@@ -11,73 +11,15 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
-import os
from datetime import datetime
from github.Issue import Issue
-from living_documentation_generator.action_inputs import ActionInputs
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
-# 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
-
-
# generate
From 7ae6a8601b4a2e090f71882e0fda716f9ebdb339 Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 18 Nov 2024 10:56:27 +0100
Subject: [PATCH 23/24] Design suggering.
---
.../action_inputs.py | 2 +-
tests/test_action_inputs.py | 66 ++++++++++++-------
2 files changed, 45 insertions(+), 23 deletions(-)
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/tests/test_action_inputs.py b/tests/test_action_inputs.py
index fe7f65a..aa34ba6 100644
--- a/tests/test_action_inputs.py
+++ b/tests/test_action_inputs.py
@@ -80,6 +80,7 @@ def test_group_output_by_topics_default():
def test_get_repositories_correct_behaviour(mocker):
+ # Arrange
repositories_json = [
{
"organization-name": "organizationABC",
@@ -98,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
@@ -113,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",
@@ -210,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(
@@ -250,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.")
From 28eec2321bd0778e2b7c4609706286764145a26e Mon Sep 17 00:00:00 2001
From: Tobias Mikula <72911271+MobiTikula@users.noreply.github.com>
Date: Mon, 18 Nov 2024 14:15:43 +0100
Subject: [PATCH 24/24] Test comment for adding context case.
---
tests/test_generator.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/test_generator.py b/tests/test_generator.py
index 41c0eeb..ea22845 100644
--- a/tests/test_generator.py
+++ b/tests/test_generator.py
@@ -261,6 +261,8 @@ def test_fetch_github_project_issues_correct_behaviour(mocker, generator):
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"