#!/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()