193 lines
7.1 KiB
Go
193 lines
7.1 KiB
Go
package logger
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// makeLogFile создаёт gpio-файл с заданным именем и выставляет ему ModTime.
|
||
func makeLogFile(t *testing.T, dir, name string, modTime time.Time) string {
|
||
t.Helper()
|
||
path := filepath.Join(dir, name)
|
||
if err := os.WriteFile(path, []byte("data"), 0644); err != nil {
|
||
t.Fatalf("write %s: %v", name, err)
|
||
}
|
||
if err := os.Chtimes(path, modTime, modTime); err != nil {
|
||
t.Fatalf("chtimes %s: %v", name, err)
|
||
}
|
||
return path
|
||
}
|
||
|
||
// nameForHour формирует имя файла в формате rotation.go для момента t.
|
||
func nameForHour(t time.Time) string {
|
||
return fmt.Sprintf("gpio-%04d-%02d-%02d-%02d.bin",
|
||
t.Year(), t.Month(), t.Day(), t.Hour())
|
||
}
|
||
|
||
func exists(path string) bool {
|
||
_, err := os.Stat(path)
|
||
return err == nil
|
||
}
|
||
|
||
// TestCleanByAge_RollingWindow: старые файлы удаляются, но свежие в окне 72ч и
|
||
// минимум minKeepFiles всегда сохраняются.
|
||
func TestCleanByAge_RollingWindow(t *testing.T) {
|
||
dir := t.TempDir()
|
||
now := time.Now()
|
||
|
||
// Имена файлов хранят время с точностью до часа, поэтому держим обе группы
|
||
// с запасом от границы 72ч, чтобы округление до часа не решало исход теста.
|
||
var oldFiles, freshFiles []string
|
||
for hoursAgo := 200; hoursAgo >= 76; hoursAgo-- { // старше 72ч с запасом 4ч
|
||
ft := now.Add(-time.Duration(hoursAgo) * time.Hour)
|
||
oldFiles = append(oldFiles, makeLogFile(t, dir, nameForHour(ft), ft))
|
||
}
|
||
for hoursAgo := 68; hoursAgo >= 1; hoursAgo-- { // свежее 72ч с запасом 4ч
|
||
ft := now.Add(-time.Duration(hoursAgo) * time.Hour)
|
||
freshFiles = append(freshFiles, makeLogFile(t, dir, nameForHour(ft), ft))
|
||
}
|
||
|
||
r := NewRetention(72, 0, nil)
|
||
deleted, _ := r.cleanByAge(dir)
|
||
|
||
if deleted != len(oldFiles) {
|
||
t.Errorf("удалено %d, ожидалось %d старых файлов", deleted, len(oldFiles))
|
||
}
|
||
for _, p := range oldFiles {
|
||
if exists(p) {
|
||
t.Errorf("старый файл не удалён: %s", filepath.Base(p))
|
||
}
|
||
}
|
||
for _, p := range freshFiles {
|
||
if !exists(p) {
|
||
t.Errorf("свежий файл ошибочно удалён: %s", filepath.Base(p))
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestCleanByAge_MinKeepFloor: даже когда ВСЕ файлы старше cutoff, minKeepFiles
|
||
// самых свежих остаются — директория не стирается целиком.
|
||
func TestCleanByAge_MinKeepFloor(t *testing.T) {
|
||
dir := t.TempDir()
|
||
now := time.Now()
|
||
|
||
// 5 файлов, все старше 72ч.
|
||
var paths []string
|
||
for i := 0; i < 5; i++ {
|
||
ft := now.Add(-time.Duration(200-i) * time.Hour)
|
||
paths = append(paths, makeLogFile(t, dir, nameForHour(ft), ft))
|
||
}
|
||
|
||
r := NewRetention(72, 0, nil)
|
||
deleted, _ := r.cleanByAge(dir)
|
||
|
||
remaining := 5 - deleted
|
||
if remaining != minKeepFiles {
|
||
t.Errorf("осталось %d файлов, ожидалось minKeepFiles=%d", remaining, minKeepFiles)
|
||
}
|
||
// Должны выжить именно самые свежие (последние два по индексу).
|
||
for _, p := range paths[len(paths)-minKeepFiles:] {
|
||
if !exists(p) {
|
||
t.Errorf("свежайший файл должен был сохраниться: %s", filepath.Base(p))
|
||
}
|
||
}
|
||
}
|
||
|
||
// TestCleanByAge_ClockJumpGuard: имя файла «старое», но ModTime свежий
|
||
// (сценарий скачка часов после NTP). Файл должен остаться.
|
||
func TestCleanByAge_ClockJumpGuard(t *testing.T) {
|
||
dir := t.TempDir()
|
||
now := time.Now()
|
||
|
||
// Достаточно файлов, чтобы floor не мешал проверке guard.
|
||
// Настоящие старые файлы (и имя, и ModTime старые).
|
||
oldTime := now.Add(-200 * time.Hour)
|
||
for i := 0; i < 4; i++ {
|
||
ft := oldTime.Add(time.Duration(i) * time.Hour)
|
||
makeLogFile(t, dir, nameForHour(ft), ft)
|
||
}
|
||
// Файл со «старым» именем, но записанный только что.
|
||
staleName := nameForHour(now.Add(-300 * time.Hour))
|
||
guarded := makeLogFile(t, dir, staleName, now)
|
||
|
||
r := NewRetention(72, 0, nil)
|
||
r.cleanByAge(dir)
|
||
|
||
if !exists(guarded) {
|
||
t.Errorf("файл со свежим ModTime не должен удаляться при скачке часов: %s", staleName)
|
||
}
|
||
}
|
||
|
||
// TestCleanByAge_ParseFailureFallsBackToModTime: нераспознаваемое имя не должно
|
||
// приводить к удалению свежего файла (fallback на ModTime).
|
||
func TestCleanByAge_ParseFailureFallsBackToModTime(t *testing.T) {
|
||
dir := t.TempDir()
|
||
now := time.Now()
|
||
|
||
// Контекст из нормальных старых файлов, чтобы был выбор для удаления.
|
||
for i := 0; i < 4; i++ {
|
||
ft := now.Add(-time.Duration(200-i) * time.Hour)
|
||
makeLogFile(t, dir, nameForHour(ft), ft)
|
||
}
|
||
// Битое имя, но свежий ModTime.
|
||
garbage := makeLogFile(t, dir, "gpio-garbage.bin", now)
|
||
|
||
r := NewRetention(72, 0, nil)
|
||
r.cleanByAge(dir)
|
||
|
||
if !exists(garbage) {
|
||
t.Error("файл с нераспознаваемым именем, но свежим ModTime, не должен удаляться")
|
||
}
|
||
|
||
// А вот битое имя со СТАРЫМ ModTime — законно удаляется как старое.
|
||
dir2 := t.TempDir()
|
||
for i := 0; i < 4; i++ {
|
||
ft := now.Add(-time.Duration(20-i) * time.Hour) // свежие, чтобы не удалялись
|
||
makeLogFile(t, dir2, nameForHour(ft), ft)
|
||
}
|
||
oldGarbage := makeLogFile(t, dir2, "gpio-garbage.bin", now.Add(-200*time.Hour))
|
||
r.cleanByAge(dir2)
|
||
if exists(oldGarbage) {
|
||
t.Error("битое имя со старым ModTime должно удаляться как старый файл")
|
||
}
|
||
}
|
||
|
||
// TestCleanBySize_KeepsFloor: очистка по размеру не опускается ниже minKeepFiles.
|
||
func TestCleanBySize_KeepsFloor(t *testing.T) {
|
||
dir := t.TempDir()
|
||
now := time.Now()
|
||
for i := 0; i < 6; i++ {
|
||
ft := now.Add(-time.Duration(6-i) * time.Hour)
|
||
makeLogFile(t, dir, nameForHour(ft), ft)
|
||
}
|
||
|
||
// Крошечный лимит: без floor удалились бы все.
|
||
r := &Retention{maxAgeHours: 0, maxSizeBytes: 1, eventLogger: nil, stopCh: make(chan struct{})}
|
||
r.cleanBySize(dir)
|
||
|
||
files, _ := r.collectFiles(dir)
|
||
if len(files) != minKeepFiles {
|
||
t.Errorf("после cleanBySize осталось %d, ожидалось minKeepFiles=%d", len(files), minKeepFiles)
|
||
}
|
||
}
|
||
|
||
// TestParseFilenameTime: корректное имя парсится, битое даёт нулевое время.
|
||
func TestParseFilenameTime(t *testing.T) {
|
||
r := NewRetention(72, 0, nil)
|
||
|
||
got := r.parseFilenameTime("gpio-2026-06-09-11.bin")
|
||
want := time.Date(2026, 6, 9, 11, 0, 0, 0, time.Local)
|
||
if !got.Equal(want) {
|
||
t.Errorf("parseFilenameTime = %v, ожидалось %v", got, want)
|
||
}
|
||
|
||
for _, bad := range []string{"gpio-garbage.bin", "gpio-2026-XX-09-11.bin", "gpio-2026-06-09.bin"} {
|
||
if !r.parseFilenameTime(bad).IsZero() {
|
||
t.Errorf("для %q ожидалось нулевое время", bad)
|
||
}
|
||
}
|
||
}
|