Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/lang/zh-CN/

## [Unreleased]

### Fixed

- 修复插件元数据异常时导致插件测试验证报错的问题

## [4.0.7] - 2024-11-20

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion src/plugins/github/plugins/publish/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ async def render_summary(test_result: DockerTestResult, output: str, project_lin
version=test_result.version,
load=test_result.load,
run=test_result.run,
metadata=dumps_json(test_result.metadata.model_dump(), False)
metadata=dumps_json(test_result.metadata, False)
if test_result.metadata
else {},
output=output,
Expand Down
4 changes: 2 additions & 2 deletions src/plugins/github/plugins/publish/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ async def validate_plugin_info_from_issue(
metadata = test_result.metadata
if metadata:
# 从插件测试结果中获得元数据
raw_data.update(metadata.model_dump())
raw_data.update(metadata)

raw_data["load"] = test_result.load
raw_data["test_output"] = test_output
Expand All @@ -130,7 +130,7 @@ async def validate_plugin_info_from_issue(
if not result.valid_data.get("metadata") and not skip_test:
# 如果没有跳过测试且缺少插件元数据,则跳过元数据相关的错误
# 因为这个时候这些项都会报错,错误在此时没有意义
metadata_keys = Metadata.model_fields.keys()
metadata_keys = Metadata.__annotations__.keys()
# 如果是重复报错,error["loc"] 是 ()
result.errors = [
error
Expand Down
42 changes: 7 additions & 35 deletions src/providers/docker_test/__init__.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,20 @@
import json
from typing import Any
from typing import TypedDict

import docker
from pydantic import BaseModel, Field, field_validator, model_validator
from pydantic_core import PydanticCustomError
from pyjson5 import Json5DecoderException
from pydantic import BaseModel, Field, SkipValidation, field_validator

from src.providers.constants import DOCKER_IMAGES, REGISTRY_PLUGINS_URL
from src.providers.utils import load_json


class Metadata(BaseModel):
class Metadata(TypedDict):
"""插件元数据"""

name: str
desc: str
homepage: str | None = None
type: str | None = None
supported_adapters: list[str] | None = None

@model_validator(mode="before")
@classmethod
def model_validator(cls, data: dict[str, Any]):
if data.get("desc") is None:
data["desc"] = data.get("description")
return data

@field_validator("supported_adapters", mode="before")
@classmethod
def supported_adapters_validator(cls, v: list[str] | str | None):
if isinstance(v, str):
try:
v = load_json(v)
except Json5DecoderException:
raise PydanticCustomError("json_type", "JSON 格式不合法")

return v
homepage: str | None
type: str | None
supported_adapters: list[str] | None


class DockerTestResult(BaseModel):
Expand All @@ -47,16 +26,9 @@ class DockerTestResult(BaseModel):
config: str = ""
# 测试环境 python==3.10 pytest==6.2.5 nonebot2==2.0.0a1 ...
test_env: str = Field(default="unknown")
metadata: Metadata | None
metadata: SkipValidation[Metadata] | None
outputs: list[str]

@field_validator("metadata", mode="before")
@classmethod
def metadata_validator(cls, v: Any):
if v:
return v
return None

@field_validator("config", mode="before")
@classmethod
def config_validator(cls, v: str | None):
Expand Down
15 changes: 7 additions & 8 deletions src/providers/docker_test/plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,17 +100,16 @@
"""

RUNNER_SCRIPT = """import json
import os

from nonebot import init, load_plugin, logger, require
from pydantic import BaseModel


class SetEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, set):
return list(obj)
return json.JSONEncoder.default(self, obj)
def default(self, o):
if isinstance(o, set):
return list(o)
return json.JSONEncoder.default(self, o)


init()
Expand All @@ -122,7 +121,7 @@
if plugin.metadata:
metadata = {{
"name": plugin.metadata.name,
"description": plugin.metadata.description,
"desc": plugin.metadata.description,
"usage": plugin.metadata.usage,
"type": plugin.metadata.type,
"homepage": plugin.metadata.homepage,
Expand Down Expand Up @@ -264,7 +263,7 @@
)
await self.run_poetry_project()

metadata = {}
metadata = None

Check warning on line 266 in src/providers/docker_test/plugin_test.py

View check run for this annotation

Codecov / codecov/patch

src/providers/docker_test/plugin_test.py#L266

Added line #L266 was not covered by tests
metadata_path = self.path / "metadata.json"
if metadata_path.exists():
with open(self.path / "metadata.json", encoding="utf-8") as f:
Expand Down Expand Up @@ -373,7 +372,7 @@
f.write(
RUNNER_SCRIPT.format(
self.module_name,
"\n".join([f"require('{i}')" for i in self._deps]),
"\n".join([f'require("{i}")' for i in self._deps]),
)
)

Expand Down
2 changes: 1 addition & 1 deletion src/providers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ def from_info(cls, info: PluginPublishInfo) -> Self:
homepage=info.homepage,
type=info.type,
supported_adapters=info.supported_adapters,
).model_dump(),
),
},
)

