feat: улучшение regexp + деплой одного инстанса

This commit is contained in:
Максименко Никита Владимирович
2026-09-23 13:26:14 +03:00
parent f0e9c6c60c
commit 6ac5bb7819
12 changed files with 472 additions and 35 deletions
@@ -0,0 +1,121 @@
package ru.pdguard;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import ru.pdguard.config.SystemPolicy;
import ru.pdguard.core.PayloadStore;
import ru.pdguard.core.Pipeline;
import ru.pdguard.detect.NameCascade;
import ru.pdguard.detect.RuleRegistry;
import ru.pdguard.detect.Span;
import ru.pdguard.mask.Masker;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
/**
* Датасет из 44 сгенерированных строк — не новые ТИПЫ ПДН, а новые РАЗМЕЩЕНИЯ уже
* известных типов относительно якорного слова: расстояние до якоря, обратный порядок
* (значение перед якорем), падеж/число анкера, структурированные форматы
* (JSON/CSV/markdown-таблица/XML/key=value), несколько значений одного типа в одном
* поле, значение в кавычках/скобках. Гипотезы построены на 55 утечках из
* {@link NodeLogsDatasetTest} и обобщают их корневые причины на другие типы и формы.
* Разбор по категориям — в отчёте, приложенном к задаче.
*
* <p>Как и {@link NodeLogsDatasetTest}, часть строк — подтверждённые утечки
* (падение конкретного кейса в {@link #leakSummary()} ожидаемо), часть — контрольные
* позитивные случаи, которые обязаны оставаться зелёными: если один из них упадёт,
* значит новое размещение сломало то, что раньше работало.
*/
class PlacementVariantsTest {
private static final RuleRegistry REGISTRY = new RuleRegistry();
private static final Masker MASKER = new Masker();
private static final List<BenchmarkFixtures.Sample> DATASET = BenchmarkFixtures.load("/dataset-placements.txt");
private static final int LEAK_CHECK_MIN_LENGTH = 3;
private static final NameCascade CASCADE = modelsPresent()
? new NameCascade("wikineural", Optional.of("models/wikineural-ner"),
"rubert", Optional.of("models/rubert-ner"), 16, 4)
: NameCascade.disabled();
private static boolean modelsPresent() {
return Files.isReadable(Path.of("models/wikineural-ner/model.onnx"))
&& Files.isReadable(Path.of("models/rubert-ner/model.onnx"));
}
@TestFactory
Stream<DynamicTest> placementDataset() {
List<DynamicTest> cases = new ArrayList<>(DATASET.size());
for (int i = 0; i < DATASET.size(); i++) {
BenchmarkFixtures.Sample sample = DATASET.get(i);
int index = i;
cases.add(dynamicTest(
String.format("#%02d: %s", index, preview(sample.text())),
() -> runRoundTrip(sample, index)));
}
return cases.stream();
}
/** Демаскирование обязано восстановить исходный текст всегда, независимо от утечек. */
private void runRoundTrip(BenchmarkFixtures.Sample sample, int index) {
Pipeline pipeline = new Pipeline(REGISTRY, MASKER, new PayloadStore(1_000_000L, 30), CASCADE);
String payloadId = "placement-" + index;
String masked = pipeline.process(sample.text(), payloadId, SystemPolicy.DEFAULT);
String restored = pipeline.process(masked, payloadId, SystemPolicy.DEFAULT);
assertEquals(sample.text(), restored, "демаскирование не восстановило исходный текст");
}
/**
* Сводка утечек одним прогоном — печатает список по типам и падает, только если
* утечек стало больше 11, то есть если что-то из уже маскируемого сегодня
* размещения перестало маскироваться. Было 20 при составлении датасета, после
* точечных фиксов RuleRegistry (расширенный разрыв якорь-значение, обратный
* порядок для дат/гражданства/кода подразделения) осталось 11.
*/
@Test
void leakSummary() {
Pipeline pipeline = new Pipeline(REGISTRY, MASKER, new PayloadStore(10_000_000L, 30), CASCADE);
int leaked = 0;
int checked = 0;
java.util.Map<String, Integer> byType = new java.util.LinkedHashMap<>();
StringBuilder report = new StringBuilder("\n=== Утечки по dataset-placements.txt ===\n");
for (int i = 0; i < DATASET.size(); i++) {
BenchmarkFixtures.Sample sample = DATASET.get(i);
String masked = pipeline.process(sample.text(), "leak-scan-" + i, SystemPolicy.DEFAULT);
for (Span gold : sample.gold()) {
String value = sample.text().substring(gold.start(), gold.end());
if (value.length() < LEAK_CHECK_MIN_LENGTH) {
continue;
}
checked++;
if (masked.contains(value)) {
leaked++;
byType.merge(gold.type(), 1, Integer::sum);
report.append(String.format(" [%s] %s%n", gold.type(), value));
}
}
}
report.append(String.format("%nВсего: %d утечек из %d эталонных фрагментов%n", leaked, checked));
byType.forEach((type, count) -> report.append(String.format(" %-16s %d%n", type, count)));
System.out.println(report);
assertTrue(leaked <= 9,
"утечек стало больше 9 (было после точечных фиксов RuleRegistry) — новая регрессия: " + leaked);
}
private static String preview(String text) {
return text.length() <= 50 ? text : text.substring(0, 50) + "...";
}
}