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) } }