diff --git a/README.md b/README.md index 5bbd4dd6..0834a807 100644 --- a/README.md +++ b/README.md @@ -41,26 +41,26 @@ Generate Release Notes action is dedicated to enhance the quality and organizati ## Inputs -| Name | Description | Required | Default | -|------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|-------------------------------------------| -| `GITHUB_TOKEN` | Your GitHub token for authentication. Store it as a secret and reference it in the workflow file as secrets.GITHUB_TOKEN. | Yes | | -| `tag-name` | The name of the tag for which you want to generate release notes. This should be the same as the tag name used in the release workflow. | Yes | | -| `from-tag-name` | The name of the tag from which you want to generate release notes. | No | '' | -| `chapters` | An YAML array defining chapters and corresponding labels for categorization. Each chapter should have a title and a label matching your GitHub issues and PRs. | Yes | | -| `hierarchy` | Set to true to enable issue hierarchy handling. When enabled, the action will organize issues based on their hierarchical relationships (e.g., Epics and their child issues). This is useful for projects that use issue types to represent different levels of work. | No | false | -| `row-format-hierarchy-issue` | The format of the row for the hierarchy issue in the release notes. Placeholders: `type`, `number`, `title` (case-sensitive). | No | `"{type}: _{title}_ {number}"` | -| `row-format-issue` | 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. | No | `"{number} _{title}_ in {pull-requests}"` | -| `row-format-pr` | The format of the row for the PR in the release notes. The format can contain placeholders for the PR `number`, and `title`. The placeholders are case-sensitive. | No | `"{number} _{title}_"` | -| `row-format-link-pr` | If defined `true`, the PR row will begin with a `"PR: "` string. Otherwise, no prefix will be added. | No | true | -| `duplicity-scope` | Set to `custom` to allow duplicity issue lines to be shown only in custom chapters. Options: `custom`, `service`, `both`, `none`. | No | `both` | -| `duplicity-icon` | The icon used to indicate duplicity issue lines in the release notes. Icon will be placed at the beginning of the line. | No | `🔔` | -| `published-at` | Set to true to enable the use of the `published-at` timestamp as the reference point for searching closed issues and PRs, instead of the `created-at` date of the latest release. If first release, repository creation date is used. | No | false | -| `skip-release-notes-labels` | List labels used for detection if issues or pull requests are ignored in the Release Notes generation process. Example: `skip-release-notes, question`. | No | `skip-release-notes` | -| `verbose` | Set to true to enable verbose logging for detailed output during the action's execution. | No | false | -| `release-notes-title` | The title of the release notes section in the PR description. | No | `[Rr]elease [Nn]otes:` | -| `coderabbit-support-active` | Enable CodeRabbit support. If true, the action will use CodeRabbit to generate release notes. | No | false | -| `coderabbit-release-notes-title` | The title of the CodeRabbit summary in the PR body. Value supports regex. | No | `Summary by CodeRabbit` | -| `coderabbit-summary-ignore-groups` | List of "group names" to be ignored by release notes detection logic. Example: `Documentation, Tests, Chores, Bug Fixes`. | No | '' | +| Name | Description | Required | Default | +|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------|---------------------------------------------------| +| `GITHUB_TOKEN` | Your GitHub token for authentication. Store it as a secret and reference it in the workflow file as secrets.GITHUB_TOKEN. | Yes | | +| `tag-name` | The name of the tag for which you want to generate release notes. This should be the same as the tag name used in the release workflow. | Yes | | +| `from-tag-name` | The name of the tag from which you want to generate release notes. | No | '' | +| `chapters` | An YAML array defining chapters and corresponding labels for categorization. Each chapter should have a title and a label matching your GitHub issues and PRs. | Yes | | +| `hierarchy` | Set to true to enable issue hierarchy handling. When enabled, the action will organize issues based on their hierarchical relationships (e.g., Epics and their child issues). This is useful for projects that use issue types to represent different levels of work. | No | false | +| `row-format-hierarchy-issue` | The format of the row for the hierarchy issue in the release notes. Placeholders: `type`, `number`, `title` (case-sensitive). | No | `"{type}: _{title}_ {number}"` | +| `row-format-issue` | The format of the row for the issue in the release notes. The format can contain placeholders for the issue `type`, `number`, `title`, and issues `pull-requests`. The placeholders are case-sensitive. | No | `"{type}: {number} _{title}_ in {pull-requests}"` | +| `row-format-pr` | The format of the row for the PR in the release notes. The format can contain placeholders for the PR `number`, and `title`. The placeholders are case-sensitive. | No | `"{number} _{title}_"` | +| `row-format-link-pr` | If defined `true`, the PR row will begin with a `"PR: "` string. Otherwise, no prefix will be added. | No | true | +| `duplicity-scope` | Set to `custom` to allow duplicity issue lines to be shown only in custom chapters. Options: `custom`, `service`, `both`, `none`. | No | `both` | +| `duplicity-icon` | The icon used to indicate duplicity issue lines in the release notes. Icon will be placed at the beginning of the line. | No | `🔔` | +| `published-at` | Set to true to enable the use of the `published-at` timestamp as the reference point for searching closed issues and PRs, instead of the `created-at` date of the latest release. If first release, repository creation date is used. | No | false | +| `skip-release-notes-labels` | List labels used for detection if issues or pull requests are ignored in the Release Notes generation process. Example: `skip-release-notes, question`. | No | `skip-release-notes` | +| `verbose` | Set to true to enable verbose logging for detailed output during the action's execution. | No | false | +| `release-notes-title` | The title of the release notes section in the PR description. | No | `[Rr]elease [Nn]otes:` | +| `coderabbit-support-active` | Enable CodeRabbit support. If true, the action will use CodeRabbit to generate release notes. | No | false | +| `coderabbit-release-notes-title` | The title of the CodeRabbit summary in the PR body. Value supports regex. | No | `Summary by CodeRabbit` | +| `coderabbit-summary-ignore-groups` | List of "group names" to be ignored by release notes detection logic. Example: `Documentation, Tests, Chores, Bug Fixes`. | No | '' | > **Notes** > - `skip-release-notes-labels` diff --git a/action.yml b/action.yml index 0eea3e9e..74b00692 100644 --- a/action.yml +++ b/action.yml @@ -77,13 +77,13 @@ inputs: required: false default: '' row-format-hierarchy-issue: - description: 'Format of the hierarchy issue in the release notes. Available placeholders: {number}, {title}, {type}. Placeholders are case-insensitive.' + description: 'Format of the hierarchy issue in the release notes. Available placeholders: {type}, {number}, {title}. Placeholders are case-insensitive.' required: false default: '{type}: _{title}_ {number}' row-format-issue: - description: 'Format of the issue row in the release notes. Available placeholders: {number}, {title}, {pull-requests}. Placeholders are case-insensitive.' + description: 'Format of the issue row in the release notes. Available placeholders: {type}, {number}, {title}, {pull-requests}. Placeholders are case-insensitive.' required: false - default: '{number} _{title}_ in {pull-requests}' + default: '{type}: {number} _{title}_ in {pull-requests}' row-format-pr: description: 'Format of the pr row in the release notes. Available placeholders: {number}, {title}. Placeholders are case-insensitive.' required: false diff --git a/integration_test.py b/integration_test.py index c86c702f..0112ca21 100644 --- a/integration_test.py +++ b/integration_test.py @@ -11,7 +11,6 @@ class MissingTokenError(ValueError): """Raised when GITHUB_TOKEN environment variable is not set.""" - pass token = os.getenv("GITHUB_TOKEN") if token is None: diff --git a/release_notes_generator/action_inputs.py b/release_notes_generator/action_inputs.py index 739a3d1f..94b0c9fe 100644 --- a/release_notes_generator/action_inputs.py +++ b/release_notes_generator/action_inputs.py @@ -326,7 +326,7 @@ def get_row_format_issue() -> str: if ActionInputs._row_format_issue is None: ActionInputs._row_format_issue = ActionInputs._detect_row_format_invalid_keywords( get_action_input( - ROW_FORMAT_ISSUE, "{number} _{title}_ in {pull-requests}" + ROW_FORMAT_ISSUE, "{type}: {number} _{title}_ in {pull-requests}" ).strip(), # type: ignore[union-attr] clean=True, # mypy: string is returned as default @@ -463,6 +463,8 @@ def validate_inputs() -> None: logger.debug("Tag name: %s", tag_name) logger.debug("From tag name: %s", from_tag_name) logger.debug("Chapters: %s", chapters) + logger.debug("Duplicity scope: %s", ActionInputs.get_duplicity_scope()) + logger.debug("Duplicity icon: %s", ActionInputs.get_duplicity_icon()) logger.debug("Hierarchy: %s", hierarchy) logger.debug("Published at: %s", published_at) logger.debug("Skip release notes labels: %s", ActionInputs.get_skip_release_notes_labels()) diff --git a/release_notes_generator/chapters/custom_chapters.py b/release_notes_generator/chapters/custom_chapters.py index 491b52b8..bc25ecee 100644 --- a/release_notes_generator/chapters/custom_chapters.py +++ b/release_notes_generator/chapters/custom_chapters.py @@ -19,16 +19,12 @@ notes. """ import logging -from typing import cast from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.chapters.base_chapters import BaseChapters from release_notes_generator.model.chapter import Chapter from release_notes_generator.model.commit_record import CommitRecord -from release_notes_generator.model.hierarchy_issue_record import HierarchyIssueRecord -from release_notes_generator.model.issue_record import IssueRecord from release_notes_generator.model.record import Record -from release_notes_generator.model.sub_issue_record import SubIssueRecord from release_notes_generator.utils.enums import DuplicityScopeEnum logger = logging.getLogger(__name__) @@ -50,6 +46,9 @@ def populate(self, records: dict[str, Record]) -> None: None """ for record_id, record in records.items(): # iterate all records + if not records[record_id].contains_change_increment(): + continue + # check if the record should be skipped if records[record_id].skip: continue @@ -65,25 +64,19 @@ def populate(self, records: dict[str, Record]) -> None: ): continue - pulls_count = 1 - if isinstance(records[record_id], (HierarchyIssueRecord, IssueRecord, SubIssueRecord)): - pulls_count = cast(IssueRecord, records[record_id]).pull_requests_count() - for record_label in records[record_id].labels: # iterate all labels of the record (issue, or 1st PR) - if record_label in ch.labels and pulls_count > 0: - if ( - not records[record_id].is_present_in_chapters - and records[record_id].contains_change_increment() - ): + if record_label in ch.labels: + if records[record_id].is_present_in_chapters: allow_dup = ActionInputs.get_duplicity_scope() in ( DuplicityScopeEnum.CUSTOM, DuplicityScopeEnum.BOTH, ) - if (allow_dup or not records[record_id].is_present_in_chapters) and records[ - record_id - ].contains_change_increment(): - ch.add_row(record_id, records[record_id].to_chapter_row(True)) - self.populated_record_numbers_list.append(record_id) + if not allow_dup: + continue + + if record_id not in ch.rows.keys(): + ch.add_row(record_id, records[record_id].to_chapter_row(True)) + self.populated_record_numbers_list.append(record_id) def from_yaml_array(self, chapters: list[dict[str, str]]) -> "CustomChapters": """ diff --git a/release_notes_generator/data/miner.py b/release_notes_generator/data/miner.py index 367f2b14..584c27bf 100644 --- a/release_notes_generator/data/miner.py +++ b/release_notes_generator/data/miner.py @@ -27,7 +27,6 @@ from github import Github from github.GitRelease import GitRelease from github.Issue import Issue -from github.PullRequest import PullRequest from github.Repository import Repository from release_notes_generator.action_inputs import ActionInputs @@ -69,9 +68,6 @@ def mine_data(self) -> MinedData: pull_requests = list( self._safe_call(repo.get_pulls)(state=PullRequestRecord.PR_STATE_CLOSED, base=repo.default_branch) ) - open_pull_requests = list( - self._safe_call(repo.get_pulls)(state=PullRequestRecord.PR_STATE_OPEN, base=repo.default_branch) - ) data.pull_requests = {pr: data.home_repository for pr in pull_requests} if data.since: commits = list(self._safe_call(repo.get_commits)(since=data.since)) @@ -82,7 +78,7 @@ def mine_data(self) -> MinedData: logger.info("Initial data mining from GitHub completed.") logger.info("Filtering duplicated issues from the list of issues...") - de_duplicated_data = self.__filter_duplicated_issues(data, open_pull_requests) + de_duplicated_data = self.__filter_duplicated_issues(data) logger.info("Filtering duplicated issues from the list of issues finished.") return de_duplicated_data @@ -135,6 +131,7 @@ def _fetch_missing_issues_and_prs(self, data: MinedData) -> dict[Issue, Reposito fetched_issues: dict[Issue, Repository] = {} origin_issue_ids = {get_id(i, r) for i, r in data.issues.items()} + issues_for_remove: list[str] = [] for parent_id in data.parents_sub_issues.keys(): if parent_id in origin_issue_ids: continue @@ -151,15 +148,32 @@ def _fetch_missing_issues_and_prs(self, data: MinedData) -> dict[Issue, Reposito issue = None r = data.get_repository(f"{org}/{repo}") if r is not None: + logger.debug("Fetching missing issue: %s", parent_id) issue = self._safe_call(r.get_issue)(num) if issue is None: logger.error("Issue not found: %s", parent_id) continue - logger.debug("Fetching missing issue: %s", parent_id) - - # add to issues list - fetched_issues[issue] = r + fetch: bool = True + if not issue.closed_at: + fetch = False + elif data.since: + if issue.closed_at and data.since > issue.closed_at: + fetch = False + + if fetch: + # add to issues list + fetched_issues[issue] = r + else: + logger.debug("Skipping issue %s since it does not meet criteria.", parent_id) + issues_for_remove.append(parent_id) + + # remove issue which does not meet criteria + for iid in issues_for_remove: + data.parents_sub_issues.pop(iid, None) + for sub_issues in data.parents_sub_issues.values(): + if iid in sub_issues: + sub_issues.remove(iid) logger.debug("Fetched %d missing issues.", len(fetched_issues)) return fetched_issues @@ -319,29 +333,20 @@ def __get_latest_semantic_release(releases) -> Optional[GitRelease]: return rls @staticmethod - def __filter_duplicated_issues(data: MinedData, open_pull_requests: list[PullRequest]) -> "MinedData": + def __filter_duplicated_issues(data: MinedData) -> "MinedData": """ Filters out duplicated issues from the list of issues. This method address problem in output of GitHub API where issues list contains PR values. Parameters: data (MinedData): The mined data containing issues and pull requests. - open_pull_requests (list[PullRequest]): List of currently open pull requests. Returns: MinedData: The mined data with duplicated issues removed. """ - pr_numbers = {pr.number for pr in data.pull_requests.keys()} - open_pr_numbers = [pr.number for pr in open_pull_requests] - - filtered_issues = { - issue: repo - for issue, repo in data.issues.items() - if issue.number not in pr_numbers and issue.number not in open_pr_numbers - } + filtered_issues = {issue: repo for issue, repo in data.issues.items() if "/issues/" in issue.html_url} logger.debug("Duplicated issues removed: %s", len(data.issues.items()) - len(filtered_issues.items())) data.issues = filtered_issues - return data diff --git a/release_notes_generator/generator.py b/release_notes_generator/generator.py index 64ea8aa4..7b0832b0 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 github.Repository import Repository from release_notes_generator.data.filter import FilterByRelease from release_notes_generator.data.miner import DataMiner @@ -33,8 +32,8 @@ from release_notes_generator.chapters.custom_chapters import CustomChapters from release_notes_generator.model.record import Record from release_notes_generator.record.factory.default_record_factory import DefaultRecordFactory -from release_notes_generator.record.factory.issue_hierarchy_record_factory import IssueHierarchyRecordFactory from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter +from release_notes_generator.utils.record_utils import get_id from release_notes_generator.utils.utils import get_change_url logger = logging.getLogger(__name__) @@ -93,6 +92,10 @@ def generate(self) -> Optional[str]: # data expansion when hierarchy is enabled if ActionInputs.get_hierarchy(): data_filtered_by_release.issues.update(miner.mine_missing_sub_issues(data_filtered_by_release)) + else: + # fill flat structure with empty lists, no hierarchy + for i, repo in data_filtered_by_release.issues.items(): + data_filtered_by_release.parents_sub_issues[get_id(i, repo)] = [] changelog_url: str = get_change_url( tag_name=ActionInputs.get_tag_name(), @@ -102,7 +105,7 @@ def generate(self) -> Optional[str]: assert data_filtered_by_release.home_repository is not None, "Repository must not be None" - rls_notes_records: dict[str, Record] = self._get_record_factory( + rls_notes_records: dict[str, Record] = DefaultRecordFactory( github=self._github_instance, home_repository=data_filtered_by_release.home_repository ).generate(data=data_filtered_by_release) @@ -111,22 +114,3 @@ def generate(self) -> Optional[str]: custom_chapters=self._custom_chapters, changelog_url=changelog_url, ).build() - - @staticmethod - def _get_record_factory(github: Github, home_repository: Repository) -> DefaultRecordFactory: - """ - Determines and returns the appropriate RecordFactory instance based on the action inputs. - - Parameters: - github (GitHub): An instance of the GitHub class. - home_repository (Repository): The home repository for which records are to be generated. - - Returns: - DefaultRecordFactory: An instance of either IssueHierarchyRecordFactory or RecordFactory. - """ - if ActionInputs.get_hierarchy(): - logger.info("Using IssueHierarchyRecordFactory based on action inputs.") - return IssueHierarchyRecordFactory(github, home_repository) - - logger.info("Using default RecordFactory based on action inputs.") - return DefaultRecordFactory(github, home_repository) diff --git a/release_notes_generator/model/issue_record.py b/release_notes_generator/model/issue_record.py index e093a35c..e5070bac 100644 --- a/release_notes_generator/model/issue_record.py +++ b/release_notes_generator/model/issue_record.py @@ -104,6 +104,7 @@ def to_chapter_row(self, add_into_chapters: bool = True) -> str: format_values: dict[str, Any] = {} # collect format values + format_values["type"] = f"{self._issue.type.name if self._issue.type else 'N/A'}" format_values["number"] = f"#{self._issue.number}" format_values["title"] = self._issue.title list_pr_links = self.get_pr_links() diff --git a/release_notes_generator/record/factory/default_record_factory.py b/release_notes_generator/record/factory/default_record_factory.py index 2639f5c8..4a5deeaa 100644 --- a/release_notes_generator/record/factory/default_record_factory.py +++ b/release_notes_generator/record/factory/default_record_factory.py @@ -15,7 +15,7 @@ # """ -DefaultRecordFactory builds Record objects (issues, pulls, commits) from mined GitHub data. +DefaultRecordFactory builds both flat and hierarchical issue records (Epics/Features/Tasks) and associates PRs/commits. """ import logging @@ -24,21 +24,22 @@ from github import Github from github.Issue import Issue from github.PullRequest import PullRequest -from github.Commit import Commit from github.Repository import Repository from release_notes_generator.model.commit_record import CommitRecord +from release_notes_generator.model.hierarchy_issue_record import HierarchyIssueRecord from release_notes_generator.model.issue_record import IssueRecord from release_notes_generator.model.mined_data import MinedData from release_notes_generator.action_inputs import ActionInputs from release_notes_generator.model.pull_request_record import PullRequestRecord from release_notes_generator.model.record import Record +from release_notes_generator.model.sub_issue_record import SubIssueRecord from release_notes_generator.record.factory.record_factory import RecordFactory - from release_notes_generator.utils.decorators import safe_call_decorator from release_notes_generator.utils.github_rate_limiter import GithubRateLimiter + from release_notes_generator.utils.pull_request_utils import get_issues_for_pr, extract_issue_numbers_from_body -from release_notes_generator.utils.record_utils import get_id +from release_notes_generator.utils.record_utils import get_id, parse_issue_id logger = logging.getLogger(__name__) @@ -56,143 +57,225 @@ def __init__(self, github: Github, home_repository: Repository) -> None: self._records: dict[str, Record] = {} + self.__registered_issues: set[str] = set() + self.__registered_commits: set[str] = set() + def generate(self, data: MinedData) -> dict[str, Record]: """ Generate records for release notes. + Parameters: data (MinedData): The MinedData instance containing repository, issues, pull requests, and commits. + Returns: - dict[str, Record]: A dictionary of records keyed by 'owner/repo#number' (or commit SHA for commits). + dict[str, Record]: A dictionary of records indexed by their IDs. """ + logger.debug("Creation of records started...") - def register_pull_request(pr: PullRequest, l_pid: str, skip_rec: bool) -> None: - l_pull_labels = [label.name for label in pr.get_labels()] - attached_any = False - detected_issues = extract_issue_numbers_from_body(pr, repository=data.home_repository) - logger.debug("Detected issues - from body: %s", detected_issues) - linked = self._safe_call(get_issues_for_pr)(pull_number=pr.number) - if linked: - detected_issues.update(linked) - logger.debug("Detected issues - merged: %s", detected_issues) - - for parent_issue_id in detected_issues: - # create an issue record if not present for PR parent - if parent_issue_id not in self._records: - logger.warning( - "Detected PR %d linked to issue %s which is not in the list of received issues. " - "Fetching ...", - pr.number, - parent_issue_id, - ) - # dev note: here we expect that PR links to an issue in the same repository !!! - pi_repo_name, pi_number_str = parent_issue_id.split("#", 1) - try: - pi_number = int(pi_number_str) - except ValueError: - logger.error("Invalid parent issue id: %s", parent_issue_id) - continue - parent_repository = data.get_repository(pi_repo_name) - if parent_repository is not None: - # cache for subsequent lookups - if data.get_repository(pi_repo_name) is None: - data.add_repository(parent_repository) - parent_issue = self._safe_call(parent_repository.get_issue)(pi_number) - else: - parent_issue = None + # Before the loop, compute a flat set of all sub-issue IDs + all_sub_issue_ids = {iid for sublist in data.parents_sub_issues.values() for iid in sublist} - if parent_issue is not None: - self._create_record_for_issue(parent_issue, parent_issue_id) + for issue, repo in data.issues.items(): + iid = get_id(issue, repo) - if parent_issue_id in self._records: - cast(IssueRecord, self._records[parent_issue_id]).register_pull_request(pr) - logger.debug("Registering PR %d: %s to Issue %s", pr.number, pr.title, parent_issue_id) - attached_any = True - else: - logger.debug( - "Registering stand-alone PR %d: %s as mentioned Issue %s not found.", - pr.number, - pr.title, - parent_issue_id, - ) + if len(data.parents_sub_issues.get(iid, [])) > 0: + # issue has sub-issues - it is either hierarchy issue or sub-hierarchy issue + self._create_record_for_hierarchy_issue(issue, iid) - if not attached_any: - self._records[l_pid] = PullRequestRecord(pr, l_pull_labels, skip=skip_rec) - logger.debug("Created stand-alone PR record %s: %s (fallback)", l_pid, pr.title) + elif iid in all_sub_issue_ids: + # issue has no sub-issues - it is sub-issue + self._create_record_for_sub_issue(issue, iid) - logger.debug("Registering issues to records...") - for issue, repo in data.issues.items(): - self._create_record_for_issue(issue, get_id(issue, repo)) + else: + # issue is not sub-issue and has no sub-issues - it is issue + self._create_record_for_issue(issue, iid) + + # dev note: Each issue is now in records dict by its issue number - all on same level - no hierarchy + # --> This is useful for population by PRs and commits - logger.debug("Registering pull requests to records...") + logger.debug("Registering Commits to Pull Requests and Pull Requests to Issues...") for pull, repo in data.pull_requests.items(): - pid = get_id(pull, repo) - pull_labels = [label.name for label in pull.get_labels()] - skip_record: bool = any(item in pull_labels for item in ActionInputs.get_skip_release_notes_labels()) - - linked_from_api = self._safe_call(get_issues_for_pr)(pull_number=pull.number) or set() - linked_from_body = extract_issue_numbers_from_body(pull, data.home_repository) - if not linked_from_api and not linked_from_body: - self._records[pid] = PullRequestRecord(pull, pull_labels, skip=skip_record) - logger.debug("Created record for PR %s: %s", pid, pull.title) - else: - logger.debug("Registering pull number: %s, title : %s", pid, pull.title) - register_pull_request(pull, pid, skip_record) + self._register_pull_and_its_commits_to_issue(pull, get_id(pull, repo), data, target_repository=repo) - logger.debug("Registering commits to records...") - detected_direct_commits_count = sum( - not self.register_commit_to_record(commit, get_id(commit, repo)) for commit, repo in data.commits.items() - ) + logger.debug("Registering direct commits to records...") + for commit, repo in data.commits.items(): + if commit.sha not in self.__registered_commits: + self._records[get_id(commit, repo)] = CommitRecord(commit) + + # dev note: now we have all PRs and commits registered to issues or as stand-alone records + logger.debug("Building issues hierarchy...") + + sub_i_ids = list({iid for sublist in data.parents_sub_issues.values() for iid in sublist}) + sub_i_prts = {sub_issue: parent for parent, sublist in data.parents_sub_issues.items() for sub_issue in sublist} + self._re_register_hierarchy_issues(sub_issues_ids=sub_i_ids, sub_issue_parents=sub_i_prts) + self.order_hierarchy_levels() logger.info( "Generated %d records from %d issues and %d PRs, with %d commits detected.", len(self._records), len(data.issues), len(data.pull_requests), - detected_direct_commits_count, + len(data.commits), ) return self._records - def register_commit_to_record(self, commit: Commit, cid: str) -> bool: - """ - Register a commit to a record. + def _create_record_for_issue(self, issue: Issue, iid: str, issue_labels: Optional[list[str]] = None) -> None: + if issue_labels is None: + issue_labels = self._get_issue_labels_mix_with_type(issue) - @param commit: The commit to register. - @return: True if the commit was registered to a record, False otherwise - """ - for record in self._records.values(): - if isinstance(record, IssueRecord): - rec_i = cast(IssueRecord, record) - for number in rec_i.get_pull_request_numbers(): - pr = rec_i.get_pull_request(number) - if pr and pr.merge_commit_sha == commit.sha: - rec_i.register_commit(pr, commit) - return True - - elif isinstance(record, PullRequestRecord): - rec_pr = cast(PullRequestRecord, record) - if rec_pr.is_commit_sha_present(commit.sha): - rec_pr.register_commit(commit) - return True - - self._records[cid] = CommitRecord(commit=commit) - logger.debug("Created record for direct commit %s: %s", commit.sha, commit.commit.message) - return False + # super()._create_record_for_issue(issue, iid, issue_labels) + skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels()) + self._records[iid] = IssueRecord(issue=issue, skip=skip_record, issue_labels=issue_labels) + self.__registered_issues.add(iid) - def _create_record_for_issue(self, issue: Issue, iid: str, issue_labels: Optional[list[str]] = None) -> None: + # pylint: disable=too-many-statements + def _register_pull_and_its_commits_to_issue( + self, pull: PullRequest, pid: str, data: MinedData, target_repository: Optional[Repository] = None + ) -> None: + pull_labels = [label.name for label in pull.get_labels()] + skip_record: bool = any(item in pull_labels for item in ActionInputs.get_skip_release_notes_labels()) + related_commits = [c for c in data.commits if c.sha == pull.merge_commit_sha] + self.__registered_commits.update(c.sha for c in related_commits) + + pr_repo = target_repository if target_repository is not None else data.home_repository + + linked_from_api: set[str] = self._safe_call(get_issues_for_pr)(pull_number=pull.number) or set() + linked_from_body: set[str] = extract_issue_numbers_from_body(pull, pr_repo) + merged_linked_issues: set[str] = linked_from_api.union(linked_from_body) + pull_issues: list[str] = list(merged_linked_issues) + attached_any = False + if len(pull_issues) > 0: + for issue_id in pull_issues: + if issue_id not in self._records: + logger.warning( + "Detected PR %d linked to issue %s which is not in the list of received issues. " + "Fetching ...", + pull.number, + issue_id, + ) + # dev note: here we expect that PR links to an issue in the same repository !!! + org, repo, num = parse_issue_id(issue_id) + r = data.get_repository(f"{org}/{repo}") + parent_issue = self._safe_call(r.get_issue)(num) if r is not None else None + if parent_issue is not None: + self._create_record_for_issue(parent_issue, get_id(parent_issue, r)) # type: ignore[arg-type] + + if issue_id in self._records and isinstance( + self._records[issue_id], (SubIssueRecord, HierarchyIssueRecord, IssueRecord) + ): + rec = cast(IssueRecord, self._records[issue_id]) + rec.register_pull_request(pull) + logger.debug("Registering pull number: %s, title : %s", pull.number, pull.title) + + for c in related_commits: # register commits to the PR record + rec.register_commit(pull, c) + logger.debug("Registering commit %s to PR %d", c.sha, pull.number) + + attached_any = True + + if not attached_any: + pr_rec = PullRequestRecord(pull, pull_labels, skip_record) + for c in related_commits: # register commits to the PR record + pr_rec.register_commit(c) + self._records[pid] = pr_rec + logger.debug("Created record for PR %s: %s", pid, pull.title) + + def _create_record_for_hierarchy_issue(self, i: Issue, iid: str, issue_labels: Optional[list[str]] = None) -> None: """ - Create a record for an issue. + Create a hierarchy issue record and register sub-issues. Parameters: - issue (Issue): The issue to create a record for. - iid (str): The ID of the issue in the format 'owner/repo#number'. - issue_labels (Optional[list[str]]): Optional set of labels for the issue. If not provided, labels will be - fetched from the issue. + i: The issue to create the record for. + issue_labels: The labels of the issue. + Returns: None """ # check for skip labels presence and skip when detected if issue_labels is None: - issue_labels = [label.name for label in issue.get_labels()] + issue_labels = self._get_issue_labels_mix_with_type(i) skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels()) - self._records[iid] = IssueRecord(issue=issue, skip=skip_record, issue_labels=issue_labels) - logger.debug("Created record for non hierarchy issue '%s': %s", iid, issue.title) + + self._records[iid] = HierarchyIssueRecord(issue=i, skip=skip_record, issue_labels=issue_labels) + self.__registered_issues.add(iid) + logger.debug("Created record for hierarchy issue %s: %s", iid, i.title) + + def _get_issue_labels_mix_with_type(self, issue: Issue) -> list[str]: + labels: list[str] = [label.name for label in issue.get_labels()] + + if issue.type is not None: + issue_type = issue.type.name.lower() + if issue_type not in labels: + labels.append(issue_type) + + return labels + + def _create_record_for_sub_issue(self, issue: Issue, iid: str, issue_labels: Optional[list[str]] = None) -> None: + if issue_labels is None: + issue_labels = self._get_issue_labels_mix_with_type(issue) + + skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels()) + logger.debug("Created record for sub issue %s: %s", iid, issue.title) + self.__registered_issues.add(iid) + self._records[iid] = SubIssueRecord(issue, issue_labels, skip_record) + + if iid.split("#")[0] == self._home_repository.full_name: + return + + self._records[iid].is_cross_repo = True + + def _re_register_hierarchy_issues(self, sub_issues_ids: list[str], sub_issue_parents: dict[str, str]): + logger.debug("Re-registering hierarchy issues ...") + reduced_sub_issue_ids: list[str] = sub_issues_ids[:] + + made_progress = False + for sub_issue_id in sub_issues_ids: + # remove issue(sub_issue_id) from current records and add it to parent + # as sub-issue or sub-hierarchy-issue + # but do it only for issue where parent issue number is not in _sub_issue_parents keys + # Why? We building hierarchy from bottom. Access in records is very easy. + if sub_issue_id in sub_issue_parents.values(): + continue + + parent_issue_id: str = sub_issue_parents[sub_issue_id] + parent_rec = cast(HierarchyIssueRecord, self._records[parent_issue_id]) + sub_rec = self._records[sub_issue_id] + + if isinstance(sub_rec, SubIssueRecord): + parent_rec.sub_issues[sub_issue_id] = sub_rec # add to parent as SubIssueRecord + self._records.pop(sub_issue_id) # remove from main records as it is sub-one + reduced_sub_issue_ids.remove(sub_issue_id) # remove from sub-parents as it is now sub-one + sub_issue_parents.pop(sub_issue_id) + made_progress = True + logger.debug("Added sub-issue %s to parent %s", sub_issue_id, parent_issue_id) + elif isinstance(sub_rec, HierarchyIssueRecord): + parent_rec.sub_hierarchy_issues[sub_issue_id] = sub_rec # add to parent as 'Sub' HierarchyIssueRecord + self._records.pop(sub_issue_id) # remove from main records as it is sub-one + reduced_sub_issue_ids.remove(sub_issue_id) # remove from sub-parents as it is now sub-one + sub_issue_parents.pop(sub_issue_id) + made_progress = True + logger.debug("Added sub-hierarchy-issue %s to parent %s", sub_issue_id, parent_issue_id) + else: + logger.error( + "Detected IssueRecord in position of SubIssueRecord - leaving as standalone and dropping mapping" + ) + # Avoid infinite recursion by removing the unresolved mapping + reduced_sub_issue_ids.remove(sub_issue_id) + sub_issue_parents.pop(sub_issue_id) + + if reduced_sub_issue_ids and made_progress: + self._re_register_hierarchy_issues(reduced_sub_issue_ids, sub_issue_parents) + + def order_hierarchy_levels(self, level: int = 0) -> None: + """ + Order hierarchy levels for proper rendering. + + Parameters: + level (int): The current level in the hierarchy. Default is 0. + """ + # we have now all hierarchy issues in records - but levels are not set + # we need to set levels for proper rendering + # This have to be done from up to down + top_hierarchy_records = [rec for rec in self._records.values() if isinstance(rec, HierarchyIssueRecord)] + for rec in top_hierarchy_records: + rec.order_hierarchy_levels(level=level) diff --git a/release_notes_generator/record/factory/issue_hierarchy_record_factory.py b/release_notes_generator/record/factory/issue_hierarchy_record_factory.py deleted file mode 100644 index 38d8483e..00000000 --- a/release_notes_generator/record/factory/issue_hierarchy_record_factory.py +++ /dev/null @@ -1,273 +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. -# - -""" -IssueHierarchyRecordFactory builds hierarchical issue records (Epics/Features/Tasks) and associates PRs/commits. -""" - -import logging -from typing import cast, Optional - -from github import Github -from github.Issue import Issue -from github.PullRequest import PullRequest -from github.Repository import Repository - -from release_notes_generator.model.commit_record import CommitRecord -from release_notes_generator.model.hierarchy_issue_record import HierarchyIssueRecord -from release_notes_generator.model.issue_record import IssueRecord -from release_notes_generator.model.mined_data import MinedData -from release_notes_generator.action_inputs import ActionInputs -from release_notes_generator.model.pull_request_record import PullRequestRecord -from release_notes_generator.model.record import Record -from release_notes_generator.model.sub_issue_record import SubIssueRecord -from release_notes_generator.record.factory.default_record_factory import DefaultRecordFactory - -from release_notes_generator.utils.pull_request_utils import get_issues_for_pr, extract_issue_numbers_from_body -from release_notes_generator.utils.record_utils import get_id, parse_issue_id - -logger = logging.getLogger(__name__) - - -class IssueHierarchyRecordFactory(DefaultRecordFactory): - """ - A class used to generate records for release notes. - """ - - def __init__(self, github: Github, home_repository: Repository) -> None: - super().__init__(github, home_repository) - - self.__registered_issues: set[str] = set() - self.__registered_commits: set[str] = set() - - def generate(self, data: MinedData) -> dict[str, Record]: - """ - Generate records for release notes. - - Parameters: - data (MinedData): The MinedData instance containing repository, issues, pull requests, and commits. - - Returns: - dict[str, Record]: A dictionary of records indexed by their IDs. - """ - logger.debug( - "Creation of records started..." - ) # NEW: uz mam mnapovani, kdo je hierarchy, kdo je SubIssue a kdo je Issue - for issue, repo in data.issues.items(): - iid = get_id(issue, repo) - - if len(data.parents_sub_issues.get(iid, [])) > 0: - # issue has sub-issues - it is either hierarchy issue or sub-hierarchy issue - self._create_record_for_hierarchy_issue(issue, iid) - - elif any(iid in sublist for sublist in data.parents_sub_issues.values()): - # issue has no sub-issues - it is sub-issue - self._create_record_for_sub_issue(issue, iid) - - else: - # issue is not sub-issue and has no sub-issues - it is issue - self._create_record_for_issue(issue, iid) - - # dev note: Each issue is now in records dict by its issue number - all on same level - no hierarchy - # This is useful for population by PRs and commits - - logger.debug("Registering Commits to Pull Requests and Pull Requests to Issues...") - for pull, repo in data.pull_requests.items(): - self._register_pull_and_its_commits_to_issue(pull, get_id(pull, repo), data) - - logger.debug("Registering direct commits to records...") - for commit, repo in data.commits.items(): - if commit.sha not in self.__registered_commits: - self._records[get_id(commit, repo)] = CommitRecord(commit) - - # dev note: now we have all PRs and commits registered to issues or as stand-alone records - # let build hierarchy - logger.debug("Building issues hierarchy...") - - self._re_register_hierarchy_issues( - sub_issues_ids=list({iid for sublist in data.parents_sub_issues.values() for iid in sublist}), - sub_issue_parents={ - sub_issue: parent for parent, sublist in data.parents_sub_issues.items() for sub_issue in sublist - }, - ) - self.order_hierarchy_levels() - - logger.info( - "Generated %d records from %d issues and %d PRs, with %d commits detected.", - len(self._records), - len(data.issues), - len(data.pull_requests), - len(data.commits), - ) - return self._records - - # pylint: disable=too-many-statements - def _register_pull_and_its_commits_to_issue( - self, pull: PullRequest, pid: str, data: MinedData, target_repository: Optional[Repository] = None - ) -> None: - pull_labels = [label.name for label in pull.get_labels()] - skip_record: bool = any(item in pull_labels for item in ActionInputs.get_skip_release_notes_labels()) - related_commits = [c for c in data.commits if c.sha == pull.merge_commit_sha] - self.__registered_commits.update(c.sha for c in related_commits) - - pr_repo = target_repository if target_repository is not None else data.home_repository - - linked_from_api = self._safe_call(get_issues_for_pr)(pull_number=pull.number) or set() - linked_from_body = extract_issue_numbers_from_body(pull, pr_repo) - pull_issues: list[str] = list(linked_from_api.union(linked_from_body)) - attached_any = False - if len(pull_issues) > 0: - for issue_id in pull_issues: - if issue_id not in self._records: - logger.warning( - "Detected PR %d linked to issue %s which is not in the list of received issues. " - "Fetching ...", - pull.number, - issue_id, - ) - # dev note: here we expect that PR links to an issue in the same repository !!! - org, repo, num = parse_issue_id(issue_id) - r = data.get_repository(f"{org}/{repo}") - parent_issue = self._safe_call(r.get_issue)(num) if r is not None else None - if parent_issue is not None: - self._create_record_for_issue(parent_issue, get_id(parent_issue, r)) # type: ignore[arg-type] - - if issue_id in self._records and isinstance( - self._records[issue_id], (SubIssueRecord, HierarchyIssueRecord, IssueRecord) - ): - rec = cast(IssueRecord, self._records[issue_id]) - rec.register_pull_request(pull) - logger.debug("Registering pull number: %s, title : %s", pull.number, pull.title) - - for c in related_commits: # register commits to the PR record - rec.register_commit(pull, c) - logger.debug("Registering commit %s to PR %d", c.sha, pull.number) - - attached_any = True - - if not attached_any: - pr_rec = PullRequestRecord(pull, pull_labels, skip_record) - for c in related_commits: # register commits to the PR record - pr_rec.register_commit(c) - self._records[pid] = pr_rec - logger.debug("Created record for PR %s: %s", pid, pull.title) - - def _create_record_for_hierarchy_issue(self, i: Issue, iid: str, issue_labels: Optional[list[str]] = None) -> None: - """ - Create a hierarchy issue record and register sub-issues. - - Parameters: - i: The issue to create the record for. - issue_labels: The labels of the issue. - - Returns: - None - """ - # check for skip labels presence and skip when detected - if issue_labels is None: - issue_labels = self._get_issue_labels_mix_with_type(i) - skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels()) - - self._records[iid] = HierarchyIssueRecord(issue=i, skip=skip_record, issue_labels=issue_labels) - self.__registered_issues.add(iid) - logger.debug("Created record for hierarchy issue %s: %s", iid, i.title) - - def _get_issue_labels_mix_with_type(self, issue: Issue) -> list[str]: - labels: list[str] = [label.name for label in issue.get_labels()] - - if issue.type is not None: - issue_type = issue.type.name.lower() - if issue_type not in labels: - labels.append(issue_type) - - return labels - - def _create_record_for_issue(self, issue: Issue, iid: str, issue_labels: Optional[list[str]] = None) -> None: - if issue_labels is None: - issue_labels = self._get_issue_labels_mix_with_type(issue) - - super()._create_record_for_issue(issue, iid, issue_labels) - self.__registered_issues.add(iid) - - def _create_record_for_sub_issue(self, issue: Issue, iid: str, issue_labels: Optional[list[str]] = None) -> None: - if issue_labels is None: - issue_labels = self._get_issue_labels_mix_with_type(issue) - - skip_record = any(item in issue_labels for item in ActionInputs.get_skip_release_notes_labels()) - logger.debug("Created record for sub issue %s: %s", iid, issue.title) - self.__registered_issues.add(iid) - self._records[iid] = SubIssueRecord(issue, issue_labels, skip_record) - - if iid.split("#")[0] == self._home_repository.full_name: - return - - self._records[iid].is_cross_repo = True - - def _re_register_hierarchy_issues(self, sub_issues_ids: list[str], sub_issue_parents: dict[str, str]): - logger.debug("Re-registering hierarchy issues ...") - reduced_sub_issue_ids: list[str] = sub_issues_ids[:] - - made_progress = False - for sub_issue_id in sub_issues_ids: - # remove issue(sub_issue_id) from current records and add it to parent - # as sub-issue or sub-hierarchy-issue - # but do it only for issue where parent issue number is not in _sub_issue_parents keys - # Why? We building hierarchy from bottom. Access in records is very easy. - if sub_issue_id in sub_issue_parents.values(): - continue - - parent_issue_id: str = sub_issue_parents[sub_issue_id] - parent_rec = cast(HierarchyIssueRecord, self._records[parent_issue_id]) - sub_rec = self._records[sub_issue_id] - - if isinstance(sub_rec, SubIssueRecord): - parent_rec.sub_issues[sub_issue_id] = sub_rec # add to parent as SubIssueRecord - self._records.pop(sub_issue_id) # remove from main records as it is sub-one - reduced_sub_issue_ids.remove(sub_issue_id) # remove from sub-parents as it is now sub-one - sub_issue_parents.pop(sub_issue_id) - made_progress = True - logger.debug("Added sub-issue %s to parent %s", sub_issue_id, parent_issue_id) - elif isinstance(sub_rec, HierarchyIssueRecord): - parent_rec.sub_hierarchy_issues[sub_issue_id] = sub_rec # add to parent as 'Sub' HierarchyIssueRecord - self._records.pop(sub_issue_id) # remove from main records as it is sub-one - reduced_sub_issue_ids.remove(sub_issue_id) # remove from sub-parents as it is now sub-one - sub_issue_parents.pop(sub_issue_id) - made_progress = True - logger.debug("Added sub-hierarchy-issue %s to parent %s", sub_issue_id, parent_issue_id) - else: - logger.error( - "Detected IssueRecord in position of SubIssueRecord - leaving as standalone and dropping mapping" - ) - # Avoid infinite recursion by removing the unresolved mapping - reduced_sub_issue_ids.remove(sub_issue_id) - sub_issue_parents.pop(sub_issue_id) - - if reduced_sub_issue_ids and made_progress: - self._re_register_hierarchy_issues(reduced_sub_issue_ids, sub_issue_parents) - - def order_hierarchy_levels(self, level: int = 0) -> None: - """ - Order hierarchy levels for proper rendering. - - Parameters: - level (int): The current level in the hierarchy. Default is 0. - """ - # we have now all hierarchy issues in records - but levels are not set - # we need to set levels for proper rendering - # This have to be done from up to down - top_hierarchy_records = [rec for rec in self._records.values() if isinstance(rec, HierarchyIssueRecord)] - for rec in top_hierarchy_records: - rec.order_hierarchy_levels(level=level) diff --git a/release_notes_generator/utils/constants.py b/release_notes_generator/utils/constants.py index 48712d41..a96302fa 100644 --- a/release_notes_generator/utils/constants.py +++ b/release_notes_generator/utils/constants.py @@ -39,7 +39,7 @@ ROW_FORMAT_PR = "row-format-pr" ROW_FORMAT_LINK_PR = "row-format-link-pr" SUPPORTED_ROW_FORMAT_KEYS_HIERARCHY_ISSUE = ["type", "number", "title"] -SUPPORTED_ROW_FORMAT_KEYS_ISSUE = ["number", "title", "pull-requests"] +SUPPORTED_ROW_FORMAT_KEYS_ISSUE = ["type", "number", "title", "pull-requests"] SUPPORTED_ROW_FORMAT_KEYS_PULL_REQUEST = ["number", "title"] # Features diff --git a/release_notes_generator/utils/pull_request_utils.py b/release_notes_generator/utils/pull_request_utils.py index 17c5789d..0aa20c67 100644 --- a/release_notes_generator/utils/pull_request_utils.py +++ b/release_notes_generator/utils/pull_request_utils.py @@ -75,7 +75,6 @@ def get_issues_for_pr(pull_number: int) -> set[str]: if data.get("errors"): raise RuntimeError(f"GitHub GraphQL errors: {data['errors']}") - # TODO - mine owner and use it in ID return { f"{owner}/{name}#{node['number']}" for node in data["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["nodes"] diff --git a/release_notes_generator/utils/record_utils.py b/release_notes_generator/utils/record_utils.py index b23f0f7e..591cb00b 100644 --- a/release_notes_generator/utils/record_utils.py +++ b/release_notes_generator/utils/record_utils.py @@ -20,8 +20,6 @@ class IssueIdParseError(ValueError): """Raised when an issue ID cannot be parsed.""" - pass - def get_id(obj, repository: Repository) -> str: """ @@ -35,10 +33,12 @@ def get_id(obj, repository: Repository) -> str: if isinstance(obj, Issue): issue = cast(Issue, obj) return _entity_id(repository.full_name, issue.number) - elif isinstance(obj, PullRequest): + + if isinstance(obj, PullRequest): pr = cast(PullRequest, obj) return _entity_id(repository.full_name, pr.number) - elif isinstance(obj, Commit): + + if isinstance(obj, Commit): commit = cast(Commit, obj) return f"{commit.sha}" diff --git a/tests/conftest.py b/tests/conftest.py index 40c453b3..0aeb38bf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -149,6 +149,7 @@ def mock_issue_open(mocker): issue.state_reason = None issue.body = "I1 open" issue.repository.full_name = "org/repo" + issue.type = None label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" @@ -168,6 +169,7 @@ def mock_issue_open_2(mocker): issue.state_reason = None issue.body = "I2 open" issue.repository.full_name = "org/repo" + issue.type = None label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" @@ -188,6 +190,8 @@ def mock_issue_closed(mocker): issue.get_sub_issues.return_value = [] issue.repository.full_name = "org/repo" issue.closed_at = datetime.now() + issue.html_url = "https://github.com/org/repo/issues/121" + issue.type = None label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" @@ -207,6 +211,8 @@ def mock_issue_closed_i1_bug(mocker): issue.body = "Some issue body text\nRelease Notes:\n- Fixed bug\n- Improved performance\n+ More nice code\n * Awesome architecture" issue.repository.full_name = "org/repo" issue.closed_at = datetime.now() + issue.html_url = "https://github.com/org/repo/issues/122" + issue.type = None label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" @@ -468,6 +474,7 @@ def mock_pull_closed_with_rls_notes_101(mocker): pull.updated_at = datetime.now() pull.merged_at = None pull.closed_at = datetime.now() + pull.html_url = "http://example.com/pull/101" label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" @@ -489,6 +496,7 @@ def mock_pull_closed_with_rls_notes_102(mocker): pull.updated_at = datetime.now() pull.merged_at = None pull.closed_at = datetime.now() + pull.html_url = "http://example.com/pull/102" label1 = mocker.Mock(spec=MockLabel) label1.name = "label1" diff --git a/tests/release_notes/builder/test_release_notes_builder.py b/tests/release_notes/builder/test_release_notes_builder.py index 50f7f127..556ce175 100644 --- a/tests/release_notes/builder/test_release_notes_builder.py +++ b/tests/release_notes/builder/test_release_notes_builder.py @@ -20,7 +20,6 @@ from release_notes_generator.builder.builder import ReleaseNotesBuilder from release_notes_generator.chapters.custom_chapters import CustomChapters from release_notes_generator.record.factory.default_record_factory import DefaultRecordFactory -from release_notes_generator.record.factory.issue_hierarchy_record_factory import IssueHierarchyRecordFactory from tests.conftest import mock_safe_call_decorator, MockLabel # pylint: disable=pointless-string-statement @@ -138,7 +137,7 @@ RELEASE_NOTES_NO_DATA_NO_EMPTY_CHAPTERS = RELEASE_NOTES_NO_DATA_NO_WARNING_NO_EMPTY_CHAPTERS RELEASE_NOTES_DATA_CUSTOM_CHAPTERS_ONE_LABEL = """### Chapter 1 🛠 -- #122 _I1+bug_ in #101, #102 +- N/A: #122 _I1+bug_ in #101, #102 - Fixed bug - Improved performance + More nice code @@ -153,11 +152,11 @@ """ RELEASE_NOTES_DATA_HIERARCHY_NO_LABELS_NO_TYPE = """### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- N/A: #121 _Fix the bug_ in - Solo issue release note ### Closed Issues without User Defined Labels ⚠️ -- 🔔 #121 _Fix the bug_ in +- 🔔 N/A: #121 _Fix the bug_ in - Solo issue release note ### Merged PRs without Issue and User Defined Labels ⚠️ @@ -176,7 +175,7 @@ - None: _HI302 open_ #302 - _Release Notes_: - Hierarchy level release note - - #451 _SI451 closed_ in #150 + - N/A: #451 _SI451 closed_ in #150 - Hierarchy level release note - Fixed bug - Improved performance @@ -185,7 +184,7 @@ - None: _HI303 open_ #303 - _Release Notes_: - Hierarchy level release note - - #452 _SI452 closed_ in #151 + - N/A: #452 _SI452 closed_ in #151 - Hierarchy level release note - Fixed bug - Improved performance @@ -197,7 +196,7 @@ - None: _HI350 open_ #350 - _Release Notes_: - Sub-hierarchy level release note - - #453 _SI453 closed_ in #152 + - N/A: #453 _SI453 closed_ in #152 - Hierarchy level release note - Fixed bug - Improved performance @@ -219,7 +218,7 @@ - None: _HI302 open_ #302 - _Release Notes_: - Hierarchy level release note - - #451 _SI451 closed_ in #150 + - N/A: #451 _SI451 closed_ in #150 - Hierarchy level release note - Fixed bug - Improved performance @@ -228,7 +227,7 @@ - None: _HI303 open_ #303 - _Release Notes_: - Hierarchy level release note - - #452 _SI452 closed_ in #151 + - N/A: #452 _SI452 closed_ in #151 - Hierarchy level release note - Fixed bug - Improved performance @@ -240,7 +239,7 @@ - None: _HI350 open_ #350 - _Release Notes_: - Sub-hierarchy level release note - - #453 _SI453 closed_ in #152 + - N/A: #453 _SI453 closed_ in #152 - Hierarchy level release note - Fixed bug - Improved performance @@ -255,7 +254,7 @@ * Awesome architecture ### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- N/A: #121 _Fix the bug_ in - Solo issue release note ### Closed Issues without User Defined Labels ⚠️ @@ -273,7 +272,7 @@ - 🔔 None: _HI302 open_ #302 - _Release Notes_: - Hierarchy level release note - - 🔔 #451 _SI451 closed_ in #150 + - 🔔 N/A: #451 _SI451 closed_ in #150 - Hierarchy level release note - Fixed bug - Improved performance @@ -282,7 +281,7 @@ - 🔔 None: _HI303 open_ #303 - _Release Notes_: - Hierarchy level release note - - 🔔 #452 _SI452 closed_ in #151 + - 🔔 N/A: #452 _SI452 closed_ in #151 - Hierarchy level release note - Fixed bug - Improved performance @@ -294,7 +293,7 @@ - 🔔 None: _HI350 open_ #350 - _Release Notes_: - Sub-hierarchy level release note - - 🔔 #453 _SI453 closed_ in #152 + - 🔔 N/A: #453 _SI453 closed_ in #152 - Hierarchy level release note - Fixed bug - Improved performance @@ -315,7 +314,7 @@ - Epic: _HI302 open_ #302 - _Release Notes_: - Hierarchy level release note - - #451 _SI451 closed_ in #150 + - Task: #451 _SI451 closed_ in #150 - Hierarchy level release note - Fixed bug - Improved performance @@ -324,7 +323,7 @@ - Epic: _HI303 open_ #303 - _Release Notes_: - Hierarchy level release note - - #452 _SI452 closed_ in #151 + - Task: #452 _SI452 closed_ in #151 - Hierarchy level release note - Fixed bug - Improved performance @@ -336,7 +335,7 @@ - Feature: _HI350 open_ #350 - _Release Notes_: - Sub-hierarchy level release note - - #453 _SI453 closed_ in #152 + - Task: #453 _SI453 closed_ in #152 - Hierarchy level release note - Fixed bug - Improved performance @@ -344,7 +343,7 @@ * Awesome architecture ### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- Feature: #121 _Fix the bug_ in - Solo issue release note ### Closed Issues without User Defined Labels ⚠️ @@ -366,7 +365,7 @@ - 🔔 Epic: _HI302 open_ #302 - _Release Notes_: - Hierarchy level release note - - 🔔 #451 _SI451 closed_ in #150 + - 🔔 Task: #451 _SI451 closed_ in #150 - Hierarchy level release note - Fixed bug - Improved performance @@ -375,7 +374,7 @@ - 🔔 Epic: _HI303 open_ #303 - _Release Notes_: - Hierarchy level release note - - 🔔 #452 _SI452 closed_ in #151 + - 🔔 Task: #452 _SI452 closed_ in #151 - Hierarchy level release note - Fixed bug - Improved performance @@ -387,7 +386,7 @@ - 🔔 Feature: _HI350 open_ #350 - _Release Notes_: - Sub-hierarchy level release note - - 🔔 #453 _SI453 closed_ in #152 + - 🔔 Task: #453 _SI453 closed_ in #152 - Hierarchy level release note - Fixed bug - Improved performance @@ -408,7 +407,7 @@ - Epic: _HI302 open_ #302 - _Release Notes_: - Hierarchy level release note - - #451 _SI451 closed_ in #150 + - Task: #451 _SI451 closed_ in #150 - Hierarchy level release note - Fixed bug - Improved performance @@ -417,7 +416,7 @@ - Epic: _HI303 open_ #303 - _Release Notes_: - Hierarchy level release note - - #452 _SI452 closed_ in #151 + - Task: #452 _SI452 closed_ in #151 - Hierarchy level release note - Fixed bug - Improved performance @@ -429,7 +428,7 @@ - Feature: _HI350 open_ #350 - _Release Notes_: - Sub-hierarchy level release note - - #453 _SI453 closed_ in #152 + - Task: #453 _SI453 closed_ in #152 - Hierarchy level release note - Fixed bug - Improved performance @@ -444,7 +443,7 @@ * Awesome architecture ### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- Bug: #121 _Fix the bug_ in - Solo issue release note ### Closed Issues without User Defined Labels ⚠️ @@ -462,7 +461,7 @@ - 🔔 Epic: _HI302 open_ #302 - _Release Notes_: - Hierarchy level release note - - 🔔 #451 _SI451 closed_ in #150 + - 🔔 Task: #451 _SI451 closed_ in #150 - Hierarchy level release note - Fixed bug - Improved performance @@ -471,7 +470,7 @@ - 🔔 Epic: _HI303 open_ #303 - _Release Notes_: - Hierarchy level release note - - 🔔 #452 _SI452 closed_ in #151 + - 🔔 Task: #452 _SI452 closed_ in #151 - Hierarchy level release note - Fixed bug - Improved performance @@ -483,7 +482,7 @@ - 🔔 Feature: _HI350 open_ #350 - _Release Notes_: - Sub-hierarchy level release note - - 🔔 #453 _SI453 closed_ in #152 + - 🔔 Task: #453 _SI453 closed_ in #152 - Hierarchy level release note - Fixed bug - Improved performance @@ -502,15 +501,15 @@ RELEASE_NOTES_NO_DATA_HIERARCHY_NO_LABELS_NO_TYPE = """### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- N/A: #121 _Fix the bug_ in - Solo issue release note - #450 _SI450 closed_ in - Hierarchy level release note ### Closed Issues without User Defined Labels ⚠️ -- 🔔 #121 _Fix the bug_ in +- 🔔 N/A: #121 _Fix the bug_ in - Solo issue release note -- 🔔 #450 _SI450 closed_ in +- 🔔 Task: #450 _SI450 closed_ in - Hierarchy level release note - #451 _SI451 closed_ in #150 - Hierarchy level release note @@ -743,7 +742,7 @@ """ RELEASE_NOTES_DATA_CUSTOM_CHAPTERS_MORE_LABELS_DUPLICITY_REDUCTION_ON = """### Chapter 1 🛠 -- #122 _I1+bug-enhancement_ in #101, #102 +- N/A: #122 _I1+bug-enhancement_ in #101, #102 - Fixed bug - Improved performance + More nice code @@ -771,10 +770,10 @@ """ RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_ISSUE_NO_PR_NO_USER_LABELS = """### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- N/A: #121 _Fix the bug_ in ### Closed Issues without User Defined Labels ⚠️ -- 🔔 #121 _Fix the bug_ in +- 🔔 N/A: #121 _Fix the bug_ in #### Full Changelog http://example.com/changelog @@ -804,7 +803,7 @@ RELEASE_NOTES_DATA_SERVICE_CHAPTERS_CLOSED_PR_NO_ISSUE_SKIP_USER_LABELS = RELEASE_NOTES_NO_DATA_NO_WARNING_NO_EMPTY_CHAPTERS RELEASE_NOTES_DATA_SERVICE_CHAPTERS_OPEN_ISSUE_AND_MERGED_PR_NO_USER_LABELS = """### Merged PRs Linked to 'Not Closed' Issue ⚠️ -- #122 _I1 open_ in #101, #102 +- N/A: #122 _I1 open_ in #101, #102 - PR 101 1st release note - PR 101 2nd release note - PR 102 1st release note @@ -826,14 +825,14 @@ """ RELEASE_NOTES_DATA_CLOSED_ISSUE_NO_PR_WITH_USER_LABELS = """### Closed Issues without Pull Request ⚠️ -- #121 _Fix the bug_ in +- N/A: #121 _Fix the bug_ in #### Full Changelog http://example.com/changelog """ RELEASE_NOTES_DATA_CLOSED_ISSUE_WITH_PR_WITHOUT_USER_LABELS = """### Closed Issues without User Defined Labels ⚠️ -- #122 _I1_ in #101, #102 +- N/A: #122 _I1_ in #101, #102 - Fixed bug - Improved performance + More nice code @@ -866,10 +865,10 @@ """ RELEASE_NOTES_DATA_MERGED_PRS_WITH_OPEN_ISSUES = """### Merged PRs Linked to 'Not Closed' Issue ⚠️ -- #122 _I1 open_ in #101 +- N/A: #122 _I1 open_ in #101 - PR 101 1st release note - PR 101 2nd release note -- #123 _I2 open_ in #102 +- N/A: #123 _I2 open_ in #102 - PR 102 1st release note - PR 102 2nd release note @@ -878,7 +877,7 @@ """ RELEASE_NOTES_DATA_CLOSED_ISSUE_WITH_MERGED_PRS_WITHOUT_USER_LABELS = """### Closed Issues without User Defined Labels ⚠️ -- #121 _Fix the bug_ in #123 +- N/A: #121 _Fix the bug_ in #123 - Fixed bug - Improved performance + More nice code @@ -891,7 +890,7 @@ RELEASE_NOTES_DATA_CLOSED_ISSUE_WITH_MERGED_PRS_WITH_USER_LABELS_WITH_SKIP_LABEL = RELEASE_NOTES_NO_DATA_NO_WARNING_NO_EMPTY_CHAPTERS RELEASE_NOTES_DATA_CLOSED_ISSUE_WITH_MERGED_PRS_WITH_USER_LABELS = """### Chapter 1 🛠 -- #122 _I1+bug_ in #124 +- N/A: #122 _I1+bug_ in #124 - Fixed bug - Improved performance + More nice code @@ -1603,7 +1602,7 @@ def test_build_hierarchy_rls_notes_no_labels_no_type( mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 mock_github_client.get_rate_limit.return_value = mock_rate_limit - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) + factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) records = factory.generate(mined_data_isolated_record_types_no_labels_no_type_defined) builder = ReleaseNotesBuilder( @@ -1635,7 +1634,7 @@ def test_build_hierarchy_rls_notes_with_labels_no_type( mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 mock_github_client.get_rate_limit.return_value = mock_rate_limit - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) + factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) records = factory.generate(mined_data_isolated_record_types_with_labels_no_type_defined) builder = ReleaseNotesBuilder( @@ -1667,7 +1666,7 @@ def test_build_hierarchy_rls_notes_no_labels_with_type( mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 mock_github_client.get_rate_limit.return_value = mock_rate_limit - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) + factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) records = factory.generate(mined_data_isolated_record_types_no_labels_with_type_defined) builder = ReleaseNotesBuilder( @@ -1698,132 +1697,6 @@ def test_build_hierarchy_rls_notes_with_labels_with_type( mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 mock_github_client.get_rate_limit.return_value = mock_rate_limit - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) - records = factory.generate(mined_data_isolated_record_types_with_labels_with_type_defined) - - builder = ReleaseNotesBuilder( - records=records, - changelog_url=DEFAULT_CHANGELOG_URL, - custom_chapters=custom_chapters_not_print_empty_chapters, - ) - - actual_release_notes = builder.build() - - assert expected_release_notes == actual_release_notes - - -def test_build_no_hierarchy_rls_notes_no_labels_no_type_with_hierarchy_data( - mocker, mock_repo, - custom_chapters_not_print_empty_chapters, mined_data_isolated_record_types_no_labels_no_type_defined -): - expected_release_notes = RELEASE_NOTES_NO_DATA_HIERARCHY_NO_LABELS_NO_TYPE - - mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) - mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=True) - mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=False) - # mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_row_format_hierarchy_issue", return_value="{type}: _{title}_ {number}") - - mock_github_client = mocker.Mock(spec=Github) - - mock_rate_limit = mocker.Mock() - mock_rate_limit.rate.remaining = 10 - mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 - mock_github_client.get_rate_limit.return_value = mock_rate_limit - - factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) - records = factory.generate(mined_data_isolated_record_types_no_labels_no_type_defined) - - builder = ReleaseNotesBuilder( - records=records, - changelog_url=DEFAULT_CHANGELOG_URL, - custom_chapters=custom_chapters_not_print_empty_chapters, - ) - - actual_release_notes = builder.build() - - assert expected_release_notes == actual_release_notes - -def test_build_no_hierarchy_rls_notes_with_labels_no_type_with_hierarchy_data( - mocker, mock_repo, - custom_chapters_not_print_empty_chapters, mined_data_isolated_record_types_with_labels_no_type_defined -): - expected_release_notes = RELEASE_NOTES_NO_DATA_HIERARCHY_WITH_LABELS_NO_TYPE - - mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) - mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=True) - mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=False) - - mock_github_client = mocker.Mock(spec=Github) - - mock_rate_limit = mocker.Mock() - mock_rate_limit.rate.remaining = 10 - mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 - mock_github_client.get_rate_limit.return_value = mock_rate_limit - - factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) - records = factory.generate(mined_data_isolated_record_types_with_labels_no_type_defined) - - builder = ReleaseNotesBuilder( - records=records, - changelog_url=DEFAULT_CHANGELOG_URL, - custom_chapters=custom_chapters_not_print_empty_chapters, - ) - - actual_release_notes = builder.build() - - assert expected_release_notes == actual_release_notes - - -def test_build_no_hierarchy_rls_notes_no_labels_with_type_with_hierarchy_data( - mocker, mock_repo, - custom_chapters_not_print_empty_chapters, mined_data_isolated_record_types_no_labels_with_type_defined -): - expected_release_notes = RELEASE_NOTES_NO_DATA_HIERARCHY_NO_LABELS_WITH_TYPE - - mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) - mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=True) - mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=False) - # mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_row_format_hierarchy_issue", return_value="{type}: _{title}_ {number}") - - mock_github_client = mocker.Mock(spec=Github) - - mock_rate_limit = mocker.Mock() - mock_rate_limit.rate.remaining = 10 - mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 - mock_github_client.get_rate_limit.return_value = mock_rate_limit - - factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) - records = factory.generate(mined_data_isolated_record_types_no_labels_with_type_defined) - - builder = ReleaseNotesBuilder( - records=records, - changelog_url=DEFAULT_CHANGELOG_URL, - custom_chapters=custom_chapters_not_print_empty_chapters, - ) - - actual_release_notes = builder.build() - - assert expected_release_notes == actual_release_notes - - -def test_build_no_hierarchy_rls_notes_with_labels_with_type_with_hierarchy_data( - mocker, mock_repo, - custom_chapters_not_print_empty_chapters, mined_data_isolated_record_types_with_labels_with_type_defined -): - expected_release_notes = RELEASE_NOTES_NO_DATA_HIERARCHY_WITH_LABELS_WITH_TYPE - - mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) - mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_print_empty_chapters", return_value=True) - mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_hierarchy", return_value=False) - # mocker.patch("release_notes_generator.builder.builder.ActionInputs.get_row_format_hierarchy_issue", return_value="{type}: _{title}_ {number}") - - mock_github_client = mocker.Mock(spec=Github) - - mock_rate_limit = mocker.Mock() - mock_rate_limit.rate.remaining = 10 - mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 - mock_github_client.get_rate_limit.return_value = mock_rate_limit - factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) records = factory.generate(mined_data_isolated_record_types_with_labels_with_type_defined) diff --git a/tests/release_notes/chapters/test_custom_chapters.py b/tests/release_notes/chapters/test_custom_chapters.py index 9f51e5bf..d3324020 100644 --- a/tests/release_notes/chapters/test_custom_chapters.py +++ b/tests/release_notes/chapters/test_custom_chapters.py @@ -56,19 +56,19 @@ def test_populate(custom_chapters, mocker): record3.skip = False records = { - 1: record1, - 2: record2, - 3: record3, + "org/repo#1": record1, + "org/repo#2": record2, + "org/repo#3": record3, } custom_chapters.populate(records) - assert 1 in custom_chapters.chapters["Chapter 1"].rows - assert custom_chapters.chapters["Chapter 1"].rows[1] == "Record 1 Chapter Row" - assert 2 in custom_chapters.chapters["Chapter 1"].rows - assert custom_chapters.chapters["Chapter 1"].rows[2] == "Record 2 Chapter Row" - assert 3 in custom_chapters.chapters["Chapter 2"].rows - assert custom_chapters.chapters["Chapter 2"].rows[3] == "Record 3 Chapter Row" + assert "org/repo#1" in custom_chapters.chapters["Chapter 1"].rows + assert custom_chapters.chapters["Chapter 1"].rows["org/repo#1"] == "Record 1 Chapter Row" + assert "org/repo#2" in custom_chapters.chapters["Chapter 1"].rows + assert custom_chapters.chapters["Chapter 1"].rows["org/repo#2"] == "Record 2 Chapter Row" + assert "org/repo#3" in custom_chapters.chapters["Chapter 2"].rows + assert custom_chapters.chapters["Chapter 2"].rows["org/repo#3"] == "Record 3 Chapter Row" def test_populate_no_pulls_count(custom_chapters, mocker): @@ -109,7 +109,7 @@ def test_populate_service_duplicity_scope(custom_chapters, mocker): record1.skip = False records = { - 1: record1, + "org/repo#1": record1, } mocker.patch( @@ -119,9 +119,9 @@ def test_populate_service_duplicity_scope(custom_chapters, mocker): custom_chapters.populate(records) - assert 1 in custom_chapters.chapters["Chapter 1"].rows - assert custom_chapters.chapters["Chapter 1"].rows[1] == "Record 1 Chapter Row" - assert 1 not in custom_chapters.chapters["Chapter 2"].rows + assert "org/repo#1" in custom_chapters.chapters["Chapter 1"].rows + assert custom_chapters.chapters["Chapter 1"].rows["org/repo#1"] == "Record 1 Chapter Row" + assert "org/repo#1" not in custom_chapters.chapters["Chapter 2"].rows def test_populate_none_duplicity_scope(custom_chapters, mocker): @@ -133,7 +133,7 @@ def test_populate_none_duplicity_scope(custom_chapters, mocker): record1.skip = False records = { - 1: record1, + "org/repo#1": record1, } mocker.patch( @@ -142,9 +142,9 @@ def test_populate_none_duplicity_scope(custom_chapters, mocker): custom_chapters.populate(records) - assert 1 in custom_chapters.chapters["Chapter 1"].rows - assert custom_chapters.chapters["Chapter 1"].rows[1] == "Record 1 Chapter Row" - assert 1 not in custom_chapters.chapters["Chapter 2"].rows + assert "org/repo#1" in custom_chapters.chapters["Chapter 1"].rows + assert custom_chapters.chapters["Chapter 1"].rows["org/repo#1"] == "Record 1 Chapter Row" + assert "org/repo#1" not in custom_chapters.chapters["Chapter 2"].rows # from_json diff --git a/tests/release_notes/record/factory/test_default_record_factory.py b/tests/release_notes/record/factory/test_default_record_factory.py index c7bc2ca4..016aa3df 100644 --- a/tests/release_notes/record/factory/test_default_record_factory.py +++ b/tests/release_notes/record/factory/test_default_record_factory.py @@ -13,22 +13,25 @@ # See the License for the specific language governing permissions and # limitations under the License. # - import time - from datetime import datetime from typing import cast from github import Github +from github.Commit import Commit from github.Issue import Issue from github.PullRequest import PullRequest -from github.Commit import Commit from release_notes_generator.model.commit_record import CommitRecord +from release_notes_generator.model.hierarchy_issue_record import HierarchyIssueRecord from release_notes_generator.model.issue_record import IssueRecord from release_notes_generator.model.mined_data import MinedData from release_notes_generator.model.pull_request_record import PullRequestRecord from release_notes_generator.record.factory.default_record_factory import DefaultRecordFactory +from tests.conftest import mock_safe_call_decorator + + +# generate - non hierarchy issue records def setup_no_issues_pulls_commits(mocker): @@ -162,13 +165,6 @@ def setup_issues_pulls_commits(mocker, mock_repo): return mock_git_issue1, mock_git_issue2, mock_git_pr1, mock_git_pr2, mock_git_commit1, mock_git_commit2 -def mock_safe_call_decorator(_rate_limiter): - def wrapper(fn): - if fn.__name__ == "get_issues_for_pr": - return mock_get_issues_for_pr - return fn - return wrapper - def mock_get_issues_for_pr(pull_number: int) -> list[str]: if pull_number == 101: return ['org/repo#1'] @@ -282,7 +278,7 @@ def test_generate_with_no_commits(mocker, mock_repo): mock_repo.get_issue.return_value = issue2 data.commits = {} # No commits - mocker.patch("release_notes_generator.record.factory.default_record_factory.get_issues_for_pr", return_value=['org/repo#2']) + mocker.patch("release_notes_generator.record.factory.default_record_factory.get_issues_for_pr", return_value={'org/repo#2'}) records = DefaultRecordFactory(mock_github_client, mock_repo).generate(data) assert 2 == len(records) @@ -316,7 +312,7 @@ def test_generate_with_no_commits_with_wrong_issue_number_in_pull_body_mention(m mock_repo.get_issue.return_value = issue2 data.commits = {} # No commits - mocker.patch("release_notes_generator.record.factory.default_record_factory.get_issues_for_pr", return_value=['org/repo#2']) + mocker.patch("release_notes_generator.record.factory.default_record_factory.get_issues_for_pr", return_value={'org/repo#2'}) records = DefaultRecordFactory(mock_github_client, mock_repo).generate(data) assert 2 == len(records) @@ -436,16 +432,280 @@ def test_generate_with_no_pulls(mocker, mock_repo): assert 0 == cast(IssueRecord, records['org/repo#2']).pull_requests_count() -def mock_safe_call_decorator_wrong_issue_number(_rate_limiter): - def wrapper(fn): - if fn.__name__ == "get_issues_for_pr": - return mock_get_issues_for_pr_with_wrong_issue_number - return fn - return wrapper - def mock_get_issues_for_pr_with_wrong_issue_number(pull_number: int) -> list[int]: if pull_number == 101: return [] elif pull_number == 102: return [2] return [] + + +# generate - hierarchy issue records + + +def test_generate_no_input_data(mocker, mock_repo): + mock_github_client = mocker.Mock(spec=Github) + factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) + data = MinedData(mock_repo) + + result = factory.generate(data) + + assert 0 == len(result.values()) + +# - single issue record (closed) +# - single hierarchy issue record - two sub-issues without PRs +# - single hierarchy issue record - two sub-issues with PRs - no commits +# - single hierarchy issue record - two sub-issues with PRs - with commits +# - single hierarchy issue record - one sub hierarchy issues - two sub-issues with PRs - with commits +# - single pull request record (closed, merged) +# - single direct commit record +def test_generate_isolated_record_types_no_labels_no_type_defined(mocker, mock_repo, + mined_data_isolated_record_types_no_labels_no_type_defined): + mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) + mock_github_client = mocker.Mock(spec=Github) + + mock_rate_limit = mocker.Mock() + mock_rate_limit.rate.remaining = 10 + mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 + mock_github_client.get_rate_limit.return_value = mock_rate_limit + + factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) + + result = factory.generate(mined_data_isolated_record_types_no_labels_no_type_defined) + + assert 8 == len(result) + assert {'org/repo#121', 'org/repo#301', 'org/repo#302', 'org/repo#303', 'org/repo#304', 'org/repo#123', 'org/repo#124', "merge_commit_sha_direct"}.issubset(result.keys()) + + assert isinstance(result['org/repo#121'], IssueRecord) + assert isinstance(result['org/repo#301'], HierarchyIssueRecord) + assert isinstance(result['org/repo#302'], HierarchyIssueRecord) + assert isinstance(result['org/repo#303'], HierarchyIssueRecord) + assert isinstance(result['org/repo#304'], HierarchyIssueRecord) + assert isinstance(result['org/repo#123'], PullRequestRecord) + assert isinstance(result['org/repo#124'], PullRequestRecord) + assert isinstance(result["merge_commit_sha_direct"], CommitRecord) + + rec_i = cast(IssueRecord, result['org/repo#121']) + assert 0 == rec_i.pull_requests_count() + + rec_hi_1 = cast(HierarchyIssueRecord, result['org/repo#301']) + assert 0 == rec_hi_1.pull_requests_count() + assert 0 == len(rec_hi_1.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_1.sub_issues.values()) + assert 0 == rec_hi_1.sub_issues['org/repo#450'].pull_requests_count() + assert 0 == rec_hi_1.level + + rec_hi_2 = cast(HierarchyIssueRecord, result['org/repo#302']) + assert 1 == rec_hi_2.pull_requests_count() + assert 0 == len(rec_hi_2.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_2.sub_issues.values()) + assert 1 == rec_hi_2.sub_issues['org/repo#451'].pull_requests_count() + assert 0 == rec_hi_2.level + + rec_hi_3 = cast(HierarchyIssueRecord, result['org/repo#303']) + assert 1 == rec_hi_3.pull_requests_count() + assert 0 == len(rec_hi_3.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_3.sub_issues.values()) + assert 1 == rec_hi_3.sub_issues['org/repo#452'].pull_requests_count() + assert "Fixed bug in PR 151" == rec_hi_3.sub_issues['org/repo#452'].get_commit(151, "merge_commit_sha_151").commit.message + assert 0 == rec_hi_3.level + + rec_hi_4 = cast(HierarchyIssueRecord, result['org/repo#304']) + assert 1 == rec_hi_4.pull_requests_count() + assert 1 == len(rec_hi_4.sub_hierarchy_issues.values()) + assert 0 == len(rec_hi_4.sub_issues.values()) + assert 1 == rec_hi_4.pull_requests_count() + assert "Fixed bug in PR 152" == rec_hi_4.sub_hierarchy_issues['org/repo#350'].sub_issues['org/repo#453'].get_commit(152, "merge_commit_sha_152").commit.message + assert 0 == rec_hi_4.level + + rec_hi_5 = cast(HierarchyIssueRecord, result['org/repo#304']) + assert 1 == rec_hi_5.sub_hierarchy_issues['org/repo#350'].level + + +# - single issue record (closed) +# - single hierarchy issue record - two sub-issues without PRs +# - single hierarchy issue record - two sub-issues with PRs - no commits +# - single hierarchy issue record - two sub-issues with PRs - with commits +# - single hierarchy issue record - one sub hierarchy issues - two sub-issues with PRs - with commits +# - single pull request record (closed, merged) +# - single direct commit record +def test_generate_isolated_record_types_with_labels_no_type_defined(mocker, mock_repo, + mined_data_isolated_record_types_with_labels_no_type_defined): + mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) + mock_github_client = mocker.Mock(spec=Github) + + mock_rate_limit = mocker.Mock() + mock_rate_limit.rate.remaining = 10 + mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 + mock_github_client.get_rate_limit.return_value = mock_rate_limit + + factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) + + result = factory.generate(mined_data_isolated_record_types_with_labels_no_type_defined) + + assert 8 == len(result) + assert {'org/repo#121', 'org/repo#301', 'org/repo#302', 'org/repo#303', 'org/repo#304', 'org/repo#123', 'org/repo#124', "merge_commit_sha_direct"}.issubset(result.keys()) + + assert isinstance(result['org/repo#121'], IssueRecord) + assert isinstance(result['org/repo#301'], HierarchyIssueRecord) + assert isinstance(result['org/repo#302'], HierarchyIssueRecord) + assert isinstance(result['org/repo#303'], HierarchyIssueRecord) + assert isinstance(result['org/repo#304'], HierarchyIssueRecord) + assert isinstance(result['org/repo#123'], PullRequestRecord) + assert isinstance(result['org/repo#124'], PullRequestRecord) + assert isinstance(result["merge_commit_sha_direct"], CommitRecord) + + rec_i = cast(IssueRecord, result['org/repo#121']) + assert 0 == rec_i.pull_requests_count() + + rec_hi_1 = cast(HierarchyIssueRecord, result['org/repo#301']) + assert 0 == rec_hi_1.pull_requests_count() + assert 0 == len(rec_hi_1.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_1.sub_issues.values()) + assert 0 == rec_hi_1.sub_issues['org/repo#450'].pull_requests_count() + + rec_hi_2 = cast(HierarchyIssueRecord, result['org/repo#302']) + assert 1 == rec_hi_2.pull_requests_count() + assert 0 == len(rec_hi_2.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_2.sub_issues.values()) + assert 1 == rec_hi_2.sub_issues['org/repo#451'].pull_requests_count() + + rec_hi_3 = cast(HierarchyIssueRecord, result['org/repo#303']) + assert 1 == rec_hi_3.pull_requests_count() + assert 0 == len(rec_hi_3.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_3.sub_issues.values()) + assert 1 == rec_hi_3.sub_issues['org/repo#452'].pull_requests_count() + assert "Fixed bug in PR 151" == rec_hi_3.sub_issues['org/repo#452'].get_commit(151, "merge_commit_sha_151").commit.message + + rec_hi_4 = cast(HierarchyIssueRecord, result['org/repo#304']) + assert 1 == rec_hi_4.pull_requests_count() + assert 1 == len(rec_hi_4.sub_hierarchy_issues.values()) + assert 0 == len(rec_hi_4.sub_issues.values()) + assert 1 == rec_hi_4.pull_requests_count() + assert "Fixed bug in PR 152" == rec_hi_4.sub_hierarchy_issues['org/repo#350'].sub_issues['org/repo#453'].get_commit(152, "merge_commit_sha_152").commit.message + + +# - single issue record (closed) +# - single hierarchy issue record - two sub-issues without PRs +# - single hierarchy issue record - two sub-issues with PRs - no commits +# - single hierarchy issue record - two sub-issues with PRs - with commits +# - single hierarchy issue record - one sub hierarchy issues - two sub-issues with PRs - with commits +# - single pull request record (closed, merged) +# - single direct commit record +def test_generate_isolated_record_types_no_labels_with_type_defined(mocker, mock_repo, + mined_data_isolated_record_types_no_labels_with_type_defined): + mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) + mock_github_client = mocker.Mock(spec=Github) + + mock_rate_limit = mocker.Mock() + mock_rate_limit.rate.remaining = 10 + mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 + mock_github_client.get_rate_limit.return_value = mock_rate_limit + + factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) + + result = factory.generate(mined_data_isolated_record_types_no_labels_with_type_defined) + + assert 8 == len(result) + assert {'org/repo#121', 'org/repo#301', 'org/repo#302', 'org/repo#303', 'org/repo#304', 'org/repo#123', 'org/repo#124', "merge_commit_sha_direct"}.issubset(result.keys()) + + assert isinstance(result['org/repo#121'], IssueRecord) + assert isinstance(result['org/repo#301'], HierarchyIssueRecord) + assert isinstance(result['org/repo#302'], HierarchyIssueRecord) + assert isinstance(result['org/repo#303'], HierarchyIssueRecord) + assert isinstance(result['org/repo#304'], HierarchyIssueRecord) + assert isinstance(result['org/repo#123'], PullRequestRecord) + assert isinstance(result['org/repo#124'], PullRequestRecord) + assert isinstance(result["merge_commit_sha_direct"], CommitRecord) + + rec_i = cast(IssueRecord, result['org/repo#121']) + assert 0 == rec_i.pull_requests_count() + + rec_hi_1 = cast(HierarchyIssueRecord, result['org/repo#301']) + assert 0 == rec_hi_1.pull_requests_count() + assert 0 == len(rec_hi_1.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_1.sub_issues.values()) + assert 0 == rec_hi_1.sub_issues['org/repo#450'].pull_requests_count() + + rec_hi_2 = cast(HierarchyIssueRecord, result['org/repo#302']) + assert 1 == rec_hi_2.pull_requests_count() + assert 0 == len(rec_hi_2.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_2.sub_issues.values()) + assert 1 == rec_hi_2.sub_issues['org/repo#451'].pull_requests_count() + + rec_hi_3 = cast(HierarchyIssueRecord, result['org/repo#303']) + assert 1 == rec_hi_3.pull_requests_count() + assert 0 == len(rec_hi_3.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_3.sub_issues.values()) + assert 1 == rec_hi_3.sub_issues['org/repo#452'].pull_requests_count() + assert "Fixed bug in PR 151" == rec_hi_3.sub_issues['org/repo#452'].get_commit(151, "merge_commit_sha_151").commit.message + + rec_hi_4 = cast(HierarchyIssueRecord, result['org/repo#304']) + assert 1 == rec_hi_4.pull_requests_count() + assert 1 == len(rec_hi_4.sub_hierarchy_issues.values()) + assert 0 == len(rec_hi_4.sub_issues.values()) + assert 1 == rec_hi_4.pull_requests_count() + assert "Fixed bug in PR 152" == rec_hi_4.sub_hierarchy_issues['org/repo#350'].sub_issues['org/repo#453'].get_commit(152, "merge_commit_sha_152").commit.message + + +# - single issue record (closed) +# - single hierarchy issue record - two sub-issues without PRs +# - single hierarchy issue record - two sub-issues with PRs - no commits +# - single hierarchy issue record - two sub-issues with PRs - with commits +# - single hierarchy issue record - one sub hierarchy issues - two sub-issues with PRs - with commits +# - single pull request record (closed, merged) +# - single direct commit record +def test_generate_isolated_record_types_with_labels_with_type_defined(mocker, mock_repo, + mined_data_isolated_record_types_with_labels_with_type_defined): + mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) + mock_github_client = mocker.Mock(spec=Github) + + mock_rate_limit = mocker.Mock() + mock_rate_limit.rate.remaining = 10 + mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 + mock_github_client.get_rate_limit.return_value = mock_rate_limit + + factory = DefaultRecordFactory(github=mock_github_client, home_repository=mock_repo) + + result = factory.generate(mined_data_isolated_record_types_with_labels_with_type_defined) + + assert 8 == len(result) + assert {'org/repo#121', 'org/repo#301', 'org/repo#302', 'org/repo#303', 'org/repo#304', 'org/repo#123', 'org/repo#124', "merge_commit_sha_direct"}.issubset(result.keys()) + + assert isinstance(result['org/repo#121'], IssueRecord) + assert isinstance(result['org/repo#301'], HierarchyIssueRecord) + assert isinstance(result['org/repo#302'], HierarchyIssueRecord) + assert isinstance(result['org/repo#303'], HierarchyIssueRecord) + assert isinstance(result['org/repo#304'], HierarchyIssueRecord) + assert isinstance(result['org/repo#123'], PullRequestRecord) + assert isinstance(result['org/repo#124'], PullRequestRecord) + assert isinstance(result["merge_commit_sha_direct"], CommitRecord) + + rec_i = cast(IssueRecord, result['org/repo#121']) + assert 0 == rec_i.pull_requests_count() + + rec_hi_1 = cast(HierarchyIssueRecord, result['org/repo#301']) + assert 0 == rec_hi_1.pull_requests_count() + assert 0 == len(rec_hi_1.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_1.sub_issues.values()) + assert 0 == rec_hi_1.sub_issues['org/repo#450'].pull_requests_count() + + rec_hi_2 = cast(HierarchyIssueRecord, result['org/repo#302']) + assert 1 == rec_hi_2.pull_requests_count() + assert 0 == len(rec_hi_2.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_2.sub_issues.values()) + assert 1 == rec_hi_2.sub_issues['org/repo#451'].pull_requests_count() + + rec_hi_3 = cast(HierarchyIssueRecord, result['org/repo#303']) + assert 1 == rec_hi_3.pull_requests_count() + assert 0 == len(rec_hi_3.sub_hierarchy_issues.values()) + assert 2 == len(rec_hi_3.sub_issues.values()) + assert 1 == rec_hi_3.sub_issues['org/repo#452'].pull_requests_count() + assert "Fixed bug in PR 151" == rec_hi_3.sub_issues['org/repo#452'].get_commit(151, "merge_commit_sha_151").commit.message + + rec_hi_4 = cast(HierarchyIssueRecord, result['org/repo#304']) + assert 1 == rec_hi_4.pull_requests_count() + assert 1 == len(rec_hi_4.sub_hierarchy_issues.values()) + assert 0 == len(rec_hi_4.sub_issues.values()) + assert 1 == rec_hi_4.pull_requests_count() + assert "Fixed bug in PR 152" == rec_hi_4.sub_hierarchy_issues['org/repo#350'].sub_issues['org/repo#453'].get_commit(152, "merge_commit_sha_152").commit.message diff --git a/tests/release_notes/record/factory/test_issue_hierarchy_record_factory.py b/tests/release_notes/record/factory/test_issue_hierarchy_record_factory.py deleted file mode 100644 index 6dcb1608..00000000 --- a/tests/release_notes/record/factory/test_issue_hierarchy_record_factory.py +++ /dev/null @@ -1,297 +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. -# -import time -from typing import cast - -from github import Github - -from release_notes_generator.model.commit_record import CommitRecord -from release_notes_generator.model.hierarchy_issue_record import HierarchyIssueRecord -from release_notes_generator.model.issue_record import IssueRecord -from release_notes_generator.model.mined_data import MinedData -from release_notes_generator.model.pull_request_record import PullRequestRecord -from release_notes_generator.record.factory.issue_hierarchy_record_factory import IssueHierarchyRecordFactory -from tests.conftest import mock_safe_call_decorator - - -# generate - -def test_generate_no_input_data(mocker, mock_repo): - mock_github_client = mocker.Mock(spec=Github) - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) - data = MinedData(mock_repo) - - result = factory.generate(data) - - assert 0 == len(result.values()) - -# - single issue record (closed) -# - single hierarchy issue record - two sub-issues without PRs -# - single hierarchy issue record - two sub-issues with PRs - no commits -# - single hierarchy issue record - two sub-issues with PRs - with commits -# - single hierarchy issue record - one sub hierarchy issues - two sub-issues with PRs - with commits -# - single pull request record (closed, merged) -# - single direct commit record -def test_generate_isolated_record_types_no_labels_no_type_defined(mocker, mock_repo, - mined_data_isolated_record_types_no_labels_no_type_defined): - mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) - mock_github_client = mocker.Mock(spec=Github) - - mock_rate_limit = mocker.Mock() - mock_rate_limit.rate.remaining = 10 - mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 - mock_github_client.get_rate_limit.return_value = mock_rate_limit - - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) - - result = factory.generate(mined_data_isolated_record_types_no_labels_no_type_defined) - - assert 8 == len(result) - assert {'org/repo#121', 'org/repo#301', 'org/repo#302', 'org/repo#303', 'org/repo#304', 'org/repo#123', 'org/repo#124', "merge_commit_sha_direct"}.issubset(result.keys()) - - assert isinstance(result['org/repo#121'], IssueRecord) - assert isinstance(result['org/repo#301'], HierarchyIssueRecord) - assert isinstance(result['org/repo#302'], HierarchyIssueRecord) - assert isinstance(result['org/repo#303'], HierarchyIssueRecord) - assert isinstance(result['org/repo#304'], HierarchyIssueRecord) - assert isinstance(result['org/repo#123'], PullRequestRecord) - assert isinstance(result['org/repo#124'], PullRequestRecord) - assert isinstance(result["merge_commit_sha_direct"], CommitRecord) - - rec_i = cast(IssueRecord, result['org/repo#121']) - assert 0 == rec_i.pull_requests_count() - - rec_hi_1 = cast(HierarchyIssueRecord, result['org/repo#301']) - assert 0 == rec_hi_1.pull_requests_count() - assert 0 == len(rec_hi_1.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_1.sub_issues.values()) - assert 0 == rec_hi_1.sub_issues['org/repo#450'].pull_requests_count() - assert 0 == rec_hi_1.level - - rec_hi_2 = cast(HierarchyIssueRecord, result['org/repo#302']) - assert 1 == rec_hi_2.pull_requests_count() - assert 0 == len(rec_hi_2.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_2.sub_issues.values()) - assert 1 == rec_hi_2.sub_issues['org/repo#451'].pull_requests_count() - assert 0 == rec_hi_2.level - - rec_hi_3 = cast(HierarchyIssueRecord, result['org/repo#303']) - assert 1 == rec_hi_3.pull_requests_count() - assert 0 == len(rec_hi_3.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_3.sub_issues.values()) - assert 1 == rec_hi_3.sub_issues['org/repo#452'].pull_requests_count() - assert "Fixed bug in PR 151" == rec_hi_3.sub_issues['org/repo#452'].get_commit(151, "merge_commit_sha_151").commit.message - assert 0 == rec_hi_3.level - - rec_hi_4 = cast(HierarchyIssueRecord, result['org/repo#304']) - assert 1 == rec_hi_4.pull_requests_count() - assert 1 == len(rec_hi_4.sub_hierarchy_issues.values()) - assert 0 == len(rec_hi_4.sub_issues.values()) - assert 1 == rec_hi_4.pull_requests_count() - assert "Fixed bug in PR 152" == rec_hi_4.sub_hierarchy_issues['org/repo#350'].sub_issues['org/repo#453'].get_commit(152, "merge_commit_sha_152").commit.message - assert 0 == rec_hi_4.level - - rec_hi_5 = cast(HierarchyIssueRecord, result['org/repo#304']) - assert 1 == rec_hi_5.sub_hierarchy_issues['org/repo#350'].level - - -# - single issue record (closed) -# - single hierarchy issue record - two sub-issues without PRs -# - single hierarchy issue record - two sub-issues with PRs - no commits -# - single hierarchy issue record - two sub-issues with PRs - with commits -# - single hierarchy issue record - one sub hierarchy issues - two sub-issues with PRs - with commits -# - single pull request record (closed, merged) -# - single direct commit record -def test_generate_isolated_record_types_with_labels_no_type_defined(mocker, mock_repo, - mined_data_isolated_record_types_with_labels_no_type_defined): - mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) - mock_github_client = mocker.Mock(spec=Github) - - mock_rate_limit = mocker.Mock() - mock_rate_limit.rate.remaining = 10 - mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 - mock_github_client.get_rate_limit.return_value = mock_rate_limit - - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) - - result = factory.generate(mined_data_isolated_record_types_with_labels_no_type_defined) - - assert 8 == len(result) - assert {'org/repo#121', 'org/repo#301', 'org/repo#302', 'org/repo#303', 'org/repo#304', 'org/repo#123', 'org/repo#124', "merge_commit_sha_direct"}.issubset(result.keys()) - - assert isinstance(result['org/repo#121'], IssueRecord) - assert isinstance(result['org/repo#301'], HierarchyIssueRecord) - assert isinstance(result['org/repo#302'], HierarchyIssueRecord) - assert isinstance(result['org/repo#303'], HierarchyIssueRecord) - assert isinstance(result['org/repo#304'], HierarchyIssueRecord) - assert isinstance(result['org/repo#123'], PullRequestRecord) - assert isinstance(result['org/repo#124'], PullRequestRecord) - assert isinstance(result["merge_commit_sha_direct"], CommitRecord) - - rec_i = cast(IssueRecord, result['org/repo#121']) - assert 0 == rec_i.pull_requests_count() - - rec_hi_1 = cast(HierarchyIssueRecord, result['org/repo#301']) - assert 0 == rec_hi_1.pull_requests_count() - assert 0 == len(rec_hi_1.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_1.sub_issues.values()) - assert 0 == rec_hi_1.sub_issues['org/repo#450'].pull_requests_count() - - rec_hi_2 = cast(HierarchyIssueRecord, result['org/repo#302']) - assert 1 == rec_hi_2.pull_requests_count() - assert 0 == len(rec_hi_2.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_2.sub_issues.values()) - assert 1 == rec_hi_2.sub_issues['org/repo#451'].pull_requests_count() - - rec_hi_3 = cast(HierarchyIssueRecord, result['org/repo#303']) - assert 1 == rec_hi_3.pull_requests_count() - assert 0 == len(rec_hi_3.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_3.sub_issues.values()) - assert 1 == rec_hi_3.sub_issues['org/repo#452'].pull_requests_count() - assert "Fixed bug in PR 151" == rec_hi_3.sub_issues['org/repo#452'].get_commit(151, "merge_commit_sha_151").commit.message - - rec_hi_4 = cast(HierarchyIssueRecord, result['org/repo#304']) - assert 1 == rec_hi_4.pull_requests_count() - assert 1 == len(rec_hi_4.sub_hierarchy_issues.values()) - assert 0 == len(rec_hi_4.sub_issues.values()) - assert 1 == rec_hi_4.pull_requests_count() - assert "Fixed bug in PR 152" == rec_hi_4.sub_hierarchy_issues['org/repo#350'].sub_issues['org/repo#453'].get_commit(152, "merge_commit_sha_152").commit.message - - -# - single issue record (closed) -# - single hierarchy issue record - two sub-issues without PRs -# - single hierarchy issue record - two sub-issues with PRs - no commits -# - single hierarchy issue record - two sub-issues with PRs - with commits -# - single hierarchy issue record - one sub hierarchy issues - two sub-issues with PRs - with commits -# - single pull request record (closed, merged) -# - single direct commit record -def test_generate_isolated_record_types_no_labels_with_type_defined(mocker, mock_repo, - mined_data_isolated_record_types_no_labels_with_type_defined): - mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) - mock_github_client = mocker.Mock(spec=Github) - - mock_rate_limit = mocker.Mock() - mock_rate_limit.rate.remaining = 10 - mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 - mock_github_client.get_rate_limit.return_value = mock_rate_limit - - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) - - result = factory.generate(mined_data_isolated_record_types_no_labels_with_type_defined) - - assert 8 == len(result) - assert {'org/repo#121', 'org/repo#301', 'org/repo#302', 'org/repo#303', 'org/repo#304', 'org/repo#123', 'org/repo#124', "merge_commit_sha_direct"}.issubset(result.keys()) - - assert isinstance(result['org/repo#121'], IssueRecord) - assert isinstance(result['org/repo#301'], HierarchyIssueRecord) - assert isinstance(result['org/repo#302'], HierarchyIssueRecord) - assert isinstance(result['org/repo#303'], HierarchyIssueRecord) - assert isinstance(result['org/repo#304'], HierarchyIssueRecord) - assert isinstance(result['org/repo#123'], PullRequestRecord) - assert isinstance(result['org/repo#124'], PullRequestRecord) - assert isinstance(result["merge_commit_sha_direct"], CommitRecord) - - rec_i = cast(IssueRecord, result['org/repo#121']) - assert 0 == rec_i.pull_requests_count() - - rec_hi_1 = cast(HierarchyIssueRecord, result['org/repo#301']) - assert 0 == rec_hi_1.pull_requests_count() - assert 0 == len(rec_hi_1.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_1.sub_issues.values()) - assert 0 == rec_hi_1.sub_issues['org/repo#450'].pull_requests_count() - - rec_hi_2 = cast(HierarchyIssueRecord, result['org/repo#302']) - assert 1 == rec_hi_2.pull_requests_count() - assert 0 == len(rec_hi_2.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_2.sub_issues.values()) - assert 1 == rec_hi_2.sub_issues['org/repo#451'].pull_requests_count() - - rec_hi_3 = cast(HierarchyIssueRecord, result['org/repo#303']) - assert 1 == rec_hi_3.pull_requests_count() - assert 0 == len(rec_hi_3.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_3.sub_issues.values()) - assert 1 == rec_hi_3.sub_issues['org/repo#452'].pull_requests_count() - assert "Fixed bug in PR 151" == rec_hi_3.sub_issues['org/repo#452'].get_commit(151, "merge_commit_sha_151").commit.message - - rec_hi_4 = cast(HierarchyIssueRecord, result['org/repo#304']) - assert 1 == rec_hi_4.pull_requests_count() - assert 1 == len(rec_hi_4.sub_hierarchy_issues.values()) - assert 0 == len(rec_hi_4.sub_issues.values()) - assert 1 == rec_hi_4.pull_requests_count() - assert "Fixed bug in PR 152" == rec_hi_4.sub_hierarchy_issues['org/repo#350'].sub_issues['org/repo#453'].get_commit(152, "merge_commit_sha_152").commit.message - - -# - single issue record (closed) -# - single hierarchy issue record - two sub-issues without PRs -# - single hierarchy issue record - two sub-issues with PRs - no commits -# - single hierarchy issue record - two sub-issues with PRs - with commits -# - single hierarchy issue record - one sub hierarchy issues - two sub-issues with PRs - with commits -# - single pull request record (closed, merged) -# - single direct commit record -def test_generate_isolated_record_types_with_labels_with_type_defined(mocker, mock_repo, - mined_data_isolated_record_types_with_labels_with_type_defined): - mocker.patch("release_notes_generator.record.factory.default_record_factory.safe_call_decorator", side_effect=mock_safe_call_decorator) - mock_github_client = mocker.Mock(spec=Github) - - mock_rate_limit = mocker.Mock() - mock_rate_limit.rate.remaining = 10 - mock_rate_limit.rate.reset.timestamp.return_value = time.time() + 3600 - mock_github_client.get_rate_limit.return_value = mock_rate_limit - - factory = IssueHierarchyRecordFactory(github=mock_github_client, home_repository=mock_repo) - - result = factory.generate(mined_data_isolated_record_types_with_labels_with_type_defined) - - assert 8 == len(result) - assert {'org/repo#121', 'org/repo#301', 'org/repo#302', 'org/repo#303', 'org/repo#304', 'org/repo#123', 'org/repo#124', "merge_commit_sha_direct"}.issubset(result.keys()) - - assert isinstance(result['org/repo#121'], IssueRecord) - assert isinstance(result['org/repo#301'], HierarchyIssueRecord) - assert isinstance(result['org/repo#302'], HierarchyIssueRecord) - assert isinstance(result['org/repo#303'], HierarchyIssueRecord) - assert isinstance(result['org/repo#304'], HierarchyIssueRecord) - assert isinstance(result['org/repo#123'], PullRequestRecord) - assert isinstance(result['org/repo#124'], PullRequestRecord) - assert isinstance(result["merge_commit_sha_direct"], CommitRecord) - - rec_i = cast(IssueRecord, result['org/repo#121']) - assert 0 == rec_i.pull_requests_count() - - rec_hi_1 = cast(HierarchyIssueRecord, result['org/repo#301']) - assert 0 == rec_hi_1.pull_requests_count() - assert 0 == len(rec_hi_1.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_1.sub_issues.values()) - assert 0 == rec_hi_1.sub_issues['org/repo#450'].pull_requests_count() - - rec_hi_2 = cast(HierarchyIssueRecord, result['org/repo#302']) - assert 1 == rec_hi_2.pull_requests_count() - assert 0 == len(rec_hi_2.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_2.sub_issues.values()) - assert 1 == rec_hi_2.sub_issues['org/repo#451'].pull_requests_count() - - rec_hi_3 = cast(HierarchyIssueRecord, result['org/repo#303']) - assert 1 == rec_hi_3.pull_requests_count() - assert 0 == len(rec_hi_3.sub_hierarchy_issues.values()) - assert 2 == len(rec_hi_3.sub_issues.values()) - assert 1 == rec_hi_3.sub_issues['org/repo#452'].pull_requests_count() - assert "Fixed bug in PR 151" == rec_hi_3.sub_issues['org/repo#452'].get_commit(151, "merge_commit_sha_151").commit.message - - rec_hi_4 = cast(HierarchyIssueRecord, result['org/repo#304']) - assert 1 == rec_hi_4.pull_requests_count() - assert 1 == len(rec_hi_4.sub_hierarchy_issues.values()) - assert 0 == len(rec_hi_4.sub_issues.values()) - assert 1 == rec_hi_4.pull_requests_count() - assert "Fixed bug in PR 152" == rec_hi_4.sub_hierarchy_issues['org/repo#350'].sub_issues['org/repo#453'].get_commit(152, "merge_commit_sha_152").commit.message diff --git a/tests/test_release_notes_generator.py b/tests/test_release_notes_generator.py index 4654e8ec..19ab8c0b 100644 --- a/tests/test_release_notes_generator.py +++ b/tests/test_release_notes_generator.py @@ -78,8 +78,8 @@ def test_generate_release_notes_latest_release_not_found( release_notes = ReleaseNotesGenerator(github_mock, custom_chapters).generate() assert release_notes is not None - assert "- #121 _Fix the bug_" in release_notes - assert "- #122 _I1+bug_" in release_notes + assert "- N/A: #121 _Fix the bug_" in release_notes + assert "- N/A: #122 _I1+bug_" in release_notes assert "- PR: #101 _Fixed bug_" in release_notes assert "- PR: #102 _Fixed bug_" in release_notes @@ -132,7 +132,7 @@ def test_generate_release_notes_latest_release_found_by_created_at( release_notes = ReleaseNotesGenerator(github_mock, custom_chapters).generate() assert release_notes is not None - assert "- #122 _I1+bug_" in release_notes + assert "- N/A: #122 _I1+bug_" in release_notes assert "- PR: #101 _Fixed bug_" not in release_notes assert "- PR: #102 _Fixed bug_" in release_notes @@ -182,6 +182,6 @@ def test_generate_release_notes_latest_release_found_by_published_at( release_notes = ReleaseNotesGenerator(github_mock, custom_chapters).generate() assert release_notes is not None - assert "- #122 _I1+bug_" in release_notes + assert "- N/A: #122 _I1+bug_" in release_notes assert "- PR: #101 _Fixed bug_" not in release_notes assert "- PR: #102 _Fixed bug_" in release_notes