style: 引入 ruff,474 项告警清零
配置见 pyproject.toml:
- line-length=100:贴着现有代码定。若用默认的 88 会重排 122 行,
噪音淹没真正有价值的修复。
- 启用 E/W/F/I/UP/B/C4/SIM/RET/PERF/RUF;**不**启用 PTH。因为本项目的路径
多是"字符串身份"——要塞进 subprocess 命令行、写进 JSON 存档、打印给用户,
全换 pathlib 只是包一层再拆一层,换来 70 处纯风格改动。
- 忽略 RUF001/002/003:中文全角标点是正确写法,不是"歧义字符"。
- 忽略 PERF203:剩下两处都是"逐项容错"语义(逐个牧场存档要立刻上报失败、
收获全部要跳过失败地块)。把 try 提到循环外会毁掉"一项失败不影响其它项"
这个行为,是错的优化。
自动修复 358 项:Dict/List/Optional 换成 PEP 585/604 写法、导入排序、
失效的 noqa、空占位 f-string 等。
手工修复的(这些是真问题,不是风格偏好):
- B904 x9:except 中 raise 补异常链。刻意区分——网络/IO 失败用 from exc 保留
根因;参数解析失败用 from None,因为底层 ValueError 对玩家只是噪音。
- RUF012 x2:StateView 的 plots/animals 原本是**类级共享可变列表**,
改成在 __init__ 里初始化(apply({}) 会提前返回,所以必须显式摆好)。
- F841:TUI 里一个赋值后再没用过的死变量。
- SIM105 x11:try/except/pass 统一改成 contextlib.suppress。
- SIM115:日志句柄改用 with。Popen 已把 fd 复制给子进程,父进程可安全关闭。
- E501 x7:拆超长行;main.py 那处顺手提取变量,避免超长嵌套表达式。
- E741:把 l/r 改成 left_line/right_line。
- 其余:RUF046 / PERF401 / RET504 / RUF005 / UP031 / B007。
ruff format 重排 24 个文件。
验证:97 项测试全绿。
This commit is contained in:
1 parent
c7c8a6308a
commit
fcd4cb4649
25 files changed
+1204
-794
No files matched your search
+16
-16
@@ -8,6 +8,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
@@ -16,10 +17,10 @@ import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from farm import config # noqa: E402
|
||||
from farm.engine import FarmEngine, GameError # noqa: E402
|
||||
from farm.models import GameState, Plot # noqa: E402
|
||||
from farm.storage import load_state, save_state # noqa: E402
|
||||
from farm import config
|
||||
from farm.engine import FarmEngine, GameError
|
||||
from farm.models import GameState, Plot
|
||||
from farm.storage import load_state, save_state
|
||||
|
||||
|
||||
class EngineTestCase(unittest.TestCase):
|
||||
@@ -190,14 +191,14 @@ class EngineTestCase(unittest.TestCase):
|
||||
self.assertEqual(ctx.exception.code, "plot_limit")
|
||||
|
||||
def test_buy_plot_without_gold(self) -> None:
|
||||
self.engine.state.level = 5 # 先解锁土地上限,才能走到"钱不够"这一步
|
||||
self.engine.state.level = 5 # 先解锁土地上限,才能走到"钱不够"这一步
|
||||
self.engine.state.gold = 0
|
||||
with self.assertRaises(GameError) as ctx:
|
||||
self.engine.buy_plot()
|
||||
self.assertEqual(ctx.exception.code, "not_enough_gold")
|
||||
|
||||
def test_buy_plot_blocked_at_level_cap(self) -> None:
|
||||
self.engine.state.gold = 10 ** 6
|
||||
self.engine.state.gold = 10**6
|
||||
with self.assertRaises(GameError) as ctx:
|
||||
self.engine.buy_plot()
|
||||
self.assertEqual(ctx.exception.code, "plot_limit")
|
||||
@@ -262,7 +263,7 @@ class EngineTestCase(unittest.TestCase):
|
||||
self.assertEqual(ctx.exception.code, "unknown_animal")
|
||||
|
||||
def test_pasture_limit(self) -> None:
|
||||
self.engine.state.gold = 10 ** 6
|
||||
self.engine.state.gold = 10**6
|
||||
for _ in range(config.MAX_ANIMALS):
|
||||
self.engine.buy_animal("chicken")
|
||||
with self.assertRaises(GameError) as ctx:
|
||||
@@ -280,7 +281,7 @@ class EngineTestCase(unittest.TestCase):
|
||||
self.assertLess(self.engine.state.gold, gold) # 被扣了高价补货费
|
||||
|
||||
def test_buy_feed_validation(self) -> None:
|
||||
self.engine.state.gold = 10 ** 6
|
||||
self.engine.state.gold = 10**6
|
||||
with self.assertRaises(GameError):
|
||||
self.engine.buy_feed(0)
|
||||
with self.assertRaises(GameError):
|
||||
@@ -350,7 +351,7 @@ class EngineTestCase(unittest.TestCase):
|
||||
def test_save_load_roundtrip(self) -> None:
|
||||
self.engine.plant(1, "carrot")
|
||||
self.engine.buy_animal("chicken")
|
||||
self.engine.state.gold = 500 # 操作完成后再定金币,避免被扣费干扰断言
|
||||
self.engine.state.gold = 500 # 操作完成后再定金币,避免被扣费干扰断言
|
||||
self.engine.state.feed = 7
|
||||
self.engine.state.day = 13
|
||||
path = os.path.join(self.tmp, "roundtrip.json")
|
||||
@@ -402,14 +403,14 @@ class EngineTestCase(unittest.TestCase):
|
||||
self.assertEqual(state.day, 1)
|
||||
self.assertEqual(state.gold, 0)
|
||||
self.assertEqual(state.level, 1)
|
||||
self.assertIsNone(state.plots[0].crop) # 非法作物被清空
|
||||
self.assertEqual(state.animals, []) # 非法动物被丢弃
|
||||
self.assertGreater(state.market["wheat"], 0) # 非法价格回落默认值
|
||||
self.assertIsNone(state.plots[0].crop) # 非法作物被清空
|
||||
self.assertEqual(state.animals, []) # 非法动物被丢弃
|
||||
self.assertGreater(state.market["wheat"], 0) # 非法价格回落默认值
|
||||
self.assertGreaterEqual(len(state.plots), config.BASE_PLOTS)
|
||||
|
||||
# ============================================================ 并发
|
||||
def test_concurrent_operations_are_safe(self) -> None:
|
||||
self.engine.state.gold = 10 ** 6
|
||||
self.engine.state.gold = 10**6
|
||||
errors = []
|
||||
|
||||
def worker(index: int) -> None:
|
||||
@@ -418,10 +419,9 @@ class EngineTestCase(unittest.TestCase):
|
||||
self.engine.snapshot()
|
||||
self.engine.available_actions()
|
||||
if index % 3 == 0:
|
||||
try:
|
||||
# 金币可能已经不够了,这里只关心它不崩溃
|
||||
with contextlib.suppress(GameError):
|
||||
self.engine.buy_feed(1)
|
||||
except GameError:
|
||||
pass
|
||||
except Exception as exc: # 记录任何非预期异常
|
||||
errors.append(exc)
|
||||
|
||||
|
||||
+73
-48
@@ -25,21 +25,21 @@ import unittest
|
||||
from farm import config
|
||||
from farm.engine import FarmEngine, GameError
|
||||
from farm.server.actions import ACTIONS, describe_actions, run_action
|
||||
from farm.server.session import FarmSession, SessionManager, validate_farm_id
|
||||
from farm.server.session import SessionManager, validate_farm_id
|
||||
|
||||
try:
|
||||
import fastapi # noqa: F401
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
HAS_FASTAPI = True
|
||||
except ImportError: # pragma: no cover - 取决于环境
|
||||
except ImportError: # pragma: no cover - 取决于环境
|
||||
HAS_FASTAPI = False
|
||||
|
||||
try:
|
||||
import uvicorn # noqa: F401
|
||||
|
||||
HAS_UVICORN = True
|
||||
except ImportError: # pragma: no cover
|
||||
except ImportError: # pragma: no cover
|
||||
HAS_UVICORN = False
|
||||
|
||||
|
||||
@@ -231,8 +231,7 @@ class TestRestAPI(unittest.TestCase):
|
||||
from farm.server.app import ServerConfig, create_app
|
||||
|
||||
self.tmp = tempfile.mkdtemp(prefix="farm-api-")
|
||||
self.cfg = ServerConfig(host="127.0.0.1", port=8765, save_dir=self.tmp,
|
||||
tick_seconds=60)
|
||||
self.cfg = ServerConfig(host="127.0.0.1", port=8765, save_dir=self.tmp, tick_seconds=60)
|
||||
self.app = create_app(self.cfg)
|
||||
self.client = TestClient(self.app)
|
||||
self.client.__enter__()
|
||||
@@ -277,15 +276,18 @@ class TestRestAPI(unittest.TestCase):
|
||||
data = self.client.post("/api/actions?farm=one", json=body).json()
|
||||
self.assertTrue(data["ok"])
|
||||
self.assertIn("已种下", data["message"])
|
||||
self.assertEqual(data["state"]["gold"], config.START_GOLD - config.CROPS["wheat"]["seed_cost"])
|
||||
self.assertEqual(
|
||||
data["state"]["gold"], config.START_GOLD - config.CROPS["wheat"]["seed_cost"]
|
||||
)
|
||||
|
||||
state = self.client.get("/api/state?farm=one").json()["state"]
|
||||
self.assertEqual(state["plots"][0]["crop"], "wheat")
|
||||
|
||||
def test_flat_params_also_accepted(self) -> None:
|
||||
"""{"action":"plant","plot":1,"crop":"wheat"} 这种平铺写法也要能用。"""
|
||||
data = self.client.post("/api/actions?farm=flat",
|
||||
json={"action": "plant", "plot": 1, "crop": "wheat"}).json()
|
||||
data = self.client.post(
|
||||
"/api/actions?farm=flat", json={"action": "plant", "plot": 1, "crop": "wheat"}
|
||||
).json()
|
||||
self.assertTrue(data["ok"])
|
||||
|
||||
def test_convenience_route(self) -> None:
|
||||
@@ -294,8 +296,9 @@ class TestRestAPI(unittest.TestCase):
|
||||
self.assertEqual(data["action"], "plant")
|
||||
|
||||
def test_action_error_shape(self) -> None:
|
||||
response = self.client.post("/api/actions?farm=err",
|
||||
json={"action": "plant", "params": {"plot": 1, "crop": "nope"}})
|
||||
response = self.client.post(
|
||||
"/api/actions?farm=err", json={"action": "plant", "params": {"plot": 1, "crop": "nope"}}
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
data = response.json()
|
||||
self.assertFalse(data["ok"])
|
||||
@@ -303,37 +306,43 @@ class TestRestAPI(unittest.TestCase):
|
||||
self.assertIn("message", data)
|
||||
|
||||
def test_out_of_range_plot(self) -> None:
|
||||
response = self.client.post("/api/actions?farm=err",
|
||||
json={"action": "water", "params": {"plot": 999}})
|
||||
response = self.client.post(
|
||||
"/api/actions?farm=err", json={"action": "water", "params": {"plot": 999}}
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error"], "no_such_plot")
|
||||
|
||||
def test_bad_json_body(self) -> None:
|
||||
response = self.client.post("/api/actions?farm=err", content=b"{oops",
|
||||
headers={"Content-Type": "application/json"})
|
||||
response = self.client.post(
|
||||
"/api/actions?farm=err", content=b"{oops", headers={"Content-Type": "application/json"}
|
||||
)
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertEqual(response.json()["error"], "bad_json")
|
||||
|
||||
def test_actions_endpoint_matches_state(self) -> None:
|
||||
data = self.client.get("/api/actions?farm=solo").json()
|
||||
names = {item["action"] for item in data["actions"]}
|
||||
self.assertIn("plant", names) # 有空地 + 有钱 → 可播种
|
||||
self.assertNotIn("harvest", names) # 还没种东西 → 不可收获
|
||||
self.assertIn("plant", names) # 有空地 + 有钱 → 可播种
|
||||
self.assertNotIn("harvest", names) # 还没种东西 → 不可收获
|
||||
|
||||
def test_batch_endpoint(self) -> None:
|
||||
items = {"actions": [
|
||||
{"action": "buy_feed", "params": {"quantity": 5}},
|
||||
{"action": "plant", "params": {"plot": 1, "crop": "wheat"}},
|
||||
{"action": "harvest", "params": {"plot": 1}}, # 必然失败
|
||||
]}
|
||||
items = {
|
||||
"actions": [
|
||||
{"action": "buy_feed", "params": {"quantity": 5}},
|
||||
{"action": "plant", "params": {"plot": 1, "crop": "wheat"}},
|
||||
{"action": "harvest", "params": {"plot": 1}}, # 必然失败
|
||||
]
|
||||
}
|
||||
data = self.client.post("/api/batch?farm=multi", json=items).json()
|
||||
self.assertEqual(data["done"], 2)
|
||||
self.assertEqual(data["failed"], 1)
|
||||
self.assertEqual(data["results"][2]["error"], "not_ripe")
|
||||
|
||||
def test_log_entries_and_since(self) -> None:
|
||||
self.client.post("/api/actions?farm=logging",
|
||||
json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}})
|
||||
self.client.post(
|
||||
"/api/actions?farm=logging",
|
||||
json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}},
|
||||
)
|
||||
entries = self.client.get("/api/log?farm=logging").json()["entries"]
|
||||
self.assertTrue(entries)
|
||||
last = entries[-1]["seq"]
|
||||
@@ -344,14 +353,14 @@ class TestRestAPI(unittest.TestCase):
|
||||
self.assertTrue(newer)
|
||||
|
||||
def test_farms_list_and_isolation(self) -> None:
|
||||
self.client.post("/api/actions?farm=alice",
|
||||
json={"action": "buy_feed", "params": {"quantity": 10}})
|
||||
self.client.post(
|
||||
"/api/actions?farm=alice", json={"action": "buy_feed", "params": {"quantity": 10}}
|
||||
)
|
||||
self.client.post("/api/actions?farm=bob", json={"action": "save"})
|
||||
rows = self.client.get("/api/farms").json()["farms"]
|
||||
by_name = {row["farm"]: row for row in rows}
|
||||
self.assertIn("alice", by_name)
|
||||
self.assertEqual(by_name["alice"]["gold"],
|
||||
config.START_GOLD - 10 * config.FEED_UNIT_PRICE)
|
||||
self.assertEqual(by_name["alice"]["gold"], config.START_GOLD - 10 * config.FEED_UNIT_PRICE)
|
||||
|
||||
def test_create_farm_reports_created_flag(self) -> None:
|
||||
first = self.client.post("/api/farms", json={"farm": "zoe", "owner": "小Z"}).json()
|
||||
@@ -370,8 +379,10 @@ class TestRestAPI(unittest.TestCase):
|
||||
self.assertEqual(data["farm"], "header-farm")
|
||||
|
||||
def test_admin_save(self) -> None:
|
||||
self.client.post("/api/actions?farm=saver",
|
||||
json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}})
|
||||
self.client.post(
|
||||
"/api/actions?farm=saver",
|
||||
json={"action": "plant", "params": {"plot": 1, "crop": "wheat"}},
|
||||
)
|
||||
data = self.client.post("/api/admin/save").json()
|
||||
self.assertIn("saver", data["farms"])
|
||||
self.assertTrue(os.path.exists(os.path.join(self.tmp, "saver.json")))
|
||||
@@ -419,8 +430,7 @@ class TestAuth(unittest.TestCase):
|
||||
self.assertEqual(self.client.get("/api/state?token=nope").status_code, 401)
|
||||
|
||||
def test_action_blocked_without_token(self) -> None:
|
||||
response = self.client.post("/api/actions",
|
||||
json={"action": "buy_plot"})
|
||||
response = self.client.post("/api/actions", json={"action": "buy_plot"})
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
|
||||
@@ -436,14 +446,27 @@ class TestLiveServer(unittest.TestCase):
|
||||
cls.tmp = tempfile.mkdtemp(prefix="farm-live-")
|
||||
cls.port = free_port()
|
||||
cls.url = f"http://127.0.0.1:{cls.port}"
|
||||
app = create_app(ServerConfig(host="127.0.0.1", port=cls.port, save_dir=cls.tmp,
|
||||
tick_seconds=600, autosave_interval=3600))
|
||||
app = create_app(
|
||||
ServerConfig(
|
||||
host="127.0.0.1",
|
||||
port=cls.port,
|
||||
save_dir=cls.tmp,
|
||||
tick_seconds=600,
|
||||
autosave_interval=3600,
|
||||
)
|
||||
)
|
||||
import uvicorn
|
||||
|
||||
cls.server = uvicorn.Server(uvicorn.Config(
|
||||
app, host="127.0.0.1", port=cls.port, log_level="warning",
|
||||
access_log=False, lifespan="on",
|
||||
))
|
||||
cls.server = uvicorn.Server(
|
||||
uvicorn.Config(
|
||||
app,
|
||||
host="127.0.0.1",
|
||||
port=cls.port,
|
||||
log_level="warning",
|
||||
access_log=False,
|
||||
lifespan="on",
|
||||
)
|
||||
)
|
||||
cls.thread = threading.Thread(target=cls.server.run, name="live-server", daemon=True)
|
||||
cls.thread.start()
|
||||
|
||||
@@ -454,7 +477,7 @@ class TestLiveServer(unittest.TestCase):
|
||||
break
|
||||
except OSError:
|
||||
time.sleep(0.2)
|
||||
else: # pragma: no cover
|
||||
else: # pragma: no cover
|
||||
raise unittest.SkipTest("服务器没能在 20 秒内起来")
|
||||
|
||||
@classmethod
|
||||
@@ -466,8 +489,9 @@ class TestLiveServer(unittest.TestCase):
|
||||
def client(self, farm: str, realtime: bool = True):
|
||||
from farm.client import RemoteFarm
|
||||
|
||||
return RemoteFarm(endpoint=self.url, farm=farm, realtime=realtime, timeout=8.0,
|
||||
autostart=True)
|
||||
return RemoteFarm(
|
||||
endpoint=self.url, farm=farm, realtime=realtime, timeout=8.0, autostart=True
|
||||
)
|
||||
|
||||
def test_http_roundtrip_through_real_client(self) -> None:
|
||||
client = self.client("live-http", realtime=False)
|
||||
@@ -480,18 +504,18 @@ class TestLiveServer(unittest.TestCase):
|
||||
self.assertFalse(client.state.plots[0].is_ripe)
|
||||
|
||||
with self.assertRaises(GameError) as ctx:
|
||||
client.plant(1, "carrot") # 地已经占用了
|
||||
client.plant(1, "carrot") # 地已经占用了
|
||||
self.assertEqual(ctx.exception.code, "plot_busy")
|
||||
|
||||
actions = client.available_actions()
|
||||
names = {item["action"] for item in actions}
|
||||
self.assertIn("plant", names) # 还有空地可以种
|
||||
self.assertNotIn("harvest", names) # 小麦还没成熟
|
||||
self.assertIn("plant", names) # 还有空地可以种
|
||||
self.assertNotIn("harvest", names) # 小麦还没成熟
|
||||
|
||||
client.water(1) # 播种后水分 2/3,可以补满
|
||||
client.water(1) # 播种后水分 2/3,可以补满
|
||||
self.assertEqual(client.state.plots[0].moisture, config.WATER_CAPACITY)
|
||||
with self.assertRaises(GameError) as ctx:
|
||||
client.water(1) # 水分已满,再浇就是错的
|
||||
client.water(1) # 水分已满,再浇就是错的
|
||||
self.assertEqual(ctx.exception.code, "already_watered")
|
||||
|
||||
catalog = client.catalog()
|
||||
@@ -515,8 +539,9 @@ class TestLiveServer(unittest.TestCase):
|
||||
deadline = time.time() + 8
|
||||
while time.time() < deadline and watcher.state.plots[0].crop != "carrot":
|
||||
time.sleep(0.1)
|
||||
self.assertEqual(watcher.state.plots[0].crop, "carrot",
|
||||
"实时通道应该把另一方的操作推过来")
|
||||
self.assertEqual(
|
||||
watcher.state.plots[0].crop, "carrot", "实时通道应该把另一方的操作推过来"
|
||||
)
|
||||
self.assertEqual(watcher.mode, "ws")
|
||||
finally:
|
||||
watcher.stop()
|
||||
@@ -554,7 +579,7 @@ class TestLiveServer(unittest.TestCase):
|
||||
client.save()
|
||||
path = os.path.join(self.tmp, "persist.json")
|
||||
self.assertTrue(os.path.exists(path))
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
with open(path, encoding="utf-8") as handle:
|
||||
payload = json.load(handle)
|
||||
self.assertEqual(payload["state"]["plots"][0]["crop"], "wheat")
|
||||
finally:
|
||||
|
||||
Reference in new issue
Block a user