Files
go-service/internal/logger/retention_test.go

193 lines
7.1 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 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)
}
}
}