Smail/test/backend/runtime_logs_test.go
smail ea2808f72b
Some checks are pending
Security Checks / security (push) Waiting to run
精简启动横幅日志并保证历史过滤分页准确
2026-09-09 00:39:01 +08:00

239 lines
7.6 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 runtimelog
import (
"bytes"
"compress/gzip"
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"git.pcux.cn/xuans/Smail/utils/logpolicy"
)
func TestRuntimeLogStartupPresentation(t *testing.T) {
header := "[2026-09-08T14:55:23Z] [info][2026-09-08 22:55:23]"
source := "git.pcux.cn/xuans/Smail/internal/app/app.go"
for _, location := range []string{source, "internal/app/app.go", "/srv/Smail/internal/app/app.go", `D:\Desktop\Smail\backend\internal\app\app.go`} {
for _, request := range []string{"", "[request-42]"} {
prefix := header + request + "[" + location + ":39]"
for _, message := range []string{
"*******************************************************************", "***\tServer Version: dev-test ",
"***\tGit Commit Hash: abc123 ", "***\tBuild Date: 2026-09-08T14:55:12Z ",
"***\tBuild GoLang Version: go version go1.26.6 windows/amd64 ",
} {
if _, keep := presentLine(prefix + message); keep {
t.Fatalf("冗余启动记录仍可见:%s", prefix+message)
}
}
for _, tail := range []string{" ", ` \n`, `\r\n`} {
line := prefix + "***\tServer Start Success" + tail
if got, keep := presentLine(line); !keep || got != line {
t.Fatalf("旧启动事件未保留完整原文:%q %v", got, keep)
}
}
}
}
for _, line := range []string{
header + "[" + source + ":39]服务启动,版本:dev-test",
header + "[" + source + ":39]***\tServer Start Success 后续内容",
header + "[" + source + ":39]***\t未知启动消息",
header + "[other/app.go:39]***\tBuild Date: 保留业务消息",
header + "[other/app.go:39]********",
header + "[" + source + ":39]Build Date: 不属于横幅",
strings.Replace(header, "[info]", "[warning]", 1) + "[" + source + ":39]***\tBuild Date: 保留警告",
strings.Replace(header, "[info]", "[error]", 1) + "[" + source + ":39]********",
"[2026-09-08T14:55:23Z] 未知格式保留",
} {
if got, keep := presentLine(line); !keep || got != line {
t.Fatalf("误改正常或未知记录:%q => %q %v", line, got, keep)
}
}
}
func TestRuntimeLogStartupFilteringBeforePagination(t *testing.T) {
for _, compressed := range []bool{false, true} {
for _, onlyNoise := range []bool{false, true} {
t.Run(fmt.Sprintf("compressed=%v/onlyNoise=%v", compressed, onlyNoise), func(t *testing.T) {
w, err := New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer w.Close()
now := time.Date(2026, 9, 8, 12, 0, 0, 0, time.UTC)
w.now = func() time.Time { return now }
header := "[2026-09-08T12:00:00Z] [info][2026-09-08 20:00:00][internal/app/app.go:39]"
noise := header + "********\n"
original := strings.Repeat(noise, 60)
var expected []string
if !onlyNoise {
original += header + "***\tServer Start Success \n" + noise
original += header + "***\tServer Version: old \n" + header + "HTTP 服务就绪\n"
original += header + "***\tBuild Date: old \n" + strings.Replace(header, "[info]", "[warning]", 1) + "需处理的警告\n"
expected = []string{header + "***\tServer Start Success ", header + "HTTP 服务就绪", strings.Replace(header, "[info]", "[warning]", 1) + "需处理的警告"}
}
original += strings.Repeat(noise, 60)
name := prefix + "2026-09-08.log"
data := []byte(original)
if compressed {
name += ".gz"
var buffer bytes.Buffer
archive := gzip.NewWriter(&buffer)
if _, err := archive.Write(data); err != nil {
t.Fatal(err)
}
if err := archive.Close(); err != nil {
t.Fatal(err)
}
data = buffer.Bytes()
}
if err := w.root.WriteFile(name, data, 0600); err != nil {
t.Fatal(err)
}
var received []string
for offset := 0; ; {
page, err := w.Read(context.Background(), name, offset, 2)
if err != nil {
t.Fatal(err)
}
received = append(received, page.Lines...)
if page.HasMore && len(page.Lines) != 2 {
t.Fatalf("过滤导致空页或短页:%+v", page)
}
if !page.HasMore {
break
}
offset += len(page.Lines)
if offset > 4 {
t.Fatal("分页重复返回记录")
}
}
if !reflect.DeepEqual(received, expected) {
t.Fatalf("分页丢失或重复记录:%q,期望 %q", received, expected)
}
page, err := w.Read(context.Background(), name, len(expected), 2)
if err != nil || len(page.Lines) != 0 || page.HasMore {
t.Fatalf("尾部隐藏记录触发多余下一页:%+v %v", page, err)
}
after, err := w.root.ReadFile(name)
if err != nil || !bytes.Equal(after, data) {
t.Fatalf("只读过滤修改了原文件:%v", err)
}
})
}
}
}
func TestRuntimeLogCompressionAndExactRetention(t *testing.T) {
root := t.TempDir()
w, err := New(root)
if err != nil {
t.Fatal(err)
}
defer w.Close()
now := time.Date(2026, 9, 8, 12, 0, 0, 0, time.UTC)
cutoff := logpolicy.Cutoff(now)
w.now = func() time.Time { return cutoff }
for _, at := range []time.Time{cutoff.Add(-time.Hour), cutoff, cutoff.Add(time.Hour), now.Add(-8 * 24 * time.Hour), now} {
if err := w.WriteAt(at, "sample\nsecond line"); err != nil {
t.Fatal(err)
}
}
w.now = func() time.Time { return now }
ctx := context.Background()
if err := w.Maintain(ctx, now); err != nil {
t.Fatal(err)
}
files, err := w.Files()
if err != nil || len(files) != 3 {
t.Fatalf("files: %+v %v", files, err)
}
name := prefix + cutoff.Format("2006-01-02") + ".log.gz"
page, err := w.Read(ctx, name, 0, 100)
if err != nil || len(page.Lines) != 2 {
t.Fatalf("boundary: %+v %v", page, err)
}
if !strings.Contains(page.Lines[0], `\n`) {
t.Fatal("multiline event split")
}
// 压缩后仍须逐条裁剪,不能按整个归档日期删除有效记录。
now = now.Add(30 * time.Minute)
if err := w.Maintain(ctx, now); err != nil {
t.Fatal(err)
}
page, err = w.Read(ctx, name, 0, 100)
if err != nil || len(page.Lines) != 1 {
t.Fatalf("compressed boundary: %+v %v", page, err)
}
now = now.Add(24 * time.Hour)
if err := w.Maintain(ctx, now); err != nil {
t.Fatal(err)
}
if _, err := w.root.Stat(name); !os.IsNotExist(err) {
t.Fatalf("expired archive still exists: %v", err)
}
if _, err := w.Read(ctx, "../config/config.json", 0, 100); err == nil {
t.Fatal("path traversal accepted")
}
if err := w.Maintain(ctx, now); err != nil {
t.Fatal(err)
}
}
func TestRuntimeLogCorruptionAndUnknownFilesArePreserved(t *testing.T) {
root := t.TempDir()
w, err := New(root)
if err != nil {
t.Fatal(err)
}
defer w.Close()
now := time.Now().UTC()
w.now = func() time.Time { return now }
directory := filepath.Join(root, "log")
name := prefix + now.Add(-40*24*time.Hour).Format("2006-01-02") + ".log.gz"
if err := os.WriteFile(filepath.Join(directory, name), []byte("broken"), 0600); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(directory, "manual.log"), []byte("keep"), 0600); err != nil {
t.Fatal(err)
}
if err := w.Maintain(context.Background(), now); err == nil {
t.Fatal("corruption not reported")
}
for _, file := range []string{name, "manual.log"} {
if _, err := os.Stat(filepath.Join(directory, file)); err != nil {
t.Fatal("unmanaged or damaged evidence deleted")
}
}
}
func TestRuntimeLogCurrentWritesDoNotWaitForArchiveRead(t *testing.T) {
w, err := New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer w.Close()
now := time.Now()
if err := w.WriteAt(now, "first"); err != nil {
t.Fatal(err)
}
w.archiveMu.Lock()
done := make(chan error, 1)
go func() { done <- w.WriteAt(now, "second") }()
select {
case err := <-done:
w.archiveMu.Unlock()
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
w.archiveMu.Unlock()
<-done
t.Fatal("current log write waited for archive lock")
}
}