v5.1.0: тесты builder(20), extractor(15), scanner(20), replacer(10) — 65 тестов, 0 ошибок
Deploy drhider / validate (push) Waiting to run
Deploy drhider / validate (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
Тесты builder.py — создание ZIP и mapping.csv.
|
||||
|
||||
Запуск: python3 tests/test_builder.py
|
||||
"""
|
||||
|
||||
import io
|
||||
import zipfile
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from drhider.builder import build_zip, build_mapping_csv
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
def check(name, condition):
|
||||
global passed, failed
|
||||
if condition:
|
||||
passed += 1
|
||||
print(f" ✅ {name}")
|
||||
else:
|
||||
failed += 1
|
||||
print(f" ❌ {name}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Test build_zip
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== build_zip ===")
|
||||
|
||||
# 1. Базовый ZIP с одним файлом
|
||||
zip_data = build_zip([("test.md", b"content")])
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
names = zf.namelist()
|
||||
check("один файл в ZIP", len(names) == 1)
|
||||
check("имя test.md", "test.md" in names)
|
||||
check("контент верный", zf.read("test.md") == b"content")
|
||||
|
||||
# 2. ZIP с несколькими файлами
|
||||
zip_data = build_zip([
|
||||
("договор_obfuscated.md", "# Договор\nТекст".encode("utf-8")),
|
||||
("спецификация_obfuscated.md", "| Услуга | Цена |".encode("utf-8")),
|
||||
])
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
names = zf.namelist()
|
||||
check("два файла", len(names) == 2)
|
||||
check("mapping.csv нет", "mapping.csv" not in names)
|
||||
check("договор читается", "Договор" in zf.read("договор_obfuscated.md").decode("utf-8"))
|
||||
|
||||
# 3. ZIP с mapping.csv
|
||||
zip_data = build_zip(
|
||||
[("file.md", b"data")],
|
||||
mapping_csv="тип,оригинал,замена\nperson,Иванов,Иванов_0001\n"
|
||||
)
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
names = zf.namelist()
|
||||
check("mapping.csv есть", "mapping.csv" in names)
|
||||
csv_content = zf.read("mapping.csv").decode("utf-8")
|
||||
check("mapping с BOM", csv_content.startswith('\ufeff'))
|
||||
check("данные в mapping", "Иванов_0001" in csv_content)
|
||||
|
||||
# 4. ZIP с кириллицей в именах
|
||||
zip_data = build_zip([("договор_№123.md", "текст".encode("utf-8"))])
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
check("кириллическое имя читается", "договор_№123.md" in zf.namelist())
|
||||
|
||||
# 5. ZIP с бинарными данными
|
||||
binary = bytes(range(256))
|
||||
zip_data = build_zip([("binary.bin", binary)])
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
check("бинарный файл сохранён", zf.read("binary.bin") == binary)
|
||||
|
||||
# 6. Пустой mapping
|
||||
zip_data = build_zip([("f.md", b"x")], mapping_csv="")
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
check("пустой mapping не добавляется", "mapping.csv" not in zf.namelist())
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Test build_mapping_csv
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== build_mapping_csv ===")
|
||||
|
||||
# 1. Простой mapping
|
||||
mapping = {"Иванов И.И.": "Иванов_0001"}
|
||||
csv_str = build_mapping_csv(mapping)
|
||||
check("заголовок есть", csv_str.startswith("тип_данных,оригинал,замена"))
|
||||
check("данные есть", "Иванов_0001" in csv_str)
|
||||
|
||||
# 2. Пустой mapping
|
||||
csv_str = build_mapping_csv({})
|
||||
lines = csv_str.strip().split("\n")
|
||||
check("пустой — только заголовок", len(lines) == 1)
|
||||
|
||||
# 3. Сортировка
|
||||
mapping = {"Б": "Б_0001", "А": "А_0001"}
|
||||
csv_str = build_mapping_csv(mapping)
|
||||
check("сортировка А перед Б", csv_str.index("А_0001") < csv_str.index("Б_0001"))
|
||||
|
||||
# 4. Кавычки в значениях
|
||||
mapping = {'ООО "Ромашка"': "ООО_Технология_0001"}
|
||||
csv_str = build_mapping_csv(mapping)
|
||||
check("кавычки экранированы", 'ООО "Ромашка"' in csv_str or 'ООО ""Ромашка""' in csv_str)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Test: интеграция build_zip + build_mapping_csv
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== build_zip + build_mapping_csv ===")
|
||||
|
||||
mapping = {"Иванов И.И.": "Иванов_0001", "ООО Ромашка": "ООО_Технология_0001"}
|
||||
csv = build_mapping_csv(mapping)
|
||||
zip_data = build_zip([("doc.md", "# Договор с Иванов_0001".encode("utf-8"))], mapping_csv=csv)
|
||||
|
||||
with zipfile.ZipFile(io.BytesIO(zip_data)) as zf:
|
||||
check("два файла в ZIP", len(zf.namelist()) == 2)
|
||||
doc = zf.read("doc.md").decode("utf-8")
|
||||
check("doc с заменой", "Иванов_0001" in doc)
|
||||
csv_content = zf.read("mapping.csv").decode("utf-8")
|
||||
check("CSV с обеими заменами", "Иванов_0001" in csv_content and "ООО_Технология_0001" in csv_content)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Результат: {passed} OK, {failed} FAIL из {passed + failed}")
|
||||
if failed > 0:
|
||||
print("❌ ТЕСТЫ ПРОВАЛЕНЫ")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("✅ ВСЕ ТЕСТЫ ПРОЙДЕНЫ")
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
Тесты extractor.py — извлечение текста, ZIP-распаковка.
|
||||
|
||||
Запуск: python3 tests/test_extractor.py
|
||||
"""
|
||||
|
||||
import io
|
||||
import zipfile
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from drhider.extractor import extract_text, expand_zips, docx_to_markdown, pdf_to_markdown
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
def check(name, condition):
|
||||
global passed, failed
|
||||
if condition:
|
||||
passed += 1
|
||||
print(f" ✅ {name}")
|
||||
else:
|
||||
failed += 1
|
||||
print(f" ❌ {name}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# extract_text
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== extract_text ===")
|
||||
|
||||
# 1. .txt
|
||||
text = extract_text("test.txt", "Просто текст".encode("utf-8"))
|
||||
check(".txt — текст извлечён", text == "Просто текст")
|
||||
|
||||
# 2. .txt с кириллицей
|
||||
text = extract_text("документ.txt", "Привет мир".encode("utf-8"))
|
||||
check(".txt — кириллица", "Привет" in text)
|
||||
|
||||
# 3. .doc (бинарный — пропускается)
|
||||
text = extract_text("старый.doc", b"\xD0\xCF\x11\xE0\x00" * 10)
|
||||
check(".doc — заглушка", "not supported" in text.lower() or "binary" in text.lower())
|
||||
|
||||
# 4. Неизвестное расширение → UTF-8
|
||||
text = extract_text("file.xyz", "hello".encode("utf-8"))
|
||||
check(".xyz — как текст", text == "hello")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# expand_zips
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== expand_zips ===")
|
||||
|
||||
# 1. Не-ZIP файлы проходят без изменений
|
||||
files = [("test.txt", b"content", "text/plain")]
|
||||
result = expand_zips(files)
|
||||
check("не-ZIP — без изменений", len(result) == 1 and result[0][0] == "test.txt")
|
||||
|
||||
# 2. ZIP распаковывается
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w') as zf:
|
||||
zf.writestr("inner.txt", b"hello")
|
||||
result = expand_zips([("archive.zip", buf.getvalue(), "application/zip")])
|
||||
check("ZIP — распакован", len(result) == 1 and result[0][0] == "inner.txt")
|
||||
check("ZIP — контент верный", result[0][1] == b"hello")
|
||||
|
||||
# 3. ZIP с несколькими файлами
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w') as zf:
|
||||
zf.writestr("a.txt", b"aaa")
|
||||
zf.writestr("b.txt", b"bbb")
|
||||
result = expand_zips([("multi.zip", buf.getvalue(), "")])
|
||||
check("ZIP — два файла", len(result) == 2)
|
||||
|
||||
# 4. Path traversal защита
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w') as zf:
|
||||
zf.writestr("../../etc/passwd", b"bad")
|
||||
result = expand_zips([("evil.zip", buf.getvalue(), "")])
|
||||
check("path traversal — заблокирован", all("/" not in r[0] and ".." not in r[0] for r in result))
|
||||
|
||||
# 5. Директории пропускаются
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, 'w') as zf:
|
||||
zf.writestr("dir/", b"")
|
||||
zf.writestr("dir/file.txt", b"ok")
|
||||
result = expand_zips([("dirs.zip", buf.getvalue(), "")])
|
||||
check("директории — пропущены", len(result) == 1)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# docx_to_markdown
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== docx_to_markdown ===")
|
||||
|
||||
try:
|
||||
from docx import Document
|
||||
# Создаём тестовый DOCX
|
||||
doc = Document()
|
||||
doc.add_heading("Заголовок", level=1)
|
||||
p = doc.add_paragraph("Обычный текст")
|
||||
p.add_run(" жирный").bold = True
|
||||
doc.add_paragraph("")
|
||||
|
||||
# Таблица
|
||||
table = doc.add_table(rows=2, cols=2)
|
||||
table.rows[0].cells[0].text = "A"
|
||||
table.rows[0].cells[1].text = "B"
|
||||
table.rows[1].cells[0].text = "1"
|
||||
table.rows[1].cells[1].text = "2"
|
||||
|
||||
buf = io.BytesIO()
|
||||
doc.save(buf)
|
||||
md = docx_to_markdown(buf.getvalue())
|
||||
|
||||
check("DOCX — заголовок #", "# Заголовок" in md)
|
||||
check("DOCX — жирный **", "жирный**" in md and "**" in md)
|
||||
check("DOCX — таблица", "| A | B |" in md)
|
||||
check("DOCX — разделитель таблицы", "---|---|" in md)
|
||||
check("DOCX — данные таблицы", "| 1 | 2 |" in md)
|
||||
|
||||
except ImportError:
|
||||
print(" ⚠️ python-docx не установлен — тесты DOCX пропущены")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Итог
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Результат: {passed} OK, {failed} FAIL из {passed + failed}")
|
||||
if failed > 0:
|
||||
print("❌ ТЕСТЫ ПРОВАЛЕНЫ")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("✅ ВСЕ ТЕСТЫ ПРОЙДЕНЫ")
|
||||
@@ -0,0 +1,102 @@
|
||||
"""
|
||||
Тесты replacer.py — apply_replacements.
|
||||
|
||||
Запуск: python3 tests/test_replacer.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from drhider.replacer import apply_replacements
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
def check(name, condition):
|
||||
global passed, failed
|
||||
if condition:
|
||||
passed += 1
|
||||
print(f" ✅ {name}")
|
||||
else:
|
||||
failed += 1
|
||||
print(f" ❌ {name}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# apply_replacements
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== apply_replacements ===")
|
||||
|
||||
# 1. Простая замена
|
||||
text = "Директор Иванов И.И. подписал"
|
||||
mapping = {"Иванов И.И.": "Иванов_0001"}
|
||||
keys = sorted(mapping.keys(), key=len, reverse=True)
|
||||
result = apply_replacements(text, mapping, keys)
|
||||
check("простая замена", result == "Директор Иванов_0001 подписал")
|
||||
|
||||
# 2. Длинное before короткого
|
||||
text = "ИНН 123456789012 и ИНН 1234567890"
|
||||
mapping = {"ИНН 123456789012": "ИНН_0001", "ИНН 1234567890": "ИНН_0002"}
|
||||
keys = sorted(mapping.keys(), key=len, reverse=True)
|
||||
result = apply_replacements(text, mapping, keys)
|
||||
check("длинное раньше короткого", "ИНН_0001" in result and "ИНН_0002" in result)
|
||||
check("12-значный не задет 10-значным", result == "ИНН_0001 и ИНН_0002")
|
||||
|
||||
# 3. Границы слова
|
||||
text = "Email: test@mail.ru написать"
|
||||
mapping = {"test@mail.ru": "email_0001@fake"}
|
||||
keys = sorted(mapping.keys(), key=len, reverse=True)
|
||||
result = apply_replacements(text, mapping, keys)
|
||||
check("email заменён", "email_0001@fake" in result)
|
||||
|
||||
# 4. Множественные замены
|
||||
text = "Иванов И.И. и Петров А.С. подписали"
|
||||
mapping = {"Иванов И.И.": "Иванов_0001", "Петров А.С.": "Петров_0002"}
|
||||
keys = sorted(mapping.keys(), key=len, reverse=True)
|
||||
result = apply_replacements(text, mapping, keys)
|
||||
check("две замены", "Иванов_0001" in result and "Петров_0002" in result)
|
||||
|
||||
# 5. Спецсимволы в ключе
|
||||
text = 'ООО "Ромашка" заключила'
|
||||
mapping = {'ООО "Ромашка"': "ООО_Технология_0001"}
|
||||
keys = sorted(mapping.keys(), key=len, reverse=True)
|
||||
result = apply_replacements(text, mapping, keys)
|
||||
check("кавычки в ключе", "ООО_Технология_0001" in result)
|
||||
|
||||
# 6. Кириллица
|
||||
text = "Москва, ул. Ленина, д. 42"
|
||||
mapping = {"Москва": "Москва_0001", "Ленина": "Ленина_0015"}
|
||||
keys = sorted(mapping.keys(), key=len, reverse=True)
|
||||
result = apply_replacements(text, mapping, keys)
|
||||
check("кириллица заменена", "Москва_0001" in result and "Ленина_0015" in result)
|
||||
|
||||
# 7. Частичное совпадение не заменяется
|
||||
text = "Ивановский проспект"
|
||||
mapping = {"Иванов": "Иванов_0001"}
|
||||
keys = sorted(mapping.keys(), key=len, reverse=True)
|
||||
result = apply_replacements(text, mapping, keys)
|
||||
check("частичное совпадение — не заменено", result == "Ивановский проспект")
|
||||
|
||||
# 8. Пустой mapping
|
||||
text = "ничего не меняется"
|
||||
result = apply_replacements(text, {}, [])
|
||||
check("пустой mapping", result == text)
|
||||
|
||||
# 9. Markdown текст
|
||||
text = "**ООО Ромашка** заключила с **Иванов И.И.**"
|
||||
mapping = {"ООО Ромашка": "ООО_Технология_0001", "Иванов И.И.": "Иванов_0002"}
|
||||
keys = sorted(mapping.keys(), key=len, reverse=True)
|
||||
result = apply_replacements(text, mapping, keys)
|
||||
check("Markdown bold сохранён", "**ООО_Технология_0001**" in result)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Результат: {passed} OK, {failed} FAIL из {passed + failed}")
|
||||
if failed > 0:
|
||||
print("❌ ТЕСТЫ ПРОВАЛЕНЫ")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("✅ ВСЕ ТЕСТЫ ПРОЙДЕНЫ")
|
||||
@@ -0,0 +1,131 @@
|
||||
"""
|
||||
Тесты scanner.py — scan_regex, _next_token.
|
||||
|
||||
Запуск: python3 tests/test_scanner.py
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from drhider.scanner import scan_regex, _next_token
|
||||
|
||||
passed = 0
|
||||
failed = 0
|
||||
|
||||
def check(name, condition):
|
||||
global passed, failed
|
||||
if condition:
|
||||
passed += 1
|
||||
print(f" ✅ {name}")
|
||||
else:
|
||||
failed += 1
|
||||
print(f" ❌ {name}")
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# _next_token
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== _next_token ===")
|
||||
|
||||
counters = {}
|
||||
|
||||
# 1. Базовый токен
|
||||
token = _next_token("person_name", counters)
|
||||
check("person → Иванов/Петров/etc", token.endswith("_0001"))
|
||||
check("счётчик увеличился", counters.get("person_Иванов", 0) == 1 or any(v == 1 for v in counters.values()))
|
||||
|
||||
# 2. Второй токен того же типа — новый номер
|
||||
token2 = _next_token("person_name", counters)
|
||||
check("второй person — новый счётчик", any(v >= 1 for v in counters.values()))
|
||||
|
||||
# 3. Разные типы — разные счётчики
|
||||
token3 = _next_token("company", counters)
|
||||
check("company ≠ person", token3 != token)
|
||||
|
||||
# 4. Неизвестный тип → fallback
|
||||
counters2 = {}
|
||||
token4 = _next_token("unknown_xyz", counters2)
|
||||
check("неизвестный тип — Данные_0001", token4.startswith("Данные_") and "_0001" in token4)
|
||||
|
||||
# 5. phone
|
||||
counters3 = {}
|
||||
token5 = _next_token("phone", counters3)
|
||||
check("phone — +7_000_000_0001", token5 == "+7_000_000_0001")
|
||||
|
||||
# 6. address
|
||||
counters4 = {}
|
||||
token6 = _next_token("address", counters4)
|
||||
check("address — город_0001", token6.endswith("_0001") and len(token6) > 5)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# scan_regex
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print("\n=== scan_regex ===")
|
||||
|
||||
# 1. Телефон
|
||||
mapping = {}
|
||||
counters = {}
|
||||
scan_regex("Звоните +7 (495) 789-41-35", mapping, counters)
|
||||
check("телефон найден", len(mapping) == 1)
|
||||
check("телефон — токен", list(mapping.values())[0].startswith("+7_000_000_"))
|
||||
|
||||
# 2. Email
|
||||
mapping = {}
|
||||
counters = {}
|
||||
scan_regex("Пишите на info@nubes.ru", mapping, counters)
|
||||
check("email найден", len(mapping) == 1)
|
||||
check("email — токен", list(mapping.values())[0].startswith("email_"))
|
||||
|
||||
# 3. ИНН
|
||||
mapping = {}
|
||||
counters = {}
|
||||
scan_regex("ИНН 9706005293", mapping, counters)
|
||||
check("ИНН найден", len(mapping) == 1)
|
||||
check("ИНН — токен", list(mapping.values())[0].startswith("ИНН_"))
|
||||
|
||||
# 4. Компания
|
||||
mapping = {}
|
||||
counters = {}
|
||||
scan_regex('ООО "Ромашка" заключила договор', mapping, counters)
|
||||
check("компания найдена", len(mapping) == 1)
|
||||
check("компания — токен", "_" in list(mapping.values())[0])
|
||||
|
||||
# 5. ФИО
|
||||
mapping = {}
|
||||
counters = {}
|
||||
scan_regex("Директор Иванов И.И. подписал", mapping, counters)
|
||||
check("ФИО найдено", len(mapping) == 1)
|
||||
check("ФИО — токен", "_" in list(mapping.values())[0])
|
||||
|
||||
# 6. Множественные сущности
|
||||
mapping = {}
|
||||
counters = {}
|
||||
scan_regex("ИНН 9706005293, КПП 772401001, ОГРН 1207700098759", mapping, counters)
|
||||
check("три сущности", len(mapping) == 3)
|
||||
|
||||
# 7. Дубликаты не перезаписываются
|
||||
mapping = {}
|
||||
counters = {}
|
||||
scan_regex("+7 (495) 789-41-35 и ещё +7 (495) 789-41-35", mapping, counters)
|
||||
check("дубликат — одна запись", len(mapping) == 1)
|
||||
|
||||
# 8. Согласованность: один счётчик
|
||||
mapping = {}
|
||||
counters = {}
|
||||
scan_regex("ИНН 9706005293 и КПП 772401001", mapping, counters)
|
||||
inn_val = mapping["ИНН 9706005293"]
|
||||
kpp_val = mapping["КПП 772401001"]
|
||||
check("ИНН и КПП разные токены", inn_val != kpp_val)
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
print(f"\n{'='*50}")
|
||||
print(f"Результат: {passed} OK, {failed} FAIL из {passed + failed}")
|
||||
if failed > 0:
|
||||
print("❌ ТЕСТЫ ПРОВАЛЕНЫ")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print("✅ ВСЕ ТЕСТЫ ПРОЙДЕНЫ")
|
||||
Reference in New Issue
Block a user