feat: улучшение regexp + деплой одного инстанса
This commit is contained in:
@@ -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) + "...";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user