Skip to content

Commit 74899fd

Browse files
tests: Reconstructed the tests and optimized the CI settings (#41)
* build: 迁移至 pyproject.toml,使用 uv 管理依赖 - 添加 pyproject.toml,声明项目元数据和 dev 依赖 (pytest) - 配置 ruff、ty 和 pytest 工具选项 - 删除旧的 requirements.txt" * ci: 优化 C 构建步骤并更新 checkout 动作 - 使用 mkdir -p 避免目录已存在时报错 - 改用 ctest 运行 C 测试 - 升级 actions/checkout 版本以保持工具链更新 * ci: 重构 Python CI,引入 ruff/ty 并添加测试 - 移除旧的 Pylint 矩阵作业 - 新增 Lint 作业,使用 ruff-action 进行 lint 和格式检查 - 新增 TypeCheck 作业,使用 uv + ty 进行类型检查 - 新增 Test 作业,在 Python 3.11~3.14 矩阵下执行 pytest - 所有 Python 相关步骤统一使用 actions/checkout@v6.0.2 和 astral-sh 工具链 * fix: 修正 .gitignore 并清理已跟踪的缓存文件 - 去掉 .gitignore 中错误的点 - 统一使用 */__pycache__/ 忽略所有 __pycache__ 目录 * refactor: 重构测试套件为标准化 pytest 单元测试 新增 conftest.py 集中管理 fixture 与资源清理,替换手动脚本为 可自动收集的测试函数,使用 monkeypatch 隔离外部依赖。 * ci: 暂时禁用 C_test 作业 * ci: 修正 ty 配置,排除 test 目录 * core-code: fix the command extension and the test * Settings: add the supported python version 3.10 * fix: 将最低支持 Python 版本调整回 3.10 * fix: 重新生成uv.lock --------- Co-authored-by: F18-Ray <xu_ruihong2009@126.com>
1 parent 357f16b commit 74899fd

15 files changed

Lines changed: 512 additions & 193 deletions

.github/workflows/ci.yml

Lines changed: 61 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,76 +3,75 @@ name: CI
33
on: [push, pull_request]
44

55
jobs:
6-
C_test:
7-
strategy:
8-
matrix:
9-
os: [ubuntu-latest]
6+
# C_test:
7+
# strategy:
8+
# matrix:
9+
# os: [ubuntu-latest]
1010

11-
runs-on: ${{ matrix.os }}
12-
timeout-minutes: 30
11+
# runs-on: ${{ matrix.os }}
12+
# timeout-minutes: 30
1313

14-
steps:
15-
- name: Checkout code
16-
uses: actions/checkout@v4
14+
# steps:
15+
# - name: Checkout code
16+
# uses: actions/checkout@v6.0.2
1717

18-
- name: Install C dependencies (Ubuntu)
19-
if: matrix.os == 'ubuntu-latest'
20-
run: |
21-
sudo apt-get remove needrestart
22-
sudo apt-get update
23-
sudo apt-get install -y cmake
18+
# - name: Install C dependencies (Ubuntu)
19+
# if: matrix.os == 'ubuntu-latest'
20+
# run: |
21+
# sudo apt-get remove needrestart
22+
# sudo apt-get update
23+
# sudo apt-get install -y cmake
2424

25-
- name: Build C code
26-
run: |
27-
mkdir build
28-
cd build
29-
cmake ..
30-
make
25+
# - name: Build C code
26+
# run: |
27+
# mkdir -p build
28+
# cd build
29+
# cmake ..
30+
# make
3131

32-
- name: Run C tests
33-
run: |
34-
cd build
35-
make test
32+
# - name: Run C tests
33+
# run: |
34+
# cd build
35+
# ctest
3636

37-
Pylint:
38-
strategy:
39-
matrix:
40-
os: [ubuntu-latest]
41-
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
37+
Lint:
38+
runs-on: ubuntu-latest
39+
timeout-minutes: 5
40+
steps:
41+
- name: Checkout code
42+
uses: actions/checkout@v6.0.2
43+
- name: Run Ruff linter on Python code
44+
uses: astral-sh/ruff-action@v4.0.0
45+
with:
46+
args: "check"
47+
continue-on-error: true
48+
- name: Check code formatting
49+
uses: astral-sh/ruff-action@v4.0.0
50+
with:
51+
args: "format --check"
52+
continue-on-error: true
4253

43-
runs-on: ${{ matrix.os }}
44-
timeout-minutes: 30
45-
54+
TypeCheck:
55+
runs-on: ubuntu-latest
56+
timeout-minutes: 5
4657
steps:
47-
- name: Checkout code
48-
uses: actions/checkout@v4
58+
- uses: actions/checkout@v6.0.2
59+
- name: Install uv
60+
uses: astral-sh/setup-uv@v8.1.0
4961

50-
- name: Set up Python
51-
uses: actions/setup-python@v4
62+
- name: Run ty
63+
run: uvx ty check
64+
continue-on-error: true
65+
Test:
66+
strategy:
67+
matrix:
68+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
69+
runs-on: ubuntu-latest
70+
timeout-minutes: 10
71+
steps:
72+
- uses: actions/checkout@v6.0.2
73+
- uses: astral-sh/setup-uv@v8.1.0
5274
with:
5375
python-version: ${{ matrix.python-version }}
54-
55-
- name: Install linting tools
56-
run: |
57-
python -m pip install --upgrade pip
58-
pip install pylint flake8 mypy black
59-
python -c "import black, flake8, mypy, pylint; print('All imports OK')"
60-
61-
# - name: Check Python code formatting
62-
# run: |
63-
# black ./src/
64-
65-
- name: Run Python linter
66-
run: |
67-
flake8 ./src/
68-
continue-on-error: true
69-
70-
- name: Run python lint score
71-
run: |
72-
pylint --max-line-length=150 ./src/
73-
continue-on-error: true
74-
75-
- name: Run Python type checking
76-
run: |
77-
mypy ./src/
78-
continue-on-error: true
76+
- run: uv sync --group dev
77+
- run: uv run pytest

.gitignore

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ _deps
1212
CMakeUserPresets.json
1313
.vscode/
1414
.vs/
15-
.test/build/
16-
.test/venv/
17-
.test/src/
18-
.src/__pycache__/
15+
test/build/
16+
test/venv/
17+
test/src/
18+
*/__pycache__/
1919

2020
# CLion
2121
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can

pyproject.toml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
[project]
2+
name = "servspy"
3+
version = "0.1.0"
4+
description = "ServSpy is a lightweight tool used for monitoring server status and simply controlling servers, such as through the command lines or the desktops."
5+
requires-python = ">=3.10"
6+
dependencies = []
7+
8+
[dependency-groups]
9+
dev = [
10+
"pytest>=9.0.0",
11+
]
12+
13+
[tool.ruff]
14+
target-version = "py310"
15+
line-length = 150
16+
exclude = ["build/", "dist/", ".git/", "__pycache__/"]
17+
18+
[tool.ruff.lint]
19+
select = ["E", "F", "PL", "I"]
20+
21+
[tool.ruff.format]
22+
quote-style = "double"
23+
indent-style = "space"
24+
25+
[tool.ty.environment]
26+
python-version = "3.10"
27+
28+
[tool.ty.src]
29+
exclude = ["test/"]
30+
31+
[tool.pytest.ini_options]
32+
testpaths = ["test"]

requirements.txt

Lines changed: 0 additions & 1 deletion
This file was deleted.

src/command_control_extension_tcp.py

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,42 @@
1313
client_instance=None
1414
command_counter={}
1515
command_counter_lock=threading.Lock()
16+
17+
def _load_json_file(path):
18+
try:
19+
with open(path, 'r', encoding='utf-8') as f:
20+
data = json.load(f)
21+
return data if isinstance(data, dict) else {}
22+
except Exception:
23+
return {}
24+
25+
26+
def _merge_log_dicts(base_data, extra_data):
27+
merged = {}
28+
for command, entries in base_data.items():
29+
merged[command] = list(entries) if isinstance(entries, list) else [entries]
30+
for command, entries in extra_data.items():
31+
if command not in merged:
32+
merged[command] = []
33+
if isinstance(entries, list):
34+
merged[command].extend(entries)
35+
else:
36+
merged[command].append(entries)
37+
return merged
38+
39+
40+
def _merge_all_logs(log_dir, merged_filename='merged_logs.json'):
41+
merged_data = {}
42+
for filename in os.listdir(log_dir):
43+
if not filename.endswith('.json') or filename == merged_filename:
44+
continue
45+
file_path = os.path.join(log_dir, filename)
46+
if os.path.isfile(file_path):
47+
merged_data = _merge_log_dicts(merged_data, _load_json_file(file_path))
48+
with open(os.path.join(log_dir, merged_filename), 'w', encoding='utf-8') as f:
49+
json.dump(merged_data, f, ensure_ascii=False, indent=2)
50+
51+
1652
def _setup_command():
1753
print("Setting up server command...")
1854
server_instance.register_command(
@@ -149,18 +185,26 @@ def _command_done_dealing_server(sock, addr, cmd):
149185
cmd_parts = shlex.split(cmd)
150186
log_filename = cmd_parts[1]
151187
log_path = cmd_parts[2]
152-
log_dir=os.path.join(os.path.dirname(__file__), 'logs')
153-
if os.path.exists(log_dir):
154-
pass
155-
else:
156-
os.mkdir(log_dir)
157-
received_log_file=os.path.join(
158-
server_instance.file_transfer_dir, log_path)
188+
log_dir = os.path.join(os.path.dirname(__file__), 'logs')
189+
os.makedirs(log_dir, exist_ok=True)
190+
191+
received_log_file = os.path.join(
192+
server_instance.file_transfer_dir,
193+
os.path.basename(log_path))
194+
destination_log_file = os.path.join(log_dir, log_filename)
159195
try:
160-
shutil.move(
161-
received_log_file, os.path.join(log_dir, log_filename))
196+
if os.path.exists(destination_log_file):
197+
existing_logs = _load_json_file(destination_log_file)
198+
incoming_logs = _load_json_file(received_log_file)
199+
merged_logs = _merge_log_dicts(existing_logs, incoming_logs)
200+
with open(destination_log_file, 'w', encoding='utf-8') as f:
201+
json.dump(merged_logs, f, ensure_ascii=False, indent=2)
202+
os.remove(received_log_file)
203+
else:
204+
shutil.move(received_log_file, destination_log_file)
205+
_merge_all_logs(log_dir)
162206
print("Command Done!")
163-
except:
207+
except Exception:
164208
traceback.print_exc()
165209
print("ErrorWhileMovingTheLogFile: moving log file failed.")
166210
def client_setup():

test/conftest.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import importlib.util
2+
import os
3+
import sys
4+
5+
import pytest
6+
7+
# Only add project root to sys.path if src package is not importable
8+
if importlib.util.find_spec("src") is None:
9+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10+
11+
import src.command_control_extension_tcp as ctl
12+
from src.connect_tcp import TCP_Client_Base, TCP_Server_Base
13+
14+
SERVER_PORT = 65001
15+
CLIENT_PORT = 65000
16+
SERVER2_PORT = 65002
17+
CLIENT2_PORT = 65003
18+
19+
20+
@pytest.fixture
21+
def server(monkeypatch):
22+
"""Create a TCP_Server_Base and inject into command_control_extension_tcp.server_instance.
23+
24+
Use this fixture when a test only needs a server instance.
25+
"""
26+
s = TCP_Server_Base(host="127.0.0.1", port=SERVER_PORT, is_extend_command=True)
27+
monkeypatch.setattr(ctl, "server_instance", s)
28+
try:
29+
yield s
30+
finally:
31+
s.stop()
32+
33+
34+
@pytest.fixture
35+
def client(monkeypatch):
36+
"""Create a TCP_Client_Base and inject into command_control_extension_tcp.client_instance.
37+
38+
Use this fixture when a test only needs a client instance.
39+
"""
40+
c = TCP_Client_Base(host="127.0.0.1", port=CLIENT_PORT, client_host="127.0.0.1", is_extend_command=True)
41+
monkeypatch.setattr(ctl, "client_instance", c)
42+
try:
43+
yield c
44+
finally:
45+
c.close()
46+
47+
48+
@pytest.fixture
49+
def server_client(monkeypatch):
50+
"""Create both server and client and inject both module globals.
51+
52+
Use this when a test requires both sides present.
53+
"""
54+
s = TCP_Server_Base(host="127.0.0.1", port=SERVER2_PORT, is_extend_command=True)
55+
c = TCP_Client_Base(host="127.0.0.1", port=CLIENT2_PORT, client_host="127.0.0.1", is_extend_command=True)
56+
monkeypatch.setattr(ctl, "server_instance", s)
57+
monkeypatch.setattr(ctl, "client_instance", c)
58+
try:
59+
yield s, c
60+
finally:
61+
try:
62+
c.close()
63+
finally:
64+
s.stop()

test/test_TCP.py

Lines changed: 0 additions & 39 deletions
This file was deleted.

test/test_TCP_client.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import os
2-
import sys
3-
package_dictionary=os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
4-
if package_dictionary not in os.sys.path:
5-
sys.path.insert(0, package_dictionary)
6-
from src import connect_tcp
7-
def test_TCP_client():
8-
connect_tcp.TCP_Client_Base(host="127.0.0.1")
9-
test_TCP_client()
1+
from src.connect_tcp import TCP_Client_Base
2+
3+
4+
def test_tcp_client_init():
5+
client = TCP_Client_Base(host="127.0.0.1", port=65003, client_host="127.0.0.1", is_extend_command=True)
6+
assert client.host == "127.0.0.1"
7+
assert client.port == 65003 # noqa: PLR2004
8+
assert callable(client.connect)

0 commit comments

Comments
 (0)