Skip to content

Commit 6a17cc2

Browse files
committed
feat: add delay tool and comprehensive test suite
- Add delay tool to misc module for time-based operations - Set up pytest configuration with coverage reporting - Create comprehensive test suite with fixtures for device mocking - Add GitHub Actions workflow for automated testing - Configure Claude permissions for test execution - Include test dependency group in pyproject.toml
1 parent 5defce9 commit 6a17cc2

File tree

11 files changed

+297
-0
lines changed

11 files changed

+297
-0
lines changed

.claude/settings.local.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(uv run pytest:*)",
5+
"Bash(uv run python:*)",
6+
"Bash(uv run test:*)"
7+
]
8+
}
9+
}

.github/workflows/test.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.11", "3.12", "3.13", "3.14"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v4
21+
with:
22+
version: "latest"
23+
enable-cache: true
24+
cache-dependency-glob: "uv.lock"
25+
26+
- name: Set up Python ${{ matrix.python-version }}
27+
run: uv python install ${{ matrix.python-version }}
28+
29+
- name: Install dependencies
30+
run: uv sync --group test
31+
32+
- name: Run tests with coverage
33+
run: uv run pytest --cov=src/u2mcp --cov-report=term-missing --cov-report=xml
34+
35+
- name: Upload coverage to Codecov
36+
uses: codecov/codecov-action@v4
37+
with:
38+
file: ./coverage.xml
39+
fail_ci_if_error: false

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ changelog = "https://github.com/tanbro/uiautomator2-mcp-server/blob/main/CHANGEL
4343

4444
[dependency-groups]
4545
dev = ["mypy"]
46+
test = ["pytest", "pytest-asyncio", "pytest-cov", "pytest-mock"]

pytest.ini

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[pytest]
2+
testpaths = tests
3+
python_files = test_*.py
4+
python_classes = Test*
5+
python_functions = test_*
6+
addopts =
7+
-v
8+
--tb=short
9+
--strict-markers
10+
--disable-warnings
11+
--cov=src/u2mcp
12+
--cov-report=term-missing
13+
--cov-report=html
14+
--asyncio-mode=auto
15+
markers =
16+
unit: Unit tests (fast, no external dependencies)
17+
integration: Integration tests (may require external services)
18+
slow: Slow running tests
19+
device: Tests that require actual Android device

src/u2mcp/tools/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from .action import *
22
from .app import *
33
from .device import *
4+
from .misc import *

src/u2mcp/tools/misc.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from __future__ import annotations
2+
3+
import asyncio
4+
5+
from ..mcp import mcp
6+
7+
8+
@mcp.tool("delay")
9+
async def delay(duration: float):
10+
"""Delay for a specific amount of time
11+
12+
Args:
13+
duration (float): Duration in seconds
14+
"""
15+
await asyncio.sleep(duration)

tests/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""
2+
Test package for uiautomator2-mcp-server.
3+
"""

tests/conftest.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
"""
2+
Pytest configuration and shared fixtures for uiautomator2-mcp-server tests.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
import asyncio
8+
from typing import AsyncGenerator
9+
from unittest.mock import AsyncMock, MagicMock, patch
10+
11+
import pytest
12+
13+
14+
@pytest.fixture
15+
def mock_u2_device() -> MagicMock:
16+
"""Create a mocked uiautomator2 device."""
17+
mock_device = MagicMock()
18+
# Setup common device methods
19+
mock_device.info = {"productName": "test_device", "version": "13", "serial": "emulator-5554"}
20+
mock_device.device_info = {"serial": "emulator-5554", "model": "test_device"}
21+
mock_device.window_size = MagicMock(return_value=(1080, 2400))
22+
mock_device.screenshot = MagicMock(return_value=MagicMock())
23+
mock_device.dump_hierarchy = MagicMock(return_value="<hierarchy/>")
24+
mock_device.click = AsyncMock()
25+
mock_device.long_click = AsyncMock()
26+
mock_device.double_click = AsyncMock()
27+
mock_device.swipe = AsyncMock()
28+
mock_device.swipe_points = AsyncMock()
29+
mock_device.drag = AsyncMock()
30+
mock_device.press = AsyncMock()
31+
mock_device.send_keys = AsyncMock()
32+
mock_device.clear_text = AsyncMock()
33+
mock_device.screen_on = AsyncMock()
34+
mock_device.screen_off = AsyncMock()
35+
36+
# App management methods
37+
mock_device.app_start = AsyncMock()
38+
mock_device.app_wait = AsyncMock()
39+
mock_device.app_stop = AsyncMock()
40+
mock_device.app_stop_all = AsyncMock()
41+
mock_device.app_info = MagicMock(return_value={"packageName": "com.example.app", "versionName": "1.0", "versionCode": 1})
42+
mock_device.app_current = MagicMock(return_value={"package": "com.example.app"})
43+
mock_device.app_list = MagicMock(return_value=["com.example.app1", "com.example.app2"])
44+
mock_device.app_list_running = MagicMock(return_value=["com.example.app1"])
45+
mock_device.app_install = AsyncMock()
46+
mock_device.app_uninstall = AsyncMock()
47+
mock_device.app_uninstall_all = AsyncMock()
48+
mock_device.app_clear = AsyncMock()
49+
mock_device.app_auto_grant_permissions = AsyncMock()
50+
51+
return mock_device
52+
53+
54+
@pytest.fixture
55+
def mock_adb() -> MagicMock:
56+
"""Create a mocked adbutils.adb object."""
57+
mock_adb = MagicMock()
58+
mock_device = MagicMock()
59+
mock_device.serial = "emulator-5554"
60+
mock_device.prop = MagicMock(return_value="test_value")
61+
mock_adb.device = MagicMock(return_value=mock_device)
62+
mock_adb.device_list = MagicMock(return_value=[mock_device])
63+
return mock_adb
64+
65+
66+
@pytest.fixture
67+
def mock_u2_module(mock_u2_device: MagicMock) -> MagicMock:
68+
"""Create a mocked uiautomator2 module."""
69+
mock_u2 = MagicMock()
70+
mock_u2.connect = MagicMock(return_value=mock_u2_device)
71+
mock_u2.Device = MagicMock(return_value=mock_u2_device)
72+
return mock_u2
73+
74+
75+
@pytest.fixture
76+
def event_loop():
77+
"""Create an event loop for async tests."""
78+
loop = asyncio.new_event_loop()
79+
yield loop
80+
loop.close()
81+
82+
83+
@pytest.fixture(autouse=True)
84+
def mock_device_dependencies( # type: ignore
85+
mock_u2_device: MagicMock,
86+
mock_u2_module: MagicMock,
87+
mock_adb: MagicMock,
88+
) -> AsyncGenerator[None, None]: # type: ignore
89+
"""
90+
Automatically mock uiautomator2 and adbutils dependencies for all tests.
91+
92+
This ensures tests don't require actual Android devices or ADB connections.
93+
"""
94+
with patch("u2mcp.tools.device.u2", mock_u2_module), patch("u2mcp.tools.device.adb", mock_adb):
95+
yield # type: ignore
96+
97+
98+
@pytest.fixture
99+
def mock_context() -> MagicMock:
100+
"""Create a mocked FastMCP context."""
101+
mock_context = MagicMock()
102+
mock_context.session = MagicMock()
103+
mock_context.session.id = "test-session-id"
104+
return mock_context

tests/integration/__init__.py

Whitespace-only changes.

tests/unit/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)