diff --git a/.github/workflows/docker-test.yml b/.github/workflows/docker-test.yml deleted file mode 100644 index 5293cec0..00000000 --- a/.github/workflows/docker-test.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Plugin Test Container Builder - -on: - push: - paths: - - "src/providers/docker_test/**" - pull_request: - paths: - - "src/providers/docker_test//**" - workflow_dispatch: - -jobs: - build: - name: Docker - runs-on: ubuntu-latest - strategy: - matrix: - python-version: ["3.9", "3.10", "3.11", "3.12"] - fail-fast: false - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Docker - uses: docker/setup-buildx-action@v3 - - - name: Login to Github Container Registry - if: github.event_name != 'pull_request' - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Generate Tags - uses: docker/metadata-action@v5 - id: metadata - with: - images: ghcr.io/nonebot/nonetest - flavor: | - prefix=${{ matrix.python-version }}-,onlatest=true - tags: | - type=semver,pattern={{version}} - type=ref,event=branch - - - name: Build and Publish - uses: docker/build-push-action@v6 - with: - context: ./src/providers/docker_test - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.metadata.outputs.tags }} - labels: ${{ steps.metadata.outputs.labels }} - build-args: PYTHON_VERSION=${{ matrix.python-version }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b0842054..885fe947 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,8 +35,8 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} - docker: - name: Docker + noneflow-docker: + name: NoneFlow Docker runs-on: ubuntu-latest needs: test steps: @@ -70,3 +70,40 @@ jobs: push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.metadata.outputs.tags }} labels: ${{ steps.metadata.outputs.labels }} + + nonetest-docker: + name: NoneTest Docker + runs-on: ubuntu-latest + needs: test + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Docker + uses: docker/setup-buildx-action@v3 + + - name: Login to Github Container Registry + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate Tags + uses: docker/metadata-action@v5 + id: metadata + with: + images: ghcr.io/nonebot/nonetest + tags: | + type=semver,pattern={{version}} + type=ref,event=branch + + - name: Build and Publish + uses: docker/build-push-action@v6 + with: + context: . + file: ./src/providers/docker_test/Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.metadata.outputs.tags }} + labels: ${{ steps.metadata.outputs.labels }} diff --git a/CHANGELOG.md b/CHANGELOG.md index d668fcea..394f7216 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 + +- 插件测试中使用 uv 来控制 Python 版本并与本体共享依赖 + ## [4.1.4] - 2024-12-08 ### Fixed diff --git a/pyproject.toml b/pyproject.toml index 0fc89df7..c64aeb3d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,6 +47,7 @@ show-bump = "bump-my-version show-bump" snapshot-create = "pytest --inline-snapshot=create" snapshot-fix = "pytest --inline-snapshot=fix" store-test = "python -m src.providers.store_test" +docker-test = "python -m src.providers.docker_test" [tool.pyright] pythonVersion = "3.12" diff --git a/src/plugins/github/plugins/publish/utils.py b/src/plugins/github/plugins/publish/utils.py index 034b6ce1..f0116cf5 100644 --- a/src/plugins/github/plugins/publish/utils.py +++ b/src/plugins/github/plugins/publish/utils.py @@ -110,6 +110,7 @@ async def resolve_conflict_pull_requests( # 如果信息验证失败,则跳过更新 if not result.valid: logger.error("信息验证失败,已跳过") + logger.error(f"验证结果: {result}") continue # 每次切换前都要确保回到主分支 diff --git a/src/providers/constants.py b/src/providers/constants.py index a1f154c9..4f2776d4 100644 --- a/src/providers/constants.py +++ b/src/providers/constants.py @@ -33,4 +33,4 @@ # 商店测试镜像 # https://github.com/orgs/nonebot/packages/container/package/nonetest DOCKER_IMAGES_VERSION = os.environ.get("DOCKER_IMAGES_VERSION") or "latest" -DOCKER_IMAGES = f"ghcr.io/nonebot/nonetest:{{}}-{DOCKER_IMAGES_VERSION}" +DOCKER_IMAGES = f"ghcr.io/nonebot/nonetest:{DOCKER_IMAGES_VERSION}" diff --git a/src/providers/docker_test/Dockerfile b/src/providers/docker_test/Dockerfile index 04ab5a4e..ecf18b28 100644 --- a/src/providers/docker_test/Dockerfile +++ b/src/providers/docker_test/Dockerfile @@ -1,8 +1,10 @@ -ARG PYTHON_VERSION=3.12 +FROM python:3.12.4 +COPY --from=ghcr.io/astral-sh/uv:0.5.6 /uv /bin/uv -FROM python:${PYTHON_VERSION} +WORKDIR /app -WORKDIR /tmp +# 设置时区 +ENV TZ=Asia/Shanghai # OpenCV 所需的依赖 RUN apt-get update \ @@ -11,11 +13,14 @@ RUN apt-get update \ && apt-get purge -y --auto-remove \ && rm -rf /var/lib/apt/lists/* -# 测试插件依赖 Poetry -RUN curl -sSL https://install.python-poetry.org | python3 - - +# 插件测试需要 Poetry ENV PATH="${PATH}:/root/.local/bin" +RUN uv tool install poetry + +# Python 依赖 +COPY pyproject.toml uv.lock /app/ +RUN uv sync --project /app/ --no-dev --frozen --compile-bytecode -COPY ./plugin_test.py /tmp/plugin_test.py +COPY src /app/src/ -CMD ["python", "plugin_test.py"] +CMD ["uv", "run", "--project", "/app/", "--no-dev", "-m", "src.providers.docker_test"] diff --git a/src/providers/docker_test/__init__.py b/src/providers/docker_test/__init__.py index 72e7700b..bd511037 100644 --- a/src/providers/docker_test/__init__.py +++ b/src/providers/docker_test/__init__.py @@ -2,9 +2,9 @@ from typing import TypedDict import docker -from pydantic import BaseModel, Field, SkipValidation, field_validator +from pydantic import BaseModel, SkipValidation, field_validator -from src.providers.constants import DOCKER_IMAGES, REGISTRY_PLUGINS_URL +from src.providers.constants import DOCKER_IMAGES class Metadata(TypedDict): @@ -24,19 +24,19 @@ class DockerTestResult(BaseModel): """ 是否运行测试 """ load: bool """ 是否加载成功 """ + output: str + """ 测试输出 """ version: str | None = None """ 测试版本 """ config: str = "" """ 测试配置 """ - test_env: str = Field(default="unknown") + test_env: str = "" """测试环境 python==3.12 nonebot2==2.4.0 pydantic==2.10.0 """ - metadata: SkipValidation[Metadata] | None + metadata: SkipValidation[Metadata] | None = None """ 插件元数据 """ - output: str - """ 测试输出 """ @field_validator("config", mode="before") @classmethod @@ -59,20 +59,20 @@ async def run(self, version: str) -> DockerTestResult: Returns: DockerTestResult: 测试结果 """ - image_name = DOCKER_IMAGES.format(version) # 连接 Docker 环境 client = docker.DockerClient(base_url="unix://var/run/docker.sock") try: # 运行 Docker 容器,捕获输出。 容器内运行的代码拥有超时设限,此处无需设置超时 output = client.containers.run( - image_name, + DOCKER_IMAGES, environment={ + # 运行测试的 Python 版本 + "PYTHON_VERSION": version, + # 插件信息 "PROJECT_LINK": self.project_link, "MODULE_NAME": self.module_name, "PLUGIN_CONFIG": self.config, - # 插件测试需要用到的插件列表来验证插件依赖是否正确加载 - "PLUGINS_URL": REGISTRY_PLUGINS_URL, }, detach=False, remove=True, @@ -83,6 +83,5 @@ async def run(self, version: str) -> DockerTestResult: "run": False, "load": False, "output": str(e), - "metadata": None, } return DockerTestResult(**data) diff --git a/src/providers/docker_test/__main__.py b/src/providers/docker_test/__main__.py new file mode 100644 index 00000000..5ed5cdb3 --- /dev/null +++ b/src/providers/docker_test/__main__.py @@ -0,0 +1,27 @@ +import asyncio +import os + +from .plugin_test import PluginTest + + +def main(): + """根据传入的环境变量进行测试 + + PYTHON_VERSION 为运行测试的 Python 版本 + PROJECT_LINK 为插件的项目名 + MODULE_NAME 为插件的模块名 + PLUGIN_CONFIG 为该插件的配置 + """ + python_version = os.environ.get("PYTHON_VERSION", "") + + project_link = os.environ.get("PROJECT_LINK", "") + module_name = os.environ.get("MODULE_NAME", "") + plugin_config = os.environ.get("PLUGIN_CONFIG", None) + + plugin = PluginTest(python_version, project_link, module_name, plugin_config) + + asyncio.run(plugin.run()) + + +if __name__ == "__main__": + main() diff --git a/src/providers/docker_test/plugin_test.py b/src/providers/docker_test/plugin_test.py index a6f1ce5a..fb7e1ba9 100644 --- a/src/providers/docker_test/plugin_test.py +++ b/src/providers/docker_test/plugin_test.py @@ -1,12 +1,6 @@ """插件加载测试 测试代码修改自 ,谢谢 [Lan 佬](https://github.com/Lancercmd)。 - -在 GitHub Actions 中运行,通过 GitHub Event 文件获取所需信息。并将测试结果保存至 GitHub Action 的输出文件中。 - -当前会输出 RESULT, OUTPUT, METADATA 三个数据,分别对应测试结果、测试输出、插件元数据。 - -经测试可以直接在 Python 3.10+ 环境下运行,无需额外依赖。 """ # ruff: noqa: T201, ASYNC109 @@ -14,132 +8,14 @@ import json import os import re -import sys from asyncio import create_subprocess_shell, subprocess from pathlib import Path -from urllib.request import urlopen - -# NoneBot Store -PLUGINS_URL = os.environ.get("PLUGINS_URL") -# 匹配信息的正则表达式 -ISSUE_PATTERN = r"### {}\s+([^\s#].*?)(?=(?:\s+###|$))" - -# 伪造的驱动 -FAKE_SCRIPT = """from typing import Optional, Union - -from nonebot import logger -from nonebot.drivers import ( - ASGIMixin, - HTTPClientMixin, - HTTPClientSession, - HTTPVersion, - Request, - Response, - WebSocketClientMixin, -) -from nonebot.drivers import Driver as BaseDriver -from nonebot.internal.driver.model import ( - CookieTypes, - HeaderTypes, - QueryTypes, -) -from typing_extensions import override - - -class Driver(BaseDriver, ASGIMixin, HTTPClientMixin, WebSocketClientMixin): - @property - @override - def type(self) -> str: - return "fake" - @property - @override - def logger(self): - return logger +import httpx - @override - def run(self, *args, **kwargs): - super().run(*args, **kwargs) - - @property - @override - def server_app(self): - return None - - @property - @override - def asgi(self): - raise NotImplementedError +from src.providers.constants import REGISTRY_PLUGINS_URL - @override - def setup_http_server(self, setup): - raise NotImplementedError - - @override - def setup_websocket_server(self, setup): - raise NotImplementedError - - @override - async def request(self, setup: Request) -> Response: - raise NotImplementedError - - @override - async def websocket(self, setup: Request) -> Response: - raise NotImplementedError - - @override - def get_session( - self, - params: QueryTypes = None, - headers: HeaderTypes = None, - cookies: CookieTypes = None, - version: Union[str, HTTPVersion] = HTTPVersion.H11, - timeout: Optional[float] = None, - proxy: Optional[str] = None, - ) -> HTTPClientSession: - raise NotImplementedError -""" - -RUNNER_SCRIPT = """import json - -from nonebot import init, load_plugin, logger, require -from pydantic import BaseModel - - -class SetEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, set): - return list(o) - return json.JSONEncoder.default(self, o) - - -init() -plugin = load_plugin("{}") - -if not plugin: - exit(1) -else: - if plugin.metadata: - metadata = {{ - "name": plugin.metadata.name, - "desc": plugin.metadata.description, - "usage": plugin.metadata.usage, - "type": plugin.metadata.type, - "homepage": plugin.metadata.homepage, - "supported_adapters": plugin.metadata.supported_adapters, - }} - with open("metadata.json", "w", encoding="utf-8") as f: - try: - f.write(f"{{json.dumps(metadata, cls=SetEncoder)}}") - except Exception: - f.write("{{}}") - - if plugin.metadata.config and not issubclass(plugin.metadata.config, BaseModel): - logger.error("插件配置项不是 Pydantic BaseModel 的子类") - exit(1) - -{} -""" +from .render import render_fake, render_runner def strip_ansi(text: str | None) -> str: @@ -155,11 +31,7 @@ def get_plugin_list() -> dict[str, str]: 通过 package_name 获取 module_name """ - if PLUGINS_URL is None: - raise ValueError("PLUGINS_URL 环境变量未设置") - - with urlopen(PLUGINS_URL) as response: - plugins = json.loads(response.read()) + plugins = httpx.get(REGISTRY_PLUGINS_URL).json() return { canonicalize_name(plugin["project_link"]): plugin["module_name"] @@ -224,7 +96,11 @@ def parse_requirements(requirements: str) -> dict[str, str]: class PluginTest: def __init__( - self, project_link: str, module_name: str, config: str | None = None + self, + python_version: str, + project_link: str, + module_name: str, + config: str | None = None, ) -> None: """插件测试构造函数 @@ -232,6 +108,8 @@ def __init__( project_info (str): 项目信息,格式为 project_link:module_name config (str | None, optional): 插件配置. 默认为 None. """ + self.python_version = python_version + self.project_link = project_link self.module_name = module_name self.config = config @@ -248,6 +126,7 @@ def __init__( # 插件测试环境 self._deps = [] self._test_env = [] + self._test_python_version = "unknown" @property def key(self) -> str: @@ -285,9 +164,13 @@ async def run(self): await asyncio.gather( self.show_package_info(), self.show_plugin_dependencies(), + self.get_python_version(), ) await self.run_poetry_project() + # 补上获取到 Python 版本 + self._test_env.insert(0, f"python=={self._test_python_version}") + # 读取插件元数据 metadata = None metadata_path = self._test_dir / "metadata.json" if metadata_path.exists(): @@ -343,7 +226,7 @@ async def create_poetry_project(self): self._test_dir.mkdir() code, stdout, stderr = await self.command( - f"""poetry init -n && sed -i "s/\\^/~/g" pyproject.toml && poetry env info --ansi && poetry add {self.project_link}""" + f"""uv venv --python {self.python_version} && poetry init -n --python "~{self.python_version}" && poetry env info --ansi && poetry add {self.project_link}""" ) self._create = code @@ -390,16 +273,13 @@ async def run_poetry_project(self) -> None: with open(self._test_dir / ".env.prod", "w", encoding="utf-8") as f: f.write(self.config) + fake_script = await render_fake() with open(self._test_dir / "fake.py", "w", encoding="utf-8") as f: - f.write(FAKE_SCRIPT) + f.write(fake_script) + runner_script = await render_runner(self.module_name, self._deps) with open(self._test_dir / "runner.py", "w", encoding="utf-8") as f: - f.write( - RUNNER_SCRIPT.format( - self.module_name, - "\n".join([f'require("{i}")' for i in self._deps]), - ) - ) + f.write(runner_script) code, stdout, stderr = await self.command( "poetry run python runner.py", timeout=600 @@ -429,6 +309,18 @@ async def show_plugin_dependencies(self) -> None: self._log_output(f"插件 {self.project_link} 依赖获取失败。") self._std_output(stdout, stderr) + async def get_python_version(self): + """获取 Python 版本""" + if self._test_dir.exists(): + code, stdout, stderr = await self.command("poetry run python --version") + if code: + version = stdout.strip() + if version.startswith("Python "): + self._test_python_version = version.removeprefix("Python ") + else: + self._log_output("Python 版本获取失败。") + self._std_output(stdout, stderr) + @property def plugin_list(self) -> dict[str, str]: """获取插件列表""" @@ -461,10 +353,7 @@ def _get_deps(self, requirements: dict[str, str]) -> list[str]: def _get_test_env(self, requirements: dict[str, str]) -> list[str]: """获取测试环境""" - # python 版本 - envs = [ - f"python=={sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}" - ] + envs = [] # 特定插件依赖 # 当前仅需记录 nonebot2 和 pydantic 的版本 if "nonebot2" in requirements: @@ -472,23 +361,3 @@ def _get_test_env(self, requirements: dict[str, str]) -> list[str]: if "pydantic" in requirements: envs.append(f"pydantic=={requirements['pydantic']}") return envs - - -def main(): - """根据传入的环境变量进行测试 - - PROJECT_LINK 为插件的项目名 - MODULE_NAME 为插件的模块名 - PLUGIN_CONFIG 即为该插件的配置 - """ - project_link = os.environ.get("PROJECT_LINK", "") - module_name = os.environ.get("MODULE_NAME", "") - plugin_config = os.environ.get("PLUGIN_CONFIG", None) - - plugin = PluginTest(project_link, module_name, plugin_config) - - asyncio.run(plugin.run()) - - -if __name__ == "__main__": - main() diff --git a/src/providers/docker_test/render.py b/src/providers/docker_test/render.py new file mode 100644 index 00000000..30ab7f67 --- /dev/null +++ b/src/providers/docker_test/render.py @@ -0,0 +1,28 @@ +from pathlib import Path + +import jinja2 + +env = jinja2.Environment( + loader=jinja2.FileSystemLoader(Path(__file__).parent / "templates"), + enable_async=True, + lstrip_blocks=True, + trim_blocks=True, + keep_trailing_newline=True, +) + + +async def render_runner(module_name: str, deps: list[str]) -> str: + """生成 runner.py 文件内容""" + template = env.get_template("runner.py.jinja") + + return await template.render_async( + module_name=module_name, + deps=deps, + ) + + +async def render_fake(): + """生成 fake.py 文件内容""" + template = env.get_template("fake.py.jinja") + + return await template.render_async() diff --git a/src/providers/docker_test/templates/fake.py.jinja b/src/providers/docker_test/templates/fake.py.jinja new file mode 100644 index 00000000..8ebe6393 --- /dev/null +++ b/src/providers/docker_test/templates/fake.py.jinja @@ -0,0 +1,73 @@ +from typing import Optional, Union + +from nonebot import logger +from nonebot.drivers import ( + ASGIMixin, + HTTPClientMixin, + HTTPClientSession, + HTTPVersion, + Request, + Response, + WebSocketClientMixin, +) +from nonebot.drivers import Driver as BaseDriver +from nonebot.internal.driver.model import ( + CookieTypes, + HeaderTypes, + QueryTypes, +) +from typing_extensions import override + + +class Driver(BaseDriver, ASGIMixin, HTTPClientMixin, WebSocketClientMixin): + @property + @override + def type(self) -> str: + return "fake" + + @property + @override + def logger(self): + return logger + + @override + def run(self, *args, **kwargs): + super().run(*args, **kwargs) + + @property + @override + def server_app(self): + return None + + @property + @override + def asgi(self): + raise NotImplementedError + + @override + def setup_http_server(self, setup): + raise NotImplementedError + + @override + def setup_websocket_server(self, setup): + raise NotImplementedError + + @override + async def request(self, setup: Request) -> Response: + raise NotImplementedError + + @override + async def websocket(self, setup: Request) -> Response: + raise NotImplementedError + + @override + def get_session( + self, + params: QueryTypes = None, + headers: HeaderTypes = None, + cookies: CookieTypes = None, + version: Union[str, HTTPVersion] = HTTPVersion.H11, + timeout: Optional[float] = None, + proxy: Optional[str] = None, + ) -> HTTPClientSession: + raise NotImplementedError diff --git a/src/providers/docker_test/templates/runner.py.jinja b/src/providers/docker_test/templates/runner.py.jinja new file mode 100644 index 00000000..0f8adc7e --- /dev/null +++ b/src/providers/docker_test/templates/runner.py.jinja @@ -0,0 +1,41 @@ +import json + +from nonebot import init, load_plugin, logger, require +from pydantic import BaseModel + + +class SetEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, set): + return list(o) + return json.JSONEncoder.default(self, o) + + +init() +plugin = load_plugin("{{ module_name }}") + +if not plugin: + exit(1) +else: + if plugin.metadata: + metadata = { + "name": plugin.metadata.name, + "desc": plugin.metadata.description, + "usage": plugin.metadata.usage, + "type": plugin.metadata.type, + "homepage": plugin.metadata.homepage, + "supported_adapters": plugin.metadata.supported_adapters, + } + with open("metadata.json", "w", encoding="utf-8") as f: + try: + f.write(f"{json.dumps(metadata, cls=SetEncoder)}") + except Exception: + f.write("{}") + + if plugin.metadata.config and not issubclass(plugin.metadata.config, BaseModel): + logger.error("插件配置项不是 Pydantic BaseModel 的子类") + exit(1) + +{% for dep in deps %} +require("{{ dep }}") +{% endfor %} diff --git a/tests/plugins/github/resolve/test_resolve_pull_request.py b/tests/plugins/github/resolve/test_resolve_pull_request.py index 8119b3d6..fc5c4dd0 100644 --- a/tests/plugins/github/resolve/test_resolve_pull_request.py +++ b/tests/plugins/github/resolve/test_resolve_pull_request.py @@ -4,6 +4,7 @@ from nonebot.adapters.github import PullRequestClosed from nonebug import App from pytest_mock import MockerFixture +from respx import MockRouter from tests.plugins.github.event import get_mock_event from tests.plugins.github.resolve.utils import get_pr_labels @@ -19,9 +20,14 @@ async def test_resolve_pull_request( - app: App, mocker: MockerFixture, mock_installation: MagicMock + app: App, + mocker: MockerFixture, + mock_installation: MagicMock, + mocked_api: MockRouter, ) -> None: """测试能正确处理拉取请求关闭后其他拉取请求的冲突问题""" + from src.plugins.github.plugins.resolve import pr_close_matcher + mock_subprocess_run = mocker.patch("subprocess.run") mock_issue = MockIssue( @@ -39,7 +45,7 @@ async def test_resolve_pull_request( mock_publish_pull.title = "Bot: test" mock_publish_pull.draft = False mock_publish_pull.head.ref = "publish/issue100" - mock_publish_pull.labels = get_pr_labels(["Bot", "Publish"]) + mock_publish_pull.labels = get_pr_labels(["Publish", "Bot"]) mock_remove_issue = MockIssue( body=generate_issue_body_remove(type="Bot", key="name:https://v2.nonebot.dev"), number=101, @@ -107,6 +113,7 @@ async def test_resolve_pull_request( ), ) ctx.receive_event(bot, event) + ctx.should_pass_rule(pr_close_matcher) # 测试 git 命令 assert_subprocess_run_calls( @@ -148,3 +155,5 @@ async def test_resolve_pull_request( ["git", "push", "origin", "remove/issue101", "-f"], ], ) + + assert mocked_api["homepage"].called diff --git a/tests/providers/docker_test/test_docker_plugin_test.py b/tests/providers/docker_test/test_docker_plugin_test.py index 8f1215b3..8277bf64 100644 --- a/tests/providers/docker_test/test_docker_plugin_test.py +++ b/tests/providers/docker_test/test_docker_plugin_test.py @@ -42,13 +42,13 @@ async def test_docker_plugin_test(mocked_api: MockRouter, mocker: MockerFixture) assert not mocked_api["store_plugins"].called mocked_run.assert_called_once_with( - "ghcr.io/nonebot/nonetest:3.12-latest", + "ghcr.io/nonebot/nonetest:latest", environment=snapshot( { "PROJECT_LINK": "project_link", "MODULE_NAME": "module_name", "PLUGIN_CONFIG": "", - "PLUGINS_URL": "https://raw.githubusercontent.com/nonebot/registry/results/plugins.json", + "PYTHON_VERSION": "3.12", } ), detach=False, @@ -83,13 +83,13 @@ async def test_docker_plugin_test_exception( assert not mocked_api["store_plugins"].called mocked_run.assert_called_once_with( - "ghcr.io/nonebot/nonetest:3.12-latest", + "ghcr.io/nonebot/nonetest:latest", environment=snapshot( { "PROJECT_LINK": "project_link", "MODULE_NAME": "module_name", "PLUGIN_CONFIG": "", - "PLUGINS_URL": "https://raw.githubusercontent.com/nonebot/registry/results/plugins.json", + "PYTHON_VERSION": "3.12", } ), detach=False, @@ -149,13 +149,13 @@ async def test_docker_plugin_test_metadata_some_fields_empty( assert not mocked_api["store_plugins"].called mocked_run.assert_called_once_with( - "ghcr.io/nonebot/nonetest:3.12-latest", + "ghcr.io/nonebot/nonetest:latest", environment=snapshot( { "PROJECT_LINK": "project_link", "MODULE_NAME": "module_name", "PLUGIN_CONFIG": "", - "PLUGINS_URL": "https://raw.githubusercontent.com/nonebot/registry/results/plugins.json", + "PYTHON_VERSION": "3.12", } ), detach=False, @@ -215,13 +215,13 @@ async def test_docker_plugin_test_metadata_some_fields_invalid( assert not mocked_api["store_plugins"].called mocked_run.assert_called_once_with( - "ghcr.io/nonebot/nonetest:3.12-latest", + "ghcr.io/nonebot/nonetest:latest", environment=snapshot( { "PROJECT_LINK": "project_link", "MODULE_NAME": "module_name", "PLUGIN_CONFIG": "", - "PLUGINS_URL": "https://raw.githubusercontent.com/nonebot/registry/results/plugins.json", + "PYTHON_VERSION": "3.12", } ), detach=False, diff --git a/tests/providers/docker_test/test_plugin_test.py b/tests/providers/docker_test/test_plugin_test.py index 87ea4ae8..ad916d4c 100644 --- a/tests/providers/docker_test/test_plugin_test.py +++ b/tests/providers/docker_test/test_plugin_test.py @@ -8,14 +8,14 @@ async def test_plugin_test(mocker: MockerFixture, tmp_path: Path): from src.providers.docker_test.plugin_test import PluginTest - test = PluginTest("project_link", "module_name", "test=123") + test = PluginTest("3.12", "project_link", "module_name", "test=123") mocker.patch.object(test, "_test_dir", tmp_path / "plugin_test") def command_output(cmd: str, timeout: int = 300): if ( cmd - == r"""poetry init -n && sed -i "s/\^/~/g" pyproject.toml && poetry env info --ansi && poetry add project_link""" + == 'uv venv --python 3.12 && poetry init -n --python "~3.12" && poetry env info --ansi && poetry add project_link' ): # create_poetry_project return ( @@ -99,6 +99,8 @@ def command_output(cmd: str, timeout: int = 300): f, ) return (True, "", "") + if cmd == "poetry run python --version": + return (True, "Python 3.12.7", "") raise ValueError(f"Unknown command: {cmd}") diff --git a/tests/providers/docker_test/test_render_plugin_test.py b/tests/providers/docker_test/test_render_plugin_test.py new file mode 100644 index 00000000..5002bb3a --- /dev/null +++ b/tests/providers/docker_test/test_render_plugin_test.py @@ -0,0 +1,136 @@ +from inline_snapshot import snapshot +from nonebug import App + + +async def test_render_runner(app: App): + """测试生成 runner.py 文件内容""" + from src.providers.docker_test.render import render_runner + + comment = await render_runner("nonebot_plugin_treehelp", ["nonebot_plugin_alconna"]) + assert comment == snapshot( + """\ +import json + +from nonebot import init, load_plugin, logger, require +from pydantic import BaseModel + + +class SetEncoder(json.JSONEncoder): + def default(self, o): + if isinstance(o, set): + return list(o) + return json.JSONEncoder.default(self, o) + + +init() +plugin = load_plugin("nonebot_plugin_treehelp") + +if not plugin: + exit(1) +else: + if plugin.metadata: + metadata = { + "name": plugin.metadata.name, + "desc": plugin.metadata.description, + "usage": plugin.metadata.usage, + "type": plugin.metadata.type, + "homepage": plugin.metadata.homepage, + "supported_adapters": plugin.metadata.supported_adapters, + } + with open("metadata.json", "w", encoding="utf-8") as f: + try: + f.write(f"{json.dumps(metadata, cls=SetEncoder)}") + except Exception: + f.write("{}") + + if plugin.metadata.config and not issubclass(plugin.metadata.config, BaseModel): + logger.error("插件配置项不是 Pydantic BaseModel 的子类") + exit(1) + +require("nonebot_plugin_alconna") +""" + ) + + +async def test_render_fake(app: App): + """测试生成 fake.py 文件内容""" + from src.providers.docker_test.render import render_fake + + comment = await render_fake() + assert comment == snapshot( + """\ +from typing import Optional, Union + +from nonebot import logger +from nonebot.drivers import ( + ASGIMixin, + HTTPClientMixin, + HTTPClientSession, + HTTPVersion, + Request, + Response, + WebSocketClientMixin, +) +from nonebot.drivers import Driver as BaseDriver +from nonebot.internal.driver.model import ( + CookieTypes, + HeaderTypes, + QueryTypes, +) +from typing_extensions import override + + +class Driver(BaseDriver, ASGIMixin, HTTPClientMixin, WebSocketClientMixin): + @property + @override + def type(self) -> str: + return "fake" + + @property + @override + def logger(self): + return logger + + @override + def run(self, *args, **kwargs): + super().run(*args, **kwargs) + + @property + @override + def server_app(self): + return None + + @property + @override + def asgi(self): + raise NotImplementedError + + @override + def setup_http_server(self, setup): + raise NotImplementedError + + @override + def setup_websocket_server(self, setup): + raise NotImplementedError + + @override + async def request(self, setup: Request) -> Response: + raise NotImplementedError + + @override + async def websocket(self, setup: Request) -> Response: + raise NotImplementedError + + @override + def get_session( + self, + params: QueryTypes = None, + headers: HeaderTypes = None, + cookies: CookieTypes = None, + version: Union[str, HTTPVersion] = HTTPVersion.H11, + timeout: Optional[float] = None, + proxy: Optional[str] = None, + ) -> HTTPClientSession: + raise NotImplementedError +""" + ) diff --git a/tests/providers/store_test/output.json b/tests/providers/store_test/output.json index 388a6b13..7e387165 100644 --- a/tests/providers/store_test/output.json +++ b/tests/providers/store_test/output.json @@ -11,5 +11,6 @@ "load": true, "run": true, "version": "0.2.0", - "config": null + "config": null, + "test_env": "python==3.12.7" } diff --git a/tests/providers/store_test/output_failed.json b/tests/providers/store_test/output_failed.json index e17f15eb..f75d8620 100644 --- a/tests/providers/store_test/output_failed.json +++ b/tests/providers/store_test/output_failed.json @@ -4,5 +4,6 @@ "load": false, "run": true, "version": "0.3.9", - "config": null + "config": null, + "test_env": "python==3.12.7" } diff --git a/tests/providers/store_test/test_validate_plugin.py b/tests/providers/store_test/test_validate_plugin.py index 4bbd496b..79cec7eb 100644 --- a/tests/providers/store_test/test_validate_plugin.py +++ b/tests/providers/store_test/test_validate_plugin.py @@ -58,7 +58,7 @@ async def test_validate_plugin(mocked_api: MockRouter, mocker: MockerFixture) -> """, "metadata": { "name": "TREEHELP", - "desc": "订阅牛客/CF/AT平台的比赛信息", + "description": "订阅牛客/CF/AT平台的比赛信息", "usage": """\ /contest.list 获取所有/CF/牛客/AT平台的比赛信息 /contest.subscribe 订阅CF/牛客/AT平台的比赛信息 @@ -70,7 +70,7 @@ async def test_validate_plugin(mocked_api: MockRouter, mocker: MockerFixture) -> }, }, results={"validation": True, "load": True, "metadata": True}, - test_env={"unknown": True}, + test_env={"python==3.12.7": True}, version="0.2.0", ) ) @@ -154,7 +154,7 @@ async def test_validate_plugin_with_previous( """, "metadata": { "name": "TREEHELP", - "desc": "订阅牛客/CF/AT平台的比赛信息", + "description": "订阅牛客/CF/AT平台的比赛信息", "usage": """\ /contest.list 获取所有/CF/牛客/AT平台的比赛信息 /contest.subscribe 订阅CF/牛客/AT平台的比赛信息 @@ -166,7 +166,7 @@ async def test_validate_plugin_with_previous( }, }, results={"validation": True, "load": True, "metadata": True}, - test_env={"unknown": True}, + test_env={"python==3.12.7": True}, version="0.2.0", ) ) @@ -232,7 +232,7 @@ async def test_validate_plugin_skip_test( """, "metadata": { "name": "TREEHELP", - "desc": "订阅牛客/CF/AT平台的比赛信息", + "description": "订阅牛客/CF/AT平台的比赛信息", "usage": """\ /contest.list 获取所有/CF/牛客/AT平台的比赛信息 /contest.subscribe 订阅CF/牛客/AT平台的比赛信息 @@ -244,7 +244,7 @@ async def test_validate_plugin_skip_test( }, }, results={"validation": True, "load": True, "metadata": True}, - test_env={"unknown": True}, + test_env={"python==3.12.7": True}, version="0.2.0", ) ) @@ -329,7 +329,7 @@ async def test_validate_plugin_skip_test_plugin_test_failed( "metadata": None, }, results={"validation": True, "load": False, "metadata": False}, - test_env={"unknown": True}, + test_env={"python==3.12.7": True}, version="0.3.9", ) ) @@ -450,7 +450,7 @@ async def test_validate_plugin_failed_with_previous( "metadata": None, }, results={"validation": False, "load": False, "metadata": False}, - test_env={"unknown": True}, + test_env={"python==3.12.7": True}, version="0.3.9", ) )