mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-27 20:57:13 +00:00
test(unit): improve taskmgr tests to test real classes
U-004 improved: Tests now import and test actual classes: - TaskContext: new(), trace(), to_dict(), placeholder() - TaskWrapper: task creation, context, exception/result capture, cancel, to_dict - AsyncTaskManager: create_task, create_user_task, cancel_task, cancel_by_scope - Task pruning behavior Uses pre-mocking technique: - Mock langbot.pkg.core.app before import (breaks circular chain) - Mock langbot.pkg.core.entities with proper Enum All 24 tests now test real class behavior, not patterns. taskmgr.py coverage should improve significantly. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -8,7 +8,7 @@ Tests cover async task lifecycle management:
|
|||||||
- Task cancellation
|
- Task cancellation
|
||||||
- Multiple task isolation
|
- Multiple task isolation
|
||||||
|
|
||||||
Uses module mocking to break circular import chain.
|
Uses module pre-mocking to break circular import chain.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -16,314 +16,509 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
import asyncio
|
import asyncio
|
||||||
import sys
|
import sys
|
||||||
|
import enum
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
|
|
||||||
# Break the circular import chain before importing taskmgr:
|
# Pre-mock app module BEFORE importing taskmgr to break circular chain:
|
||||||
# taskmgr → app → http_controller → groups/knowledge/migration → taskmgr (partial)
|
# taskmgr → app → http_controller → groups/knowledge/migration → taskmgr (partial)
|
||||||
_mock_app = MagicMock()
|
class FakeMinimalApp:
|
||||||
_mock_app.AsyncTaskManager = object
|
"""Minimal app that only provides event_loop."""
|
||||||
_mock_app.TaskWrapper = object
|
|
||||||
_mock_app.TaskContext = object
|
def __init__(self, event_loop):
|
||||||
sys.modules.setdefault('langbot.pkg.core.app', _mock_app)
|
self.event_loop = event_loop
|
||||||
|
self.instance_config = MagicMock()
|
||||||
|
self.instance_config.data = {}
|
||||||
|
|
||||||
|
# Pre-register mock app module
|
||||||
|
_mock_app_module = MagicMock()
|
||||||
|
_mock_app_module.Application = FakeMinimalApp
|
||||||
|
sys.modules['langbot.pkg.core.app'] = _mock_app_module
|
||||||
|
|
||||||
|
# Pre-register mock entities module - use proper Enum
|
||||||
|
class LifecycleControlScope(enum.Enum):
|
||||||
|
APPLICATION = 'application'
|
||||||
|
PLATFORM = 'platform'
|
||||||
|
PLUGIN = 'plugin'
|
||||||
|
PROVIDER = 'provider'
|
||||||
|
|
||||||
|
_mock_entities_module = MagicMock()
|
||||||
|
_mock_entities_module.LifecycleControlScope = LifecycleControlScope
|
||||||
|
sys.modules['langbot.pkg.core.entities'] = _mock_entities_module
|
||||||
|
|
||||||
|
|
||||||
def get_taskmgr():
|
def get_taskmgr():
|
||||||
"""Import taskmgr with circular import workaround."""
|
"""Import taskmgr after pre-mocking."""
|
||||||
return import_module('langbot.pkg.core.taskmgr')
|
return import_module('langbot.pkg.core.taskmgr')
|
||||||
|
|
||||||
|
|
||||||
def get_entities():
|
def get_entities():
|
||||||
"""Import entities."""
|
"""Get pre-registered mock entities module."""
|
||||||
return import_module('langbot.pkg.core.entities')
|
return sys.modules['langbot.pkg.core.entities']
|
||||||
|
|
||||||
|
|
||||||
class TestTaskContextBasic:
|
class TestTaskContextReal:
|
||||||
"""Basic tests for TaskContext behavior."""
|
"""Tests for real TaskContext class (no circular import)."""
|
||||||
|
|
||||||
def test_task_context_trace_format(self):
|
@pytest.mark.asyncio
|
||||||
"""TaskContext trace should format log entries."""
|
async def test_task_context_new(self):
|
||||||
# Import TaskContext class definition directly from source
|
"""TaskContext.new() creates instance."""
|
||||||
import datetime
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
# Simulate TaskContext behavior without importing the module
|
ctx = taskmgr.TaskContext.new()
|
||||||
# (since the circular import breaks the class definition)
|
|
||||||
|
|
||||||
# We can test the logic inline
|
assert ctx.current_action == 'default'
|
||||||
log = ''
|
assert ctx.log == ''
|
||||||
current_action = 'default'
|
assert ctx.metadata == {}
|
||||||
|
|
||||||
def trace(msg: str, action: str = None):
|
@pytest.mark.asyncio
|
||||||
nonlocal current_action, log
|
async def test_task_context_trace(self):
|
||||||
if action is not None:
|
"""TaskContext.trace adds formatted log."""
|
||||||
current_action = action
|
taskmgr = get_taskmgr()
|
||||||
log += f'{datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} | {current_action} | {msg}\n'
|
|
||||||
|
|
||||||
trace('test message', action='test_action')
|
ctx = taskmgr.TaskContext.new()
|
||||||
|
ctx.trace('test message', action='test_action')
|
||||||
|
|
||||||
assert current_action == 'test_action'
|
assert ctx.current_action == 'test_action'
|
||||||
assert 'test message' in log
|
assert 'test message' in ctx.log
|
||||||
assert 'test_action' in log
|
assert 'test_action' in ctx.log
|
||||||
|
# Contains timestamp format
|
||||||
|
assert '|' in ctx.log
|
||||||
|
|
||||||
def test_task_context_to_dict_format(self):
|
@pytest.mark.asyncio
|
||||||
"""TaskContext to_dict should include expected fields."""
|
async def test_task_context_multiple_traces(self):
|
||||||
# Expected fields based on source code
|
"""TaskContext accumulates multiple traces."""
|
||||||
expected_keys = ['current_action', 'log', 'metadata']
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
# Simulate
|
ctx = taskmgr.TaskContext.new()
|
||||||
result = {
|
ctx.trace('first')
|
||||||
'current_action': 'default',
|
ctx.trace('second')
|
||||||
'log': 'test log',
|
|
||||||
'metadata': {},
|
|
||||||
}
|
|
||||||
|
|
||||||
for key in expected_keys:
|
assert 'first' in ctx.log
|
||||||
assert key in result
|
assert 'second' in ctx.log
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_task_context_to_dict(self):
|
||||||
|
"""TaskContext.to_dict returns all fields."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
ctx = taskmgr.TaskContext.new()
|
||||||
|
ctx.trace('log entry')
|
||||||
|
|
||||||
|
result = ctx.to_dict()
|
||||||
|
|
||||||
|
assert 'current_action' in result
|
||||||
|
assert 'log' in result
|
||||||
|
assert 'metadata' in result
|
||||||
|
assert result['log'] == ctx.log
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_task_context_set_current_action(self):
|
||||||
|
"""set_current_action updates action."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
ctx = taskmgr.TaskContext.new()
|
||||||
|
ctx.set_current_action('new_action')
|
||||||
|
|
||||||
|
assert ctx.current_action == 'new_action'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_task_context_metadata(self):
|
||||||
|
"""TaskContext metadata can be set."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
ctx = taskmgr.TaskContext.new()
|
||||||
|
ctx.metadata['key'] = 'value'
|
||||||
|
|
||||||
|
assert ctx.metadata['key'] == 'value'
|
||||||
|
assert ctx.to_dict()['metadata']['key'] == 'value'
|
||||||
|
|
||||||
|
def test_task_context_placeholder_singleton(self):
|
||||||
|
"""placeholder returns same instance."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
ctx1 = taskmgr.TaskContext.placeholder()
|
||||||
|
ctx2 = taskmgr.TaskContext.placeholder()
|
||||||
|
|
||||||
|
assert ctx1 is ctx2
|
||||||
|
|
||||||
|
|
||||||
class TestTaskWrapperBehavior:
|
class TestTaskWrapperReal:
|
||||||
"""Tests for TaskWrapper behavior patterns."""
|
"""Tests for real TaskWrapper class."""
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_task_wrapper_creates_task(self):
|
async def test_task_wrapper_creates_task(self):
|
||||||
"""TaskWrapper should create asyncio.Task."""
|
"""TaskWrapper creates and wraps asyncio.Task."""
|
||||||
# Test the pattern without importing
|
taskmgr = get_taskmgr()
|
||||||
async def coro():
|
|
||||||
return 42
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
task = loop.create_task(coro())
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
assert isinstance(task, asyncio.Task)
|
async def simple_coro():
|
||||||
|
return 42
|
||||||
|
|
||||||
result = await task
|
wrapper = taskmgr.TaskWrapper(app, simple_coro(), name='test')
|
||||||
|
|
||||||
|
assert wrapper.name == 'test'
|
||||||
|
assert wrapper.task is not None
|
||||||
|
assert isinstance(wrapper.task, asyncio.Task)
|
||||||
|
|
||||||
|
result = await wrapper.task
|
||||||
assert result == 42
|
assert result == 42
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_task_exception_capture_pattern(self):
|
async def test_task_wrapper_with_custom_context(self):
|
||||||
"""Task exception can be captured via task.exception()."""
|
"""TaskWrapper uses provided TaskContext."""
|
||||||
async def failing():
|
taskmgr = get_taskmgr()
|
||||||
raise ValueError('error')
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
task = loop.create_task(failing())
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
# Let it fail
|
ctx = taskmgr.TaskContext.new()
|
||||||
await asyncio.sleep(0.01)
|
ctx.set_current_action('custom')
|
||||||
|
|
||||||
# Capture exception
|
|
||||||
try:
|
|
||||||
exception = task.exception()
|
|
||||||
assert isinstance(exception, ValueError)
|
|
||||||
except asyncio.CancelledError:
|
|
||||||
pass # Task was cancelled
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_task_cancel_pattern(self):
|
|
||||||
"""Task can be cancelled."""
|
|
||||||
async def long_running():
|
|
||||||
await asyncio.sleep(10)
|
|
||||||
return 'done'
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
task = loop.create_task(long_running())
|
|
||||||
|
|
||||||
task.cancel()
|
|
||||||
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
|
|
||||||
assert task.cancelled() or task.done()
|
|
||||||
|
|
||||||
|
|
||||||
class TestAsyncTaskManagerPatterns:
|
|
||||||
"""Tests for AsyncTaskManager behavior patterns."""
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
|
||||||
async def test_task_tracking_pattern(self):
|
|
||||||
"""Manager tracks created tasks."""
|
|
||||||
# Simulate manager behavior pattern
|
|
||||||
tasks = []
|
|
||||||
|
|
||||||
async def coro():
|
async def coro():
|
||||||
return 'result'
|
return 'done'
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
wrapper = taskmgr.TaskWrapper(app, coro(), context=ctx)
|
||||||
wrapper = {'id': 0, 'name': 'test', 'task': loop.create_task(coro())}
|
|
||||||
tasks.append(wrapper)
|
|
||||||
|
|
||||||
assert wrapper in tasks
|
assert wrapper.task_context.current_action == 'custom'
|
||||||
assert len(tasks) == 1
|
|
||||||
|
|
||||||
await wrapper['task']
|
await wrapper.task
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_multiple_tasks_isolated(self):
|
async def test_task_wrapper_exception_capture(self):
|
||||||
"""Multiple tasks run independently."""
|
"""TaskWrapper captures exception from failed task."""
|
||||||
results = []
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
async def task_a():
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
results.append('a')
|
|
||||||
|
|
||||||
async def task_b():
|
|
||||||
await asyncio.sleep(0.01)
|
|
||||||
results.append('b')
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
task_a_inst = loop.create_task(task_a())
|
app = FakeMinimalApp(loop)
|
||||||
task_b_inst = loop.create_task(task_b())
|
|
||||||
|
|
||||||
await asyncio.gather(task_a_inst, task_b_inst)
|
async def failing_coro():
|
||||||
|
raise ValueError('test error')
|
||||||
|
|
||||||
assert 'a' in results
|
wrapper = taskmgr.TaskWrapper(app, failing_coro())
|
||||||
assert 'b' in results
|
|
||||||
|
# Let task complete with exception
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
exception = wrapper.assume_exception()
|
||||||
|
assert exception is not None
|
||||||
|
assert isinstance(exception, ValueError)
|
||||||
|
assert 'test error' in str(exception)
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_cancel_by_id_pattern(self):
|
async def test_task_wrapper_result_capture(self):
|
||||||
"""Task can be cancelled by ID lookup."""
|
"""TaskWrapper captures result from completed task."""
|
||||||
tasks = []
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
return 'result_value'
|
||||||
|
|
||||||
|
wrapper = taskmgr.TaskWrapper(app, coro())
|
||||||
|
|
||||||
|
await wrapper.task
|
||||||
|
|
||||||
|
result = wrapper.assume_result()
|
||||||
|
assert result == 'result_value'
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_task_wrapper_cancel(self):
|
||||||
|
"""TaskWrapper.cancel cancels the task."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
async def long_coro():
|
async def long_coro():
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
return 'done'
|
return 'done'
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
wrapper = taskmgr.TaskWrapper(app, long_coro())
|
||||||
wrapper = {'id': 1, 'task': loop.create_task(long_coro())}
|
|
||||||
tasks.append(wrapper)
|
|
||||||
|
|
||||||
# Cancel by ID
|
wrapper.cancel()
|
||||||
task_id = 1
|
|
||||||
for w in tasks:
|
|
||||||
if w['id'] == task_id:
|
|
||||||
w['task'].cancel()
|
|
||||||
|
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
assert wrapper['task'].cancelled() or wrapper['task'].done()
|
assert wrapper.task.cancelled() or wrapper.task.done()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_stats_calculation_pattern(self):
|
async def test_task_wrapper_to_dict(self):
|
||||||
"""Stats count running/completed tasks."""
|
"""TaskWrapper.to_dict serializes task info."""
|
||||||
tasks = []
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
async def quick():
|
|
||||||
return 'done'
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
for i in range(3):
|
app = FakeMinimalApp(loop)
|
||||||
wrapper = {'id': i, 'task': loop.create_task(quick())}
|
|
||||||
tasks.append(wrapper)
|
|
||||||
await wrapper['task']
|
|
||||||
|
|
||||||
completed = sum(1 for w in tasks if w['task'].done())
|
async def coro():
|
||||||
|
return 42
|
||||||
|
|
||||||
assert completed == 3
|
wrapper = taskmgr.TaskWrapper(app, coro(), name='dict_test', label='Test')
|
||||||
|
|
||||||
|
await wrapper.task
|
||||||
|
|
||||||
|
result = wrapper.to_dict()
|
||||||
|
|
||||||
|
assert result['name'] == 'dict_test'
|
||||||
|
assert result['label'] == 'Test'
|
||||||
|
assert 'runtime' in result
|
||||||
|
assert result['runtime']['done'] is True
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_pruning_completed_tasks(self):
|
async def test_task_wrapper_id_increment(self):
|
||||||
"""Completed tasks are pruned when over limit."""
|
"""TaskWrapper IDs increment."""
|
||||||
completed_limit = 5
|
taskmgr = get_taskmgr()
|
||||||
tasks = []
|
|
||||||
|
|
||||||
async def quick():
|
|
||||||
return 'done'
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
for i in range(10):
|
app = FakeMinimalApp(loop)
|
||||||
wrapper = {'id': i, 'task': loop.create_task(quick())}
|
|
||||||
tasks.append(wrapper)
|
|
||||||
await wrapper['task']
|
|
||||||
|
|
||||||
# Prune
|
async def coro():
|
||||||
completed = [w for w in tasks if w['task'].done()]
|
return 1
|
||||||
overflow = len(completed) - completed_limit
|
|
||||||
if overflow > 0:
|
|
||||||
remove_ids = {w['id'] for w in completed[:overflow]}
|
|
||||||
tasks = [w for w in tasks if w['id'] not in remove_ids]
|
|
||||||
|
|
||||||
remaining_completed = sum(1 for w in tasks if w['task'].done())
|
wrapper1 = taskmgr.TaskWrapper(app, coro())
|
||||||
assert remaining_completed <= completed_limit
|
wrapper2 = taskmgr.TaskWrapper(app, coro())
|
||||||
|
|
||||||
|
assert wrapper2.id > wrapper1.id
|
||||||
|
|
||||||
|
|
||||||
class TestScopeBasedCancellation:
|
class TestAsyncTaskManagerReal:
|
||||||
"""Tests for scope-based cancellation pattern."""
|
"""Tests for real AsyncTaskManager class."""
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_cancel_by_scope_pattern(self):
|
async def test_manager_create_task(self):
|
||||||
"""Tasks can be filtered and cancelled by scope."""
|
"""AsyncTaskManager creates and tracks tasks."""
|
||||||
tasks = []
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
return 'result'
|
||||||
|
|
||||||
|
wrapper = manager.create_task(coro(), name='test')
|
||||||
|
|
||||||
|
assert wrapper in manager.tasks
|
||||||
|
assert wrapper.name == 'test'
|
||||||
|
|
||||||
|
await wrapper.task
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_manager_create_user_task(self):
|
||||||
|
"""create_user_task creates user-type task."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
return 'user_result'
|
||||||
|
|
||||||
|
wrapper = manager.create_user_task(coro())
|
||||||
|
|
||||||
|
assert wrapper.task_type == 'user'
|
||||||
|
|
||||||
|
await wrapper.task
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_manager_multiple_tasks_isolated(self):
|
||||||
|
"""Multiple tasks run independently."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
async def task_a():
|
||||||
|
results.append('a')
|
||||||
|
|
||||||
|
async def task_b():
|
||||||
|
results.append('b')
|
||||||
|
|
||||||
|
w1 = manager.create_task(task_a(), name='a')
|
||||||
|
w2 = manager.create_task(task_b(), name='b')
|
||||||
|
|
||||||
|
await asyncio.gather(w1.task, w2.task)
|
||||||
|
|
||||||
|
assert 'a' in results
|
||||||
|
assert 'b' in results
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_manager_get_task_by_id(self):
|
||||||
|
"""get_task_by_id finds task."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
return 1
|
||||||
|
|
||||||
|
wrapper = manager.create_task(coro())
|
||||||
|
|
||||||
|
found = manager.get_task_by_id(wrapper.id)
|
||||||
|
assert found is wrapper
|
||||||
|
|
||||||
|
not_found = manager.get_task_by_id(99999)
|
||||||
|
assert not_found is None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_manager_cancel_task(self):
|
||||||
|
"""cancel_task cancels specific task."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
|
|
||||||
async def long():
|
async def long():
|
||||||
await asyncio.sleep(10)
|
await asyncio.sleep(10)
|
||||||
return 'done'
|
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
wrapper = manager.create_task(long())
|
||||||
|
|
||||||
# Create tasks with different scopes
|
manager.cancel_task(wrapper.id)
|
||||||
platform_task = {
|
|
||||||
'id': 1,
|
|
||||||
'scopes': ['platform'],
|
|
||||||
'task': loop.create_task(long()),
|
|
||||||
}
|
|
||||||
app_task = {
|
|
||||||
'id': 2,
|
|
||||||
'scopes': ['application'],
|
|
||||||
'task': loop.create_task(long()),
|
|
||||||
}
|
|
||||||
tasks.extend([platform_task, app_task])
|
|
||||||
|
|
||||||
# Cancel platform scope
|
|
||||||
scope = 'platform'
|
|
||||||
for w in tasks:
|
|
||||||
if not w['task'].done() and scope in w['scopes']:
|
|
||||||
w['task'].cancel()
|
|
||||||
|
|
||||||
await asyncio.sleep(0.01)
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
assert platform_task['task'].cancelled() or platform_task['task'].done()
|
assert wrapper.task.cancelled() or wrapper.task.done()
|
||||||
# App task still running (pending)
|
|
||||||
|
|
||||||
|
|
||||||
class TestTaskTypeFiltering:
|
|
||||||
"""Tests for filtering tasks by type/kind."""
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_filter_by_type_pattern(self):
|
async def test_manager_cancel_by_scope(self):
|
||||||
"""Tasks can be filtered by type."""
|
"""cancel_by_scope cancels matching scope tasks."""
|
||||||
tasks = [
|
taskmgr = get_taskmgr()
|
||||||
{'id': 1, 'task_type': 'system', 'kind': 'internal'},
|
entities = get_entities()
|
||||||
{'id': 2, 'task_type': 'user', 'kind': 'user_action'},
|
|
||||||
{'id': 3, 'task_type': 'system', 'kind': 'maintenance'},
|
|
||||||
]
|
|
||||||
|
|
||||||
system_tasks = [t for t in tasks if t['task_type'] == 'system']
|
loop = asyncio.get_running_loop()
|
||||||
user_tasks = [t for t in tasks if t['task_type'] == 'user']
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
assert len(system_tasks) == 2
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
assert len(user_tasks) == 1
|
|
||||||
assert all(t['task_type'] == 'system' for t in system_tasks)
|
|
||||||
|
|
||||||
|
async def long():
|
||||||
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
class TestWaitAllTasks:
|
async def app_long():
|
||||||
"""Tests for waiting for all tasks."""
|
await asyncio.sleep(10)
|
||||||
|
|
||||||
|
# Create task with PLATFORM scope
|
||||||
|
platform_wrapper = manager.create_task(
|
||||||
|
long(),
|
||||||
|
scopes=[entities.LifecycleControlScope.PLATFORM],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create task with APPLICATION scope
|
||||||
|
manager.create_task(
|
||||||
|
app_long(),
|
||||||
|
scopes=[entities.LifecycleControlScope.APPLICATION],
|
||||||
|
)
|
||||||
|
|
||||||
|
manager.cancel_by_scope(entities.LifecycleControlScope.PLATFORM)
|
||||||
|
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
# Platform task cancelled
|
||||||
|
assert platform_wrapper.task.cancelled() or platform_wrapper.task.done()
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_wait_all_pattern(self):
|
async def test_manager_get_stats(self):
|
||||||
"""Can wait for all tasks to complete."""
|
"""get_stats returns task counts."""
|
||||||
tasks = []
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
|
|
||||||
|
async def quick():
|
||||||
|
return 1
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
w = manager.create_task(quick())
|
||||||
|
await w.task
|
||||||
|
|
||||||
|
stats = manager.get_stats()
|
||||||
|
|
||||||
|
assert stats['total'] >= 3
|
||||||
|
assert stats['completed'] >= 3
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_manager_get_tasks_dict(self):
|
||||||
|
"""get_tasks_dict filters by type."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
|
|
||||||
|
async def coro():
|
||||||
|
return 1
|
||||||
|
|
||||||
|
system_w = manager.create_task(coro(), task_type='system')
|
||||||
|
user_w = manager.create_user_task(coro())
|
||||||
|
|
||||||
|
await asyncio.gather(system_w.task, user_w.task)
|
||||||
|
|
||||||
|
system_tasks = manager.get_tasks_dict(type='system')
|
||||||
|
assert all(t['task_type'] == 'system' for t in system_tasks['tasks'])
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_manager_wait_all(self):
|
||||||
|
"""wait_all waits for all tasks."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
|
loop = asyncio.get_running_loop()
|
||||||
|
app = FakeMinimalApp(loop)
|
||||||
|
|
||||||
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
|
|
||||||
async def delayed():
|
async def delayed():
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(0.05)
|
||||||
return 'delayed'
|
|
||||||
|
for _ in range(3):
|
||||||
|
manager.create_task(delayed())
|
||||||
|
|
||||||
|
await manager.wait_all()
|
||||||
|
|
||||||
|
stats = manager.get_stats()
|
||||||
|
assert stats['running'] == 0
|
||||||
|
|
||||||
|
|
||||||
|
class TestTaskPruningReal:
|
||||||
|
"""Tests for real task pruning behavior."""
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_prune_completed_tasks(self):
|
||||||
|
"""Completed tasks are pruned when exceeding limit."""
|
||||||
|
taskmgr = get_taskmgr()
|
||||||
|
|
||||||
loop = asyncio.get_running_loop()
|
loop = asyncio.get_running_loop()
|
||||||
for i in range(3):
|
app = FakeMinimalApp(loop)
|
||||||
wrapper = {'id': i, 'task': loop.create_task(delayed())}
|
app.instance_config.data = {'system': {'task_retention': {'completed_limit': 3}}}
|
||||||
tasks.append(wrapper)
|
|
||||||
|
|
||||||
# Wait for all
|
manager = taskmgr.AsyncTaskManager(app)
|
||||||
await asyncio.gather(*[w['task'] for w in tasks], return_exceptions=True)
|
|
||||||
|
|
||||||
# All done
|
async def quick():
|
||||||
assert all(w['task'].done() for w in tasks)
|
return 1
|
||||||
|
|
||||||
|
# Create more than limit
|
||||||
|
for _ in range(5):
|
||||||
|
w = manager.create_task(quick())
|
||||||
|
await w.task
|
||||||
|
await asyncio.sleep(0.01)
|
||||||
|
|
||||||
|
# Completed count should be <= limit
|
||||||
|
completed = sum(1 for w in manager.tasks if w.task.done())
|
||||||
|
assert completed <= 3
|
||||||
Reference in New Issue
Block a user