Netpulse_SasS/server/internal/difftext/diff_test.go
byrsapty cdf5f1fb54 Візуальний diff конфігів
Збір працював, але подивитись на зібране було ніде: жодної сторінки, а
lines_added завжди 0 — порівняння ніхто не рахував.

difftext: власне порядкове порівняння з ділянками й контекстом.
Бібліотека принесла б підтримку слів, символів і кольорів у терміналі —
десяток речей, які тут не знадобляться.

- спільний початок і кінець відкидаються до основного алгоритму: у
  конфігах змінюється кілька рядків із тисячі, і квадратична таблиця
  будувалася б там, де досить порівняти десяток
- понад чотири мільйони клітинок дають truncated і грубу заміну блоку:
  точність там нічого не дає, а чесна позначка краща за правдоподібний,
  але вигаданий diff
- сусідні зміни зливаються в одну ділянку, інакше контекст дублюється

Результат кешується в ncm.diffs — diff двох версій незмінний назавжди.
Підсумок +N/−M заразом дозаписується у version, щоб список історії не
розшифровував два тіла на кожен рядок.

UI: сторінка «Конфіги» — хости зліва, історія й diff справа. Порівняння
з попередньою версією відкривається одразу: питання завжди одне — що
змінилось цього разу.

Перевірено наживо: друга версія стенду дала @@ −8,3 +8,4 @@ з одним
доданим рядком і контекстом, лічильник +1 −0 дозаписався, повний текст
на 292 байти читається.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 12:30:30 +03:00

159 lines
5.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package difftext
import (
"strings"
"testing"
)
func TestIdenticalGivesNoHunks(t *testing.T) {
cfg := "hostname sw1\ninterface Gi0/1\n description uplink\n"
r := Compare(cfg, cfg)
if len(r.Hunks) != 0 {
t.Fatalf("однакові конфіги дали %d ділянок", len(r.Hunks))
}
if r.LinesAdded != 0 || r.LinesRemoved != 0 {
t.Fatalf("+%d/-%d на однакових", r.LinesAdded, r.LinesRemoved)
}
}
func TestSingleLineChange(t *testing.T) {
before := "hostname sw1\nntp server 10.0.0.1\nlogging host 10.0.0.5\n"
after := "hostname sw1\nntp server 10.0.0.2\nlogging host 10.0.0.5\n"
r := Compare(before, after)
if r.LinesAdded != 1 || r.LinesRemoved != 1 {
t.Fatalf("очікували +1/-1, отримали +%d/-%d", r.LinesAdded, r.LinesRemoved)
}
if len(r.Hunks) != 1 {
t.Fatalf("очікували одну ділянку, отримали %d", len(r.Hunks))
}
var added, removed string
for _, l := range r.Hunks[0].Lines {
switch l.Op {
case OpAdd:
added = l.Text
case OpRemove:
removed = l.Text
}
}
if removed != "ntp server 10.0.0.1" || added != "ntp server 10.0.0.2" {
t.Fatalf("не ті рядки: -%q +%q", removed, added)
}
}
// Головна причина існування ділянок: у конфізі на тисячу рядків
// змінюється кілька, і показувати все — це ховати зміну.
func TestFarApartChangesGiveSeparateHunks(t *testing.T) {
var before, after []string
for i := 0; i < 200; i++ {
before = append(before, "line "+itoa(i))
after = append(after, "line "+itoa(i))
}
after[10] = "ЗМІНЕНО ЗВЕРХУ"
after[150] = "ЗМІНЕНО ЗНИЗУ"
r := CompareLines(before, after)
if len(r.Hunks) != 2 {
t.Fatalf("очікували дві ділянки, отримали %d", len(r.Hunks))
}
// Кожна ділянка має нести контекст, а не самий змінений рядок.
for i, h := range r.Hunks {
if len(h.Lines) < 3 {
t.Fatalf("ділянка %d без контексту: %d рядків", i, len(h.Lines))
}
}
}
// Сусідні зміни не мають плодити ділянки з дубльованим контекстом.
func TestNearbyChangesMerge(t *testing.T) {
before := strings.Split("a\nb\nc\nd\ne\nf\ng", "\n")
after := strings.Split("a\nB\nc\nD\ne\nf\ng", "\n")
r := CompareLines(before, after)
if len(r.Hunks) != 1 {
t.Fatalf("сусідні зміни дали %d ділянок замість однієї", len(r.Hunks))
}
}
func TestPureAddition(t *testing.T) {
r := Compare("a\nb\n", "a\nb\nc\n")
if r.LinesAdded != 1 || r.LinesRemoved != 0 {
t.Fatalf("+%d/-%d", r.LinesAdded, r.LinesRemoved)
}
}
func TestPureRemoval(t *testing.T) {
r := Compare("a\nb\nc\n", "a\nc\n")
if r.LinesAdded != 0 || r.LinesRemoved != 1 {
t.Fatalf("+%d/-%d", r.LinesAdded, r.LinesRemoved)
}
}
func TestEmptySides(t *testing.T) {
r := Compare("", "a\nb\n")
if r.LinesAdded != 2 || r.LinesRemoved != 0 {
t.Fatalf("порожнє → дві: +%d/-%d", r.LinesAdded, r.LinesRemoved)
}
r = Compare("a\nb\n", "")
if r.LinesAdded != 0 || r.LinesRemoved != 2 {
t.Fatalf("дві → порожнє: +%d/-%d", r.LinesAdded, r.LinesRemoved)
}
}
// Номери рядків мають вказувати на справжні позиції у файлах, інакше
// «перейти до рядка» в UI веде не туди.
func TestLineNumbersAreAbsolute(t *testing.T) {
before := "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nСТАРИЙ\nk\nl\nm\nn\no\np"
after := "a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nНОВИЙ\nk\nl\nm\nn\no\np"
r := Compare(before, after)
for _, h := range r.Hunks {
for _, l := range h.Lines {
if l.Op == OpRemove && l.Text == "СТАРИЙ" && l.OldNum != 11 {
t.Fatalf("старий рядок має номер %d замість 11", l.OldNum)
}
if l.Op == OpAdd && l.Text == "НОВИЙ" && l.NewNum != 11 {
t.Fatalf("новий рядок має номер %d замість 11", l.NewNum)
}
}
}
}
// Дуже великі й повністю різні тексти не мають з'їдати пам'ять: краще
// чесно позначити результат грубим, ніж будувати таблицю на мільярд
// клітинок.
func TestHugeDifferentTextsAreTruncated(t *testing.T) {
var a, b []string
for i := 0; i < 2500; i++ {
a = append(a, "старий "+itoa(i))
b = append(b, "новий "+itoa(i))
}
r := CompareLines(a, b)
if !r.Truncated {
t.Fatal("очікували позначку про грубе порівняння")
}
if r.LinesAdded != 2500 || r.LinesRemoved != 2500 {
t.Fatalf("+%d/-%d", r.LinesAdded, r.LinesRemoved)
}
}
// CRLF не має виглядати як зміна кожного рядка.
func TestCRLFIsNormalised(t *testing.T) {
r := Compare("a\r\nb\r\n", "a\nb\n")
if len(r.Hunks) != 0 {
t.Fatalf("різні переводи рядка показані як зміна: %+v", r.Hunks)
}
}
func itoa(n int) string {
if n == 0 {
return "0"
}
var b []byte
for n > 0 {
b = append([]byte{byte('0' + n%10)}, b...)
n /= 10
}
return string(b)
}