package logger import ( "fmt" "os" "path/filepath" "strings" "testing" "time" ) func testStore(t *testing.T, cfg StoreConfig) *Store { t.Helper() cfg.Dir = t.TempDir() s, err := NewStore(cfg) if err != nil { t.Fatalf("NewStore: %v", err) } t.Cleanup(s.Close) return s } func TestStorePiseIVracaLinije(t *testing.T) { s := testStore(t, StoreConfig{}) t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) if err := s.Write("1CDBD4A6A460", t0, []string{"prva", "druga"}); err != nil { t.Fatalf("Write: %v", err) } if err := s.Write("1CDBD4A6A460", t0.Add(time.Second), []string{"treca"}); err != nil { t.Fatalf("Write: %v", err) } lines, err := s.Tail("1CDBD4A6A460", 10, "") if err != nil { t.Fatalf("Tail: %v", err) } if len(lines) != 3 { t.Fatalf("linija = %d: %q", len(lines), lines) } if !strings.HasSuffix(lines[0], "prva") || !strings.HasSuffix(lines[2], "treca") { t.Errorf("redosled nije hronoloski: %q", lines) } if !strings.HasPrefix(lines[0], "2026-07-25 12:00:00") { t.Errorf("nedostaje vremenska oznaka: %q", lines[0]) } } func TestStorePretraga(t *testing.T) { s := testStore(t, StoreConfig{}) t0 := time.Now() s.Write("dev", t0, []string{"[stampac] greska", "[kapija] prolaz", "[stampac] ok"}) lines, _ := s.Tail("dev", 10, "STAMPAC") if len(lines) != 2 { t.Fatalf("pretraga = %d linija: %q", len(lines), lines) } lines, _ = s.Tail("dev", 10, "nepostojece") if len(lines) != 0 { t.Errorf("ocekivano 0 linija, dobijeno %q", lines) } } func TestStoreOgranicavaBrojLinija(t *testing.T) { s := testStore(t, StoreConfig{}) t0 := time.Now() for i := 0; i < 100; i++ { s.Write("dev", t0, []string{fmt.Sprintf("linija %d", i)}) } lines, _ := s.Tail("dev", 10, "") if len(lines) != 10 { t.Fatalf("linija = %d", len(lines)) } if !strings.HasSuffix(lines[9], "linija 99") { t.Errorf("poslednja linija = %q", lines[9]) } } func TestStoreRotacijaPoVelicini(t *testing.T) { s := testStore(t, StoreConfig{MaxFileBytes: 200}) t0 := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) for i := 0; i < 30; i++ { if err := s.Write("dev", t0, []string{fmt.Sprintf("popunjavanje %02d", i)}); err != nil { t.Fatalf("Write: %v", err) } } files, err := os.ReadDir(filepath.Join(s.cfg.Dir, "dev")) if err != nil { t.Fatalf("ReadDir: %v", err) } if len(files) < 2 { t.Fatalf("ocekivana rotacija, fajlova = %d", len(files)) } var imena []string for _, f := range files { imena = append(imena, f.Name()) if info, _ := f.Info(); info.Size() > 400 { t.Errorf("fajl %s je %d B — granica se ne postuje", f.Name(), info.Size()) } } if imena[0] != "2026-07-25.log" || imena[1] != "2026-07-25.log.001" { t.Errorf("imena fajlova = %q", imena) } // Sve linije se i dalje citaju, hronoloski. lines, _ := s.Tail("dev", 100, "") if len(lines) != 30 { t.Errorf("procitano %d linija, ocekivano 30", len(lines)) } if !strings.HasSuffix(lines[0], "popunjavanje 00") || !strings.HasSuffix(lines[29], "popunjavanje 29") { t.Errorf("redosled kroz rotirane fajlove nije dobar: %q ... %q", lines[0], lines[29]) } } func TestStoreNoviDanNoviFajl(t *testing.T) { s := testStore(t, StoreConfig{}) t0 := time.Date(2026, 7, 25, 23, 59, 0, 0, time.UTC) s.Write("dev", t0, []string{"juce"}) s.Write("dev", t0.Add(2*time.Minute), []string{"danas"}) dani := s.Days("dev") if len(dani) != 2 || dani[0] != "2026-07-26" || dani[1] != "2026-07-25" { t.Errorf("dani = %q", dani) } } func TestCleanupBriseStareDnevnike(t *testing.T) { s := testStore(t, StoreConfig{KeepDays: 3}) now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) for i := 0; i < 10; i++ { s.Write("dev", now.AddDate(0, 0, -i), []string{"nesto"}) } s.Close() // da ciscenje sme da dira i tekuci fajl n, err := s.Cleanup(now) if err != nil { t.Fatalf("Cleanup: %v", err) } if n == 0 { t.Fatal("nista nije obrisano") } dani := s.Days("dev") for _, d := range dani { dd, _ := time.Parse(dayLayout, d) if dd.Before(now.AddDate(0, 0, -3)) { t.Errorf("ostao stari dnevnik %s", d) } } } func TestCleanupPostujeKvotuPoUredjaju(t *testing.T) { s := testStore(t, StoreConfig{MaxFileBytes: 300, MaxDeviceBytes: 600, KeepDays: 3650}) base := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC) for i := 0; i < 20; i++ { s.Write("dev", base.AddDate(0, 0, i), []string{strings.Repeat("x", 200)}) } s.Close() if _, err := s.Cleanup(base.AddDate(0, 0, 20)); err != nil { t.Fatalf("Cleanup: %v", err) } total, _ := s.Usage() if total > 600 { t.Errorf("posle ciscenja %d B, kvota je 600 B", total) } if total == 0 { t.Error("ciscenje je obrisalo sve — najnoviji dnevnik mora ostati") } } func TestCleanupPostujeUkupnuKvotu(t *testing.T) { s := testStore(t, StoreConfig{MaxFileBytes: 300, MaxDeviceBytes: 1 << 20, MaxTotalBytes: 800, KeepDays: 3650}) base := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC) for i := 0; i < 10; i++ { s.Write(fmt.Sprintf("dev%d", i), base.AddDate(0, 0, i), []string{strings.Repeat("y", 200)}) } s.Close() if _, err := s.Cleanup(base.AddDate(0, 0, 10)); err != nil { t.Fatalf("Cleanup: %v", err) } total, _ := s.Usage() if total > 800 { t.Errorf("posle ciscenja %d B, ukupna kvota je 800 B", total) } } func TestCleanupNeBriseOtvoreniFajl(t *testing.T) { // Fajl u koji se trenutno pise ne sme da nestane: na Linuxu bi upisi // posle brisanja tiho odlazili u nepostojeci fajl. s := testStore(t, StoreConfig{MaxTotalBytes: 1, MaxDeviceBytes: 1, KeepDays: 1}) now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) s.Write("dev", now, []string{"vazna linija"}) if _, err := s.Cleanup(now); err != nil { t.Fatalf("Cleanup: %v", err) } if _, err := os.Stat(filepath.Join(s.cfg.Dir, "dev", "2026-07-25.log")); err != nil { t.Errorf("otvoreni fajl je obrisan: %v", err) } } func TestStoreOdbijaIzlazakIzFoldera(t *testing.T) { s := testStore(t, StoreConfig{}) now := time.Now() if err := s.Write("../../../pobegao", now, []string{"linija"}); err != nil { t.Fatalf("Write: %v", err) } // Sve mora ostati unutar Dir. var vanFoldera []string filepath.Walk(filepath.Dir(s.cfg.Dir), func(p string, info os.FileInfo, err error) error { if err != nil || info.IsDir() { return nil } if !strings.HasPrefix(p, s.cfg.Dir) { vanFoldera = append(vanFoldera, p) } return nil }) if len(vanFoldera) > 0 { t.Errorf("fajlovi napravljeni van foldera dnevnika: %q", vanFoldera) } } func TestTailPreseceneLinijeNaGranici(t *testing.T) { s := testStore(t, StoreConfig{}) now := time.Now() s.Write("dev", now, []string{"cela linija jedan", "cela linija dva"}) lines, _ := s.Tail("dev", 5, "") for _, ln := range lines { if !strings.Contains(ln, "cela linija") { t.Errorf("krnja linija u ispisu: %q", ln) } } } func TestTailNepostojeciUredjaj(t *testing.T) { s := testStore(t, StoreConfig{}) lines, err := s.Tail("nema-ga", 10, "") if err != nil { t.Fatalf("Tail: %v", err) } if len(lines) != 0 { t.Errorf("linije = %q", lines) } }