From 178819b38906d931a7cc5e8847dbbc0629731170 Mon Sep 17 00:00:00 2001 From: miroslavpojer Date: Wed, 25 Sep 2024 08:20:49 +0200 Subject: [PATCH] #54 - Chapter line formatting - default (#73) #54 - Chapter line formatting - default - Removed not used record formatter logic. No more needed. - Introduces action input parameters for row formats and PR: prefix presence. - Updated unit tests. - Limited pylint and black to non-test code only. --- README.md | 15 +++ action.yml | 16 ++++ main.py | 1 - release_notes_generator/action_inputs.py | 41 ++++++++ release_notes_generator/builder.py | 3 - release_notes_generator/generator.py | 2 - release_notes_generator/model/record.py | 46 ++++----- .../record/record_formatter.py | 94 ------------------- release_notes_generator/utils/constants.py | 10 +- release_notes_generator/utils/utils.py | 20 ++++ tests/conftest.py | 7 ++ tests/release_notes/model/test_record.py | 6 ++ tests/release_notes/test_record_formatter.py | 59 ------------ .../test_release_notes_builder.py | 33 +------ tests/utils/test_logging_config.py | 83 ++++++++++++++++ tests/utils/test_utils.py | 18 +++- 16 files changed, 231 insertions(+), 223 deletions(-) delete mode 100644 release_notes_generator/record/record_formatter.py delete mode 100644 tests/release_notes/test_record_formatter.py create mode 100644 tests/utils/test_logging_config.py diff --git a/README.md b/README.md index 152b0035..8f943f89 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,21 @@ Generate Release Notes action is dedicated to enhance the quality and organizati - **Description**: A JSON string defining chapters and corresponding labels for categorization. Each chapter should have a title and a label matching your GitHub issues and PRs. - **Required**: Yes +### `row-format-issue` +- **Description**: The format of the row for the issue in the release notes. The format can contain placeholders for the issue `number`, `title`, and issues `pull-requests`. The placeholders are case-sensitive. +- **Required**: No +- **Default**: `#{number} _{title}_ in {pull-requests}"` + +### `row-format-pr` +- **Description**: The format of the row for the PR in the release notes. The format can contain placeholders for the PR `number`, `title`, and PR `pull-requests`. The placeholders are case-sensitive. +- **Required**: No +- **Default**: `#{number} _{title}_"` + +### `row-format-link-pr` +- **Description**: If defined `true`, the PR row will begin with a `"PR: "` string. Otherwise, no prefix will be added. +- **Required**: No +- **Default**: true + ### `duplicity-scope` - **Description**: Set to `custom` to allow duplicity issue lines to be shown only in custom chapters. Options: `custom`, `service`, `both`, `none`. - **Required**: No diff --git a/action.yml b/action.yml index 5a0f6106..45a06a21 100644 --- a/action.yml +++ b/action.yml @@ -55,6 +55,19 @@ inputs: description: 'Print verbose logs.' required: false default: 'false' + row-format-issue: + description: 'Format of the issue row in the release notes. Available placeholders: {link}, {title}, {pull-requests}. Placeholders are case-insensitive.' + required: false + default: '#{number} _{title}_ in {pull-requests}' + row-format-pr: + description: 'Format of the pr row in the release notes. Available placeholders: {link}, {title}, {pull-requests}. Placeholders are case-insensitive.' + required: false + default: '#{number} _{title}_' + row-format-link-pr: + description: 'Add prefix "PR:" before link to PR when not linked an Issue.' + required: false + default: 'true' + outputs: release-notes: description: 'Generated release notes.' @@ -108,6 +121,9 @@ runs: INPUT_CHAPTERS_TO_PR_WITHOUT_ISSUE: ${{ inputs.chapters-to-pr-without-issue }} INPUT_VERBOSE: ${{ inputs.verbose }} INPUT_GITHUB_REPOSITORY: ${{ github.repository }} + INPUT_ROW_FORMAT_ISSUE: ${{ inputs.row-format-issue }} + INPUT_ROW_FORMAT_PR: ${{ inputs.row-format-pr }} + INPUT_ROW_FORMAT_LINK_PR: ${{ inputs.row-format-link-pr }} run: | python ${{ github.action_path }}/main.py shell: bash diff --git a/main.py b/main.py index efc1eb7d..cdfde699 100644 --- a/main.py +++ b/main.py @@ -45,7 +45,6 @@ def run() -> None: py_github = Github(auth=Auth.Token(token=ActionInputs.get_github_token()), per_page=100) ActionInputs.validate_inputs() - # Load custom chapters configuration custom_chapters = CustomChapters(print_empty_chapters=ActionInputs.get_print_empty_chapters()).from_json( ActionInputs.get_chapters_json() diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 623253d7..519b689d 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -37,13 +37,18 @@ CHAPTERS_TO_PR_WITHOUT_ISSUE, DUPLICITY_SCOPE, DUPLICITY_ICON, + ROW_FORMAT_LINK_PR, + ROW_FORMAT_ISSUE, + ROW_FORMAT_PR, ) from release_notes_generator.utils.enums import DuplicityScopeEnum from release_notes_generator.utils.gh_action import get_action_input +from release_notes_generator.utils.utils import detect_row_format_invalid_keywords logger = logging.getLogger(__name__) +# pylint: disable=too-many-branches, too-many-statements, too-many-locals class ActionInputs: """ A class representing the inputs provided to the GH action. @@ -157,6 +162,27 @@ def validate_input(input_value, expected_type: type, error_message: str, error_b return False return True + @staticmethod + def get_row_format_issue() -> str: + """ + Get the issue row format for the release notes. + """ + return get_action_input(ROW_FORMAT_ISSUE, "#{number} _{title}_ in {pull-requests}").strip() + + @staticmethod + def get_row_format_pr() -> str: + """ + Get the pr row format for the release notes. + """ + return get_action_input(ROW_FORMAT_PR, "#{number} _{title}_").strip() + + @staticmethod + def get_row_format_link_pr() -> bool: + """ + Get the value controlling whether the row format should include a 'PR:' prefix when linking to PRs. + """ + return get_action_input(ROW_FORMAT_LINK_PR, "true").lower() == "true" + @staticmethod def validate_inputs(): """ @@ -201,6 +227,21 @@ def validate_inputs(): verbose = ActionInputs.get_verbose() ActionInputs.validate_input(verbose, bool, "Verbose logging must be a boolean.", errors) + row_format_issue = ActionInputs.get_row_format_issue() + if not isinstance(row_format_issue, str) or not row_format_issue.strip(): + errors.append("Issue row format must be a non-empty string.") + + errors.extend(detect_row_format_invalid_keywords(row_format_issue)) + + row_format_pr = ActionInputs.get_row_format_pr() + if not isinstance(row_format_pr, str) or not row_format_pr.strip(): + errors.append("PR Row format must be a non-empty string.") + + errors.extend(detect_row_format_invalid_keywords(row_format_pr, row_type="PR")) + + row_format_link_pr = ActionInputs.get_row_format_link_pr() + ActionInputs.validate_input(row_format_link_pr, bool, "'row-format-link-pr' value must be a boolean.", errors) + # Features print_empty_chapters = ActionInputs.get_print_empty_chapters() ActionInputs.validate_input(print_empty_chapters, bool, "Print empty chapters must be a boolean.", errors) diff --git a/release_notes_generator/builder.py b/release_notes_generator/builder.py index cf92ae24..37df7cad 100644 --- a/release_notes_generator/builder.py +++ b/release_notes_generator/builder.py @@ -21,7 +21,6 @@ import logging from itertools import chain -from release_notes_generator.record.record_formatter import RecordFormatter from release_notes_generator.model.custom_chapters import CustomChapters from release_notes_generator.model.record import Record from release_notes_generator.model.service_chapters import ServiceChapters @@ -43,12 +42,10 @@ def __init__( self, records: dict[int, Record], changelog_url: str, - formatter: RecordFormatter, custom_chapters: CustomChapters, ): self.records = records self.changelog_url = changelog_url - self.formatter = formatter self.custom_chapters = custom_chapters self.warnings = ActionInputs.get_warnings() diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index 079f47cc..c59abb2a 100644 --- a/release_notes_generator/generator.py +++ b/release_notes_generator/generator.py @@ -24,7 +24,6 @@ from typing import Optional from github import Github -from release_notes_generator.record.record_formatter import RecordFormatter from release_notes_generator.model.custom_chapters import CustomChapters from release_notes_generator.model.record import Record from release_notes_generator.builder import ReleaseNotesBuilder @@ -113,7 +112,6 @@ def generate(self) -> Optional[str]: release_notes_builder = ReleaseNotesBuilder( records=rls_notes_records, custom_chapters=self.custom_chapters, - formatter=RecordFormatter(), changelog_url=changelog_url, ) diff --git a/release_notes_generator/model/record.py b/release_notes_generator/model/record.py index 4d8102fc..c14f4b90 100644 --- a/release_notes_generator/model/record.py +++ b/release_notes_generator/model/record.py @@ -264,47 +264,37 @@ def register_commit(self, commit: Commit) -> None: logger.error("Commit %s not registered in any PR of record %s", commit.sha, self.number) - # TODO in Issue named 'Chapter line formatting - default' - def to_chapter_row(self, row_format="") -> str: + def to_chapter_row(self) -> str: """ - Converts the record to a row in a chapter. + Converts the record to a string row in a chapter. - @param row_format: The format of the row. - @param increment_in_chapters: A boolean indicating whether to increment the count of chapters. - @return: The record as a row in a chapter. + @return: The record as a row string. """ self.increment_present_in_chapters() row_prefix = f"{ActionInputs.get_duplicity_icon()} " if self.present_in_chapters() > 1 else "" + format_values = {} if self.__gh_issue is None: p = self.__pulls[0] + format_values["number"] = p.number + format_values["title"] = p.title + format_values["authors"] = self.authors if self.authors is not None else "" + format_values["contributors"] = self.contributors if self.contributors is not None else "" - row = f"{row_prefix}PR: #{p.number} _{p.title}_" - - # Issue can have more authors (as multiple PRs can be present) - if self.authors is not None: - row = f"{row}, implemented by {self.authors}" - - if self.contributors is not None: - row = f"{row}, contributed by {self.contributors}" - - if self.contains_release_notes: - return f"{row}\n{self.get_rls_notes()}" + pr_prefix = "PR: " if ActionInputs.get_row_format_link_pr() else "" + row = f"{row_prefix}{pr_prefix}" + ActionInputs.get_row_format_pr().format(**format_values) else: - row = f"{row_prefix}#{self.__gh_issue.number} _{self.__gh_issue.title}_" - - if self.authors is not None: - row = f"{row}, implemented by {self.authors}" - - if self.contributors is not None: - row = f"{row}, contributed by {self.contributors}" + format_values["number"] = self.__gh_issue.number + format_values["title"] = self.__gh_issue.title + format_values["pull-requests"] = self.pr_links if len(self.__pulls) > 0 else "" + format_values["authors"] = self.authors if self.authors is not None else "" + format_values["contributors"] = self.contributors if self.contributors is not None else "" - if len(self.__pulls) > 0: - row = f"{row} in {self.pr_links}" + row = f"{row_prefix}" + ActionInputs.get_row_format_issue().format(**format_values) - if self.contains_release_notes: - row = f"{row}\n{self.get_rls_notes()}" + if self.contains_release_notes: + row = f"{row}\n{self.get_rls_notes()}" return row diff --git a/release_notes_generator/record/record_formatter.py b/release_notes_generator/record/record_formatter.py deleted file mode 100644 index 9730feb1..00000000 --- a/release_notes_generator/record/record_formatter.py +++ /dev/null @@ -1,94 +0,0 @@ -# -# Copyright 2023 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. -# - -""" -This module contains the BaseChapters class which is responsible for representing the base chapters. -""" - -from github.PullRequest import PullRequest - -from release_notes_generator.model.record import Record -from release_notes_generator.utils.constants import DEFAULT_ISSUE_PATTERN, DEFAULT_PULL_REQUESTS_PATTERN - - -# pylint: disable=too-few-public-methods -class RecordFormatter: - """ - A class used to format records for release notes. - """ - - def __init__( - self, issue_pattern: str = DEFAULT_ISSUE_PATTERN, pull_requests_pattern: str = DEFAULT_PULL_REQUESTS_PATTERN - ): - self.issue_pattern = issue_pattern - self.pull_requests_pattern = pull_requests_pattern - - def format(self, record: Record) -> str: - """ - Formats a record. - - @param record: The Record object to format. - @return: The formatted record as a string. - """ - # create a dict of supported keys and values - from record - params = { - "title": record.issue.title if record.issue is not None else record.pulls[0].title, - "number": record.issue.number if record.issue is not None else record.pulls[0].number, - "labels": record.labels, - "is_closed": record.is_closed, - "is_pr": record.is_pr, - "is_issue": record.is_issue, - "developers": self._format_developers(record), - "release_note_rows": self._format_release_note_rows(record), - "pull_requests": self._format_pulls(record.pulls), - } - - # apply to pattern - return self.issue_pattern.format(**params) - - def _format_pulls(self, pulls: list["PullRequest"]) -> str: - """ - Formats a list of pull requests. - - @param pulls: The list of PullRequest objects to format. - @return: The formatted pull requests as a string. - """ - return ", ".join([self.pull_requests_pattern.format(number=pr.number, url=pr.url) for pr in pulls]) - - def _format_developers(self, record: "Record") -> str: - """ - Formats the developers of a record. - - @param record: The Record object whose developers to format. - @return: The formatted developers as a string. - """ - developers = [] - # if record.gh_issue and record.gh_issue.assignees: - # developers.extend(record.gh_issue.assignees) - # for pr in record.pulls: - # if pr.assignees: - # developers.extend(pr.assignees) - return ", ".join(developers) if developers else "developers" - - def _format_release_note_rows(self, record: "Record") -> str: - """ - Formats the release note rows of a record. - - @param record: The Record object whose release note rows to format. - @return: The formatted release note rows as a string. - """ - return " - release notes 1\n - release notes 2" - # return "\n - ".join(record.release_note_rows()) if record.release_note_rows() else " - No release notes" diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index eef22f87..5d2bcdef 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -29,6 +29,10 @@ SKIP_RELEASE_NOTES_LABEL = "skip-release-notes-label" VERBOSE = "verbose" RUNNER_DEBUG = "RUNNER_DEBUG" +ROW_FORMAT_ISSUE = "row-format-issue" +ROW_FORMAT_PR = "row-format-pr" +ROW_FORMAT_LINK_PR = "row-format-link-pr" +SUPPORTED_ROW_FORMAT_KEYS = ["number", "title", "pull-requests"] # Features WARNINGS = "warnings" @@ -58,9 +62,3 @@ MERGED_PRS_LINKED_TO_NOT_CLOSED_ISSUES: str = "Merged PRs Linked to 'Not Closed' Issue ⚠️" OTHERS_NO_TOPIC: str = "Others - No Topic ⚠️" - -# Record formatter patterns -DEFAULT_ISSUE_PATTERN: str = ( - "- #{number} _{title}_ implemented by {developers} in {pull_requests}\n" "{release_note_rows}" -) -DEFAULT_PULL_REQUESTS_PATTERN: str = "[#{number}]({url})" diff --git a/release_notes_generator/utils/utils.py b/release_notes_generator/utils/utils.py index d463dfcc..f1a38695 100644 --- a/release_notes_generator/utils/utils.py +++ b/release_notes_generator/utils/utils.py @@ -19,11 +19,15 @@ """ import logging +import re + from typing import Optional from github.GitRelease import GitRelease from github.Repository import Repository +from release_notes_generator.utils.constants import SUPPORTED_ROW_FORMAT_KEYS + logger = logging.getLogger(__name__) @@ -53,3 +57,19 @@ def get_change_url( changelog_url = f"https://github.com/{repo.full_name}/compare/{rls.tag_name}...{tag_name}" return changelog_url + + +def detect_row_format_invalid_keywords(row_format: str, row_type: str = "Issue") -> list[str]: + """ + Detects invalid keywords in the row format. + + @param row_format: The row format to be checked for invalid keywords. + @param row_type: The type of row format. Default is "Issue". + @return: A list of errors if invalid keywords are found, otherwise an empty list. + """ + errors = [] + keywords_in_braces = re.findall(r"\{(.*?)\}", row_format) + invalid_keywords = [keyword for keyword in keywords_in_braces if keyword not in SUPPORTED_ROW_FORMAT_KEYS] + if invalid_keywords: + errors.append(f"Invalid {row_type} row format keyword(s) found: {', '.join(invalid_keywords)}") + return errors diff --git a/tests/conftest.py b/tests/conftest.py index 11829859..cdd09f7e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -471,3 +471,10 @@ def record_with_no_issue_one_pull_closed_no_rls_notes(request): record = Record(repo=request.getfixturevalue("mock_repo")) record.register_pull_request(request.getfixturevalue("mock_pull_no_rls_notes")) return record + + +@pytest.fixture +def mock_logging_setup(mocker): + """Fixture to mock the basic logging setup using pytest-mock.""" + mock_log_config = mocker.patch("logging.basicConfig") + yield mock_log_config diff --git a/tests/release_notes/model/test_record.py b/tests/release_notes/model/test_record.py index 8540228b..7c46fcc3 100644 --- a/tests/release_notes/model/test_record.py +++ b/tests/release_notes/model/test_record.py @@ -141,6 +141,12 @@ def test_to_chapter_row_with_pull(record_with_no_issue_one_pull_closed): assert expected_row == record_with_no_issue_one_pull_closed.to_chapter_row() +def test_to_chapter_row_with_pull_no_pr_prefix(record_with_no_issue_one_pull_closed, mocker): + mocker.patch("release_notes_generator.builder.ActionInputs.get_row_format_link_pr", return_value=False) + expected_row = "#123 _Fixed bug_\n - Fixed bug\n - Improved performance" + assert expected_row == record_with_no_issue_one_pull_closed.to_chapter_row() + + def test_to_chapter_row_with_issue(record_with_issue_closed_one_pull): expected_row = """#121 _Fix the bug_ in [#123](https://github.com/org/repo/pull/123) - Fixed bug diff --git a/tests/release_notes/test_record_formatter.py b/tests/release_notes/test_record_formatter.py deleted file mode 100644 index 44136f74..00000000 --- a/tests/release_notes/test_record_formatter.py +++ /dev/null @@ -1,59 +0,0 @@ -# -# Copyright 2023 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. -# - -from release_notes_generator.record.record_formatter import RecordFormatter - - -def test_format_record_with_issue(record_with_issue_closed_one_pull): - formatter = RecordFormatter() - formatted = formatter.format(record_with_issue_closed_one_pull) - expected_output = ( - "- #121 _Fix the bug_ implemented by developers in " - "[#123](http://example.com/pull/123)\n - release notes 1\n - release notes 2" - ) - - assert expected_output == formatted - - -# TODO - following tests could be used when topic will be implemented -# def test_format_record_without_issue(record_without_issue): -# formatter = RecordFormatter() -# formatted = formatter.format(record_without_issue) -# expected_output = "- #1 _Pull Request Title_ implemented by developers in [#1](http://example.com/pull/1), -# [#2](http://example.com/pull/2)\n - release notes 1\n - release notes 2" -# assert formatted == expected_output -# -# -# def test_format_pulls(): -# formatter = RecordFormatter() -# pulls = [PullRequest(number=1, url="http://example.com/pull/1"), -# PullRequest(number=2, url="http://example.com/pull/2")] -# formatted_pulls = formatter._format_pulls(pulls) -# expected_output = "[#1](http://example.com/pull/1), [#2](http://example.com/pull/2)" -# assert formatted_pulls == expected_output -# -# -# def test_format_developers(record_with_issue): -# formatter = RecordFormatter() -# developers = formatter._format_developers(record_with_issue) -# assert developers == "developers" -# -# -# def test_format_release_note_rows(record_with_issue): -# formatter = RecordFormatter() -# release_note_rows = formatter._format_release_note_rows(record_with_issue) -# expected_output = " - release notes 1\n - release notes 2" -# assert release_note_rows == expected_output diff --git a/tests/release_notes/test_release_notes_builder.py b/tests/release_notes/test_release_notes_builder.py index 9a4dc418..9002b719 100644 --- a/tests/release_notes/test_release_notes_builder.py +++ b/tests/release_notes/test_release_notes_builder.py @@ -16,7 +16,6 @@ import json -from release_notes_generator.record.record_formatter import RecordFormatter from release_notes_generator.model.custom_chapters import CustomChapters from release_notes_generator.builder import ReleaseNotesBuilder @@ -78,7 +77,6 @@ def __init__(self, name): self.name = name -default_formatter = RecordFormatter() DEFAULT_CHANGELOG_URL = "http://example.com/changelog" default_chapters_json = json.dumps( [ @@ -175,15 +173,16 @@ def __init__(self, name): """ RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_ISSUE_NO_PR_NO_USER_LABELS = """### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ +- #121 _Fix the bug_ in ### Closed Issues without User Defined Labels ⚠️ -- 🔔 #121 _Fix the bug_ +- 🔔 #121 _Fix the bug_ in #### Full Changelog http://example.com/changelog """ +# pylint: disable=line-too-long RELEASE_NOTES_DATA_SERVICE_CHAPTERS_MERGED_PR_NO_ISSUE_NO_USER_LABELS = """### Merged PRs without Issue and User Defined Labels ⚠️ - PR: #123 _Fixed bug_ - Fixed bug @@ -225,7 +224,7 @@ def __init__(self, name): """ RELEASE_NOTES_DATA_CLOSED_ISSUE_NO_PR_WITH_USER_LABELS = """### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ +- #121 _Fix the bug_ in #### Full Changelog http://example.com/changelog @@ -302,7 +301,6 @@ def test_build_no_data(): builder = ReleaseNotesBuilder( records={}, # empty record data set changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters, ) @@ -320,7 +318,6 @@ def test_build_no_data_no_warnings(mocker): builder = ReleaseNotesBuilder( records={}, # empty record data set changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters, ) @@ -340,7 +337,6 @@ def test_build_no_data_no_warnings_no_empty_chapters(mocker): builder = ReleaseNotesBuilder( records={}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_no_empty_chapters, ) @@ -359,7 +355,6 @@ def test_build_no_data_no_empty_chapters(mocker): builder = ReleaseNotesBuilder( records={}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_no_empty_chapters, ) @@ -532,7 +527,6 @@ def test_build_closed_issue_with_one_custom_label( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -553,7 +547,6 @@ def test_build_closed_issue_with_more_custom_labels_duplicity_reduction_on( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -572,7 +565,6 @@ def test_build_closed_issue_service_chapter_without_pull_request_and_user_define builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -591,7 +583,6 @@ def test_build_merged_pr_service_chapter_without_issue_and_user_labels( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -610,7 +601,6 @@ def test_build_closed_pr_service_chapter_without_issue_and_user_labels( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -629,7 +619,6 @@ def test_build_open_issue_with_merged_pr_service_chapter_linked_to_not_closed_is builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -646,7 +635,6 @@ def test_build_open_issue(custom_chapters_not_print_empty_chapters, record_with_ builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -663,7 +651,6 @@ def test_build_closed_issue(custom_chapters_not_print_empty_chapters, record_wit builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -683,7 +670,6 @@ def test_build_reopened_issue(custom_chapters_not_print_empty_chapters, record_w builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -703,7 +689,6 @@ def test_build_closed_not_planned_issue( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -723,7 +708,6 @@ def test_build_closed_issue_with_user_labels_no_prs( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -744,7 +728,6 @@ def test_build_closed_issue_with_prs_without_user_label( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -763,7 +746,6 @@ def test_build_open_pr_without_issue( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -782,7 +764,6 @@ def test_build_merged_pr_without_issue_ready_for_review( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -801,7 +782,6 @@ def test_build_closed_pr_without_issue_ready_for_review( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -821,7 +801,6 @@ def test_build_closed_pr_without_issue_draft( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -841,7 +820,6 @@ def test_merged_pr_without_issue_with_more_user_labels_duplicity_reduction_on( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -860,7 +838,6 @@ def test_merged_pr_with_open_init_issue_mention( builder = ReleaseNotesBuilder( records=records, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -879,7 +856,6 @@ def test_merged_pr_with_closed_issue_mention_without_user_labels( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) @@ -898,7 +874,6 @@ def test_merged_pr_with_closed_issue_mention_with_user_labels( builder = ReleaseNotesBuilder( records={rec.number: rec}, changelog_url=DEFAULT_CHANGELOG_URL, - formatter=default_formatter, custom_chapters=custom_chapters_not_print_empty_chapters, ) diff --git a/tests/utils/test_logging_config.py b/tests/utils/test_logging_config.py new file mode 100644 index 00000000..f8e16ef9 --- /dev/null +++ b/tests/utils/test_logging_config.py @@ -0,0 +1,83 @@ +import logging +import os +import sys +from logging import StreamHandler + +from release_notes_generator.utils.logging_config import setup_logging + + +def test_default_logging_level(mock_logging_setup, caplog): + """Test default logging level when no environment variables are set.""" + with caplog.at_level(logging.INFO): + setup_logging() + + mock_logging_setup.assert_called_once() + + # Get the actual call arguments from the mock + call_args = mock_logging_setup.call_args[1] # Extract the kwargs from the call + + # Validate the logging level and format + assert call_args["level"] == logging.INFO + assert call_args["format"] == "%(asctime)s - %(levelname)s - %(message)s" + assert call_args["datefmt"] == "%Y-%m-%d %H:%M:%S" + + # Check that the handler is a StreamHandler and outputs to sys.stdout + handlers = call_args["handlers"] + assert len(handlers) == 1 # Only one handler is expected + assert isinstance(handlers[0], StreamHandler) # Handler should be StreamHandler + assert handlers[0].stream is sys.stdout # Stream should be sys.stdout + + # Check that the log message is present + assert "Setting up logging configuration" in caplog.text + + +def test_verbose_logging_enabled(mock_logging_setup, caplog): + """Test that verbose logging is enabled with INPUT_VERBOSE set to true.""" + os.environ["INPUT_VERBOSE"] = "true" + + with caplog.at_level(logging.DEBUG): + setup_logging() + + mock_logging_setup.assert_called_once() + + # Get the actual call arguments from the mock + call_args = mock_logging_setup.call_args[1] # Extract the kwargs from the call + + # Validate the logging level and format + assert call_args["level"] == logging.DEBUG + assert call_args["format"] == "%(asctime)s - %(levelname)s - %(message)s" + assert call_args["datefmt"] == "%Y-%m-%d %H:%M:%S" + + # Check that the handler is a StreamHandler and outputs to sys.stdout + handlers = call_args["handlers"] + assert len(handlers) == 1 # Only one handler is expected + assert isinstance(handlers[0], StreamHandler) + assert handlers[0].stream is sys.stdout + + assert "Verbose logging enabled" in caplog.text + + +def test_debug_mode_enabled(mock_logging_setup, caplog): + """Test that debug mode is enabled when RUNNER_DEBUG is set to 1.""" + os.environ["RUNNER_DEBUG"] = "1" + + with caplog.at_level(logging.DEBUG): + setup_logging() + + mock_logging_setup.assert_called_once() + + # Get the actual call arguments from the mock + call_args = mock_logging_setup.call_args[1] # Extract the kwargs from the call + + # Validate the logging level and format + assert call_args["level"] == logging.DEBUG + assert call_args["format"] == "%(asctime)s - %(levelname)s - %(message)s" + assert call_args["datefmt"] == "%Y-%m-%d %H:%M:%S" + + # Check that the handler is a StreamHandler and outputs to sys.stdout + handlers = call_args["handlers"] + assert len(handlers) == 1 # Only one handler is expected + assert isinstance(handlers[0], StreamHandler) + assert handlers[0].stream is sys.stdout + + assert "Debug mode enabled by CI runner" in caplog.text diff --git a/tests/utils/test_utils.py b/tests/utils/test_utils.py index a02fd3d4..c7d3f15f 100644 --- a/tests/utils/test_utils.py +++ b/tests/utils/test_utils.py @@ -14,7 +14,7 @@ # limitations under the License. # -from release_notes_generator.utils.utils import get_change_url +from release_notes_generator.utils.utils import get_change_url, detect_row_format_invalid_keywords # get_change_url @@ -33,3 +33,19 @@ def test_get_change_url_no_git_release(mock_repo): def test_get_change_url_with_git_release(mock_repo, mock_git_release): url = get_change_url(tag_name="v2.0.0", repository=mock_repo, git_release=mock_git_release) assert url == "https://github.com/org/repo/compare/v1.0.0...v2.0.0" + + +# detect_row_format_invalid_keywords + + +def test_valid_row_format(): + row_format = "{number} - {title} in {pull-requests}" + errors = detect_row_format_invalid_keywords(row_format) + assert not errors, "Expected no errors for valid keywords" + + +def test_multiple_invalid_keywords(): + row_format = "{number} - {link} - {Title} and {Pull-requests}" + errors = detect_row_format_invalid_keywords(row_format) + assert len(errors) == 1 + assert "Invalid Issue row format keyword(s) found: link, Title, Pull-requests" in errors[0]