Files
ratatoskr-go/internal/ui/log_panel_test.go
ki.sagidullin ee47785e1a
Some checks failed
CI / test (pull_request) Failing after 1m21s
CI / build-and-package (amd64, linux) (pull_request) Failing after 1m9s
CI / build-and-package (amd64, windows) (pull_request) Successful in 31s
perf(ui): кап буфера логов 10MB, O(1) вытеснение, дебаунс рендера
- LogBuffer: ленивое вытеснение через start + инкрементальный total убирает
  O(n^2) копирование entries[1:] при большом числе записей
- LogPanel: лимит 10MB (maxLogBytes), перерисовка ленты коалесцируется
  (200мс) — строка не пересобирается на каждый append
- build-publish-ui.ps1: корень проекта задаётся через PROJECT_PATH в
  .gitea-creds (относительно scripts/), без него — папка рядом со скриптом
- тесты: живые записи Entries/Render, кап по байтам
2026-08-21 23:01:54 +05:00

232 lines
8.4 KiB
Go
Raw 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 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")
}
}