package main // log_buffer_test.go — M8-4 ring buffer + level parse + rate limit 的單元測試 import ( "fmt" "testing" "time" ) func TestLogBuffer_AppendAndSnapshot(t *testing.T) { b := NewLogBuffer() if got := b.Size(); got != 0 { t.Fatalf("initial size=%d, want 0", got) } // 先塞 3 行 for i := 0; i < 3; i++ { b.Append(LogLine{ Ts: time.Now().UnixMilli(), Stream: "stdout", Line: fmt.Sprintf("line-%d", i), }) } if got := b.Size(); got != 3 { t.Fatalf("size after 3 appends=%d, want 3", got) } // Snapshot(0) 應回全部 all := b.Snapshot(0) if len(all) != 3 { t.Fatalf("snapshot(0) len=%d, want 3", len(all)) } for i := 0; i < 3; i++ { if all[i].Line != fmt.Sprintf("line-%d", i) { t.Fatalf("snapshot[%d].Line=%q", i, all[i].Line) } } // Snapshot(2) 應回最後 2 行 last2 := b.Snapshot(2) if len(last2) != 2 { t.Fatalf("snapshot(2) len=%d", len(last2)) } if last2[0].Line != "line-1" || last2[1].Line != "line-2" { t.Fatalf("snapshot(2) 內容錯誤: %+v", last2) } } func TestLogBuffer_WrapAround(t *testing.T) { b := NewLogBuffer() // 塞超過 cap 100 行 total := logBufferCap + 100 for i := 0; i < total; i++ { b.Append(LogLine{Line: fmt.Sprintf("line-%d", i)}) } if got := b.Size(); got != logBufferCap { t.Fatalf("size=%d, want %d", got, logBufferCap) } if got := b.Dropped(); got != 100 { t.Fatalf("dropped=%d, want 100", got) } // 取最後 3 行,應該是 line-{total-3} ... line-{total-1} last3 := b.Snapshot(3) if len(last3) != 3 { t.Fatalf("snapshot len=%d, want 3", len(last3)) } expected := []string{ fmt.Sprintf("line-%d", total-3), fmt.Sprintf("line-%d", total-2), fmt.Sprintf("line-%d", total-1), } for i, e := range expected { if last3[i].Line != e { t.Fatalf("last3[%d]=%q, want %q", i, last3[i].Line, e) } } // 取全部應該是 line-100 ... line-2099 all := b.Snapshot(0) if len(all) != logBufferCap { t.Fatalf("all len=%d", len(all)) } if all[0].Line != fmt.Sprintf("line-%d", total-logBufferCap) { t.Fatalf("all[0]=%q", all[0].Line) } if all[len(all)-1].Line != fmt.Sprintf("line-%d", total-1) { t.Fatalf("all[last]=%q", all[len(all)-1].Line) } } func TestLogBuffer_Reset(t *testing.T) { b := NewLogBuffer() for i := 0; i < 10; i++ { b.Append(LogLine{Line: fmt.Sprintf("x-%d", i)}) } b.Reset() if got := b.Size(); got != 0 { t.Fatalf("size after reset=%d, want 0", got) } snap := b.Snapshot(0) if len(snap) != 0 { t.Fatalf("snapshot after reset len=%d, want 0", len(snap)) } } func TestLogBuffer_RateLimit(t *testing.T) { b := NewLogBuffer() // 一秒內前 logRateLimitBurst 次 ShouldEmit 都應為 true allowed := 0 denied := 0 for i := 0; i < logRateLimitBurst+50; i++ { if b.ShouldEmit() { allowed++ } else { denied++ } } if allowed != logRateLimitBurst { t.Fatalf("allowed=%d, want %d", allowed, logRateLimitBurst) } if denied != 50 { t.Fatalf("denied=%d, want 50", denied) } } func TestParseLogLevel(t *testing.T) { cases := []struct { line string level string }{ {"2026/04/14 14:23:01 [INFO] server started", "info"}, {"[WARN] something odd", "warn"}, {"[ERROR] python died", "error"}, {"[DEBUG] trace", "debug"}, {"INFO: message", "info"}, {"[GIN] 200 | 2.1ms | GET /api/system/info", "info"}, {"[GIN] 404 | 0.8ms | GET /missing", "warn"}, {"[GIN] 500 | 12ms | POST /api/boom", "error"}, {"random text line", ""}, {"", ""}, } for _, c := range cases { got := parseLogLevel(c.line) if got != c.level { t.Errorf("parseLogLevel(%q)=%q, want %q", c.line, got, c.level) } } }