mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-19 19:11:13 +00:00
test: Add integration tests for execution flows in native Python runner (#19198)
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import pytest_asyncio
|
||||
from src.message_types.broker import Items
|
||||
from src.message_serde import NODE_MODE_MAP
|
||||
|
||||
from tests.fixtures.local_task_broker import LocalTaskBroker
|
||||
from tests.fixtures.task_runner_manager import TaskRunnerManager
|
||||
from tests.fixtures.test_constants import (
|
||||
TASK_RESPONSE_WAIT,
|
||||
)
|
||||
|
||||
NODE_MODE_TO_BROKER_STYLE = {v: k for k, v in NODE_MODE_MAP.items()}
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def manager():
|
||||
manager = TaskRunnerManager()
|
||||
await manager.start()
|
||||
yield manager
|
||||
await manager.stop()
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def broker():
|
||||
broker = LocalTaskBroker()
|
||||
await broker.start()
|
||||
yield broker
|
||||
await broker.stop()
|
||||
|
||||
|
||||
def create_task_settings(
|
||||
code: str,
|
||||
node_mode: str,
|
||||
items: Items | None = None,
|
||||
continue_on_fail: bool = False,
|
||||
can_log: bool = False,
|
||||
):
|
||||
return {
|
||||
"code": code,
|
||||
"nodeMode": NODE_MODE_TO_BROKER_STYLE[node_mode],
|
||||
"items": items if items is not None else [],
|
||||
"continueOnFail": continue_on_fail,
|
||||
"canLog": can_log,
|
||||
}
|
||||
|
||||
|
||||
async def wait_for_task_done(broker, task_id: str, timeout: float = TASK_RESPONSE_WAIT):
|
||||
return await broker.wait_for_msg(
|
||||
"runner:taskdone",
|
||||
timeout=timeout,
|
||||
predicate=lambda msg: msg.get("taskId") == task_id,
|
||||
)
|
||||
|
||||
|
||||
async def wait_for_task_error(
|
||||
broker, task_id: str, timeout: float = TASK_RESPONSE_WAIT
|
||||
):
|
||||
return await broker.wait_for_msg(
|
||||
"runner:taskerror",
|
||||
timeout=timeout,
|
||||
predicate=lambda msg: msg.get("taskId") == task_id,
|
||||
)
|
||||
|
||||
|
||||
def get_browser_console_msgs(broker: LocalTaskBroker, task_id: str) -> list[list[str]]:
|
||||
console_msgs = []
|
||||
for msg in broker.get_task_rpc_messages(task_id):
|
||||
if msg.get("method") == "logNodeOutput":
|
||||
console_msgs.append(msg.get("params", []))
|
||||
return console_msgs
|
||||
@@ -0,0 +1,193 @@
|
||||
import asyncio
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
from src.nanoid import nanoid
|
||||
|
||||
from tests.integration.conftest import (
|
||||
create_task_settings,
|
||||
wait_for_task_done,
|
||||
wait_for_task_error,
|
||||
)
|
||||
from tests.fixtures.test_constants import TASK_TIMEOUT
|
||||
|
||||
|
||||
# ========== all_items mode ==========
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_all_items_with_success(broker, manager):
|
||||
task_id = nanoid()
|
||||
items = [
|
||||
{"json": {"name": "Alice", "age": 30}},
|
||||
{"json": {"name": "Bob", "age": 16}},
|
||||
{"json": {"name": "Charlie", "age": 35}},
|
||||
]
|
||||
code = textwrap.dedent("""
|
||||
result = []
|
||||
for item in _items:
|
||||
person = item['json']
|
||||
result.append({
|
||||
'name': person['name'],
|
||||
'age': person['age'],
|
||||
'adult': person['age'] >= 18
|
||||
})
|
||||
return result
|
||||
""")
|
||||
task_settings = create_task_settings(code=code, node_mode="all_items", items=items)
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
result = await wait_for_task_done(broker, task_id)
|
||||
|
||||
assert result["data"]["result"] == [
|
||||
{"name": "Alice", "age": 30, "adult": True},
|
||||
{"name": "Bob", "age": 16, "adult": False},
|
||||
{"name": "Charlie", "age": 35, "adult": True},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_all_items_with_error(broker, manager):
|
||||
task_id = nanoid()
|
||||
code = "raise ValueError('Intentional error')"
|
||||
task_settings = create_task_settings(code=code, node_mode="all_items")
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
error_msg = await wait_for_task_error(broker, task_id)
|
||||
|
||||
assert error_msg["taskId"] == task_id
|
||||
assert "Intentional error" in str(error_msg["error"]["message"])
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_all_items_with_continue_on_fail(broker, manager):
|
||||
task_id = nanoid()
|
||||
code = "raise ValueError('Intentional error')"
|
||||
task_settings = create_task_settings(
|
||||
code=code, node_mode="all_items", continue_on_fail=True
|
||||
)
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
done_msg = await wait_for_task_done(broker, task_id)
|
||||
|
||||
assert len(done_msg["data"]["result"]) == 1
|
||||
assert "error" in done_msg["data"]["result"][0]["json"]
|
||||
assert "Intentional error" in str(done_msg["data"]["result"][0]["json"]["error"])
|
||||
|
||||
|
||||
# ========== per_item mode ==========
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_per_item_with_success(broker, manager):
|
||||
task_id = nanoid()
|
||||
items = [
|
||||
{"json": {"value": 10}},
|
||||
{"json": {"value": 20}},
|
||||
{"json": {"value": 30}},
|
||||
]
|
||||
code = "return {'doubled': _item['json']['value'] * 2}"
|
||||
task_settings = create_task_settings(code=code, node_mode="per_item", items=items)
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
done_msg = await wait_for_task_done(broker, task_id)
|
||||
|
||||
assert done_msg["taskId"] == task_id
|
||||
assert done_msg["data"]["result"] == [
|
||||
{"doubled": 20, "pairedItem": {"item": 0}},
|
||||
{"doubled": 40, "pairedItem": {"item": 1}},
|
||||
{"doubled": 60, "pairedItem": {"item": 2}},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_per_item_with_filtering(broker, manager):
|
||||
task_id = nanoid()
|
||||
items = [
|
||||
{"json": {"value": 5}},
|
||||
{"json": {"value": 15}},
|
||||
{"json": {"value": 25}},
|
||||
{"json": {"value": 8}},
|
||||
]
|
||||
code = textwrap.dedent("""
|
||||
value = _item['json']['value']
|
||||
if value > 10:
|
||||
return {'value': value, 'passed': True}
|
||||
else:
|
||||
return None # Filter out this item
|
||||
""")
|
||||
task_settings = create_task_settings(code=code, node_mode="per_item", items=items)
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
result = await wait_for_task_done(broker, task_id)
|
||||
|
||||
assert result["data"]["result"] == [
|
||||
{"value": 15, "passed": True, "pairedItem": {"item": 1}},
|
||||
{"value": 25, "passed": True, "pairedItem": {"item": 2}},
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_per_item_with_continue_on_fail(broker, manager):
|
||||
task_id = nanoid()
|
||||
items = [
|
||||
{"json": {"value": 10}},
|
||||
{"json": {"value": 0}}, # Will cause division by zero
|
||||
{"json": {"value": 20}},
|
||||
]
|
||||
code = "return {'result': 100 / _item['json']['value']}"
|
||||
task_settings = create_task_settings(
|
||||
code=code,
|
||||
node_mode="per_item",
|
||||
items=items,
|
||||
continue_on_fail=True,
|
||||
)
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
done_msg = await wait_for_task_done(broker, task_id)
|
||||
|
||||
assert len(done_msg["data"]["result"]) == 1
|
||||
assert "error" in done_msg["data"]["result"][0]["json"]
|
||||
assert "division by zero" in done_msg["data"]["result"][0]["json"]["error"]
|
||||
|
||||
|
||||
# ========== edge cases ===========
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_during_execution(broker, manager):
|
||||
task_id = nanoid()
|
||||
code = textwrap.dedent("""
|
||||
import time
|
||||
for i in range(20):
|
||||
time.sleep(0.05)
|
||||
if i == 10:
|
||||
# Should be cancelled around here
|
||||
pass
|
||||
return [{"completed": "should not reach here"}]
|
||||
""")
|
||||
task_settings = create_task_settings(code=code, node_mode="all_items")
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
await asyncio.sleep(0.3)
|
||||
await broker.cancel_task(task_id, reason="Cancelled during execution")
|
||||
|
||||
error_msg = await wait_for_task_error(broker, task_id)
|
||||
|
||||
assert error_msg["taskId"] == task_id
|
||||
assert "error" in error_msg
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_timeout_during_execution(broker, manager):
|
||||
task_id = nanoid()
|
||||
code = textwrap.dedent("""
|
||||
while True:
|
||||
pass
|
||||
""")
|
||||
task_settings = create_task_settings(code=code, node_mode="all_items")
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
error_msg = await wait_for_task_error(broker, task_id, timeout=TASK_TIMEOUT + 0.5)
|
||||
|
||||
assert error_msg["taskId"] == task_id
|
||||
assert "timed out" in error_msg["error"]["message"].lower()
|
||||
@@ -0,0 +1,40 @@
|
||||
import asyncio
|
||||
import textwrap
|
||||
|
||||
import aiohttp
|
||||
import pytest
|
||||
from src.nanoid import nanoid
|
||||
|
||||
from tests.integration.conftest import create_task_settings
|
||||
from tests.fixtures.test_constants import HEALTH_CHECK_URL
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_server_responds(broker, manager):
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for _ in range(10):
|
||||
try:
|
||||
response = await session.get(HEALTH_CHECK_URL)
|
||||
if response.status == 200:
|
||||
assert await response.text() == "OK"
|
||||
return
|
||||
except aiohttp.ClientConnectionError:
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check_server_ressponds_mid_execution(broker, manager):
|
||||
task_id = nanoid()
|
||||
code = textwrap.dedent("""
|
||||
for _ in range(10_000_000):
|
||||
pass
|
||||
return [{"result": "completed"}]
|
||||
""")
|
||||
task_settings = create_task_settings(code=code, node_mode="all_items")
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
response = await session.get(HEALTH_CHECK_URL)
|
||||
assert response.status == 200
|
||||
assert await response.text() == "OK"
|
||||
116
packages/@n8n/task-runner-python/tests/integration/test_rpc.py
Normal file
116
packages/@n8n/task-runner-python/tests/integration/test_rpc.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
from src.nanoid import nanoid
|
||||
|
||||
from tests.integration.conftest import (
|
||||
create_task_settings,
|
||||
get_browser_console_msgs,
|
||||
wait_for_task_done,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_print_basic_types(broker, manager):
|
||||
task_id = nanoid()
|
||||
code = textwrap.dedent("""
|
||||
print("Hello, World!")
|
||||
print(42)
|
||||
print(3.14)
|
||||
print(True)
|
||||
print(None)
|
||||
print("Multiple", "args", 123, False)
|
||||
return [{"printed": "ok"}]
|
||||
""")
|
||||
task_settings = create_task_settings(code=code, node_mode="all_items", can_log=True)
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
done_msg = await wait_for_task_done(broker, task_id, timeout=5.0)
|
||||
|
||||
assert done_msg["taskId"] == task_id
|
||||
assert done_msg["data"]["result"] == [{"printed": "ok"}]
|
||||
|
||||
msgs = get_browser_console_msgs(broker, task_id)
|
||||
|
||||
assert len(msgs) > 0, "Should have captured console messages"
|
||||
|
||||
all_args = []
|
||||
for msg in msgs:
|
||||
all_args.extend(msg)
|
||||
|
||||
expected = [
|
||||
"'Hello, World!'",
|
||||
"42",
|
||||
"3.14",
|
||||
"True",
|
||||
"None",
|
||||
"'Multiple'",
|
||||
"'args'",
|
||||
"123",
|
||||
"False",
|
||||
]
|
||||
for item in expected:
|
||||
assert item in all_args, f"Expected '{item}' not found in console output"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_print_complex_types(broker, manager):
|
||||
task_id = nanoid()
|
||||
code = textwrap.dedent("""
|
||||
print({"name": "John", "age": 30, "active": True})
|
||||
print([1, 2, "three", {"four": 4}])
|
||||
print({"users": [{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]})
|
||||
return [{"result": "success"}]
|
||||
""")
|
||||
task_settings = create_task_settings(code=code, node_mode="all_items", can_log=True)
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
result_msg = await wait_for_task_done(broker, task_id, timeout=5.0)
|
||||
|
||||
assert result_msg["data"]["result"] == [{"result": "success"}]
|
||||
|
||||
msgs = get_browser_console_msgs(broker, task_id)
|
||||
assert len(msgs) > 0, "Should have captured console messages"
|
||||
|
||||
all_output = " ".join(["".join(msg) for msg in msgs]).replace(" ", "")
|
||||
expected = [
|
||||
'{"name":"John","age":30,"active":true}',
|
||||
'[1,2,"three",{"four":4}]',
|
||||
]
|
||||
for item in expected:
|
||||
assert item in all_output, f"Expected '{item}' not found in console output"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_print_edge_cases(broker, manager):
|
||||
task_id = nanoid()
|
||||
code = textwrap.dedent("""
|
||||
print("Hello 世界 🌍")
|
||||
print({"emoji": "🚀", "chinese": "你好", "arabic": "مرحبا"})
|
||||
print("Line\\nbreak")
|
||||
print("Tab\\tseparated")
|
||||
print('Quote "test" here')
|
||||
print()
|
||||
print("")
|
||||
print(" ")
|
||||
print([])
|
||||
print({})
|
||||
print(())
|
||||
print("x" * 1_000)
|
||||
return [{"test": "complete"}]
|
||||
""")
|
||||
task_settings = create_task_settings(code=code, node_mode="all_items", can_log=True)
|
||||
|
||||
await broker.send_task(task_id=task_id, task_settings=task_settings)
|
||||
|
||||
done_msg = await wait_for_task_done(broker, task_id, timeout=5.0)
|
||||
|
||||
assert done_msg["data"]["result"] == [{"test": "complete"}]
|
||||
|
||||
msgs = get_browser_console_msgs(broker, task_id)
|
||||
assert len(msgs) > 0, "Should have captured console messages"
|
||||
|
||||
all_output = " ".join(["".join(msg) for msg in msgs])
|
||||
expected = ["世界", "🌍", "🚀", "你好", "[]", "{}"]
|
||||
for item in expected:
|
||||
assert item in all_output, f"Expected '{item}' not found in console output"
|
||||
Reference in New Issue
Block a user