package applog import ( "fmt" "os" "path/filepath" "reflect" "strings" "sync" "testing" ) func testService(t *testing.T) *Service { t.Helper() return &Service{dir: t.TempDir(), limit: maxFileBytes} } func TestRotationAndRestart(t *testing.T) { s := testService(t) // Fill exactly 2 MiB: no file may exceed the threshold and entries may not split. line := strings.Repeat("x", 65535) for i := 0; i < 32; i++ { if err := s.Append(line); err != nil { t.Fatal(err) } } info, err := os.Stat(filepath.Join(s.dir, "runtime.log")) if err != nil || info.Size() != maxFileBytes { t.Fatalf("threshold: %v, %v", info, err) } if err := s.Append("[2026-11-04 12:34:56] 重启后仍可查看"); err != nil { t.Fatal(err) } files, err := s.files() if err != nil || len(files) != 2 { t.Fatalf("archives: %v, %v", files, err) } for _, path := range files { info, err := os.Stat(path) if err != nil || info.Size() > maxFileBytes { t.Fatalf("oversized: %v, %v", info, err) } } restarted := &Service{dir: s.dir, limit: maxFileBytes} lines, err := restarted.Read() if err != nil || len(lines) != 33 || !strings.Contains(lines[32], "重启") { t.Fatalf("restore: %d, %v", len(lines), err) } if lines[0] != line { t.Fatal("archive contents changed") } } func TestRecentLimitAndClear(t *testing.T) { s := testService(t) s.limit = 20000 var content strings.Builder for i := 0; i < 2200; i++ { fmt.Fprintf(&content, "line %04d\n", i) } if err := os.WriteFile(filepath.Join(s.dir, "runtime-20261103.log"), []byte(content.String()), 0600); err != nil { t.Fatal(err) } if err := s.Append("latest\nmultiline\rerror"); err != nil { t.Fatal(err) } lines, err := s.Read() if err != nil || len(lines) != recentLines || lines[0] != "line 0201" || lines[len(lines)-1] != `latest\nmultiline\rerror` { t.Fatalf("recent lines: %d %v", len(lines), err) } other := filepath.Join(s.dir, "unrelated.txt") if err := os.WriteFile(other, []byte("keep"), 0600); err != nil { t.Fatal(err) } if err := s.Clear(); err != nil { t.Fatal(err) } if _, err := os.Stat(other); err != nil { t.Fatal("cleared unrelated file") } lines, err = s.Read() if err != nil || len(lines) != 0 { t.Fatalf("clear: %v %v", lines, err) } if err := s.Append("after clear"); err != nil { t.Fatal(err) } lines, err = s.Read() if err != nil || !reflect.DeepEqual(lines, []string{"after clear"}) { t.Fatalf("append after clear: %v %v", lines, err) } } func TestConcurrentAppendAndFailures(t *testing.T) { s := testService(t) s.limit = 30 var wg sync.WaitGroup for i := 0; i < 20; i++ { wg.Add(1) go func(i int) { defer wg.Done() if err := s.Append(fmt.Sprintf("event %02d", i)); err != nil { t.Error(err) } }(i) } wg.Wait() lines, err := s.Read() if err != nil || len(lines) != 20 { t.Fatalf("concurrent: %v %v", lines, err) } seen := map[string]bool{} for _, line := range lines { seen[line] = true } if len(seen) != 20 { t.Fatal("lost or duplicate entries") } if err := s.Append(strings.Repeat("x", maxEntryBytes+1)); err == nil { t.Fatal("accepted oversized record") } broken := &Service{dir: filepath.Join(s.dir, "runtime.log"), limit: maxFileBytes} if _, err := broken.Read(); err == nil { t.Fatal("read failure hidden") } if err := broken.Append("no room"); err == nil { t.Fatal("write failure hidden") } }