01fd572a89
## Summary Completed Phase 5 refactoring and reached the target architecture. **Architectural milestone achieved:** - Service layer owns all state and is the sole writer - UI is a thin Fyne view, all widget updates marshaled via `fyne.Do` - Core engines are stateless and injectable - Domain types are pure (no `yaml:"-"` fields) - Full module builds and `go vet ./...` clean ## Changes - Bump version: 0.3.6 → 0.4.0 - Update CHANGELOG with Phase 5 summary - Add ROADMAP "Refactoring Follow-Ups" section ## Known follow-up work 1. **Linux test build broken** — `runner_test.go` needs `//go:build windows` tag 2. **File-size limits exceeded** — `operations.go` (486 lines), `jobs_view.go` (415 lines) See ROADMAP.md for details. --------- Co-authored-by: mixeme <mix.public@ya.ru> Reviewed-on: #1
155 lines
4.5 KiB
Go
155 lines
4.5 KiB
Go
package runner
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func writeLogFile(t *testing.T, dir, name string) string {
|
|
t.Helper()
|
|
path := filepath.Join(dir, name)
|
|
if err := os.WriteFile(path, []byte("log"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func setModTime(t *testing.T, path string, age time.Duration) {
|
|
t.Helper()
|
|
mt := time.Now().Add(-age)
|
|
if err := os.Chtimes(path, mt, mt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCleanupLogsMissingDirReturnsNil(t *testing.T) {
|
|
err := CleanupLogs(filepath.Join(t.TempDir(), "nonexistent"), 100, 30)
|
|
if err != nil {
|
|
t.Errorf("missing dir should return nil, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCleanupLogsRemovesFilesPastMaxAge(t *testing.T) {
|
|
dir := t.TempDir()
|
|
old := writeLogFile(t, dir, "old.log")
|
|
recent := writeLogFile(t, dir, "recent.log")
|
|
setModTime(t, old, 31*24*time.Hour) // 31 days old → past the 30-day limit
|
|
setModTime(t, recent, 5*24*time.Hour) // 5 days old → within limit
|
|
|
|
if err := CleanupLogs(dir, 100, 30); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(old); !os.IsNotExist(err) {
|
|
t.Error("file older than maxAgeDays should be deleted")
|
|
}
|
|
if _, err := os.Stat(recent); err != nil {
|
|
t.Errorf("file within maxAgeDays should be kept: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCleanupLogsKeepsFilesWithinAgeLimit(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for i := 1; i <= 3; i++ {
|
|
path := writeLogFile(t, dir, fmt.Sprintf("job_%d.log", i))
|
|
setModTime(t, path, time.Duration(i)*24*time.Hour)
|
|
}
|
|
|
|
if err := CleanupLogs(dir, 100, 30); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, _ := os.ReadDir(dir)
|
|
if len(entries) != 3 {
|
|
t.Errorf("expected 3 files kept within age limit, got %d", len(entries))
|
|
}
|
|
}
|
|
|
|
// TestCleanupLogsByCountDeletesOldest verifies the count-based policy: when more
|
|
// than maxFiles log files exist the oldest (by modification time) are removed.
|
|
// maxAgeDays=0 disables age-based cleanup so the test exercises count only.
|
|
func TestCleanupLogsByCountDeletesOldest(t *testing.T) {
|
|
dir := t.TempDir()
|
|
// Create 5 files; i=0 is newest (1 day old), i=4 is oldest (5 days old).
|
|
var paths []string
|
|
for i := 0; i < 5; i++ {
|
|
path := writeLogFile(t, dir, fmt.Sprintf("job_%03d.log", i))
|
|
setModTime(t, path, time.Duration(i+1)*24*time.Hour)
|
|
paths = append(paths, path)
|
|
}
|
|
|
|
if err := CleanupLogs(dir, 3, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, _ := os.ReadDir(dir)
|
|
if len(entries) != 3 {
|
|
t.Errorf("expected 3 files after count cleanup, got %d", len(entries))
|
|
}
|
|
// The 3 newest files (paths[0..2]) must survive.
|
|
for _, kept := range paths[:3] {
|
|
if _, err := os.Stat(kept); err != nil {
|
|
t.Errorf("newest file %s should be kept: %v", filepath.Base(kept), err)
|
|
}
|
|
}
|
|
// The 2 oldest files (paths[3..4]) must be removed.
|
|
for _, deleted := range paths[3:] {
|
|
if _, err := os.Stat(deleted); !os.IsNotExist(err) {
|
|
t.Errorf("oldest file %s should have been deleted", filepath.Base(deleted))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCleanupLogsNonLogFilesNotDeleted(t *testing.T) {
|
|
dir := t.TempDir()
|
|
logFile := writeLogFile(t, dir, "job.log")
|
|
notALog := writeLogFile(t, dir, "notes.txt")
|
|
// Both are old enough that age-based cleanup would remove them if it applied.
|
|
setModTime(t, logFile, 35*24*time.Hour)
|
|
setModTime(t, notALog, 35*24*time.Hour)
|
|
|
|
if err := CleanupLogs(dir, 100, 30); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(logFile); !os.IsNotExist(err) {
|
|
t.Error("old .log file should be deleted")
|
|
}
|
|
if _, err := os.Stat(notALog); err != nil {
|
|
t.Errorf(".txt file should not be deleted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCleanupLogsSubdirsNotDeleted(t *testing.T) {
|
|
dir := t.TempDir()
|
|
subdir := filepath.Join(dir, "archive.log") // name looks like a log but is a dir
|
|
if err := os.Mkdir(subdir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
setModTime(t, subdir, 60*24*time.Hour)
|
|
|
|
if err := CleanupLogs(dir, 100, 30); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := os.Stat(subdir); err != nil {
|
|
t.Errorf("subdirectory should not be deleted: %v", err)
|
|
}
|
|
}
|
|
|
|
// TestCleanupLogsZeroLimitsDisableBothPolicies confirms that maxFiles=0 disables
|
|
// count-based cleanup and maxAgeDays=0 disables age-based cleanup independently.
|
|
func TestCleanupLogsZeroLimitsDisableBothPolicies(t *testing.T) {
|
|
dir := t.TempDir()
|
|
for i := 0; i < 5; i++ {
|
|
path := writeLogFile(t, dir, fmt.Sprintf("job_%d.log", i))
|
|
setModTime(t, path, 60*24*time.Hour) // very old
|
|
}
|
|
|
|
if err := CleanupLogs(dir, 0, 0); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries, _ := os.ReadDir(dir)
|
|
if len(entries) != 5 {
|
|
t.Errorf("expected all 5 files kept with both limits disabled, got %d", len(entries))
|
|
}
|
|
}
|