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"