Збір працював, але подивитись на зібране було ніде: жодної сторінки, а 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>
159 lines
5.1 KiB
Go
159 lines
5.1 KiB
Go
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)
|
||
}
|