diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ccef83e..6350b10 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -2,6 +2,7 @@ name: Cargo Build & Test on: push: + branches: [main] pull_request: env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f9dc44..e9c4c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,4 +6,8 @@ - (lint) [#5](https://github.com/MalteHerrmann/changelog-utils/pull/5) Implement fix flag for linter CLI. - (lint) [#4](https://github.com/MalteHerrmann/changelog-utils/pull/4) Rewrite linter implementation in Rust. -- (lint) [#1](https://github.com/MalteHerrmann/changelog-utils/pull/1) Add initial implementation for linter in Python. \ No newline at end of file +- (lint) [#1](https://github.com/MalteHerrmann/changelog-utils/pull/1) Add initial implementation for linter in Python. + +### Improvements + +- (lint) [#6](https://github.com/MalteHerrmann/changelog-utils/pull/6) Remove Python implementation. diff --git a/linter/.gitignore b/linter/.gitignore deleted file mode 100644 index c18dd8d..0000000 --- a/linter/.gitignore +++ /dev/null @@ -1 +0,0 @@ -__pycache__/ diff --git a/linter/README.md b/linter/README.md deleted file mode 100644 index 8e4c368..0000000 --- a/linter/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# Changelog Checker - -This utility checks if the contents of the changelog fit the desired formatting and spelling requirements. -Run the checker by executing `make check-changelog` in the root directory of the repository. - -```bash -make check-changelog -``` - -It is also possible to have the script fix some easily fixable issues automatically. - -```bash -make fix-changelog -``` - -## Configuration - -It is possible to adjust the configuration of the changelog checker -by adjusting the contents of `config.py`. - -Things that can be adjusted include: - -- the allowed change types with a release -- the allowed description categories (i.e. the `(...)` portion at the beginning of an entry) -- PRs that are allowed to occur twice in the changelog (e.g. backports of bug fixes) -- a set of known patterns in PR descriptions and their preferred way of spelling -- known exceptions that do not need to follow the formatting rules -- the legacy version at which to stop the checking diff --git a/linter/change_type.py b/linter/change_type.py deleted file mode 100644 index 47f6bab..0000000 --- a/linter/change_type.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -This file contains the definition for the ChangeType class. It is used to parse the section header for a -given type of changes like improvements or bug fixes. -""" - -import re -from typing import List - -from config import ALLOWED_CHANGE_TYPES -from entry import check_spelling - -# Allowed change type pattern, e.g. `### Bug Fixes` -CHANGE_TYPE_PATTERN = re.compile( - r"^### (?P[a-zA-Z0-9\- ]+)\s*$", -) - - -class ChangeType: - """ - This class represents a section header in the changelog. - """ - - def __init__(self, line: str): - self.line: str = line - self.fixed: str = line - self.type: str = "" - self.problems: List[str] = [] - - def parse(self) -> bool: - """ - Parses a change type entry from a line of text. - - :return: boolean indicating whether the parsing was successful - """ - - problems: List[str] = [] - match = CHANGE_TYPE_PATTERN.match(self.line) - if not match: - problems.append(f'Malformed change type: "{self.line}"') - self.problems = problems - return False - - self.type = match.group("type") - - type_found, fixed_type, spelling_problems = check_spelling( - self.type, ALLOWED_CHANGE_TYPES - ) - if not type_found: - problems.append(f'"{self.type}" is not a valid change type') - if spelling_problems: - problems.extend(spelling_problems) - - self.fixed = f"### {fixed_type}" - self.problems = problems - - return problems == [] diff --git a/linter/check_changelog.py b/linter/check_changelog.py deleted file mode 100644 index 69dd0b7..0000000 --- a/linter/check_changelog.py +++ /dev/null @@ -1,168 +0,0 @@ -""" -This file contains the logic to check the changelog contents. - -It is possible to run this script with the `--fix` flag to automatically -fix a selection of common problems in the changelog. - -Usage: - python3 check_changelog.py [--fix] - -""" - -import io -import os -import sys -from typing import Dict, List, Union - -from change_type import ChangeType -from config import ALLOWED_DUPLICATES, LEGACY_VERSION -from entry import Entry -from release import Release - - -def write(file: Union[None, io.TextIOWrapper], line: str): - """ - This function writes a line to a file. - - :param file: The file to write to. - :param line: The line to write. - """ - - if file is not None: - file.write(line) - - -class Changelog: - """ - This class represents the contents of the changelog and provides methods to parse it. - """ - - def __init__(self, filename: str): - self.contents: List[str] - self.filename: str = filename - - self.problems: List[str] = [] - self.releases: Dict[str, Dict[str, Dict[int, Dict[str, str]]]] = {} - - if not os.path.exists(self.filename): - raise FileNotFoundError(f'Changelog file "{self.filename}" not found') - - with open(self.filename, "r") as file: - self.contents = file.read().split("\n") - - def parse(self, fix: bool = False) -> bool: - """ - This function parses the changelog and checks if the structure is as expected. - - :param fix: An optional parameter specifying if the changelog should be fixed automatically. - """ - - current_release: str = "" - current_category: str = "" - f = None - is_legacy: bool = False - seen_prs: List[int] = [] - - if fix: - f = open(self.filename, "w") - - try: - for line in self.contents: - if is_legacy: - if fix: - write(f, line + "\n") - continue - - # Check for Header 2 (##) to identify releases - stripped_line = line.strip() - if stripped_line[:3] == "## ": - release = Release(line) - release.parse() - current_release = release.version - if current_release in self.releases: - self.problems.append( - f'Release "{current_release}" is duplicated in the changelog' - ) - else: - self.releases[current_release] = {} - self.problems.extend(release.problems) - - if release <= LEGACY_VERSION: - is_legacy = True - - if fix: - write(f, release.fixed + "\n") - - continue - - # Check for Header 3 (###) to identify change types - if stripped_line[:4] == "### ": - change_type = ChangeType(line) - change_type.parse() - current_category = change_type.type - if current_category in self.releases[current_release]: - self.problems.append( - f'Change type "{current_category}" is duplicated in {current_release}' - ) - else: - self.releases[current_release][current_category] = {} - self.problems.extend(change_type.problems) - - if fix: - write(f, change_type.fixed + "\n") - - continue - - # Check for individual entries - if stripped_line[:2] != "- ": - if fix: - write(f, line + "\n") - - continue - - entry = Entry(line) - entry.parse() - self.problems.extend(entry.problems) - if fix: - write(f, entry.fixed + "\n") - - if not current_category: - raise ValueError(f'Entry "{line}" is missing a category') - - if entry.pr_number in seen_prs: - if ( - not entry.is_exception - and entry.pr_number not in ALLOWED_DUPLICATES - ): - self.problems.append( - f"PR #{entry.pr_number} is duplicated in the changelog" - ) - else: - seen_prs.append(entry.pr_number) - - self.releases[current_release][current_category][entry.pr_number] = { - "description": entry.description - } - finally: - if f is not None: - f.close() - - return self.problems == [] - - -if __name__ == "__main__": - changelog = Changelog(sys.argv[1]) - - fix_mode = False - if len(sys.argv) > 2 and sys.argv[2] == "--fix": - fix_mode = True - - passed = changelog.parse(fix=fix_mode) - if passed: - print(" -> Changelog is valid.") - else: - print( - f"Changelog file is not valid - check the following {len(changelog.problems)} problems:\n" - ) - print("\n".join(changelog.problems)) - sys.exit(1) diff --git a/linter/config.py b/linter/config.py deleted file mode 100644 index 50e8e86..0000000 --- a/linter/config.py +++ /dev/null @@ -1,140 +0,0 @@ -""" -This file contains the configurations for the changelog checker. -You can adjust the variables in this file to change the results of the checker. - -Things that can be adjusted are: - - - the allowed change types with a release - - the allowed description categories (i.e. the `(...)` portion at the beginning of an entry) - - PRs that are allowed to occur twice in the changelog (e.g. backports of bug fixes) - - a set of known patterns in PR descriptions and their preferred way of spelling - - known exceptions that do not need to follow the formatting rules - - the legacy version at which to stop the checking - -""" - -import re -from typing import List - - -def get_allowed_categories() -> List[str]: - """ - Returns a list of allowed categories for an individual changelog entry. - - It is using a set of predefined categories, that are extended by the entries in - - the `x/...` modules - - the `precompiles/...` subdirectories - - the `precompiles/outposts/...` subdirectories - - :return: a list of allowed categories for an individual changelog entry - """ - - allowed_categories = [ - "all", - "ante", - "api", - "app", - "build", - "ci", - "cli", - "crisis", - "db", - "deps", - "docs", - "docker", - "eip712", - "fees", - "go", - "make", - "metrics", - "outposts", - "post", - "precompiles", - "proto", - "release", - "rpc", - "swagger", - "testnet", - "tests", - "types", - "utils", - "upgrade", - # third party modules - "bank", - "distribution", - "gov", - "ics20", - "staking", - # outdated modules (we have to keep them since they're in the changelog) - "claims", - "consensus", - "recovery", - "incentives", - "revenue", - "osmosis-outpost", - "stride-outpost", - # precompiles - "p256-precompile", - "distribution-precompile", - # modules - "evm", - "vesting", - "erc20", - "inflation", - ] - - return allowed_categories - - -# List of allowed categories for an individual changelog entry. -ALLOWED_CATEGORIES = get_allowed_categories() - -# A dictionary of allowed spellings for some common patterns in changelog entries. -ALLOWED_SPELLINGS = { - "ABI": re.compile("abi", re.IGNORECASE), - "API": re.compile("api", re.IGNORECASE), - "CI": re.compile("ci", re.IGNORECASE), - "Cosmos-SDK": re.compile(r"cosmos[\s-]*sdk", re.IGNORECASE), - "CLI": re.compile("cli", re.IGNORECASE), - "EIP-712": re.compile(r"eip[\s-]*712", re.IGNORECASE), - "ERC-20": re.compile(r"erc[\s-]*20", re.IGNORECASE), - "EVM": re.compile("evm", re.IGNORECASE), - "IBC": re.compile("ibc", re.IGNORECASE), - "ICS": re.compile("ics", re.IGNORECASE), - "ICS-20": re.compile(r"ics[\s-]*20", re.IGNORECASE), - "outpost": re.compile("outpost", re.IGNORECASE), - "Osmosis": re.compile("osmosis", re.IGNORECASE), - "PR": re.compile(r"(pr)(\s|$)", re.IGNORECASE), - "precompile": re.compile("precompile", re.IGNORECASE), - "SDK": re.compile("sdk", re.IGNORECASE), - "Stride": re.compile("stride", re.IGNORECASE), - "WERC-20": re.compile(r"werc[\s-]*20", re.IGNORECASE), -} - -# Collection of allowed change types and the matching patterns. -ALLOWED_CHANGE_TYPES = { - "API Breaking": re.compile(r"api\s*breaking", re.IGNORECASE), - "Bug Fixes": re.compile(r"bug\s*fixes", re.IGNORECASE), - "Features": re.compile("features", re.IGNORECASE), - "Improvements": re.compile("improvements", re.IGNORECASE), - "State Machine Breaking": re.compile(r"state\s*machine\s*breaking", re.IGNORECASE), -} - -# A list of pull requests that are allowed to be mentioned multiple times in the changelog. -# Usually, this only applies to bug fixes that were patched on two versions (e.g. v12.1.6 and v13.0.0). -ALLOWED_DUPLICATES = [ - 1370, - 1635, -] - -# A list of known exceptions to the formattiing. This usually applies to PRs that e.g. merged contents from -# a security advisory. -KNOWN_EXCEPTIONS = [ - "- (vesting) Refactor vesting flow.", - "- (vesting) Fix vesting bug.", - "- (vesting) [GHSA-2q3r-p2m3-898g](https://github.com/evmos/evmos/commit/39b750cdaf1d69158ab93da85bd43ae4a7da1456" - + ") Apply ClawbackVestingAccount Barberry patch & Bump SDK to v0.46.13", -] - -# The legacy major version at which to stop the checking. -LEGACY_VERSION: int = 2 diff --git a/linter/entry.py b/linter/entry.py deleted file mode 100644 index 1ee67fd..0000000 --- a/linter/entry.py +++ /dev/null @@ -1,255 +0,0 @@ -""" -This file contains the definition for the Entry class. It is used to parse the individual entries, that -relate to the changes in a specific pull request. - -The expected structure of an entry is: `- (category) [#PR](link) description` -""" - -import re -from typing import Dict, List, Tuple - -from config import ALLOWED_CATEGORIES, ALLOWED_SPELLINGS, KNOWN_EXCEPTIONS - -# Allowed entry pattern: `- (category) [#PR](link) - description` -ENTRY_PATTERN = re.compile( - r"^-(?P\s*)\((?P[a-zA-Z0-9\-]+)\)" - + r"(?P\s*)\[(?P\\)?#(?P\d+)](?P\s*)\((?P[^)]*)\)" - + r"(?P\s*)(?P.+)$", -) - - -class Entry: - """ - This class represents an individual changelog entry that is describing the changes on one specific PR. - """ - - def __init__(self, line: str): - self.line: str = line - self.fixed: str = line - self.backslash: bool = False - self.category: str = "" - self.description: str = "" - self.is_exception: bool = False - self.link: str = "" - self.pr_number: int = 0 - self.problems: List[str] = [] - self.whitespaces: List[str] = [] - - def parse(self) -> bool: - """ - Parses a changelog entry from a line of text. - - :return: a tuple indicating whether the parsing was successful and an error message in case of failure - """ - - problems: List[str] = [] - match = ENTRY_PATTERN.match(self.line) - if not match: - if self.line in KNOWN_EXCEPTIONS: - self.is_exception = True - - return True - - problems.append(f'Malformed entry: "{self.line}"') - self.problems = problems - return False - - self.pr_number = int(match.group("pr")) - self.category = match.group("category") - self.backslash = True if match.group("bs") else False - self.link = match.group("link") - self.description = match.group("desc") - self.whitespaces = [ - match.group("ws1"), - match.group("ws2"), - match.group("ws3"), - match.group("ws4"), - ] - - if self.backslash: - problems.append( - "There should be no backslash in front of the # in the PR link" - ) - - ws_problems = check_whitespace(self.whitespaces) - if ws_problems: - problems.extend(ws_problems) - - fixed_cat, cat_problems = check_category(self.category) - if cat_problems: - problems.extend(cat_problems) - - fixed_link, link_problems = check_link(self.link, self.pr_number) - if link_problems: - problems.extend(link_problems) - - fixed_desc, description_problems = check_description(self.description) - if description_problems: - problems.extend(description_problems) - - self.fixed = f"- ({fixed_cat}) [#{self.pr_number}]({fixed_link}) {fixed_desc}" - self.problems = problems - - return problems == [] - - -def check_whitespace(whitespaces: List[str]) -> List[str]: - """ - Check if the whitespaces are valid. - - :param whitespaces: the whitespaces to check - :return: a list of problems, empty if there are none - """ - - problems: List[str] = [] - - if whitespaces[0] != " ": - problems.append( - "There should be exactly one space between the leading dash and the category" - ) - - if whitespaces[1] != " ": - problems.append( - "There should be exactly one space between the category and PR link" - ) - - if whitespaces[2] != "": - problems.append("There should be no whitespace inside of the markdown link") - - if whitespaces[3] != " ": - problems.append( - "There should be exactly one space between the PR link and the description" - ) - - return problems - - -def check_category(category: str) -> Tuple[str, List[str]]: - """ - Check if the category is valid. - - :param category: the category to check - :return: a tuple containing the fixed category and a list of problems, which is empty if there are none - """ - - problems: List[str] = [] - fixed: str = category - - if not category.islower(): - problems.append(f'Category should be lowercase: "({category})"') - fixed = category.lower() - - if category.lower() not in ALLOWED_CATEGORIES: - problems.append(f'Invalid change category: "({category})"') - - return fixed, problems - - -def check_link(link: str, pr_number: int) -> Tuple[str, List[str]]: - """ - Check if the link is valid. - - :param link: the link to check - :param pr_number: the PR number to match in the link - :return: a tuple containing the fixed link and a list of problems, which is empty if there are none - """ - - problems: List[str] = [] - fixed = link - - if not link.startswith("https://github.com/evmos/evmos/pull/"): - fixed = f"https://github.com/evmos/evmos/pull/{pr_number}" - problems.append(f'PR link should point to evmos repository: "{link}"') - - if str(pr_number) not in link: - fixed = f"https://github.com/evmos/evmos/pull/{pr_number}" - problems.append(f'PR link is not matching PR number {pr_number}: "{link}"') - - return fixed, problems - - -def check_description(description: str) -> Tuple[str, List[str]]: - """ - Check if the description is valid. - - :param description: the description to check - :return: a tuple containing the fixed description and a list of problems, which is empty if there are none - """ - - problems: List[str] = [] - fixed: str = description - - if re.search(r"\w", description[0]) and not description[0].isupper(): - fixed = description[0].upper() + description[1:] - problems.append( - f'PR description should start with capital letter: "{description}"' - ) - - if description[-1] != ".": - problems.append(f'PR description should end with a dot: "{description}"') - fixed += "." - - _, fixed, abbreviation_problems = check_spelling(fixed, ALLOWED_SPELLINGS) - if abbreviation_problems: - problems.extend(abbreviation_problems) - - return fixed, problems - - -def check_spelling( - description: str, expected_spellings: Dict[str, re.Pattern] -) -> Tuple[bool, str, List[str]]: - """ - Checks some common spelling requirements. - Any matches that occur inside of code blocks, are part of a link or inside a word are ignored. - - :param expected_spellings: a dictionary of expected spellings and the matching patterns - :param description: the description to check - :return: a tuple containing a boolean value indicating whether a matching pattern was found and - a list of problems with the match - """ - - problems: List[str] = [] - found: bool = False - fixed: str = description - - for spelling, pattern in expected_spellings.items(): - match = get_match(pattern, description) - if match: - if match != spelling: - problems.append(f'"{spelling}" should be used instead of "{match}"') - fixed = pattern.sub(spelling, fixed) - found = True - - return found, fixed, problems - - -def get_match(pattern: re.Pattern, text: str) -> str: - """ - Returns the first match of the pattern in the text. - Matching patterns inside of code blocks, inside of links or inside of words are ignored. - - :param pattern: the pattern to match - :param text: the text to match against - :return: the first match of the pattern in the text - """ - - codeblocks_pattern = re.compile( - r"`[^`]*(" + pattern.pattern + r")[^`]*`", pattern.flags - ) - match = codeblocks_pattern.search(text) - if match: - return "" - - isolated_word_pattern = re.compile( - r"(^|\s)(" + pattern.pattern + r")(?=$|[\s.])", pattern.flags - ) - match = isolated_word_pattern.search(text) - if match: - return match.group(2) - - return "" - - -if __name__ == "__main__": - print("This is a library file and should not be executed directly.") diff --git a/linter/release.py b/linter/release.py deleted file mode 100644 index 9394cd8..0000000 --- a/linter/release.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -This file contains the definition of the Release class. It is used to parse a release section header in the -changelog. -""" - -import re -from typing import List, Tuple - -# Allowed unreleased pattern -UNRELEASED_PATTERN = re.compile(r"^## Unreleased$") - -# Unreleased version -UNRELEASED_VERSION = "Unreleased" - -# Allowed release pattern: [vX.Y.Z(-rcN)](LINK) - (YYYY-MM-DD) -RELEASE_PATTERN = re.compile( - r"^## \[(?Pv\d+\.\d+\.\d+(-rc\d+)?)](?P\(.*\))? - (?P\d{4}-\d{2}-\d{2})$", -) - - -class Release: - """ - This class represents a release in the changelog. - """ - - def __init__(self, line: str): - self.line: str = line - self.fixed: str = "" - self.link: str = "" - self.version: str = "" - self.problems: List[str] = [] - - def parse(self) -> bool: - """ - This function parses the release section header. - - :return: a boolean value indicating if the parsing was successful. - """ - - problems: List[str] = [] - - if UNRELEASED_PATTERN.match(self.line): - self.fixed = self.line - self.version = UNRELEASED_VERSION - return True - - release_match = RELEASE_PATTERN.match(self.line) - if not release_match: - problems.append(f'Malformed release header: "{self.line}"') - self.problems = problems - return False - - date = release_match.group("date") - self.link = release_match.group("link") - self.version = release_match.group("version") - - fixed_link, link_problems = check_link(self.link, self.version) - if link_problems: - problems.extend(link_problems) - - fixed = f"## [{self.version}]{fixed_link} - {date}" - self.fixed = fixed - self.problems = problems - - return problems == [] - - def __le__(self, other: int): - if self.version == UNRELEASED_VERSION: - return False - - version_match = re.match( - r"^v(?P\d+)\.(\d+)\.(\d+)(-rc\d+)?$", self.version - ) - if not version_match: - raise ValueError( - f'Invalid release version in line "{self.line}" ' - + "or possibly wrong header style used" - ) - - major = int(version_match.group("major")) - return major <= other - - -def check_link(link: str, version: str) -> Tuple[str, List[str]]: - """ - This function checks if the link in the release header is correct. - - :param link: the link in the release header. - :param version: the version in the release header. - :return: a tuple containing the fixed link and a list of problems, - which is empty if there are none. - """ - - base_url: str = "https://github.com/evmos/evmos/releases/tag/" - problems: List[str] = [] - # NOTE: the fixed link is the same for all problems - fixed: str = f"({base_url}{version})" - - if link == "" or link is None: - problems.append(f'Release link is missing for "{version}"') - return fixed, problems - - link = link[1:-1] - if not link.startswith(base_url): - problems.append(f'Release link should point to an Evmos release: "{link}"') - - if version not in link: - problems.append( - f'Release header version "{version}" ' - + f'does not match version in link "{link}"' - ) - - return fixed, problems diff --git a/linter/test_change_type.py b/linter/test_change_type.py deleted file mode 100644 index d99be79..0000000 --- a/linter/test_change_type.py +++ /dev/null @@ -1,29 +0,0 @@ -from change_type import ChangeType - - -class TestChangeType: - def test_pass(self): - change_type = ChangeType("### Bug Fixes") - assert change_type.parse() is True - assert change_type.type == "Bug Fixes" - assert change_type.problems == [] - - def test_malformed(self): - change_type = ChangeType("###Bug Fixes") - assert change_type.parse() is False - assert change_type.type == "" - assert change_type.problems == ['Malformed change type: "###Bug Fixes"'] - - def test_spelling(self): - change_type = ChangeType("### BugFixes") - assert change_type.parse() is False - assert change_type.type == "BugFixes" - assert change_type.problems == [ - '"Bug Fixes" should be used instead of "BugFixes"' - ] - - def test_invalid_type(self): - change_type = ChangeType("### Invalid Type") - assert change_type.parse() is False - assert change_type.type == "Invalid Type" - assert change_type.problems == ['"Invalid Type" is not a valid change type'] diff --git a/linter/test_check_changelog.py b/linter/test_check_changelog.py deleted file mode 100644 index b5dc792..0000000 --- a/linter/test_check_changelog.py +++ /dev/null @@ -1,135 +0,0 @@ -import os -from shutil import copyfile - -import pytest # type: ignore -from check_changelog import Changelog # type: ignore - -# Get the directory of this script -SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) - - -@pytest.fixture -def create_tmp_copy(): - tmp_file = os.path.join(SCRIPT_DIR, "testdata", "changelog_tmp.md") - copyfile( - os.path.join(SCRIPT_DIR, "testdata", "changelog_fail.md"), - tmp_file, - ) - yield tmp_file - os.remove(tmp_file) - - -class TestParseChangelog: - """ - This class collects all tests that are actually parsing dummy changelogs stored in - markdown files in the testdata directory. - """ - - def test_pass(self): - expected_result = { - "Unreleased": { - "State Machine Breaking": { - 1922: {"description": "Add `secp256r1` curve precompile."}, - 1949: {"description": "Add `ClaimRewards` custom transaction."}, - 2218: { - "description": "Use correct version of proto dependencies to generate swagger." - }, - 1687: {"description": "Bump Evmos version to v14."}, - }, - "API Breaking": { - 2015: { - "description": "Rename `inflation` module to `inflation/v1`." - }, - 2078: {"description": "Deprecate legacy EIP-712 ante handler."}, - 1851: { - "description": "Enable [EIP 3855](https://eips.ethereum.org/EIPS/eip-3855) " - + "(`PUSH0` opcode) during upgrade." - }, - }, - "Improvements": { - 1864: { - "description": "Add `--base-fee` and `--min-gas-price` flags.", - }, - 1912: {"description": "Add Stride outpost interface and ABI."}, - 2104: { - "description": "Refactor to use `sdkmath.Int` and `sdkmath.LegacyDec` instead of SDK types." - }, - 701: {"description": "Rename Go module to `evmos/evmos`."}, - }, - "Bug Fixes": { - 1801: {"description": "Fixed the problem `gas_used` is 0."}, - 109: { - "description": "Fix hardcoded ERC-20 nonce and `UpdateTokenPairERC20` proposal " - + "to support ERC-20s with 0 decimals." - }, - }, - }, - "v15.0.0": { - "API Breaking": { - 1862: { - "description": "Add Authorization Grants to the Vesting extension." - }, - 555: {"description": "`v4.0.0` upgrade logic."}, - }, - }, - "v2.0.0": {}, - } - - changelog = Changelog(os.path.join(SCRIPT_DIR, "testdata", "changelog_ok.md")) - ok = changelog.parse() - assert changelog.problems == [], "expected no failed entries" - assert ok is True - assert changelog.releases == expected_result, "expected different parsed result" - - def test_fail(self): - changelog = Changelog(os.path.join(SCRIPT_DIR, "testdata", "changelog_fail.md")) - assert changelog.parse() is False - assert changelog.problems == [ - 'PR link is not matching PR number 1948: "https://github.com/evmos/evmos/pull/1949"', - "There should be no backslash in front of the # in the PR link", - '"ABI" should be used instead of "ABi"', - '"outpost" should be used instead of "Outpost"', - 'PR description should end with a dot: "Fixed the problem `gas_used` is 0"', - '"Invalid Category" is not a valid change type', - 'Change type "Bug Fixes" is duplicated in Unreleased', - "PR #1801 is duplicated in the changelog", - 'Release "v15.0.0" is duplicated in the changelog', - 'Change type "API Breaking" is duplicated in v15.0.0', - "PR #1862 is duplicated in the changelog", - 'Malformed entry: "- malformed entry in changelog"', - ] - - def test_fix(self, create_tmp_copy): - changelog = Changelog(create_tmp_copy) - assert changelog.parse(fix=True) is False - assert changelog.problems == [ - 'PR link is not matching PR number 1948: "https://github.com/evmos/evmos/pull/1949"', - "There should be no backslash in front of the # in the PR link", - '"ABI" should be used instead of "ABi"', - '"outpost" should be used instead of "Outpost"', - 'PR description should end with a dot: "Fixed the problem `gas_used` is 0"', - '"Invalid Category" is not a valid change type', - 'Change type "Bug Fixes" is duplicated in Unreleased', - "PR #1801 is duplicated in the changelog", - 'Release "v15.0.0" is duplicated in the changelog', - 'Change type "API Breaking" is duplicated in v15.0.0', - "PR #1862 is duplicated in the changelog", - 'Malformed entry: "- malformed entry in changelog"', - ] - - # Here we parse the fixed changelog again and check that the automatic fixes were applied. - fixed_changelog = Changelog(changelog.filename) - assert fixed_changelog.parse(fix=False) is False - assert fixed_changelog.problems == [ - '"Invalid Category" is not a valid change type', - 'Change type "Bug Fixes" is duplicated in Unreleased', - "PR #1801 is duplicated in the changelog", - 'Release "v15.0.0" is duplicated in the changelog', - 'Change type "API Breaking" is duplicated in v15.0.0', - "PR #1862 is duplicated in the changelog", - 'Malformed entry: "- malformed entry in changelog"', - ] - - def test_parse_changelog_nonexistent_file(self): - with pytest.raises(FileNotFoundError): - Changelog(os.path.join(SCRIPT_DIR, "testdata", "nonexistent_file.md")) diff --git a/linter/test_config.py b/linter/test_config.py deleted file mode 100644 index 473b3bc..0000000 --- a/linter/test_config.py +++ /dev/null @@ -1,18 +0,0 @@ -from config import get_allowed_categories - - -class TestGetAllowedCategories: - def test_pass(self): - allowed_categories = get_allowed_categories() - assert ( - "app" in allowed_categories - ), "expected pre-configured value to be in allowed categories" - assert ( - "evm" in allowed_categories - ), "expected module to be in allowed categories" - assert ( - "osmosis-outpost" in allowed_categories - ), "expected outpost to be in allowed categories" - assert ( - "distribution-precompile" in allowed_categories - ), "expected precompile to be in allowed categories" diff --git a/linter/test_entry.py b/linter/test_entry.py deleted file mode 100644 index d5280e3..0000000 --- a/linter/test_entry.py +++ /dev/null @@ -1,229 +0,0 @@ -import re - -from entry import ( # type: ignore - ALLOWED_SPELLINGS, - Entry, - check_category, - check_description, - check_link, - check_spelling, - check_whitespace, - get_match, -) - - -class TestEntry: - """ - This class collects all tests that are checking individual changelog entries. - """ - - example = ( - "- (distribution-precompile) [#1949](https://github.com/evmos/evmos/pull/1949) " - + "Add `ClaimRewards` custom transaction." - ) - - def test_pass(self): - entry = Entry(self.example) - ok = entry.parse() - assert entry.problems == [] - assert ok is True - assert entry.fixed == self.example - - def test_pass_includes_link(self): - example = ( - "- (evm) [#1851](https://github.com/evmos/evmos/pull/1851) " - + "Enable [EIP 3855](https://eips.ethereum.org/EIPS/eip-3855) (`PUSH0` opcode) during upgrade." - ) - entry = Entry(example) - ok = entry.parse() - assert entry.link == "https://github.com/evmos/evmos/pull/1851" - assert entry.description == ( - "Enable [EIP 3855](https://eips.ethereum.org/EIPS/eip-3855) (`PUSH0` opcode) during upgrade." - ) - assert entry.problems == [] - assert ok is True - assert entry.fixed == example - - def test_fail_has_backslash_in_link(self): - example = r"- (evm) [\#1851](https://github.com/evmos/evmos/pull/1851) Test." - entry = Entry(example) - ok = entry.parse() - assert entry.problems == [ - "There should be no backslash in front of the # in the PR link" - ] - assert ok is False - assert entry.fixed == example.replace(r"\#", "#") - - def test_entry_wrong_pr_link_and_missing_dot(self): - entry = Entry( - "- (distribution-precompile) [#1949](https://github.com/evmos/evmos/pull/1948) " - + "Add `ClaimRewards` custom transaction" - ) - assert entry.parse() is False - assert entry.fixed == self.example - assert entry.problems == [ - 'PR link is not matching PR number 1949: "https://github.com/evmos/evmos/pull/1948"', - 'PR description should end with a dot: "Add `ClaimRewards` custom transaction"', - ] - - def test_malformed_entry(self): - malformed_example = ( - "- (distribution-precompile) [#194tps://github.com/evmos/evmos/pull/1" - ) - entry = Entry(malformed_example) - assert entry.parse() is False - assert entry.fixed == malformed_example - assert entry.problems == [ - 'Malformed entry: "- (distribution-precompile) [#194tps://github.com/evmos/evmos/pull/1"' - ] - - -class TestCheckCategory: - def test_pass(self): - fixed, problems = check_category("evm") - assert fixed == "evm" - assert problems == [] - - def test_invalid_category(self): - fixed, problems = check_category("invalid") - assert fixed == "invalid" - assert problems == ['Invalid change category: "(invalid)"'] - - def test_non_lower_category(self): - fixed, problems = check_category("eVm") - assert fixed == "evm" - assert problems == ['Category should be lowercase: "(eVm)"'] - - -class TestCheckLink: - example = "https://github.com/evmos/evmos/pull/1949" - - def test_pass(self): - fixed, problems = check_link(self.example, 1949) - assert fixed == self.example - assert problems == [] - - def test_wrong_base_url(self): - fixed, problems = check_link("https://github.com/evmds/evmos/pull/1949", 1949) - assert fixed == self.example - assert problems == [ - 'PR link should point to evmos repository: "https://github.com/evmds/evmos/pull/1949"' - ] - - def test_wrong_pr_number(self): - fixed, problems = check_link("https://github.com/evmos/evmos/pull/1948", 1949) - assert fixed == self.example - assert problems == [ - 'PR link is not matching PR number 1949: "https://github.com/evmos/evmos/pull/1948"' - ] - - -class TestCheckDescription: - def test_pass(self): - example = "Add `ClaimRewards` custom transaction." - fixed, problems = check_description(example) - assert fixed == example - assert problems == [] - - def test_start_with_lowercase(self): - fixed, problems = check_description("add `ClaimRewards` custom transaction.") - assert fixed == "Add `ClaimRewards` custom transaction." - assert problems == [ - 'PR description should start with capital letter: "add `ClaimRewards` custom transaction."' - ] - - def test_end_with_dot(self): - fixed, problems = check_description("Add `ClaimRewards` custom transaction") - assert fixed == "Add `ClaimRewards` custom transaction." - assert problems == [ - 'PR description should end with a dot: "Add `ClaimRewards` custom transaction"' - ] - - def test_start_with_codeblock(self): - fixed, problems = check_description( - "```\nAdd `ClaimRewards` custom transaction." - ) - assert fixed == "```\nAdd `ClaimRewards` custom transaction." - assert problems == [] - - -class TestCheckWhitespace: - def test_missing_whitespace(self): - assert check_whitespace(["", " ", "", " "]) == [ - "There should be exactly one space between the leading dash and the category" - ] - - def test_multiple_spaces(self): - assert check_whitespace([" ", " ", "", " "]) == [ - "There should be exactly one space between the PR link and the description" - ] - - def test_space_in_link(self): - assert check_whitespace([" ", " ", " ", " "]) == [ - "There should be no whitespace inside of the markdown link" - ] - - -class TestCheckSpelling: - def test_pass(self): - found, fixed, problems = check_spelling("Fix API.", ALLOWED_SPELLINGS) - assert found is True - assert fixed == "Fix API." - assert problems == [] - - def test_spelling(self): - found, fixed, problems = check_spelling("Fix APi.", ALLOWED_SPELLINGS) - assert found is True - assert fixed == "Fix API." - assert problems == ['"API" should be used instead of "APi"'] - - def test_multiple_problems(self): - found, fixed, problems = check_spelling( - "Fix Stride Outpost and AbI.", ALLOWED_SPELLINGS - ) - assert found is True - assert fixed == "Fix Stride outpost and ABI." - assert problems == [ - '"ABI" should be used instead of "AbI"', - '"outpost" should be used instead of "Outpost"', - ] - - def test_pass_codeblocks(self): - found, fixed, problems = check_spelling("Fix `in evm code`.", ALLOWED_SPELLINGS) - assert found is False - assert fixed == "Fix `in evm code`." - assert problems == [] - - def test_fail_in_word(self): - found, fixed, problems = check_spelling("FixAbI in word.", ALLOWED_SPELLINGS) - assert found is False - assert fixed == "FixAbI in word." - assert problems == [] - - def test_erc_20(self): - found, fixed, problems = check_spelling( - "Add ERC20 contract.", ALLOWED_SPELLINGS - ) - assert found is True - assert fixed == "Add ERC-20 contract." - assert problems == ['"ERC-20" should be used instead of "ERC20"'] - - -class TestGetMatch: - def test_pass(self): - assert get_match(re.compile("abi", re.IGNORECASE), "Fix ABI.") == "ABI" - - def test_fail_codeblocks(self): - assert get_match(re.compile("abi", re.IGNORECASE), "Fix `in AbI code`.") == "" - - def test_fail_in_word(self): - assert get_match(re.compile("abi", re.IGNORECASE), "FixAbI in word.") == "" - - def test_fail_in_link(self): - assert ( - get_match( - re.compile("abi", re.IGNORECASE), - "Fix [abcdef](https://example/aBi.com).", - ) - == "" - ) diff --git a/linter/test_release.py b/linter/test_release.py deleted file mode 100644 index 35429da..0000000 --- a/linter/test_release.py +++ /dev/null @@ -1,54 +0,0 @@ -from release import Release - - -class TestRelease: - def test_pass(self): - release = Release( - "## [v15.0.2](https://github.com/evmos/evmos/releases/tag/v15.0.2) - 2021-08-02" - ) - assert release.parse() is True - assert release.version == "v15.0.2" - assert release.problems == [] - - # Check version comparisons - assert (release <= 15) is True - assert (release <= 14) is False - - def test_pass_unreleased(self): - release = Release("## Unreleased") - assert release.parse() is True - assert release.version == "Unreleased" - assert release.problems == [] - - def test_malformed(self): - release = Release("## `v15.0.2])") - assert release.parse() is False - assert release.version == "" - assert release.problems == ['Malformed release header: "## `v15.0.2])"'] - - def test_missing_link(self): - release = Release("## [v15.0.2] - 2021-08-02") - assert release.parse() is False - assert release.version == "v15.0.2" - assert release.problems == ['Release link is missing for "v15.0.2"'] - - def test_wrong_version_in_link(self): - release = Release( - "## [v15.0.2](https://github.com/evmos/evmos/releases/tag/v16.0.0) - 2021-08-02" - ) - assert release.parse() is False - assert release.version == "v15.0.2" - assert release.problems == [ - 'Release header version "v15.0.2" does not match version in link ' - + '"https://github.com/evmos/evmos/releases/tag/v16.0.0"' - ] - - def test_wrong_base_url(self): - release = Release( - "## [v15.0.2](https://github.com/evmos/evmds/releases/tag/v15.0.2) - 2021-08-02" - ) - assert release.parse() is False - assert release.version == "v15.0.2" - assert release.problems == [ - 'Release link should point to an Evmos release: "https://github.com/evmos/evmds/releases/tag/v15.0.2"' - ]