Собирает список сервисов проверки IP (ip-lookup, leak-test, geoip-db, fraud-score) в geosite.dat для Xray/3x-ui, а также в sing-box, Clash/mihomo, AdGuard, plain text и JSON. Сборщик на чистой стандартной библиотеке: пишет protobuf вручную, после записи разбирает .dat обратно и сверяет с исходником. Формат сверен с настоящим geosite.dat и проверен реальным Xray 26.3.27. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
6.6 KiB
Python
175 lines
6.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Тесты сборщика: python -m unittest discover -s tools -t tools"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import json
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
import build
|
|
from build import BuildError, Rule
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def data(**files: str):
|
|
"""Подменяет data/ временным каталогом с заданными категориями."""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
for name, content in files.items():
|
|
(root / f"{name.replace('_', '-')}.txt").write_text(content, encoding="utf-8")
|
|
original = build.DATA_DIR
|
|
build.DATA_DIR = root
|
|
try:
|
|
yield root
|
|
finally:
|
|
build.DATA_DIR = original
|
|
|
|
|
|
class ParseTest(unittest.TestCase):
|
|
def test_default_type_is_domain(self):
|
|
with data(a="example.com\n"):
|
|
self.assertEqual(build.resolve("a"), [Rule("domain", "example.com")])
|
|
|
|
def test_all_prefixes(self):
|
|
source = "domain:a.com\nfull:b.com\nkeyword:cc\nregexp:^d[0-9]+\\.com$\n"
|
|
with data(a=source):
|
|
self.assertEqual(
|
|
build.resolve("a"),
|
|
[
|
|
Rule("domain", "a.com"),
|
|
Rule("full", "b.com"),
|
|
Rule("keyword", "cc"),
|
|
Rule("regexp", r"^d[0-9]+\.com$"),
|
|
],
|
|
)
|
|
|
|
def test_comments_blank_lines_and_attributes(self):
|
|
with data(a="# заголовок\n\na.com @ads @cn # хвост\n"):
|
|
self.assertEqual(build.resolve("a"), [Rule("domain", "a.com", ("ads", "cn"))])
|
|
|
|
def test_normalisation(self):
|
|
with data(a="Example.COM.\nDOMAIN:пример.рф\n"):
|
|
values = [r.value for r in build.resolve("a")]
|
|
self.assertIn("example.com", values)
|
|
self.assertIn("xn--e1afmkfd.xn--p1ai", values)
|
|
|
|
def test_include_dedupes_and_sorts(self):
|
|
with data(a="include:b\nz.com\na.com\n", b="a.com\nfull:m.com\n"):
|
|
self.assertEqual(
|
|
build.resolve("a"),
|
|
[Rule("domain", "a.com"), Rule("domain", "z.com"), Rule("full", "m.com")],
|
|
)
|
|
|
|
def test_rejects_bad_input(self):
|
|
cases = {
|
|
"битый домен": "not_a_domain\n",
|
|
"битый regexp": "regexp:[unclosed\n",
|
|
"атрибут без @": "a.com ads\n",
|
|
"неизвестный include": "include:missing\n",
|
|
}
|
|
for label, source in cases.items():
|
|
with self.subTest(label), data(a=source):
|
|
with self.assertRaises(BuildError):
|
|
build.resolve("a")
|
|
|
|
def test_detects_include_cycle(self):
|
|
with data(a="include:b\n", b="include:a\n"):
|
|
with self.assertRaisesRegex(BuildError, "цикл"):
|
|
build.resolve("a")
|
|
|
|
|
|
class ProtobufTest(unittest.TestCase):
|
|
def test_known_byte_layout(self):
|
|
blob = build.encode_geosite_dat({"t": [Rule("domain", "a.com")]})
|
|
# 0a 0e GeoSiteList.entry, 14 байт
|
|
# 0a 01 54 country_code = "T" (в .dat теги в верхнем регистре)
|
|
# 12 09 GeoSite.domain, 9 байт
|
|
# 08 02 type = Domain(2)
|
|
# 12 05 612e636f6d value = "a.com"
|
|
self.assertEqual(blob.hex(), "0a0e" "0a0154" "1209" "0802" "1205" "612e636f6d")
|
|
|
|
def test_keyword_type_is_omitted_as_proto3_default(self):
|
|
blob = build.encode_geosite_dat({"t": [Rule("keyword", "ip")]})
|
|
self.assertNotIn(b"\x08\x00", blob)
|
|
self.assertEqual(build.decode_geosite_dat(blob)["T"], [Rule("keyword", "ip")])
|
|
|
|
def test_round_trip_with_attributes(self):
|
|
categories = {
|
|
"one": [Rule("domain", "a.com", ("ads",)), Rule("full", "b.com")],
|
|
"two": [Rule("keyword", "ip"), Rule("regexp", r"^x\d+$")],
|
|
}
|
|
decoded = build.decode_geosite_dat(build.encode_geosite_dat(categories))
|
|
self.assertEqual(decoded, {k.upper(): v for k, v in categories.items()})
|
|
|
|
|
|
class RenderTest(unittest.TestCase):
|
|
def test_singbox_expands_suffix_to_apex_plus_dot(self):
|
|
rendered = json.loads(build.render_singbox([Rule("domain", "a.com"), Rule("full", "b.com")]))
|
|
self.assertEqual(rendered["rules"][0]["domain"], ["a.com", "b.com"])
|
|
self.assertEqual(rendered["rules"][0]["domain_suffix"], [".a.com"])
|
|
|
|
def test_clash_yaml_skips_unsupported_types(self):
|
|
rules = [Rule("domain", "a.com"), Rule("full", "b.com"), Rule("keyword", "ip")]
|
|
payload = build.render_clash_yaml("t", rules)
|
|
self.assertIn("- '+.a.com'", payload)
|
|
self.assertIn("- 'b.com'", payload)
|
|
self.assertNotIn("ip", payload.split("payload:")[1])
|
|
|
|
def test_clash_list_keeps_all_types(self):
|
|
rules = [Rule("domain", "a.com"), Rule("keyword", "ip"), Rule("regexp", "^x$")]
|
|
body = build.render_clash_list("t", rules)
|
|
self.assertIn("DOMAIN-SUFFIX,a.com", body)
|
|
self.assertIn("DOMAIN-KEYWORD,ip", body)
|
|
self.assertIn("DOMAIN-REGEX,^x$", body)
|
|
|
|
|
|
class CheckTest(unittest.TestCase):
|
|
def test_clean_sources_pass(self):
|
|
with data(a="a.com\nb.com\n"):
|
|
self.assertEqual(build.check(), 0)
|
|
|
|
def test_duplicate_is_reported(self):
|
|
with data(a="a.com\na.com\n"):
|
|
self.assertEqual(build.check(), 1)
|
|
|
|
def test_subdomain_covered_by_parent_is_reported(self):
|
|
with data(a="a.com\ndeep.sub.a.com\n"):
|
|
self.assertEqual(build.check(), 1)
|
|
|
|
def test_real_repository_data_is_clean(self):
|
|
self.assertEqual(build.check(), 0)
|
|
|
|
|
|
class BuildTest(unittest.TestCase):
|
|
def test_produces_every_format(self):
|
|
expected = [
|
|
"geosite.dat",
|
|
"ipcheck.dat",
|
|
"geosite.dat.sha256sum",
|
|
"metadata.json",
|
|
"txt/b.txt",
|
|
"src/b.txt",
|
|
"json/b.json",
|
|
"sing-box/b.json",
|
|
"clash/b.yaml",
|
|
"clash/b.list",
|
|
"adguard/b.txt",
|
|
]
|
|
with data(a="a.com\n", b="include:a\nfull:c.com\n"), tempfile.TemporaryDirectory() as out:
|
|
out_dir = Path(out) / "dist"
|
|
self.assertEqual(build.build(out_dir, None, None), 0)
|
|
for rel in expected:
|
|
self.assertTrue((out_dir / rel).is_file(), rel)
|
|
self.assertEqual(
|
|
(out_dir / "geosite.dat").read_bytes(), (out_dir / "ipcheck.dat").read_bytes()
|
|
)
|
|
meta = json.loads((out_dir / "metadata.json").read_text(encoding="utf-8"))
|
|
self.assertEqual(meta["categories"], {"a": 1, "b": 2})
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|