From 1ee13f007648c316705978688e63765f6eedb6df Mon Sep 17 00:00:00 2001 From: uy/sun Date: Mon, 12 Jun 2023 09:36:26 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=99=90=E5=88=B6=E5=90=8D=E7=A7=B0?= =?UTF-8?q?=E7=9A=84=E9=95=BF=E5=BA=A6=E4=B8=8D=E8=B6=85=E8=BF=87=2050=20?= =?UTF-8?q?=E4=B8=AA=E5=AD=97=E7=AC=A6=20(#138)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 4 + src/plugins/publish/__init__.py | 14 +- src/plugins/publish/constants.py | 3 + src/plugins/publish/validation.py | 7 + tests/publish/models/test_adapter.py | 9 +- tests/publish/models/test_name.py | 27 +++- tests/publish/process/test_publish_check.py | 154 +++++++++++++++++++- 7 files changed, 201 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a30fc46..da03705c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/ ## [Unreleased] +### Added + +- 限制名称的长度不超过 50 个字符 + ### Changed - 通过议题标签判断商店发布类型 diff --git a/src/plugins/publish/__init__.py b/src/plugins/publish/__init__.py index 51a34c98..e62cc9ab 100644 --- a/src/plugins/publish/__init__.py +++ b/src/plugins/publish/__init__.py @@ -11,7 +11,7 @@ from nonebot.params import Depends from .config import plugin_config -from .constants import BOT_MARKER, BRANCH_NAME_PREFIX +from .constants import BOT_MARKER, BRANCH_NAME_PREFIX, MAX_NAME_LENGTH from .depends import ( get_installation_id, get_issue_number, @@ -167,9 +167,16 @@ async def handle_publish_check( # 检查是否满足发布要求 # 仅在通过检查的情况下创建拉取请求 info = extract_publish_info_from_issue(issue, publish_type) + + # 设置拉取请求与议题的标题 + if isinstance(info, PublishInfo): + name = info.name + else: + name = info.raw_data.get("name") or "" + # 限制标题长度,过长的标题不好看 + title = f"{publish_type.value}: {name[:MAX_NAME_LENGTH]}" + if isinstance(info, PublishInfo): - # 拉取请求与议题的标题 - title = f"{info.get_type().value}: {info.name}" # 创建新分支 # 命名示例 publish/issue123 branch_name = f"{BRANCH_NAME_PREFIX}{issue_number}" @@ -183,7 +190,6 @@ async def handle_publish_check( ) message = info.validation_message else: - title = f"{publish_type.value}: {info.raw_data.get('name') or ''}" message = info.message logger.info("发布没通过检查,暂不创建拉取请求") diff --git a/src/plugins/publish/constants.py b/src/plugins/publish/constants.py index c4e3aeb6..7f41e9f6 100644 --- a/src/plugins/publish/constants.py +++ b/src/plugins/publish/constants.py @@ -56,6 +56,9 @@ re.IGNORECASE, ) +MAX_NAME_LENGTH = 50 +"""名称最大长度""" + # 匹配信息的正则表达式 # 格式:### {标题}\n\n{内容} ISSUE_PATTERN = r"### {}\s+([^\s#].*?)(?=(?:\s+###|$))" diff --git a/src/plugins/publish/validation.py b/src/plugins/publish/validation.py index cb073acd..4b9d2ebe 100644 --- a/src/plugins/publish/validation.py +++ b/src/plugins/publish/validation.py @@ -20,6 +20,7 @@ BOT_HOMEPAGE_PATTERN, BOT_NAME_PATTERN, DETAIL_MESSAGE_TEMPLATE, + MAX_NAME_LENGTH, PLUGIN_DESC_PATTERN, PLUGIN_HOMEPAGE_PATTERN, PLUGIN_MODULE_NAME_PATTERN, @@ -94,6 +95,12 @@ class PublishInfo(abc.ABC, BaseModel): tags: list[Tag] is_official: bool = False + @validator("name", pre=True) + def name_validator(cls, v: str) -> str: + if len(v) > MAX_NAME_LENGTH: + raise ValueError(f"⚠️ 名称过长。
请确保名称不超过 {MAX_NAME_LENGTH} 个字符。
") + return v + @validator("homepage", pre=True) def homepage_validator(cls, v: str) -> str: if v: diff --git a/tests/publish/models/test_adapter.py b/tests/publish/models/test_adapter.py index 1c37c6bf..07608c44 100644 --- a/tests/publish/models/test_adapter.py +++ b/tests/publish/models/test_adapter.py @@ -152,11 +152,16 @@ async def test_adapter_info_validation_partial_failed( async def test_adapter_info_name_validation_failed( mocker: MockerFixture, mocked_api: MockRouter ) -> None: - """测试名称重复检测失败的情况""" + """测试名称不符合规范的情况 + + 名称过长 + 重复的项目名与报名 + """ from src.plugins.publish.validation import AdapterPublishInfo, MyValidationError mock_issue = mocker.MagicMock() mock_issue.body = generate_issue_body_adapter( + name="looooooooooooooooooooooooooooooooooooooooooooooooooooooooong", module_name="module_name1", project_link="project_link1", ) @@ -167,7 +172,7 @@ async def test_adapter_info_name_validation_failed( assert ( e.value.message - == """> Adapter: name\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n
  • ⚠️ PyPI 项目名 project_link1 加包名 module_name1 的值与商店重复。
    请确保没有重复发布。
  • \n
    详情
  • ✅ 标签: test-#ffffff。
  • ✅ 项目 主页 返回状态码 200。
  • ✅ 包 project_link1 已发布至 PyPI。
  • """ + == """> Adapter: looooooooooooooooooooooooooooooooooooooooooooooooooooooooong\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n
  • ⚠️ 名称过长。
    请确保名称不超过 50 个字符。
  • ⚠️ PyPI 项目名 project_link1 加包名 module_name1 的值与商店重复。
    请确保没有重复发布。
  • \n
    详情
  • ✅ 标签: test-#ffffff。
  • ✅ 项目 主页 返回状态码 200。
  • ✅ 包 project_link1 已发布至 PyPI。
  • """ ) assert mocked_api["project_link1"].called diff --git a/tests/publish/models/test_name.py b/tests/publish/models/test_name.py index 286e7d22..2bca0954 100644 --- a/tests/publish/models/test_name.py +++ b/tests/publish/models/test_name.py @@ -26,9 +26,7 @@ async def test_pypi_project_name_invalid(mocked_api: MockRouter) -> None: assert mocked_api["homepage"].called -async def test_module_name_invalid( - mocker: MockerFixture, mocked_api: MockRouter -) -> None: +async def test_module_name_invalid(mocked_api: MockRouter) -> None: """测试模块名称不正确的情况""" from src.plugins.publish.validation import AdapterPublishInfo @@ -49,7 +47,7 @@ async def test_module_name_invalid( assert mocked_api["homepage"].called -async def test_name_duplication(mocker: MockerFixture, mocked_api: MockRouter) -> None: +async def test_name_duplication(mocked_api: MockRouter) -> None: """测试名称重复的情况""" from src.plugins.publish.validation import AdapterPublishInfo @@ -71,3 +69,24 @@ async def test_name_duplication(mocker: MockerFixture, mocked_api: MockRouter) - assert mocked_api["project_link1"].called assert mocked_api["homepage"].called + + +async def test_name_too_long(mocked_api: MockRouter) -> None: + """测试名称过长的情况""" + from src.plugins.publish.validation import AdapterPublishInfo + + with pytest.raises(ValidationError) as e: + AdapterPublishInfo( + module_name="module_name", + project_link="project_link", + name="looooooooooooooooooooooooooooooooooooooooooooooooooooooooong", + desc="desc", + author="author", + homepage="https://nonebot.dev", + tags=json.dumps([]), # type: ignore + is_official=False, + ) + assert "⚠️ 名称过长。
    请确保名称不超过 50 个字符。
    " in str(e.value) + + assert mocked_api["project_link"].called + assert mocked_api["homepage"].called diff --git a/tests/publish/process/test_publish_check.py b/tests/publish/process/test_publish_check.py index 3d02448f..c4a0f2e2 100644 --- a/tests/publish/process/test_publish_check.py +++ b/tests/publish/process/test_publish_check.py @@ -2,6 +2,9 @@ from pathlib import Path from typing import Any, cast +import httpx +from githubkit import Response +from githubkit.exception import RequestFailed from nonebot import get_adapter from nonebot.adapters.github import ( Adapter, @@ -206,7 +209,7 @@ async def test_edit_title( ) -> None: """测试编辑标题 - 插件名被修改后,标题也应该被修改 + 名称被修改后,标题也应该被修改 """ from src.plugins.publish import publish_check_matcher from src.plugins.publish.config import plugin_config @@ -242,7 +245,7 @@ async def test_edit_title( mock_pull = mocker.MagicMock() mock_pull.number = 2 mock_pulls_resp = mocker.MagicMock() - mock_pulls_resp.parsed_data = mock_pull + mock_pulls_resp.parsed_data = [mock_pull] with open(tmp_path / "bots.json", "w") as f: json.dump([], f) @@ -276,7 +279,6 @@ async def test_edit_title( {"owner": "he0119", "repo": "action-test", "issue_number": 80}, mock_list_comments_resp, ) - # TODO: 抛出一个异常,然后执行修改拉取请求标题的逻辑 ctx.should_call_api( "rest.pulls.async_create", { @@ -287,19 +289,32 @@ async def test_edit_title( "base": "master", "head": "publish/issue80", }, + exception=RequestFailed( + Response( + httpx.Response(422, request=httpx.Request("test", "test")), None + ) + ), + ) + ctx.should_call_api( + "rest.pulls.async_list", + { + "owner": "he0119", + "repo": "action-test", + "head": "he0119:publish/issue80", + }, mock_pulls_resp, ) + # 修改标题 ctx.should_call_api( - "rest.issues.async_add_labels", + "rest.pulls.async_update", { "owner": "he0119", "repo": "action-test", - "issue_number": 2, - "labels": ["Bot"], + "pull_number": 2, + "title": "Bot: test1", }, True, ) - # 修改标题 ctx.should_call_api( "rest.issues.async_update", { @@ -396,6 +411,130 @@ async def test_edit_title( assert mocked_api["homepage"].called +async def test_edit_title_too_long( + app: App, mocker: MockerFixture, mocked_api: MockRouter, tmp_path: Path +) -> None: + """测试编辑标题 + + 标题过长的情况 + """ + from src.plugins.publish import publish_check_matcher + from src.plugins.publish.config import plugin_config + + mock_subprocess_run = mocker.patch( + "subprocess.run", side_effect=lambda *args, **kwargs: mocker.MagicMock() + ) + + mock_installation = mocker.MagicMock() + mock_installation.id = 123 + mock_installation_resp = mocker.MagicMock() + mock_installation_resp.parsed_data = mock_installation + + mock_issue = mocker.MagicMock() + mock_issue.pull_request = None + mock_issue.title = "Bot: test" + mock_issue.number = 80 + mock_issue.state = "open" + mock_issue.body = generate_issue_body_bot( + name="looooooooooooooooooooooooooooooooooooooooooooooooooooooong" + ) + mock_issue.user.login = "test" + + mock_event = mocker.MagicMock() + mock_event.issue = mock_issue + + mock_issues_resp = mocker.MagicMock() + mock_issues_resp.parsed_data = mock_issue + + mock_comment = mocker.MagicMock() + mock_comment.body = "Bot: test" + mock_list_comments_resp = mocker.MagicMock() + mock_list_comments_resp.parsed_data = [mock_comment] + + mock_pull = mocker.MagicMock() + mock_pull.number = 2 + mock_pulls_resp = mocker.MagicMock() + mock_pulls_resp.parsed_data = [mock_pull] + + with open(tmp_path / "bots.json", "w") as f: + json.dump([], f) + + check_json_data(plugin_config.input_config.bot_path, []) + + async with app.test_matcher(publish_check_matcher) as ctx: + adapter = get_adapter(Adapter) + bot = ctx.create_bot( + base=GitHubBot, + adapter=adapter, + self_id=GitHubApp(app_id="1", private_key="1"), # type: ignore + ) + bot = cast(GitHubBot, bot) + event_path = Path(__file__).parent.parent / "events" / "issue-open.json" + event = Adapter.payload_to_event("1", "issues", event_path.read_bytes()) + assert isinstance(event, IssuesOpened) + + ctx.should_call_api( + "rest.apps.async_get_repo_installation", + {"owner": "he0119", "repo": "action-test"}, + mock_installation_resp, + ) + ctx.should_call_api( + "rest.issues.async_get", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_issues_resp, + ) + ctx.should_call_api( + "rest.issues.async_list_comments", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_list_comments_resp, + ) + # 修改标题,应该被阶段,且不会更新拉取请求的标题 + ctx.should_call_api( + "rest.issues.async_update", + { + "owner": "he0119", + "repo": "action-test", + "issue_number": 80, + "title": "Bot: looooooooooooooooooooooooooooooooooooooooooooooooo", + }, + True, + ) + + ctx.should_call_api( + "rest.issues.async_list_comments", + {"owner": "he0119", "repo": "action-test", "issue_number": 80}, + mock_list_comments_resp, + ) + ctx.should_call_api( + "rest.issues.async_create_comment", + { + "owner": "he0119", + "repo": "action-test", + "issue_number": 80, + "body": """# 📃 商店发布检查结果\n\n> Bot: looooooooooooooooooooooooooooooooooooooooooooooooooooooong\n\n**⚠️ 在发布检查过程中,我们发现以下问题:**\n
  • ⚠️ 名称过长。
    请确保名称不超过 50 个字符。
  • \n
    详情
  • ✅ 标签: test-#ffffff。
  • ✅ 项目 主页 返回状态码 200。
  • \n\n---\n\n💡 如需修改信息,请直接修改 issue,机器人会自动更新检查结果。\n💡 当插件加载测试失败时,请发布新版本后在当前页面下评论任意内容以触发测试。\n\n💪 Powered by [NoneFlow](https://github.com/nonebot/noneflow)\n\n""", + }, + True, + ) + + ctx.receive_event(bot, event) + + # 测试 git 命令 + mock_subprocess_run.assert_has_calls( + [ + mocker.call( + ["git", "config", "--global", "safe.directory", "*"], + check=True, + capture_output=True, + ) # type: ignore + ] + ) + + # 检查文件是否正确 + check_json_data(plugin_config.input_config.bot_path, []) + + assert mocked_api["homepage"].called + + async def test_process_publish_check_not_pass( app: App, mocker: MockerFixture, mocked_api: MockRouter, tmp_path: Path ) -> None: @@ -767,3 +906,4 @@ async def test_skip_plugin_check( check_json_data(plugin_config.input_config.plugin_path, []) assert mocked_api["project_link"].called + assert mocked_api["project_link"].called