From 8aeef36e2d16c9b9ba41088aee937d0348b5cbec Mon Sep 17 00:00:00 2001 From: yihong Date: Thu, 29 Jan 2026 18:17:40 +0800 Subject: [PATCH] feat: use xdist to make make test faster (#30824) Signed-off-by: yihong0618 --- .github/workflows/api-tests.yml | 1 + Makefile | 2 +- api/pyproject.toml | 1 + api/tests/unit_tests/conftest.py | 17 +++++++++++++ .../console/app/test_app_response_models.py | 7 ++++++ api/uv.lock | 24 +++++++++++++++++++ dev/pytest/pytest_unit_tests.sh | 10 ++++++-- 7 files changed, 59 insertions(+), 3 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 190e00d9fe..52e3272f99 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -72,6 +72,7 @@ jobs: OPENDAL_FS_ROOT: /tmp/dify-storage run: | uv run --project api pytest \ + -n auto \ --timeout "${PYTEST_TIMEOUT:-180}" \ api/tests/integration_tests/workflow \ api/tests/integration_tests/tools \ diff --git a/Makefile b/Makefile index 20cede9a5e..984e8676ee 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,7 @@ test: echo "Target: $(TARGET_TESTS)"; \ uv run --project api --dev pytest $(TARGET_TESTS); \ else \ - uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \ + PYTEST_XDIST_ARGS="-n auto" uv run --project api --dev dev/pytest/pytest_unit_tests.sh; \ fi @echo "✅ Tests complete" diff --git a/api/pyproject.toml b/api/pyproject.toml index 575c1434c5..af2dba6fac 100644 --- a/api/pyproject.toml +++ b/api/pyproject.toml @@ -175,6 +175,7 @@ dev = [ # "locust>=2.40.4", # Temporarily removed due to compatibility issues. Uncomment when resolved. "sseclient-py>=1.8.0", "pytest-timeout>=2.4.0", + "pytest-xdist>=3.8.0", ] ############################################################ diff --git a/api/tests/unit_tests/conftest.py b/api/tests/unit_tests/conftest.py index c5e1576186..e3c1a617f7 100644 --- a/api/tests/unit_tests/conftest.py +++ b/api/tests/unit_tests/conftest.py @@ -3,6 +3,7 @@ from unittest.mock import MagicMock, patch import pytest from flask import Flask +from sqlalchemy import create_engine # Getting the absolute path of the current file's directory ABS_PATH = os.path.dirname(os.path.abspath(__file__)) @@ -36,6 +37,7 @@ import sys sys.path.insert(0, PROJECT_DIR) +from core.db.session_factory import configure_session_factory, session_factory from extensions import ext_redis @@ -102,3 +104,18 @@ def reset_secret_key(): yield finally: dify_config.SECRET_KEY = original + + +@pytest.fixture(scope="session") +def _unit_test_engine(): + engine = create_engine("sqlite:///:memory:") + yield engine + engine.dispose() + + +@pytest.fixture(autouse=True) +def _configure_session_factory(_unit_test_engine): + try: + session_factory.get_session_maker() + except RuntimeError: + configure_session_factory(_unit_test_engine, expire_on_commit=False) diff --git a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py index 40eb59a8f4..c557605916 100644 --- a/api/tests/unit_tests/controllers/console/app/test_app_response_models.py +++ b/api/tests/unit_tests/controllers/console/app/test_app_response_models.py @@ -31,6 +31,13 @@ def _load_app_module(): def schema_model(self, name, schema): self.models[name] = schema + return schema + + def model(self, name, model_dict=None, **kwargs): + """Register a model with the namespace (flask-restx compatibility).""" + if model_dict is not None: + self.models[name] = model_dict + return model_dict def _decorator(self, obj): return obj diff --git a/api/uv.lock b/api/uv.lock index 7808c16a8c..a3ad292168 100644 --- a/api/uv.lock +++ b/api/uv.lock @@ -1479,6 +1479,7 @@ dev = [ { name = "pytest-env" }, { name = "pytest-mock" }, { name = "pytest-timeout" }, + { name = "pytest-xdist" }, { name = "ruff" }, { name = "scipy-stubs" }, { name = "sseclient-py" }, @@ -1678,6 +1679,7 @@ dev = [ { name = "pytest-env", specifier = "~=1.1.3" }, { name = "pytest-mock", specifier = "~=3.14.0" }, { name = "pytest-timeout", specifier = ">=2.4.0" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "ruff", specifier = "~=0.14.0" }, { name = "scipy-stubs", specifier = ">=1.15.3.0" }, { name = "sseclient-py", specifier = ">=1.8.0" }, @@ -1896,6 +1898,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/19/d8/2a1c638d9e0aa7e269269a1a1bf423ddd94267f1a01bbe3ad03432b67dd4/eval_type_backport-0.3.0-py3-none-any.whl", hash = "sha256:975a10a0fe333c8b6260d7fdb637698c9a16c3a9e3b6eb943fee6a6f67a37fe8", size = 6061, upload-time = "2025-11-13T20:56:49.499Z" }, ] +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + [[package]] name = "faker" version = "38.2.0" @@ -5141,6 +5152,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fa/b6/3127540ecdf1464a00e5a01ee60a1b09175f6913f0644ac748494d9c4b21/pytest_timeout-2.4.0-py3-none-any.whl", hash = "sha256:c42667e5cdadb151aeb5b26d114aff6bdf5a907f176a007a30b940d3d865b5c2", size = 14382, upload-time = "2025-05-05T19:44:33.502Z" }, ] +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "python-calamine" version = "0.5.4" diff --git a/dev/pytest/pytest_unit_tests.sh b/dev/pytest/pytest_unit_tests.sh index 496cb40952..7c39a48bf4 100755 --- a/dev/pytest/pytest_unit_tests.sh +++ b/dev/pytest/pytest_unit_tests.sh @@ -5,6 +5,12 @@ SCRIPT_DIR="$(dirname "$(realpath "$0")")" cd "$SCRIPT_DIR/../.." PYTEST_TIMEOUT="${PYTEST_TIMEOUT:-20}" +PYTEST_XDIST_ARGS="${PYTEST_XDIST_ARGS:--n auto}" -# libs -pytest --timeout "${PYTEST_TIMEOUT}" api/tests/unit_tests +# Run most tests in parallel (excluding controllers which have import conflicts with xdist) +# Controller tests have module-level side effects (Flask route registration) that cause +# race conditions when imported concurrently by multiple pytest-xdist workers. +pytest --timeout "${PYTEST_TIMEOUT}" ${PYTEST_XDIST_ARGS} api/tests/unit_tests --ignore=api/tests/unit_tests/controllers + +# Run controller tests sequentially to avoid import race conditions +pytest --timeout "${PYTEST_TIMEOUT}" api/tests/unit_tests/controllers