From 5734735e2def5ab4349738ab2a316d1a998475bd Mon Sep 17 00:00:00 2001 From: Deepankar Mahapatro Date: Tue, 1 Jul 2025 18:37:25 +0530 Subject: [PATCH] test: add pyleak for task and event loop block detections (#8802) * test: add pyleak for task and event loop block detections * test: add pyleak for task and event loop block detections * ci: add env variables for verbose logging * chore: dummy sleep to shwocase error * chore: dummy sleep to showcase error * chore: remove dummy sleep --- .github/workflows/python_test.yml | 3 +++ pyproject.toml | 1 + .../components/helpers/test_parse_json_data.py | 4 +++- .../components/inputs/test_chat_input.py | 4 +++- .../components/inputs/test_text_input.py | 4 +++- .../components/prompts/test_prompt.py | 4 +++- src/backend/tests/integration/conftest.py | 4 ++++ .../integration/flows/test_basic_prompting.py | 3 ++- src/backend/tests/integration/utils.py | 9 +++++++++ uv.lock | 16 +++++++++++++++- 10 files changed, 46 insertions(+), 6 deletions(-) diff --git a/.github/workflows/python_test.yml b/.github/workflows/python_test.yml index 78c919793..88892d7b1 100644 --- a/.github/workflows/python_test.yml +++ b/.github/workflows/python_test.yml @@ -84,6 +84,9 @@ jobs: run: uv sync - name: Run integration tests run: make integration_tests_no_api_keys + env: + PYLEAK_LOG_LEVEL: debug # enable pyleak logging + DO_NOT_TRACK: true # disable telemetry reporting test-cli: name: Test CLI - Python ${{ matrix.python-version }} diff --git a/pyproject.toml b/pyproject.toml index a969e3ca5..be51ddeaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -175,6 +175,7 @@ dev = [ "faker>=37.0.0", "pytest-timeout>=2.3.1", "pyyaml>=6.0.2", + "pyleak>=0.1.12", ] [tool.uv.sources] diff --git a/src/backend/tests/integration/components/helpers/test_parse_json_data.py b/src/backend/tests/integration/components/helpers/test_parse_json_data.py index c73302bd7..2671c4a53 100644 --- a/src/backend/tests/integration/components/helpers/test_parse_json_data.py +++ b/src/backend/tests/integration/components/helpers/test_parse_json_data.py @@ -3,7 +3,9 @@ from langflow.components.processing.parse_json_data import ParseJSONDataComponen from langflow.schema import Data from tests.integration.components.mock_components import TextToData -from tests.integration.utils import ComponentInputHandle, run_single_component +from tests.integration.utils import ComponentInputHandle, pyleak_marker, run_single_component + +pytestmark = pyleak_marker() async def test_from_data(): diff --git a/src/backend/tests/integration/components/inputs/test_chat_input.py b/src/backend/tests/integration/components/inputs/test_chat_input.py index 84f17cefd..69fbaef8d 100644 --- a/src/backend/tests/integration/components/inputs/test_chat_input.py +++ b/src/backend/tests/integration/components/inputs/test_chat_input.py @@ -2,7 +2,9 @@ from langflow.components.input_output import ChatInput from langflow.memory import aget_messages from langflow.schema.message import Message -from tests.integration.utils import run_single_component +from tests.integration.utils import pyleak_marker, run_single_component + +pytestmark = pyleak_marker() async def test_default(): diff --git a/src/backend/tests/integration/components/inputs/test_text_input.py b/src/backend/tests/integration/components/inputs/test_text_input.py index e276559ee..362dab4ec 100644 --- a/src/backend/tests/integration/components/inputs/test_text_input.py +++ b/src/backend/tests/integration/components/inputs/test_text_input.py @@ -1,7 +1,9 @@ from langflow.components.input_output import TextInputComponent from langflow.schema.message import Message -from tests.integration.utils import run_single_component +from tests.integration.utils import pyleak_marker, run_single_component + +pytestmark = pyleak_marker() async def test_text_input(): diff --git a/src/backend/tests/integration/components/prompts/test_prompt.py b/src/backend/tests/integration/components/prompts/test_prompt.py index 75a57d637..eb58edf06 100644 --- a/src/backend/tests/integration/components/prompts/test_prompt.py +++ b/src/backend/tests/integration/components/prompts/test_prompt.py @@ -1,7 +1,9 @@ from langflow.components.processing import PromptComponent from langflow.schema.message import Message -from tests.integration.utils import run_single_component +from tests.integration.utils import pyleak_marker, run_single_component + +pytestmark = pyleak_marker() async def test(): diff --git a/src/backend/tests/integration/conftest.py b/src/backend/tests/integration/conftest.py index d8dcd30aa..685c50113 100644 --- a/src/backend/tests/integration/conftest.py +++ b/src/backend/tests/integration/conftest.py @@ -4,3 +4,7 @@ import pytest @pytest.fixture(autouse=True) def _start_app(client): pass + + +def pytest_configure(config): + config.addinivalue_line("markers", "no_leaks: detect asyncio task leaks, thread leaks, and event loop blocking") diff --git a/src/backend/tests/integration/flows/test_basic_prompting.py b/src/backend/tests/integration/flows/test_basic_prompting.py index 66d00cf39..cc225225c 100644 --- a/src/backend/tests/integration/flows/test_basic_prompting.py +++ b/src/backend/tests/integration/flows/test_basic_prompting.py @@ -3,9 +3,10 @@ from langflow.components.processing import PromptComponent from langflow.graph import Graph from langflow.schema.message import Message -from tests.integration.utils import run_flow +from tests.integration.utils import pyleak_marker, run_flow +@pyleak_marker() async def test_simple_no_llm(): graph = Graph() flow_input = graph.add_component(ChatInput()) diff --git a/src/backend/tests/integration/utils.py b/src/backend/tests/integration/utils.py index 53b803e31..b23bc23dc 100644 --- a/src/backend/tests/integration/utils.py +++ b/src/backend/tests/integration/utils.py @@ -3,6 +3,7 @@ import os import uuid from typing import Any +import pytest import requests from astrapy.admin import parse_api_endpoint from langflow.api.v1.schemas import InputValueRequest @@ -187,3 +188,11 @@ def build_component_instance_for_tests(version: str, module: str, file_name: str component = download_component_from_github(module, file_name, version) cc_class = eval_custom_component_code(component._code) return cc_class(**kwargs), component._code + + +def pyleak_marker(**extra_args): + default_args = { + "enable_task_creation_tracking": True, # log task creation stacks + "thread_name_filter": r"^(?!asyncio_\d+$).*", # exclude `asyncio_{num}` threads + } + return pytest.mark.no_leaks(**default_args, **extra_args) diff --git a/uv.lock b/uv.lock index 66b3983de..c7634e0a8 100644 --- a/uv.lock +++ b/uv.lock @@ -2309,7 +2309,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -4980,6 +4980,7 @@ dev = [ { name = "pandas-stubs" }, { name = "pre-commit" }, { name = "pydantic-ai" }, + { name = "pyleak" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, @@ -5155,6 +5156,7 @@ dev = [ { name = "pandas-stubs", specifier = ">=2.1.4.231227" }, { name = "pre-commit", specifier = ">=3.7.0" }, { name = "pydantic-ai", specifier = ">=0.0.19" }, + { name = "pyleak", specifier = ">=0.1.12" }, { name = "pytest", specifier = ">=8.2.0" }, { name = "pytest-asyncio", specifier = ">=0.23.0" }, { name = "pytest-cov", specifier = ">=5.0.0" }, @@ -8474,6 +8476,18 @@ version = "2.10" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/5d/ab/34ec41718af73c00119d0351b7a2531d2ebddb51833a36448fc7b862be60/pylatexenc-2.10.tar.gz", hash = "sha256:3dd8fd84eb46dc30bee1e23eaab8d8fb5a7f507347b23e5f38ad9675c84f40d3", size = 162597, upload-time = "2021-04-06T07:56:07.854Z" } +[[package]] +name = "pyleak" +version = "0.1.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/55/bdc246baf6870185555d6ebcb35a7361f9bb95ba3fd0ff295d8fc4ff9e6e/pyleak-0.1.12.tar.gz", hash = "sha256:6af479b2dd5d4c71ff2b33ba5eeccda04b2d248f884801cf414fc84777a636de", size = 104742, upload-time = "2025-07-01T12:04:44.708Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/08/e8bc7e2f63deca307575bcf98d112fe75f80ecd716d3dcc69330ad096fff/pyleak-0.1.12-py3-none-any.whl", hash = "sha256:17a1c8b11d89fdcee79f0a479378bd6de23de323754a81efe8d8d9550ac12863", size = 23762, upload-time = "2025-07-01T12:04:43.494Z" }, +] + [[package]] name = "pylint" version = "3.3.7"