diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b994af..9e6bcc61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/ ### Added - 从插件测试中提取环境信息 +- 支持修改插件配置并重新测试 ### Fixed diff --git a/src/plugins/github/constants.py b/src/plugins/github/constants.py index 8a7ea278..5513f1e8 100644 --- a/src/plugins/github/constants.py +++ b/src/plugins/github/constants.py @@ -14,3 +14,4 @@ SKIP_COMMENT = "/skip" REMOVE_LABEL = "Remove" +CONFIG_LABEL = "Config" diff --git a/src/plugins/github/plugins/config/__init__.py b/src/plugins/github/plugins/config/__init__.py new file mode 100644 index 00000000..b6bb0c94 --- /dev/null +++ b/src/plugins/github/plugins/config/__init__.py @@ -0,0 +1,158 @@ +from githubkit.exception import RequestFailed +from nonebot import logger, on_type +from nonebot.adapters.github import GitHubBot +from nonebot.adapters.github.event import ( + IssueCommentCreated, + IssuesEdited, + IssuesOpened, + IssuesReopened, + PullRequestReviewSubmitted, +) +from nonebot.params import Depends + +from src.plugins.github.constants import CONFIG_LABEL, TITLE_MAX_LENGTH +from src.plugins.github.depends import ( + RepoInfo, + bypass_git, + get_installation_id, + get_issue_handler, + get_repo_info, + get_type_by_labels_name, + install_pre_commit_hooks, + is_bot_triggered_workflow, +) +from src.plugins.github.models import IssueHandler +from src.plugins.github.plugins.publish.render import render_comment +from src.plugins.github.plugins.remove.depends import check_labels +from src.plugins.github.typing import IssuesEvent +from src.plugins.github.utils import run_shell_command +from src.providers.validation.models import PublishType + +from .constants import BRANCH_NAME_PREFIX, COMMIT_MESSAGE_PREFIX, RESULTS_BRANCH +from .utils import ( + update_file, + validate_info_from_issue, +) + + +async def check_rule( + event: IssuesEvent, + is_config: bool = check_labels(CONFIG_LABEL), + is_bot: bool = Depends(is_bot_triggered_workflow), + publish_type: PublishType = Depends(get_type_by_labels_name), +) -> bool: + if is_bot: + logger.info("机器人触发的工作流,已跳过") + return False + if publish_type != PublishType.PLUGIN: + logger.info("与插件无关,已跳过") + return False + if event.payload.issue.pull_request: + logger.info("评论在拉取请求下,已跳过") + return False + if is_config is False: + logger.info("非配置工作流,已跳过") + return False + return True + + +config_check_matcher = on_type( + (IssuesOpened, IssuesReopened, IssuesEdited, IssueCommentCreated), rule=check_rule +) + + +@config_check_matcher.handle( + parameterless=[Depends(bypass_git), Depends(install_pre_commit_hooks)] +) +async def handle_remove_check( + bot: GitHubBot, + installation_id: int = Depends(get_installation_id), + handler: IssueHandler = Depends(get_issue_handler), +): + async with bot.as_installation(installation_id): + if handler.issue.state != "open": + logger.info("议题未开启,已跳过") + await config_check_matcher.finish() + + # 需要先切换到结果分支 + run_shell_command(["git", "fetch", "origin", RESULTS_BRANCH]) + run_shell_command(["git", "checkout", RESULTS_BRANCH]) + + # 检查是否满足发布要求 + # 仅在通过检查的情况下创建拉取请求 + result = await validate_info_from_issue(handler) + + # 渲染评论信息 + comment = await render_comment(result, True) + + # 对议题评论 + await handler.comment_issue(comment) + + branch_name = f"{BRANCH_NAME_PREFIX}{handler.issue_number}" + + # 设置拉取请求与议题的标题 + # 限制标题长度,过长的标题不好看 + title = f"{result.type}: {result.name[:TITLE_MAX_LENGTH]}" + + if result.valid: + commit_message = f"{COMMIT_MESSAGE_PREFIX} {result.type.value.lower()} {result.name} (#{handler.issue_number})" + + # 创建新分支 + run_shell_command(["git", "switch", "-C", branch_name]) + # 更新文件 + update_file(result) + handler.commit_and_push(commit_message, branch_name, handler.author) + # 创建拉取请求 + try: + await handler.create_pull_request( + RESULTS_BRANCH, + title, + branch_name, + [result.type.value, CONFIG_LABEL], + ) + except RequestFailed: + await handler.update_pull_request_status(title, branch_name) + logger.info("该分支的拉取请求已创建,请前往查看") + else: + # 如果之前已经创建了拉取请求,则将其转换为草稿 + await handler.draft_pull_request(branch_name) + + +async def review_submitted_rule( + event: PullRequestReviewSubmitted, + is_config: bool = check_labels(CONFIG_LABEL), +) -> bool: + if not is_config: + logger.info("拉取请求与配置无关,已跳过") + return False + if event.payload.review.author_association not in ["OWNER", "MEMBER"]: + logger.info("审查者不是仓库成员,已跳过") + return False + if event.payload.review.state != "approved": + logger.info("未通过审查,已跳过") + return False + + return True + + +auto_merge_matcher = on_type(PullRequestReviewSubmitted, rule=review_submitted_rule) + + +@auto_merge_matcher.handle( + parameterless=[Depends(bypass_git), Depends(install_pre_commit_hooks)] +) +async def handle_auto_merge( + bot: GitHubBot, + event: PullRequestReviewSubmitted, + installation_id: int = Depends(get_installation_id), + repo_info: RepoInfo = Depends(get_repo_info), +) -> None: + async with bot.as_installation(installation_id): + # 如果有冲突的话,不会触发 Github Actions + # 所以直接合并即可 + await bot.rest.pulls.async_merge( + **repo_info.model_dump(), + pull_number=event.payload.pull_request.number, + merge_method="rebase", + ) + logger.info(f"已自动合并 #{event.payload.pull_request.number}") diff --git a/src/plugins/github/plugins/config/constants.py b/src/plugins/github/plugins/config/constants.py new file mode 100644 index 00000000..0dba0aa5 --- /dev/null +++ b/src/plugins/github/plugins/config/constants.py @@ -0,0 +1,4 @@ +RESULTS_BRANCH = "results" + +COMMIT_MESSAGE_PREFIX = "chore: edit config" +BRANCH_NAME_PREFIX = "config/issue" diff --git a/src/plugins/github/plugins/config/utils.py b/src/plugins/github/plugins/config/utils.py new file mode 100644 index 00000000..7de0405e --- /dev/null +++ b/src/plugins/github/plugins/config/utils.py @@ -0,0 +1,134 @@ +from typing import Any + +from nonebot import logger + +from src.plugins.github.models import AuthorInfo +from src.plugins.github.models.issue import IssueHandler +from src.plugins.github.plugins.publish.constants import ( + PLUGIN_CONFIG_PATTERN, + PLUGIN_MODULE_NAME_PATTERN, + PROJECT_LINK_PATTERN, +) +from src.plugins.github.plugins.publish.render import render_summary +from src.plugins.github.plugins.publish.validation import add_step_summary, strip_ansi +from src.plugins.github.utils import extract_issue_info_from_issue +from src.providers.constants import PYPI_KEY_TEMPLATE +from src.providers.docker_test import DockerPluginTest, Metadata +from src.providers.models import RegistryPlugin, StoreTestResult +from src.providers.utils import dump_json, load_json_from_file +from src.providers.validation import PublishType, ValidationDict, validate_info +from src.providers.validation.models import PluginPublishInfo + + +async def validate_info_from_issue(handler: IssueHandler) -> ValidationDict: + """从议题中获取插件信息,并且运行插件测试加载且获取插件元信息后进行验证""" + body = handler.issue.body if handler.issue.body else "" + + # 从议题里提取插件所需信息 + raw_data: dict[str, Any] = extract_issue_info_from_issue( + { + "module_name": PLUGIN_MODULE_NAME_PATTERN, + "project_link": PROJECT_LINK_PATTERN, + "test_config": PLUGIN_CONFIG_PATTERN, + }, + body, + ) + # 从历史插件中获取标签 + previous_plugins: dict[str, RegistryPlugin] = { + PYPI_KEY_TEMPLATE.format( + project_link=plugin["project_link"], module_name=plugin["module_name"] + ): RegistryPlugin(**plugin) + for plugin in load_json_from_file("plugins.json") + } + raw_data["tags"] = previous_plugins[PYPI_KEY_TEMPLATE.format(**raw_data)].tags + # 更新作者信息 + raw_data.update(AuthorInfo.from_issue(handler.issue).model_dump()) + + module_name: str = raw_data.get("module_name", None) + project_link: str = raw_data.get("project_link", None) + test_config: str = raw_data.get("test_config", "") + + # 因为修改插件重新测试,所以上次的数据不需要加载,不然会报错重复 + previous_data = [] + + # 修改插件配置肯定是为了通过插件测试,所以一定不跳过测试 + raw_data["skip_test"] = False + + # 运行插件测试 + test = DockerPluginTest(project_link, module_name, test_config) + test_result = await test.run("3.12") + + # 去除颜色字符 + test_output = strip_ansi("\n".join(test_result.outputs)) + metadata = test_result.metadata + if metadata: + # 从插件测试结果中获得元数据 + raw_data.update(metadata) + + # 更新插件测试结果 + raw_data["version"] = test_result.version + raw_data["load"] = test_result.load + raw_data["test_output"] = test_output + raw_data["metadata"] = bool(metadata) + + # 输出插件测试相关信息 + add_step_summary(await render_summary(test_result, test_output, project_link)) + logger.info( + f"插件 {project_link}({test_result.version}) 插件加载{'成功' if test_result.load else '失败'} {'插件已尝试加载' if test_result.run else '插件并未开始运行'}" + ) + logger.info(f"插件元数据:{metadata}") + logger.info("插件测试输出:") + for output in test_result.outputs: + logger.info(output) + + # 验证插件相关信息 + result = validate_info(PublishType.PLUGIN, raw_data, previous_data) + + if not result.valid_data.get("metadata"): + # 如果没有跳过测试且缺少插件元数据,则跳过元数据相关的错误 + # 因为这个时候这些项都会报错,错误在此时没有意义 + metadata_keys = Metadata.__annotations__.keys() + # 如果是重复报错,error["loc"] 是 () + result.errors = [ + error + for error in result.errors + if error["loc"] == () or error["loc"][0] not in metadata_keys + ] + # 元数据缺失时,需要删除元数据相关的字段 + for key in metadata_keys: + result.valid_data.pop(key, None) + + return result + + +def update_file(result: ValidationDict) -> None: + """更新文件""" + if not isinstance(result.info, PluginPublishInfo): + raise ValueError("仅支持修改插件配置") + + logger.info("正在更新配置文件和最新测试结果") + + # 读取文件 + previous_plugins: dict[str, RegistryPlugin] = { + PYPI_KEY_TEMPLATE.format( + project_link=plugin["project_link"], module_name=plugin["module_name"] + ): RegistryPlugin(**plugin) + for plugin in load_json_from_file("plugins.json") + } + previous_results: dict[str, StoreTestResult] = { + key: StoreTestResult(**value) + for key, value in load_json_from_file("results.json").items() + } + plugin_configs: dict[str, str] = load_json_from_file("plugin_configs.json") + + # 更新信息 + plugin = RegistryPlugin.from_publish_info(result.info) + previous_plugins[plugin.key] = plugin + previous_results[plugin.key] = StoreTestResult.from_info(result.info) + plugin_configs[plugin.key] = result.info.test_config + + dump_json("plugins.json", list(previous_plugins.values())) + dump_json("results.json", previous_results) + dump_json("plugin_configs.json", plugin_configs, False) + + logger.info("文件更新完成") diff --git a/src/plugins/github/plugins/publish/__init__.py b/src/plugins/github/plugins/publish/__init__.py index 2d69e69c..2c93820d 100644 --- a/src/plugins/github/plugins/publish/__init__.py +++ b/src/plugins/github/plugins/publish/__init__.py @@ -16,6 +16,7 @@ from src.plugins.github.constants import ( BRANCH_NAME_PREFIX, + CONFIG_LABEL, REMOVE_LABEL, TITLE_MAX_LENGTH, ) @@ -59,11 +60,13 @@ async def publish_related_rule( """确保与发布相关 通过标签判断 - 仅包含发布相关标签,不包含 remove 标签 + 仅包含发布相关标签,不包含 remove/config 标签 """ for label in labels: if label == REMOVE_LABEL: return False + if label == CONFIG_LABEL: + return False return True diff --git a/src/providers/utils.py b/src/providers/utils.py index 01c537c7..b0073510 100644 --- a/src/providers/utils.py +++ b/src/providers/utils.py @@ -8,7 +8,7 @@ from pydantic_core import to_jsonable_python -def load_json_from_file(file_path: Path): +def load_json_from_file(file_path: str | Path): """从文件加载 JSON5 文件""" with open(file_path, encoding="utf-8") as file: return pyjson5.decode_io(file) # type: ignore @@ -36,7 +36,7 @@ def dumps_json(data: Any, minify: bool = True) -> str: return data -def dump_json(path: Path, data: Any, minify: bool = True) -> None: +def dump_json(path: str | Path, data: Any, minify: bool = True) -> None: """保存 JSON 文件""" data = to_jsonable_python(data) diff --git a/tests/github/config/conftest.py b/tests/github/config/conftest.py new file mode 100644 index 00000000..95743e47 --- /dev/null +++ b/tests/github/config/conftest.py @@ -0,0 +1,62 @@ +from pathlib import Path + +import pytest + + +@pytest.fixture +def mock_results(tmp_path: Path): + from src.providers.utils import dump_json + + plugins_path = tmp_path / "plugins.json" + results_path = tmp_path / "results.json" + plugin_configs_path = tmp_path / "plugin_configs.json" + + plugins = [ + { + "module_name": "nonebot_plugin_treehelp", + "project_link": "nonebot-plugin-treehelp", + "name": "帮助", + "desc": "获取插件帮助信息", + "author": "he0119", + "homepage": "https://github.com/he0119/nonebot-plugin-treehelp", + "tags": [{"label": "test", "color": "#ffffff"}], + "is_official": False, + "type": "application", + "supported_adapters": None, + "valid": False, + "time": "2022-01-01T00:00:00Z", + "version": "0.0.1", + "skip_test": False, + } + ] + results = { + "nonebot-plugin-treehelp:nonebot_plugin_treehelp": { + "time": "2022-01-01T00:00:00.420957+08:00", + "config": "", + "version": "0.0.1", + "test_env": {"python==3.12": False}, + "results": {"validation": False, "load": True, "metadata": True}, + "outputs": { + "validation": None, + "load": "test_output", + "metadata": { + "name": "帮助", + "desc": "获取插件帮助信息", + "homepage": "https://github.com/he0119/nonebot-plugin-treehelp", + "type": "application", + "supported_adapters": None, + }, + }, + } + } + plugin_configs = {"nonebot-plugin-treehelp:nonebot_plugin_treehelp": ""} + + dump_json(plugins_path, plugins) + dump_json(results_path, results) + dump_json(plugin_configs_path, plugin_configs) + + return { + "plugins": plugins_path, + "results": results_path, + "plugin_configs": plugin_configs_path, + } diff --git a/tests/github/config/process/test_config_auto_merge.py b/tests/github/config/process/test_config_auto_merge.py new file mode 100644 index 00000000..52999a22 --- /dev/null +++ b/tests/github/config/process/test_config_auto_merge.py @@ -0,0 +1,148 @@ +from nonebot.adapters.github import PullRequestReviewSubmitted +from nonebug import App +from pytest_mock import MockerFixture + +from tests.github.event import get_mock_event +from tests.github.utils import get_github_bot + + +def get_issue_labels(labels: list[str]): + from githubkit.rest import ( + WebhookPullRequestReviewSubmittedPropPullRequestPropLabelsItems as Label, + ) + + return [ + Label.model_construct( + **{ + "color": "2A2219", + "default": False, + "description": "", + "id": 2798075966, + "name": label, + "node_id": "MDU6TGFiZWwyNzk4MDc1OTY2", + "url": "https://api.github.com/repos/he0119/action-test/labels/Config", + } + ) + for label in labels + ] + + +async def test_config_auto_merge( + app: App, mocker: MockerFixture, mock_installation +) -> None: + """测试审查后自动合并 + + 可直接合并的情况 + """ + from src.plugins.github.plugins.config import auto_merge_matcher + + mock_subprocess_run = mocker.patch("subprocess.run") + + async with app.test_matcher() as ctx: + adapter, bot = get_github_bot(ctx) + event = get_mock_event(PullRequestReviewSubmitted) + event.payload.pull_request.labels = get_issue_labels(["Config", "Plugin"]) + + ctx.receive_event(bot, event) + ctx.should_pass_rule(auto_merge_matcher) + ctx.should_call_api( + "rest.apps.async_get_repo_installation", + {"owner": "he0119", "repo": "action-test"}, + mock_installation, + ) + ctx.should_call_api( + "rest.pulls.async_merge", + { + "owner": "he0119", + "repo": "action-test", + "pull_number": 100, + "merge_method": "rebase", + }, + True, + ) + + # 测试 git 命令 + mock_subprocess_run.assert_has_calls( + [ + mocker.call( + ["git", "config", "--global", "safe.directory", "*"], + check=True, + capture_output=True, + ), + mocker.call( + ["pre-commit", "install", "--install-hooks"], + check=True, + capture_output=True, + ), + ], # type: ignore + any_order=True, + ) + + +async def test_auto_merge_not_remove(app: App, mocker: MockerFixture) -> None: + """测试审查后自动合并 + + 和配置无关 + """ + from src.plugins.github.plugins.config import auto_merge_matcher + + mock_subprocess_run = mocker.patch("subprocess.run") + mock_resolve_conflict_pull_requests = mocker.patch( + "src.plugins.github.plugins.remove.resolve_conflict_pull_requests" + ) + + async with app.test_matcher() as ctx: + adapter, bot = get_github_bot(ctx) + event = get_mock_event(PullRequestReviewSubmitted) + event.payload.pull_request.labels = [] + ctx.receive_event(bot, event) + ctx.should_not_pass_rule(auto_merge_matcher) + + # 测试 git 命令 + mock_subprocess_run.assert_not_called() + mock_resolve_conflict_pull_requests.assert_not_called() + + +async def test_auto_merge_not_member(app: App, mocker: MockerFixture) -> None: + """测试审查后自动合并 + + 审核者不是仓库成员 + """ + from src.plugins.github.plugins.config import auto_merge_matcher + + mock_subprocess_run = mocker.patch("subprocess.run") + + async with app.test_matcher() as ctx: + adapter, bot = get_github_bot(ctx) + event = get_mock_event(PullRequestReviewSubmitted) + event.payload.review.author_association = "CONTRIBUTOR" + event.payload.pull_request.labels = get_issue_labels(["Config", "Plugin"]) + + ctx.receive_event(bot, event) + ctx.should_not_pass_rule(auto_merge_matcher) + + # 测试 git 命令 + mock_subprocess_run.assert_not_called() + + +async def test_auto_merge_not_approve(app: App, mocker: MockerFixture) -> None: + """测试审查后自动合并 + + 审核未通过 + """ + from src.plugins.github.plugins.config import auto_merge_matcher + + mock_subprocess_run = mocker.patch("subprocess.run") + + async with app.test_matcher() as ctx: + adapter, bot = get_github_bot(ctx) + event = get_mock_event(PullRequestReviewSubmitted) + event.payload.pull_request.labels = get_issue_labels(["Config", "Plugin"]) + event.payload.review.state = "commented" + + ctx.receive_event(bot, event) + ctx.should_not_pass_rule(auto_merge_matcher) + + # 测试 git 命令 + + mock_subprocess_run.assert_not_called() diff --git a/tests/github/config/process/test_config_check.py b/tests/github/config/process/test_config_check.py new file mode 100644 index 00000000..0ffb7097 --- /dev/null +++ b/tests/github/config/process/test_config_check.py @@ -0,0 +1,282 @@ +import os +from datetime import datetime +from pathlib import Path +from zoneinfo import ZoneInfo + +from inline_snapshot import snapshot +from nonebot.adapters.github import IssuesOpened +from nonebug import App +from pytest_mock import MockerFixture +from respx import MockRouter + +from tests.github.config.utils import generate_issue_body +from tests.github.event import get_mock_event +from tests.github.utils import ( + MockIssue, + check_json_data, + get_github_bot, + get_issue_labels, +) + + +def get_config_labels(): + return get_issue_labels(["Config", "Plugin"]) + + +async def test_process_config_check( + app: App, + mocker: MockerFixture, + mocked_api: MockRouter, + tmp_path: Path, + mock_installation, + mock_results: dict[str, Path], +) -> None: + """测试发布检查不通过""" + from src.providers.docker_test import Metadata + + # 更改当前工作目录为临时目录 + os.chdir(tmp_path) + + mock_datetime = mocker.patch("src.providers.models.datetime") + mock_datetime.now.return_value = datetime( + 2023, 8, 23, 9, 22, 14, 836035, tzinfo=ZoneInfo("Asia/Shanghai") + ) + + mock_subprocess_run = mocker.patch( + "subprocess.run", side_effect=lambda *args, **kwargs: mocker.MagicMock() + ) + + mock_issue = MockIssue( + body=generate_issue_body( + module_name="nonebot_plugin_treehelp", + project_link="nonebot-plugin-treehelp", + ) + ).as_mock(mocker) + + mock_issues_resp = mocker.MagicMock() + mock_issues_resp.parsed_data = mock_issue + + mock_pull = mocker.MagicMock() + mock_pull.number = 100 + mock_pull_resp = mocker.MagicMock() + mock_pull_resp.parsed_data = mock_pull + + mock_comment = mocker.MagicMock() + mock_comment.body = "Plugin: test" + mock_list_comments_resp = mocker.MagicMock() + mock_list_comments_resp.parsed_data = [mock_comment] + + mock_test_result = mocker.MagicMock() + mock_test_result.metadata = Metadata( + name="name", + desc="desc", + homepage="https://nonebot.dev", + type="application", + supported_adapters=["~onebot.v11"], + ) + mock_test_result.load = True + mock_test_result.version = "1.0.0" + mock_docker = mocker.patch("src.providers.docker_test.DockerPluginTest.run") + mock_docker.return_value = mock_test_result + + async with app.test_matcher() as ctx: + adapter, bot = get_github_bot(ctx) + event = get_mock_event(IssuesOpened) + event.payload.issue.labels = get_config_labels() + + ctx.should_call_api( + "rest.apps.async_get_repo_installation", + {"owner": "he0119", "repo": "action-test"}, + mock_installation, + ) + 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_create_comment", + { + "owner": "he0119", + "repo": "action-test", + "issue_number": 80, + "body": snapshot( + """\ +# 📃 商店发布检查结果 + +> Plugin: name + +**✅ 所有测试通过,一切准备就绪!** + + +
+详情 +
  • ✅ 项目 nonebot-plugin-treehelp 已发布至 PyPI。
  • ✅ 项目 主页 返回状态码 200。
  • ✅ 标签: test-#ffffff。
  • ✅ 插件类型: application。
  • ✅ 插件支持的适配器: nonebot.adapters.onebot.v11。
  • ✅ 插件 加载测试 通过。
  • +
    + +--- + +💡 如需修改信息,请直接修改 issue,机器人会自动更新检查结果。 +💡 当插件加载测试失败时,请发布新版本后勾选插件测试勾选框重新运行插件测试。 + +♻️ 评论已更新至最新检查结果 + +💪 Powered by [NoneFlow](https://github.com/nonebot/noneflow) + +""" + ), + }, + True, + ) + ctx.should_call_api( + "rest.pulls.async_create", + snapshot( + { + "owner": "he0119", + "repo": "action-test", + "title": "Plugin: name", + "body": "resolve #80", + "base": "results", + "head": "config/issue80", + } + ), + mock_pull_resp, + ) + ctx.should_call_api( + "rest.issues.async_add_labels", + snapshot( + { + "owner": "he0119", + "repo": "action-test", + "issue_number": 100, + "labels": ["Plugin", "Config"], + } + ), + None, + ) + + ctx.receive_event(bot, event) + + # 测试 git 命令 + mock_subprocess_run.assert_has_calls( + [ + mocker.call( + ["git", "config", "--global", "safe.directory", "*"], + check=True, + capture_output=True, + ), + mocker.call( + ["pre-commit", "install", "--install-hooks"], + check=True, + capture_output=True, + ), + mocker.call( + ["git", "fetch", "origin", "results"], check=True, capture_output=True + ), + mocker.call( + ["git", "checkout", "results"], check=True, capture_output=True + ), + mocker.call( + ["git", "switch", "-C", "config/issue80"], + check=True, + capture_output=True, + ), + mocker.call( + ["git", "config", "--global", "user.name", "test"], + check=True, + capture_output=True, + ), + mocker.call( + [ + "git", + "config", + "--global", + "user.email", + "test@users.noreply.github.com", + ], + check=True, + capture_output=True, + ), + mocker.call(["git", "add", "-A"], check=True, capture_output=True), + mocker.call( + ["git", "commit", "-m", "chore: edit config plugin name (#80)"], + check=True, + capture_output=True, + ), + mocker.call(["git", "fetch", "origin"], check=True, capture_output=True), + mocker.call( + ["git", "diff", "origin/config/issue80", "config/issue80"], + check=True, + capture_output=True, + ), + mocker.call( + ["git", "push", "origin", "config/issue80", "-f"], + check=True, + capture_output=True, + ), + ] # type: ignore + ) + + # 检查文件是否正确 + check_json_data( + mock_results["plugins"], + snapshot( + [ + { + "module_name": "nonebot_plugin_treehelp", + "project_link": "nonebot-plugin-treehelp", + "name": "name", + "desc": "desc", + "author": "test", + "homepage": "https://nonebot.dev", + "tags": [{"label": "test", "color": "#ffffff"}], + "is_official": False, + "type": "application", + "supported_adapters": ["nonebot.adapters.onebot.v11"], + "valid": True, + "time": "2021-08-01T00:00:00+00:00", + "version": "1.0.0", + "skip_test": False, + } + ] + ), + ) + check_json_data( + mock_results["results"], + snapshot( + { + "nonebot-plugin-treehelp:nonebot_plugin_treehelp": { + "time": "2023-08-23T09:22:14.836035+08:00", + "config": "log_level=DEBUG", + "version": "1.0.0", + "test_env": {"python==3.12": True}, + "results": {"validation": True, "load": True, "metadata": True}, + "outputs": { + "validation": None, + "load": "", + "metadata": { + "name": "name", + "desc": "desc", + "homepage": "https://nonebot.dev", + "type": "application", + "supported_adapters": ["nonebot.adapters.onebot.v11"], + }, + }, + } + } + ), + ) + check_json_data( + mock_results["plugin_configs"], + snapshot( + {"nonebot-plugin-treehelp:nonebot_plugin_treehelp": "log_level=DEBUG"} + ), + ) + + assert mocked_api["homepage"].called diff --git a/tests/github/config/utils.py b/tests/github/config/utils.py new file mode 100644 index 00000000..cd225341 --- /dev/null +++ b/tests/github/config/utils.py @@ -0,0 +1,6 @@ +def generate_issue_body( + module_name: str = "module_name", + project_link: str = "project_link", + config: str = "log_level=DEBUG", +): + return f"""### PyPI 项目名\n\n{project_link}\n\n### 插件 import 包名\n\n{module_name}\n\n### 插件配置项\n\n```dotenv\n{config}\n```""" diff --git a/tests/github/config/utils/test_config_update_file.py b/tests/github/config/utils/test_config_update_file.py new file mode 100644 index 00000000..46bf11d2 --- /dev/null +++ b/tests/github/config/utils/test_config_update_file.py @@ -0,0 +1,115 @@ +import os +from datetime import datetime +from pathlib import Path +from zoneinfo import ZoneInfo + +from inline_snapshot import snapshot +from nonebug import App +from pytest_mock import MockerFixture + +from tests.github.utils import check_json_data + + +async def test_update_file( + app: App, + mocker: MockerFixture, + tmp_path: Path, + mock_results: dict[str, Path], +) -> None: + from src.plugins.github.plugins.config.utils import update_file + from src.providers.validation.models import ( + PluginPublishInfo, + PublishType, + ValidationDict, + ) + + # 更改当前工作目录为临时目录 + os.chdir(tmp_path) + + mock_datetime = mocker.patch("src.providers.models.datetime") + mock_datetime.now.return_value = datetime( + 2023, 8, 23, 9, 22, 14, 836035, tzinfo=ZoneInfo("Asia/Shanghai") + ) + + raw_data = { + "module_name": "nonebot_plugin_treehelp", + "project_link": "nonebot-plugin-treehelp", + "name": "帮助", + "desc": "获取插件帮助信息", + "author": "he0119", + "author_id": 1, + "homepage": "https://github.com/he0119/nonebot-plugin-treehelp", + "tags": [], + "is_official": False, + "type": "application", + "supported_adapters": None, + "load": True, + "skip_test": False, + "test_config": "log_level=DEBUG", + "test_output": "test_output", + "time": "2023-01-01T00:00:00Z", + "version": "1.0.0", + } + result = ValidationDict( + type=PublishType.PLUGIN, + raw_data=raw_data, + valid_data=raw_data, + info=PluginPublishInfo.model_construct(**raw_data), + errors=[], + ) + update_file(result) + + check_json_data( + mock_results["plugins"], + snapshot( + [ + { + "module_name": "nonebot_plugin_treehelp", + "project_link": "nonebot-plugin-treehelp", + "name": "帮助", + "desc": "获取插件帮助信息", + "author": "he0119", + "homepage": "https://github.com/he0119/nonebot-plugin-treehelp", + "tags": [], + "is_official": False, + "type": "application", + "supported_adapters": None, + "valid": True, + "time": "2023-01-01T00:00:00Z", + "version": "1.0.0", + "skip_test": False, + } + ] + ), + ) + check_json_data( + mock_results["results"], + snapshot( + { + "nonebot-plugin-treehelp:nonebot_plugin_treehelp": { + "time": "2023-08-23T09:22:14.836035+08:00", + "config": "log_level=DEBUG", + "version": "1.0.0", + "test_env": {"python==3.12": True}, + "results": {"validation": True, "load": True, "metadata": True}, + "outputs": { + "validation": None, + "load": "test_output", + "metadata": { + "name": "帮助", + "desc": "获取插件帮助信息", + "homepage": "https://github.com/he0119/nonebot-plugin-treehelp", + "type": "application", + "supported_adapters": None, + }, + }, + } + } + ), + ) + check_json_data( + mock_results["plugin_configs"], + snapshot( + {"nonebot-plugin-treehelp:nonebot_plugin_treehelp": "log_level=DEBUG"} + ), + )