feat: 执行并提交 PDD 采集任务 (#32)

This commit is contained in:
chengma
2026-08-07 17:38:29 +08:00
parent 34b4b54b63
commit 2d1fdbea3f
17 changed files with 2156 additions and 108 deletions
+78 -8
View File
@@ -62,6 +62,18 @@ class LoadingDevice(FakeCollectDevice):
return '<hierarchy><node text="正在加载" /></hierarchy>'
class DisconnectingDevice(FakeCollectDevice):
def __init__(self, home_xml, spec_xml):
super().__init__(home_xml, spec_xml)
self.current_calls = 0
def app_current(self):
self.current_calls += 1
if self.current_calls > 1:
raise RuntimeError("device not found")
return super().app_current()
def keep_only_one_sku(xml_data: str) -> str:
"""从脱敏固件中删除蓝色和 L,只保留一个组合。"""
@@ -114,6 +126,19 @@ class PddCollectParserTest(unittest.TestCase):
["M", "L"],
)
def test_truncated_spec_name_prefers_full_content_description(self):
xml_data = self.spec_xml.replace(
'content-desc="红色" clickable="true"',
'text="红…" content-desc="红色" clickable="true"',
)
result = parse_spec_panel(xml_data)
self.assertEqual(result.dimensions[0].values[0].text, "红色")
without_description = xml_data.replace(' content-desc="红色"', "")
with self.assertRaises(PddCollectError) as raised:
parse_spec_panel(without_description)
self.assertEqual(raised.exception.code, "PDD_DATA_SKU_NAME_TRUNCATED")
def test_login_and_invalid_xml_have_different_error_codes(self):
with self.assertRaises(PddCollectError) as login:
parse_goods_page('<hierarchy><node text="手机号登录" /></hierarchy>')
@@ -150,14 +175,46 @@ class PddCollectParserTest(unittest.TestCase):
data = result.to_pdd_data()
self.assertEqual(data["schema_version"], 1)
self.assertEqual(data["goods"]["goods_id"], "123")
self.assertEqual(data["goods_id"], "123")
self.assertEqual(data["title"], "测试纯棉短袖商品")
self.assertEqual(data["shop_name"], "测试服饰旗舰店")
self.assertEqual(data["price_granularity"], "color")
self.assertEqual(data["metrics"]["sales"]["value"], 12000)
self.assertEqual(data["dimensions"][0]["key"], "color")
self.assertEqual(data["skus"][0]["options"], {"color": "红色", "size": "M"})
self.assertEqual(data["skus"][0]["price_cent"], 1000)
self.assertEqual(
data["skus"][0]["price_observed_at"],
{"color": "红色", "size": "M"},
)
self.assertEqual(data["source"]["device_address"], "USB-001")
self.assertIsNone(data["purchase"])
def test_price_is_sampled_once_per_available_color(self):
device = FakeCollectDevice(self.home_xml, self.spec_xml)
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
"client-001",
sleeper=lambda _seconds: None,
max_page_swipes=0,
max_spec_swipes=0,
)
data = service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
).to_pdd_data()
self.assertEqual(len(data["skus"]), 4)
red_skus = [
item for item in data["skus"] if item["options"]["color"] == "红色"
]
self.assertEqual(
{item["price_observed_at"]["size"] for item in red_skus}, {"M"}
)
# 1 次打开规格面板,另有红色点 1 次;尺码只读不点击。
self.assertEqual(len(device.clicks), 2)
def test_missing_goods_id_is_structured_error(self):
service = PddCollectService(
PddDeviceService(lambda _serial: object()),
@@ -195,7 +252,7 @@ class PddCollectParserTest(unittest.TestCase):
def test_page_timeout_has_specific_error_code(self):
device = LoadingDevice(self.home_xml, self.spec_xml)
ticks = iter((0.0, 2.0))
ticks = iter((0.0, 0.0, 0.0, 2.0))
service = PddCollectService(
PddDeviceService(lambda _serial: device),
"USB-001",
@@ -211,7 +268,21 @@ class PddCollectParserTest(unittest.TestCase):
)
self.assertEqual(raised.exception.code, "PDD_PAGE_TIMEOUT")
def test_incomplete_goods_data_does_not_return_partial_success(self):
def test_runtime_device_disconnect_has_stable_error_code(self):
service = PddCollectService(
PddDeviceService(
lambda _serial: DisconnectingDevice(self.home_xml, self.spec_xml)
),
"USB-001",
"client-001",
)
with self.assertRaises(PddCollectError) as raised:
service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
)
self.assertEqual(raised.exception.code, "DEVICE_DISCONNECTED")
def test_missing_shop_name_is_allowed(self):
incomplete_home = self.home_xml.replace(
'<node class="android.widget.TextView" text="店铺:测试服饰旗舰店" '
'bounds="[30,1050][600,1110]" visible-to-user="true" '
@@ -227,11 +298,10 @@ class PddCollectParserTest(unittest.TestCase):
max_page_swipes=0,
)
with self.assertRaises(PddCollectError) as raised:
service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
)
self.assertEqual(raised.exception.code, "PDD_DATA_SHOP_MISSING")
result = service.collect(
FakeTask("https://mobile.yangkeduo.com/goods.html?goods_id=123")
)
self.assertIsNone(result.shop_name)
if __name__ == "__main__":