321 lines
12 KiB
Go
321 lines
12 KiB
Go
package ui
|
||
|
||
import "testing"
|
||
|
||
// NilLogPanel — no-op контракт (спец 12.8): Append ничего не делает, безопасно
|
||
// вызывается из горутин Hub и из тестов без Fyne.
|
||
func TestNilLogPanelAppendNoop(t *testing.T) {
|
||
p := NewNilLogPanel()
|
||
p.Append(LogLevelError, "line 1")
|
||
p.Append("", "")
|
||
p.Append(LogLevelDebug, "line 3")
|
||
}
|
||
|
||
// NormalizeLogLevel приводит любые уровни к каноническим четырём:
|
||
// синонимы маппятся, неизвестные/пустые значения считаются info.
|
||
func TestNormalizeLogLevel(t *testing.T) {
|
||
cases := []struct {
|
||
in string
|
||
want string
|
||
}{
|
||
{LogLevelError, LogLevelError},
|
||
{"ERROR", LogLevelError},
|
||
{" err ", LogLevelError},
|
||
{"fatal", LogLevelError},
|
||
{LogLevelWarning, LogLevelWarning},
|
||
{"WARN", LogLevelWarning},
|
||
{"Warning", LogLevelWarning},
|
||
{LogLevelInfo, LogLevelInfo},
|
||
{"INFO", LogLevelInfo},
|
||
{LogLevelDebug, LogLevelDebug},
|
||
{"DEBUG", LogLevelDebug},
|
||
{"trace", LogLevelDebug},
|
||
// события LogWriter без явного уровня и прочий мусор → info
|
||
{"log", LogLevelInfo},
|
||
{"", LogLevelInfo},
|
||
{" ", LogLevelInfo},
|
||
{"unknown", LogLevelInfo},
|
||
}
|
||
for _, c := range cases {
|
||
if got := NormalizeLogLevel(c.in); got != c.want {
|
||
t.Errorf("NormalizeLogLevel(%q) = %q, want %q", c.in, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// LogLevels — ровно четыре уровня фильтра в фиксированном порядке.
|
||
func TestLogLevelsContract(t *testing.T) {
|
||
want := []string{LogLevelError, LogLevelWarning, LogLevelInfo, LogLevelDebug}
|
||
if len(LogLevels) != len(want) {
|
||
t.Fatalf("len(LogLevels) = %d, want %d", len(LogLevels), len(want))
|
||
}
|
||
for i := range want {
|
||
if LogLevels[i] != want[i] {
|
||
t.Fatalf("LogLevels = %v, want %v", LogLevels, want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// allLevels — набор «отмечены все уровни» (состояние фильтра по умолчанию).
|
||
func allLevels() map[string]bool {
|
||
m := make(map[string]bool, len(LogLevels))
|
||
for _, lvl := range LogLevels {
|
||
m[lvl] = true
|
||
}
|
||
return m
|
||
}
|
||
|
||
// По умолчанию (все уровни отмечены) рендерится полный лог; уровни
|
||
// нормализуются при добавлении.
|
||
func TestLogBufferRenderAllByDefault(t *testing.T) {
|
||
b := NewLogBuffer(1000)
|
||
b.Append("error", "e1")
|
||
b.Append("warn", "w1") // синоним → warning
|
||
b.Append("log", "i1") // неизвестный уровень → info
|
||
b.Append("trace", "d1") // синоним → debug
|
||
|
||
got := b.Render(allLevels())
|
||
want := "e1\nw1\ni1\nd1\n"
|
||
if got != want {
|
||
t.Fatalf("Render(all) = %q, want %q", got, want)
|
||
}
|
||
|
||
entries := b.Entries()
|
||
wantLvls := []string{LogLevelError, LogLevelWarning, LogLevelInfo, LogLevelDebug}
|
||
for i, e := range entries {
|
||
if e.Level != wantLvls[i] {
|
||
t.Fatalf("entry[%d].Level = %q, want %q", i, e.Level, wantLvls[i])
|
||
}
|
||
}
|
||
}
|
||
|
||
// Показываются только записи выбранных уровней.
|
||
func TestLogBufferRenderSelectedOnly(t *testing.T) {
|
||
b := NewLogBuffer(1000)
|
||
b.Append(LogLevelError, "err line")
|
||
b.Append(LogLevelWarning, "warn line")
|
||
b.Append(LogLevelInfo, "info line")
|
||
b.Append(LogLevelDebug, "debug line")
|
||
|
||
onlyErrors := map[string]bool{LogLevelError: true}
|
||
if got, want := b.Render(onlyErrors), "err line\n"; got != want {
|
||
t.Fatalf("Render(error only) = %q, want %q", got, want)
|
||
}
|
||
|
||
errorsAndDebug := map[string]bool{LogLevelError: true, LogLevelDebug: true}
|
||
if got, want := b.Render(errorsAndDebug), "err line\ndebug line\n"; got != want {
|
||
t.Fatalf("Render(error+debug) = %q, want %q", got, want)
|
||
}
|
||
}
|
||
|
||
// Ни один уровень не выбран — лента пуста (и это не ошибка).
|
||
func TestLogBufferRenderEmptySelection(t *testing.T) {
|
||
b := NewLogBuffer(1000)
|
||
b.Append(LogLevelError, "err line")
|
||
b.Append(LogLevelInfo, "info line")
|
||
|
||
if got := b.Render(map[string]bool{}); got != "" {
|
||
t.Fatalf("Render(none) = %q, want empty", got)
|
||
}
|
||
if got := b.Render(nil); got != "" {
|
||
t.Fatalf("Render(nil) = %q, want empty", got)
|
||
}
|
||
}
|
||
|
||
// Фильтрация применяется и к уже загруженным записям: смена набора уровней
|
||
// меняет результат рендера того же буфера без новых Append.
|
||
func TestLogBufferRefilterLoadedEntries(t *testing.T) {
|
||
b := NewLogBuffer(1000)
|
||
b.Append(LogLevelError, "err line")
|
||
b.Append(LogLevelInfo, "info line")
|
||
|
||
all := allLevels()
|
||
if got, want := b.Render(all), "err line\ninfo line\n"; got != want {
|
||
t.Fatalf("Render(all) = %q, want %q", got, want)
|
||
}
|
||
delete(all, LogLevelInfo)
|
||
if got, want := b.Render(all), "err line\n"; got != want {
|
||
t.Fatalf("Render(error) after refilter = %q, want %q", got, want)
|
||
}
|
||
all[LogLevelInfo] = true // вернули галочку — запись снова видима
|
||
if got, want := b.Render(all), "err line\ninfo line\n"; got != want {
|
||
t.Fatalf("Render(all) after re-check = %q, want %q", got, want)
|
||
}
|
||
}
|
||
|
||
// Новые записи сразу попадают в ленту своего уровня.
|
||
func TestLogBufferNewEntriesVisibleImmediately(t *testing.T) {
|
||
b := NewLogBuffer(1000)
|
||
sel := map[string]bool{LogLevelError: true}
|
||
|
||
b.Append(LogLevelError, "first")
|
||
if got, want := b.Render(sel), "first\n"; got != want {
|
||
t.Fatalf("Render = %q, want %q", got, want)
|
||
}
|
||
|
||
b.Append(LogLevelInfo, "noise") // невидимый уровень — ленту не меняет
|
||
if got, want := b.Render(sel), "first\n"; got != want {
|
||
t.Fatalf("Render after invisible append = %q, want %q", got, want)
|
||
}
|
||
|
||
b.Append(LogLevelError, "second")
|
||
if got, want := b.Render(sel), "first\nsecond\n"; got != want {
|
||
t.Fatalf("Render after visible append = %q, want %q", got, want)
|
||
}
|
||
}
|
||
|
||
// Буфер ограничен: старые записи вытесняются, суммарный объём ≤ лимита.
|
||
func TestLogBufferBounded(t *testing.T) {
|
||
b := NewLogBuffer(10)
|
||
for i := 0; i < 5; i++ {
|
||
b.Append(LogLevelInfo, "0123456789") // каждая запись — ровно лимит
|
||
}
|
||
entries := b.Entries()
|
||
if len(entries) != 1 {
|
||
t.Fatalf("len(entries) = %d, want 1 (старые вытеснены)", len(entries))
|
||
}
|
||
if entries[0].Text != "0123456789" {
|
||
t.Fatalf("text = %q", entries[0].Text)
|
||
}
|
||
|
||
// одиночная запись длиннее лимита обрезается до последних maxLen символов
|
||
b2 := NewLogBuffer(4)
|
||
b2.Append(LogLevelInfo, "abcdefgh")
|
||
if got := b2.Entries()[0].Text; got != "efgh" {
|
||
t.Fatalf("oversized entry text = %q, want %q", got, "efgh")
|
||
}
|
||
}
|
||
|
||
// Буфер держит только живые записи: ленивое вытеснение (start) не даёт
|
||
// «мёртвым» записям попасть в Entries/Render.
|
||
func TestLogBufferLiveEntriesOnly(t *testing.T) {
|
||
b := NewLogBuffer(30)
|
||
for i := 0; i < 3; i++ {
|
||
b.Append(LogLevelInfo, "0123456789") // 10 байт + \n каждая
|
||
}
|
||
// сумма 33 > 30 → вытесняется первая запись, остаются две живые
|
||
entries := b.Entries()
|
||
if len(entries) != 2 {
|
||
t.Fatalf("len(Entries) = %d, want 2 (мёртвая запись скрыта)", len(entries))
|
||
}
|
||
if entries[0].Text != "0123456789" || entries[1].Text != "0123456789" {
|
||
t.Fatalf("unexpected live entries: %+v", entries)
|
||
}
|
||
if got, want := b.Render(allLevels()), "0123456789\n0123456789\n"; got != want {
|
||
t.Fatalf("Render = %q, want %q", got, want)
|
||
}
|
||
}
|
||
|
||
// Кап считается по байтам текста, а не по числу записей: короткие записи
|
||
// не вытесняются, пока их суммарный объём не превысит лимит.
|
||
func TestLogBufferBytesCap(t *testing.T) {
|
||
b := NewLogBuffer(100)
|
||
for i := 0; i < 3; i++ {
|
||
b.Append(LogLevelInfo, "hello") // 5 байт + \n
|
||
}
|
||
if got := len(b.Entries()); got != 3 {
|
||
t.Fatalf("len(Entries) = %d, want 3 (все помещаются)", got)
|
||
}
|
||
|
||
// одна большая запись вытесняет всё старое (live=1) и обрезается
|
||
b2 := NewLogBuffer(8)
|
||
b2.Append(LogLevelInfo, "short")
|
||
b2.Append(LogLevelInfo, "verylongtext")
|
||
entries := b2.Entries()
|
||
if len(entries) != 1 {
|
||
t.Fatalf("len(Entries) = %d, want 1", len(entries))
|
||
}
|
||
if entries[0].Text != "longtext" {
|
||
t.Fatalf("oversized entry text = %q, want %q", entries[0].Text, "longtext")
|
||
}
|
||
}
|
||
|
||
// Первый RenderNew отдаёт весь видимый текст, последующие — только новые
|
||
// записи (инкрементальная отрисовка: повторный вызов без Append пуст).
|
||
func TestLogBufferRenderNewDelta(t *testing.T) {
|
||
b := NewLogBuffer(1000)
|
||
sel := allLevels()
|
||
|
||
b.Append(LogLevelError, "e1")
|
||
b.Append(LogLevelInfo, "i1")
|
||
text, full := b.RenderNew(sel)
|
||
if full {
|
||
t.Fatalf("первый RenderNew не должен требовать полного пересбора, got full")
|
||
}
|
||
if text != "e1\ni1\n" {
|
||
t.Fatalf("первый RenderNew = %q, want %q", text, "e1\ni1\n")
|
||
}
|
||
|
||
// Без новых записей — пустой дельта.
|
||
text, full = b.RenderNew(sel)
|
||
if full || text != "" {
|
||
t.Fatalf("повторный RenderNew без новых записей = (%q, %v), want (\"\", false)", text, full)
|
||
}
|
||
|
||
// Новые записи — только их текст, без повторной отдачи старых.
|
||
b.Append(LogLevelInfo, "i2")
|
||
text, _ = b.RenderNew(sel)
|
||
if text != "i2\n" {
|
||
t.Fatalf("дельта RenderNew = %q, want %q", text, "i2\n")
|
||
}
|
||
}
|
||
|
||
// RenderNew применяет фильтр: невидимые уровни не попадают в дельту.
|
||
func TestLogBufferRenderNewFiltered(t *testing.T) {
|
||
b := NewLogBuffer(1000)
|
||
sel := map[string]bool{LogLevelError: true}
|
||
b.Append(LogLevelError, "e1")
|
||
b.Append(LogLevelInfo, "i1")
|
||
b.Append(LogLevelError, "e2")
|
||
|
||
text, _ := b.RenderNew(sel)
|
||
if text != "e1\ne2\n" {
|
||
t.Fatalf("RenderNew(error only) = %q, want %q", text, "e1\ne2\n")
|
||
}
|
||
}
|
||
|
||
// RenderNew уважает ResetRendered: после полного рендера новые записи
|
||
// добавляются дельтом, а не вытесняют уже выведенный текст.
|
||
func TestLogBufferRenderNewAfterReset(t *testing.T) {
|
||
b := NewLogBuffer(1000)
|
||
sel := allLevels()
|
||
b.Append(LogLevelInfo, "old")
|
||
b.Append(LogLevelInfo, "base")
|
||
|
||
text, full := b.RenderNew(sel)
|
||
if full || text != "old\nbase\n" {
|
||
t.Fatalf("RenderNew = (%q, %v), want (\"old\\nbase\\n\", false)", text, full)
|
||
}
|
||
b.ResetRendered() // имитация полной пересборки ленты
|
||
|
||
b.Append(LogLevelInfo, "new")
|
||
text, full = b.RenderNew(sel)
|
||
if full || text != "new\n" {
|
||
t.Fatalf("RenderNew после Reset = (%q, %v), want (\"new\\n\", false)", text, full)
|
||
}
|
||
}
|
||
|
||
// Вытеснение уже выведенных записей помечает рендер как требующий полного
|
||
// пересбора (stale): дельта не может убрать верхние строки.
|
||
func TestLogBufferRenderNewEvictionStale(t *testing.T) {
|
||
b := NewLogBuffer(25) // записи по 10 байт + \n: помещаются 2, 3-я вытесняет 1-ю
|
||
sel := allLevels()
|
||
|
||
b.Append(LogLevelInfo, "0123456789")
|
||
b.Append(LogLevelInfo, "0123456789")
|
||
text, full := b.RenderNew(sel)
|
||
if full || text != "0123456789\n0123456789\n" {
|
||
t.Fatalf("RenderNew = (%q, %v), want two lines", text, full)
|
||
}
|
||
|
||
// Третья запись вытесняет первую (уже отрисованную) — stale.
|
||
b.Append(LogLevelInfo, "0123456789")
|
||
text, full = b.RenderNew(sel)
|
||
if !full {
|
||
t.Fatalf("RenderNew после вытеснения = (full=%v), want full=true", full)
|
||
}
|
||
if text != "0123456789\n0123456789\n" {
|
||
t.Fatalf("полный текст после вытеснения = %q, want последние две записи", text)
|
||
}
|
||
}
|