package main import ( "strings" "testing" ) const state = `logrotate state -- version 2 "/var/log/nginx/error.log" 2026-3-15-0:34:52 "/var/log/wtmp" 2024-6-27-11:0:0 "/var/log/old.log" 2026-1-2 ` func TestTheStatusFileIsReadPerLog(t *testing.T) { r := ParseState(state) if len(r) != 3 || r[0].Log != "/var/log/nginx/error.log" || !strings.HasPrefix(r[0].LastRotated, "2026-03-15T00:34:52") || !strings.HasPrefix(r[1].LastRotated, "2026-01-02T00:00:00") { t.Fatalf("%+v", r) } m := machine(fake(func(c call) Ran { if c.String() == "sudo -n cat "+StateFile { return Ran{Stdout: state} } return Ran{Stdout: "ActiveState=active\n"} }, nil), 1000) s, err := m.Status("nginx") if err != nil || len(s["logs"].([]Rotation)) != 1 || s["state_file_present"] != true { t.Fatalf("%v %v", s, err) } } func TestAMachineWhereLogrotateNeverRanSaysSo(t *testing.T) { m := machine(fake(func(c call) Ran { if c.name == "sudo" { return Ran{Status: 1, Stderr: "cat: /var/lib/logrotate.status: No such file or directory\n"} } return Ran{Stdout: "LoadState=not-found\n"} }, nil), 1000) s, err := m.Status("") if err != nil || s["state_file_present"] != false || !strings.Contains(s["note"].(string), "never run") { t.Fatalf("%v %v", s, err) } refused := machine(fake(func(call) Ran { return Ran{Status: 1, Stderr: "sudo: a password is required\n"} }, nil), 1000) if _, err := refused.Status(""); err == nil || !strings.Contains(err.Error(), "without a prompt") { t.Fatalf("a refusal is an error: %v", err) } } const samba = `/var/log/samba/log.smbd /var/log/samba/log.nmbd /var/log/samba/*.log { notifempty missingok copytruncate } # a comment { with a brace /var/log/one.log /var/log/two.log { postrotate kill -HUP 1 endscript } ` func TestARuleFilesLogsAreThePathsBeforeEachBrace(t *testing.T) { got := RulesIn(samba) if strings.Join(got, " ") != "/var/log/samba/log.smbd /var/log/samba/log.nmbd /var/log/samba/*.log /var/log/one.log /var/log/two.log" { t.Fatalf("%v", got) } } func TestADirectiveIsNotALog(t *testing.T) { got := RulesIn("weekly\nolddir /var/log/archive\ninclude /etc/logrotate.d\n/var/log/wtmp {\n monthly\n}\n") if strings.Join(got, " ") != "/var/log/wtmp" { t.Fatalf("%v", got) } } func TestGlobalsAreTheBaseWithoutIncludesOrBlocks(t *testing.T) { g := manifest(t).resource(t, "config")["content"].(string) got := Globals(g) if strings.Join(got, "|") != "weekly|rotate 4|create|compress|delaycompress|tabooext + .pacorig .pacnew .pacsave" { t.Fatalf("%v", got) } } func TestForcingOneRuleCarriesTheGlobalsInAFileRootOwns(t *testing.T) { var calls []call m := machine(fake(func(c call) Ran { if c.args[1] == "logrotate" { return Ran{Stderr: "reading config file /run/mesh-logrotate-force.conf\nrotating log /var/log/samba/log.smbd, log->rotateCount is 4\nerror: error renaming x: Permission denied\n"} } return Ran{} }, &calls), 1000) files := map[string]string{BaseConf: "weekly\nrotate 4\ninclude /etc/logrotate.d\n/var/log/wtmp {\n monthly\n}\n", RulesDir + "/samba": samba} m.ReadFile = func(p string) ([]byte, error) { if s, ok := files[p]; ok { return []byte(s), nil } return nil, errNoFile } var written string removed := false r, err := m.Force("samba", func(s string) (string, func(), error) { written = s return "/tmp/x.conf", func() { removed = true }, nil }) if err != nil { t.Fatal(err) } if !strings.Contains(written, "weekly\nrotate 4\n") || strings.Contains(written, "include") || strings.Contains(written, "wtmp") || !removed { t.Fatalf("written %q removed %v", written, removed) } var seen []string for _, c := range calls { seen = append(seen, c.String()) } want := []string{ "sudo -n install -m 0644 -o root -g root /tmp/x.conf " + forcedConf, "sudo -n logrotate -f -v " + forcedConf + " " + RulesDir + "/samba", "sudo -n rm -f " + forcedConf, } if strings.Join(seen, "\n") != strings.Join(want, "\n") { t.Fatalf("ran:\n%s", strings.Join(seen, "\n")) } if r["ok"] != false || strings.Join(r["rotated"].([]string), ",") != "/var/log/samba/log.smbd" || len(r["errors"].([]string)) != 1 { t.Fatalf("%v", r) } if _, err := m.Force("../../etc/shadow", nil); err == nil { t.Fatal("a path was taken for a rule file") } if _, err := m.Force("absent", nil); err == nil { t.Fatal("a rule file that is not there was forced") } } func TestBigLogsAreSortedAndBounded(t *testing.T) { m := machine(byLine(map[string]Ran{ "sudo -n find /var/log -xdev -type f -printf %s\t%TY-%Tm-%Td %TH:%TM\t%p\n": {Status: 1, Stdout: "10\t2026-10-04 10:00\t/var/log/a.log\n4294967296\t2026-10-04 11:00\t/var/log/journal/x/system.journal\n2048\t2026-10-01 09:00\t/var/log/b.log\n", Stderr: "find: something vanished\n"}, }, nil), 1000) r, err := m.BigLogs(2, true) if err != nil { t.Fatal(err) } l := r["largest"].([]LogFile) if r["files"] != 3 || len(l) != 2 || !l[0].Journal || l[0].Size != "4.0G" || l[1].Path != "/var/log/b.log" || l[1].Size != "2.0K" { t.Fatalf("%v", r) } r, _ = m.BigLogs(5, false) if l := r["largest"].([]LogFile); len(l) != 2 || l[0].Path != "/var/log/b.log" || r["journal"] != "4.0G" { t.Fatalf("journals counted, not listed: %v", r) } } func TestTheDryRunReportsErrorsAndNotItsOwnWarning(t *testing.T) { m := machine(byLine(map[string]Ran{ "sudo -n logrotate -d /etc/logrotate.conf": {Stderr: "warning: logrotate in debug mode does nothing except printing debug messages!\nerror: /etc/logrotate.d/x:3 unknown option 'bogus'\nwarning: something real\n"}, }, nil), 1000) r, err := m.Check() if err != nil || r["ok"] != false || len(r["errors"].([]string)) != 1 || len(r["warnings"].([]string)) != 1 { t.Fatalf("%v %v", r, err) } } func TestTheJournalIsMeasuredAndVacuumedAsRoot(t *testing.T) { var calls []call m := machine(fake(func(c call) Ran { switch c.String() { case "sudo -n journalctl --disk-usage": return Ran{Stdout: "Archived and active journals take up 4G in the file system.\n"} case "systemd-analyze cat-config systemd/journald.conf": return Ran{Stdout: "# /etc/systemd/journald.conf\n[Journal]\n#SystemMaxUse=\nSystemMaxUse=1G\n"} case "sudo -n journalctl --vacuum-size=500M --vacuum-time=4weeks": return Ran{Stderr: "Deleted archived journal /var/log/journal/x/system@a.journal (128M).\nVacuuming done, freed 128M of archived journals from /var/log/journal/x.\n"} } return Ran{Status: 99} }, &calls), 1000) u, err := m.JournalUsage() if err != nil || u["usage"] != "4G" || u["settings"].(map[string]string)["SystemMaxUse"] != "1G" { t.Fatalf("%v %v", u, err) } v, err := m.Vacuum("500M", "4weeks") if err != nil || v["files_deleted"] != 1 || v["usage_after"] != "4G" { t.Fatalf("%v %v", v, err) } for _, bad := range [][2]string{{"", ""}, {"lots", ""}, {"", "forever"}, {"1G; rm", ""}} { if _, err := m.Vacuum(bad[0], bad[1]); err == nil { t.Errorf("%v accepted", bad) } } }