Expand Down
2 changes: 1 addition & 1 deletion src/providers/store_test/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ async def validate_plugin(
# 使用最新的插件元数据更新插件信息
raw_data["metadata"] = bool(plugin_metadata)
if plugin_metadata:
raw_data.update(plugin_metadata.model_dump())
raw_data.update(plugin_metadata)

# 通过 Github API 获取插件作者名称
try:
Expand Down
76 changes: 70 additions & 6 deletions tests/utils/docker_test/test_docker_plugin_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ async def test_docker_plugin_test(mocked_api: MockRouter, mocker: MockerFixture)
mocked_run = mocker.Mock()
mocked_run.return_value = json.dumps(
{
"metadata": {},
"metadata": None,
"outputs": ["test"],
"load": True,
"run": True,
Expand Down Expand Up @@ -58,7 +58,7 @@ async def test_docker_plugin_test_metadata_some_fields_empty(
mocked_api: MockRouter, mocker: MockerFixture
):
"""测试 metadata 的部分字段为空"""
from src.providers.docker_test import DockerPluginTest, DockerTestResult, Metadata
from src.providers.docker_test import DockerPluginTest, DockerTestResult

mocked_run = mocker.Mock()
mocked_run.return_value = json.dumps(
Expand All @@ -85,16 +85,80 @@ async def test_docker_plugin_test_metadata_some_fields_empty(
test = DockerPluginTest("project_link", "module_name")
result = await test.run("3.12")

assert result == snapshot(
DockerTestResult(
config="",
load=True,
metadata={
"name": "name",
"desc": "desc",
"homepage": None,
"type": None,
"supported_adapters": None,
},
outputs=["test"],
run=True,
test_env="python==3.12",
version="0.0.1",
)
)

assert not mocked_api["store_plugins"].called
mocked_run.assert_called_once_with(
"ghcr.io/nonebot/nonetest:3.12-latest",
environment=snapshot(
{
"PLUGIN_INFO": "project_link:module_name",
"PLUGIN_CONFIG": "",
"PLUGINS_URL": "https://raw.githubusercontent.com/nonebot/registry/results/plugins.json",
}
),
detach=False,
remove=True,
)


async def test_docker_plugin_test_metadata_some_fields_invalid(
mocked_api: MockRouter, mocker: MockerFixture
):
"""测试 metadata 的部分字段不符合规范"""
from src.providers.docker_test import DockerPluginTest, DockerTestResult, Metadata

mocked_run = mocker.Mock()
mocked_run.return_value = json.dumps(
{
"metadata": {
"name": "name",
"desc": "desc",
"homepage": 12,
"type": True,
"supported_adapters": {},
},
"outputs": ["test"],
"load": True,
"run": True,
"version": "0.0.1",
"config": "",
}
).encode()
mocked_client = mocker.Mock()
mocked_client.containers.run = mocked_run
mocked_docker = mocker.patch("docker.DockerClient")
mocked_docker.return_value = mocked_client

test = DockerPluginTest("project_link", "module_name")
result = await test.run("3.12")

assert result == snapshot(
DockerTestResult(
config="",
load=True,
metadata=Metadata(
desc="desc",
homepage=None,
name="name",
supported_adapters=None,
type=None,
desc="desc",
homepage=12, # type: ignore
type=True, # type: ignore
supported_adapters={}, # type: ignore
),
outputs=["test"],
run=True,
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/store_test/output.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"metadata": {
"name": "TREEHELP",
"description": "\u8ba2\u9605\u725b\u5ba2/CF/AT\u5e73\u53f0\u7684\u6bd4\u8d5b\u4fe1\u606f",
"desc": "\u8ba2\u9605\u725b\u5ba2/CF/AT\u5e73\u53f0\u7684\u6bd4\u8d5b\u4fe1\u606f",
"usage": "/contest.list \u83b7\u53d6\u6240\u6709/CF/\u725b\u5ba2/AT\u5e73\u53f0\u7684\u6bd4\u8d5b\u4fe1\u606f\n/contest.subscribe \u8ba2\u9605CF/\u725b\u5ba2/AT\u5e73\u53f0\u7684\u6bd4\u8d5b\u4fe1\u606f\n/contest.update \u624b\u52a8\u66f4\u65b0\u6bd4\u8d5b\u4fe1\u606f\n",
"type": "application",
"homepage": "https://nonebot.dev/",
Expand Down
2 changes: 1 addition & 1 deletion tests/utils/store_test/output_failed.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"metadata": {},
"metadata": null,
"outputs": [
"\u521b\u5efa\u6d4b\u8bd5\u76ee\u5f55 plugin_test",
" For further information visit https://errors.pydantic.dev/2.9/v/model_type\u001b[0m"
Expand Down
67 changes: 38 additions & 29 deletions tests/utils/store_test/test_validate_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,7 @@ def mock_docker_result(path: Path, mocker: MockerFixture):

async def test_validate_plugin(mocked_api: MockRouter, mocker: MockerFixture) -> None:
"""验证插件信息"""
from src.providers.models import (
Metadata,
RegistryPlugin,
StorePlugin,
StoreTestResult,
)
from src.providers.models import RegistryPlugin, StorePlugin, StoreTestResult
from src.providers.store_test.validation import validate_plugin

mock_datetime = mocker.patch("src.providers.models.datetime")
Expand Down Expand Up @@ -61,13 +56,18 @@ async def test_validate_plugin(mocked_api: MockRouter, mocker: MockerFixture) ->
创建测试目录 plugin_test
require("nonebot_plugin_alconna")\
""",
"metadata": Metadata(
desc="订阅牛客/CF/AT平台的比赛信息",
homepage="https://nonebot.dev/",
name="TREEHELP",
supported_adapters=None,
type="application",
),
"metadata": {
"name": "TREEHELP",
"desc": "订阅牛客/CF/AT平台的比赛信息",
"usage": """\
/contest.list 获取所有/CF/牛客/AT平台的比赛信息
/contest.subscribe 订阅CF/牛客/AT平台的比赛信息
/contest.update 手动更新比赛信息
""",
"type": "application",
"homepage": "https://nonebot.dev/",
"supported_adapters": None,
},
},
results={"validation": True, "load": True, "metadata": True},
test_env={"unknown": True},
Expand Down Expand Up @@ -103,7 +103,6 @@ async def test_validate_plugin_with_previous(

需要能够正常更新 author_id, tags 和 is_official 等信息
"""
from src.providers.docker_test import Metadata
from src.providers.models import Color, RegistryPlugin, StoreTestResult, Tag
from src.providers.store_test.validation import StorePlugin, validate_plugin

Expand Down Expand Up @@ -153,13 +152,18 @@ async def test_validate_plugin_with_previous(
创建测试目录 plugin_test
require("nonebot_plugin_alconna")\
""",
"metadata": Metadata(
desc="订阅牛客/CF/AT平台的比赛信息",
homepage="https://nonebot.dev/",
name="TREEHELP",
supported_adapters=None,
type="application",
),
"metadata": {
"name": "TREEHELP",
"desc": "订阅牛客/CF/AT平台的比赛信息",
"usage": """\
/contest.list 获取所有/CF/牛客/AT平台的比赛信息
/contest.subscribe 订阅CF/牛客/AT平台的比赛信息
/contest.update 手动更新比赛信息
""",
"type": "application",
"homepage": "https://nonebot.dev/",
"supported_adapters": None,
},
},
results={"validation": True, "load": True, "metadata": True},
test_env={"unknown": True},
Expand Down Expand Up @@ -196,7 +200,7 @@ async def test_validate_plugin_skip_test(

如果插件之前是跳过测试的,如果插件测试成功,应将 skip_test 设置为 False。
"""
from src.providers.models import Metadata, RegistryPlugin, StoreTestResult
from src.providers.models import RegistryPlugin, StoreTestResult
from src.providers.store_test.validation import StorePlugin, validate_plugin

mock_datetime = mocker.patch("src.providers.models.datetime")
Expand Down Expand Up @@ -226,13 +230,18 @@ async def test_validate_plugin_skip_test(
创建测试目录 plugin_test
require("nonebot_plugin_alconna")\
""",
"metadata": Metadata(
desc="订阅牛客/CF/AT平台的比赛信息",
homepage="https://nonebot.dev/",
name="TREEHELP",
supported_adapters=None,
type="application",
),
"metadata": {
"name": "TREEHELP",
"desc": "订阅牛客/CF/AT平台的比赛信息",
"usage": """\
/contest.list 获取所有/CF/牛客/AT平台的比赛信息
/contest.subscribe 订阅CF/牛客/AT平台的比赛信息
/contest.update 手动更新比赛信息
""",
"type": "application",
"homepage": "https://nonebot.dev/",
"supported_adapters": None,
},
},
results={"validation": True, "load": True, "metadata": True},
test_env={"unknown": True},
Expand Down
Loading
Loading