diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/goprecords/aggregate.go | 1 | ||||
| -rw-r--r-- | internal/goprecords/aggregate_test.go | 3 | ||||
| -rw-r--r-- | internal/goprecords/db.go | 7 | ||||
| -rw-r--r-- | internal/goprecords/report.go | 27 | ||||
| -rw-r--r-- | internal/goprecords/report_format.go | 69 | ||||
| -rw-r--r-- | internal/goprecords/report_test.go | 73 | ||||
| -rw-r--r-- | internal/goprecords/types.go | 14 | ||||
| -rw-r--r-- | internal/recordsdir/recordsdir.go | 43 | ||||
| -rw-r--r-- | internal/recordsdir/recordsdir_test.go | 3 | ||||
| -rw-r--r-- | internal/storage/db.go | 48 | ||||
| -rw-r--r-- | internal/storage/db_test.go | 58 | ||||
| -rw-r--r-- | internal/version/version.go | 2 |
12 files changed, 284 insertions, 64 deletions
diff --git a/internal/goprecords/aggregate.go b/internal/goprecords/aggregate.go index aa9c3fe..102ab7f 100644 --- a/internal/goprecords/aggregate.go +++ b/internal/goprecords/aggregate.go @@ -58,6 +58,7 @@ func (ag *Aggregator) Aggregate(ctx context.Context) (*Aggregates, error) { return nil, fmt.Errorf("last kernel %s: %w", relPath, err) } out.Host[host] = NewHostAggregate(host, lastKernel) + out.Host[host].LastUpdated = f.ModTime if err := processRecordsFile(ctx, ag.fsys, relPath, host, out); err != nil { return nil, err } diff --git a/internal/goprecords/aggregate_test.go b/internal/goprecords/aggregate_test.go index d675b4f..12fceae 100644 --- a/internal/goprecords/aggregate_test.go +++ b/internal/goprecords/aggregate_test.go @@ -118,6 +118,9 @@ func TestAggregateFixturesContent(t *testing.T) { if host.LastKernel == "" { t.Error("expected non-empty LastKernel for earth") } + if host.LastUpdated.IsZero() { + t.Error("expected non-zero LastUpdated for earth") + } } else { t.Error("expected earth host in aggregates") } diff --git a/internal/goprecords/db.go b/internal/goprecords/db.go index b2b095b..0746dfa 100644 --- a/internal/goprecords/db.go +++ b/internal/goprecords/db.go @@ -14,6 +14,10 @@ func LoadAggregates(ctx context.Context, db *sql.DB) (*Aggregates, error) { if err != nil { return nil, fmt.Errorf("load records: %w", err) } + hostMeta, err := storage.LoadHostMeta(ctx, db) + if err != nil { + return nil, fmt.Errorf("load host meta: %w", err) + } out := &Aggregates{ Host: make(map[string]*HostAggregate), Kernel: make(map[string]*Aggregate), @@ -38,6 +42,9 @@ func LoadAggregates(ctx context.Context, db *sql.DB) (*Aggregates, error) { } for host, h := range out.Host { h.LastKernel = hostLastKernel[host] + if t, ok := hostMeta[host]; ok { + h.LastUpdated = t + } } return out, nil } diff --git a/internal/goprecords/report.go b/internal/goprecords/report.go index 6300163..12c3951 100644 --- a/internal/goprecords/report.go +++ b/internal/goprecords/report.go @@ -186,9 +186,9 @@ func (r *HTMLReporter) Report() string { func (r reportBuilder) Report(outputFormat OutputFormat) string { var rows []tableRow - var hasLastKernel bool + var hasLastKernel, hasLastUpdated bool if r.category == CategoryHost { - rows, hasLastKernel = r.buildHostTable() + rows, hasLastKernel, hasLastUpdated = r.buildHostTable() } else { rows, hasLastKernel = r.buildCategoryTable() } @@ -196,12 +196,12 @@ func (r reportBuilder) Report(outputFormat OutputFormat) string { return "" } if outputFormat == FormatHTML { - return r.formatReportHTML(rows, hasLastKernel) + return r.formatReportHTML(rows, hasLastKernel, hasLastUpdated) } - return r.formatReport(rows, hasLastKernel, outputFormat) + return r.formatReport(rows, hasLastKernel, hasLastUpdated, outputFormat) } -func (r reportBuilder) buildHostTable() ([]tableRow, bool) { +func (r reportBuilder) buildHostTable() ([]tableRow, bool, bool) { type keyVal struct { agg *HostAggregate key uint64 @@ -214,6 +214,7 @@ func (r reportBuilder) buildHostTable() ([]tableRow, bool) { } sort.Slice(list, func(i, j int) bool { return list[i].key > list[j].key }) var rows []tableRow + var hasLastUpdated bool for i, kv := range list { if uint(i) >= r.limit { break @@ -223,14 +224,20 @@ func (r reportBuilder) buildHostTable() ([]tableRow, bool) { if h.IsActive(90) { active = "*" } + lastUpdated := "" + if !h.LastUpdated.IsZero() { + lastUpdated = h.LastUpdated.UTC().Format("2006-01-02 15:04") + hasLastUpdated = true + } rows = append(rows, tableRow{ - Pos: fmt.Sprintf("%d.", i+1), - Name: active + h.Stats.Name, - Value: r.humanStrHost(h), - LastKernel: h.LastKernel, + Pos: fmt.Sprintf("%d.", i+1), + Name: active + h.Stats.Name, + Value: r.humanStrHost(h), + LastKernel: h.LastKernel, + LastUpdated: lastUpdated, }) } - return rows, true + return rows, true, hasLastUpdated } func (r reportBuilder) buildCategoryTable() ([]tableRow, bool) { diff --git a/internal/goprecords/report_format.go b/internal/goprecords/report_format.go index 7630564..8d06b04 100644 --- a/internal/goprecords/report_format.go +++ b/internal/goprecords/report_format.go @@ -7,12 +7,12 @@ import ( "strings" ) -func (r reportBuilder) formatReport(rows []tableRow, hasLastKernel bool, outputFormat OutputFormat) string { - cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel) - border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel) - header := r.buildReportHeader(cW, nW, vW, lkW, hasLastKernel, border, outputFormat) - fmtStr := r.buildFormatStr(cW, nW, vW, lkW, hasLastKernel) - body := r.buildReportBody(rows, fmtStr, hasLastKernel) +func (r reportBuilder) formatReport(rows []tableRow, hasLastKernel, hasLastUpdated bool, outputFormat OutputFormat) string { + cW, nW, vW, lkW, luW := r.reportWidths(rows, hasLastKernel, hasLastUpdated) + border := r.buildBorder(cW, nW, vW, lkW, luW, hasLastKernel, hasLastUpdated) + header := r.buildReportHeader(cW, nW, vW, lkW, luW, hasLastKernel, hasLastUpdated, border, outputFormat) + fmtStr := r.buildFormatStr(cW, nW, vW, lkW, luW, hasLastKernel, hasLastUpdated) + body := r.buildReportBody(rows, fmtStr, hasLastKernel, hasLastUpdated) out := header + body + border if outputFormat == FormatMarkdown || outputFormat == FormatGemtext { out += "```\n" @@ -20,17 +20,21 @@ func (r reportBuilder) formatReport(rows []tableRow, hasLastKernel bool, outputF return out } -func (r reportBuilder) formatReportHTML(rows []tableRow, hasLastKernel bool) string { - cW, nW, vW, lkW := r.reportWidths(rows, hasLastKernel) - border := r.buildBorder(cW, nW, vW, lkW, hasLastKernel) - fmtStr := r.buildFormatStr(cW, nW, vW, lkW, hasLastKernel) +func (r reportBuilder) formatReportHTML(rows []tableRow, hasLastKernel, hasLastUpdated bool) string { + cW, nW, vW, lkW, luW := r.reportWidths(rows, hasLastKernel, hasLastUpdated) + border := r.buildBorder(cW, nW, vW, lkW, luW, hasLastKernel, hasLastUpdated) + fmtStr := r.buildFormatStr(cW, nW, vW, lkW, luW, hasLastKernel, hasLastUpdated) var headRow string - if hasLastKernel { + if hasLastKernel && hasLastUpdated { + headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel", "Updated") + } else if hasLastKernel { headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel") + } else if hasLastUpdated { + headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Updated") } else { headRow = fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String()) } - body := r.buildReportBody(rows, fmtStr, hasLastKernel) + body := r.buildReportBody(rows, fmtStr, hasLastKernel, hasLastUpdated) ascii := border + headRow + border + body + border hl := int(r.headerIndent) @@ -66,13 +70,16 @@ func (r reportBuilder) formatReportHTML(rows []tableRow, hasLastKernel bool) str return b.String() } -func (r reportBuilder) reportWidths(rows []tableRow, hasLastKernel bool) (countW, nameW, valueW, lastKernelW int) { +func (r reportBuilder) reportWidths(rows []tableRow, hasLastKernel, hasLastUpdated bool) (countW, nameW, valueW, lastKernelW, lastUpdatedW int) { countW = 3 nameW = len(r.category.String()) valueW = len(r.metric.String()) if hasLastKernel { lastKernelW = len("Last Kernel") } + if hasLastUpdated { + lastUpdatedW = len("Updated") + } for _, row := range rows { if len(row.Pos) > countW { countW = len(row.Pos) @@ -86,11 +93,14 @@ func (r reportBuilder) reportWidths(rows []tableRow, hasLastKernel bool) (countW if len(row.LastKernel) > lastKernelW { lastKernelW = len(row.LastKernel) } + if len(row.LastUpdated) > lastUpdatedW { + lastUpdatedW = len(row.LastUpdated) + } } - return countW, nameW, valueW, lastKernelW + return countW, nameW, valueW, lastKernelW, lastUpdatedW } -func (r reportBuilder) buildBorder(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string { +func (r reportBuilder) buildBorder(countW, nameW, valueW, lastKernelW, lastUpdatedW int, hasLastKernel, hasLastUpdated bool) string { parts := []string{ "+" + strings.Repeat("-", 2+countW), "+" + strings.Repeat("-", 2+nameW), @@ -99,10 +109,13 @@ func (r reportBuilder) buildBorder(countW, nameW, valueW, lastKernelW int, hasLa if hasLastKernel { parts = append(parts, "+"+strings.Repeat("-", 2+lastKernelW)) } + if hasLastUpdated { + parts = append(parts, "+"+strings.Repeat("-", 2+lastUpdatedW)) + } return strings.Join(parts, "") + "+\n" } -func (r reportBuilder) buildReportHeader(countW, nameW, valueW, lastKernelW int, hasLastKernel bool, border string, outputFormat OutputFormat) string { +func (r reportBuilder) buildReportHeader(countW, nameW, valueW, lastKernelW, lastUpdatedW int, hasLastKernel, hasLastUpdated bool, border string, outputFormat OutputFormat) string { var b strings.Builder if outputFormat == FormatMarkdown || outputFormat == FormatGemtext { b.WriteString(strings.Repeat("#", int(r.headerIndent))) @@ -120,9 +133,13 @@ func (r reportBuilder) buildReportHeader(countW, nameW, valueW, lastKernelW int, b.WriteString("```\n") } b.WriteString(border) - fmtStr := r.buildFormatStr(countW, nameW, valueW, lastKernelW, hasLastKernel) - if hasLastKernel { + fmtStr := r.buildFormatStr(countW, nameW, valueW, lastKernelW, lastUpdatedW, hasLastKernel, hasLastUpdated) + if hasLastKernel && hasLastUpdated { + b.WriteString(fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel", "Updated")) + } else if hasLastKernel { b.WriteString(fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Last Kernel")) + } else if hasLastUpdated { + b.WriteString(fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String(), "Updated")) } else { b.WriteString(fmt.Sprintf(fmtStr+"\n", "Pos", r.category.String(), r.metric.String())) } @@ -130,18 +147,28 @@ func (r reportBuilder) buildReportHeader(countW, nameW, valueW, lastKernelW int, return b.String() } -func (r reportBuilder) buildFormatStr(countW, nameW, valueW, lastKernelW int, hasLastKernel bool) string { +func (r reportBuilder) buildFormatStr(countW, nameW, valueW, lastKernelW, lastUpdatedW int, hasLastKernel, hasLastUpdated bool) string { + if hasLastKernel && hasLastUpdated { + return fmt.Sprintf("| %%%ds | %%%ds | %%%ds | %%%ds | %%%ds |", countW, nameW, valueW, lastKernelW, lastUpdatedW) + } if hasLastKernel { return fmt.Sprintf("| %%%ds | %%%ds | %%%ds | %%%ds |", countW, nameW, valueW, lastKernelW) } + if hasLastUpdated { + return fmt.Sprintf("| %%%ds | %%%ds | %%%ds | %%%ds |", countW, nameW, valueW, lastUpdatedW) + } return fmt.Sprintf("| %%%ds | %%%ds | %%%ds |", countW, nameW, valueW) } -func (r reportBuilder) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel bool) string { +func (r reportBuilder) buildReportBody(rows []tableRow, fmtStr string, hasLastKernel, hasLastUpdated bool) string { var b strings.Builder for _, row := range rows { - if hasLastKernel { + if hasLastKernel && hasLastUpdated { + b.WriteString(fmt.Sprintf(fmtStr+"\n", row.Pos, row.Name, row.Value, row.LastKernel, row.LastUpdated)) + } else if hasLastKernel { b.WriteString(fmt.Sprintf(fmtStr+"\n", row.Pos, row.Name, row.Value, row.LastKernel)) + } else if hasLastUpdated { + b.WriteString(fmt.Sprintf(fmtStr+"\n", row.Pos, row.Name, row.Value, row.LastUpdated)) } else { b.WriteString(fmt.Sprintf(fmtStr+"\n", row.Pos, row.Name, row.Value)) } diff --git a/internal/goprecords/report_test.go b/internal/goprecords/report_test.go index 1469942..fccb0d2 100644 --- a/internal/goprecords/report_test.go +++ b/internal/goprecords/report_test.go @@ -6,6 +6,7 @@ import ( "net/url" "strings" "testing" + "time" ) func TestNewReporter(t *testing.T) { @@ -489,3 +490,75 @@ func hostName(i int) string { return "host9" } } + +func TestReportWithLastUpdated(t *testing.T) { + aggs := &Aggregates{ + Host: make(map[string]*HostAggregate), + Kernel: make(map[string]*Aggregate), + KernelMajor: make(map[string]*Aggregate), + KernelName: make(map[string]*Aggregate), + } + + hagg := NewHostAggregate("host1", "Linux 5.10") + hagg.Stats.Uptime = 86400000 + hagg.Stats.Boots = 10 + hagg.Stats.FirstBoot = 1000 + hagg.Stats.LastSeen = 86401000 + hagg.LastUpdated = time.Date(2024, 1, 15, 9, 30, 0, 0, time.UTC) + aggs.Host["host1"] = hagg + + reporter := NewReporter(aggs, CategoryHost, 20, MetricUptime, FormatPlaintext, 1) + report := reporter.Report() + + if !strings.Contains(report, "Updated") { + t.Error("expected report to contain Updated header") + } + if !strings.Contains(report, "2024-01-15 09:30") { + t.Error("expected report to contain formatted LastUpdated") + } +} + +func TestReportWithoutLastUpdated(t *testing.T) { + aggs := &Aggregates{ + Host: make(map[string]*HostAggregate), + Kernel: make(map[string]*Aggregate), + KernelMajor: make(map[string]*Aggregate), + KernelName: make(map[string]*Aggregate), + } + + hagg := NewHostAggregate("host1", "Linux 5.10") + hagg.Stats.Uptime = 86400000 + hagg.Stats.Boots = 10 + hagg.Stats.FirstBoot = 1000 + hagg.Stats.LastSeen = 86401000 + // LastUpdated is zero + aggs.Host["host1"] = hagg + + reporter := NewReporter(aggs, CategoryHost, 20, MetricUptime, FormatPlaintext, 1) + report := reporter.Report() + + if strings.Contains(report, "Updated") { + t.Error("expected report NOT to contain Updated header when LastUpdated is zero") + } +} + +func TestReportKernelNoUpdatedColumn(t *testing.T) { + aggs := &Aggregates{ + Host: make(map[string]*HostAggregate), + Kernel: make(map[string]*Aggregate), + KernelMajor: make(map[string]*Aggregate), + KernelName: make(map[string]*Aggregate), + } + + kernel := NewAggregate("Linux 5.10.0") + kernel.Uptime = 86400000 + kernel.Boots = 5 + aggs.Kernel["Linux 5.10.0"] = kernel + + reporter := NewReporter(aggs, CategoryKernel, 20, MetricUptime, FormatPlaintext, 1) + report := reporter.Report() + + if strings.Contains(report, "Updated") { + t.Error("expected Kernel report NOT to contain Updated column") + } +} diff --git a/internal/goprecords/types.go b/internal/goprecords/types.go index 418d269..487119f 100644 --- a/internal/goprecords/types.go +++ b/internal/goprecords/types.go @@ -64,8 +64,9 @@ func NewAggregate(name string) *Aggregate { // HostAggregate adds last-kernel and lifespan/downtime for host reports. type HostAggregate struct { - Stats Aggregate - LastKernel string + Stats Aggregate + LastKernel string + LastUpdated time.Time } // NewHostAggregate constructs a HostAggregate. @@ -86,10 +87,11 @@ func (h *HostAggregate) IsActive(limitDays uint) bool { // tableRow is one row in the report table. type tableRow struct { - Pos string - Name string - Value string - LastKernel string + Pos string + Name string + Value string + LastKernel string + LastUpdated string } // String returns the category name. diff --git a/internal/recordsdir/recordsdir.go b/internal/recordsdir/recordsdir.go index 94f7f0e..6ebf491 100644 --- a/internal/recordsdir/recordsdir.go +++ b/internal/recordsdir/recordsdir.go @@ -6,11 +6,13 @@ import ( "path" "path/filepath" "strings" + "time" ) type Entry struct { - Path string - Host string + Path string + Host string + ModTime time.Time } func HostFromFileName(name string) string { @@ -21,12 +23,12 @@ func HostFromFileName(name string) string { return host } -func listRecordsFileNames(fsys fs.FS, root string) ([]string, error) { +func listRecordsFileNames(fsys fs.FS, root string) ([]Entry, error) { entries, err := fs.ReadDir(fsys, root) if err != nil { return nil, err } - var names []string + var out []Entry for _, e := range entries { if e.IsDir() || !strings.HasSuffix(e.Name(), ".records") { continue @@ -36,38 +38,27 @@ func listRecordsFileNames(fsys fs.FS, root string) ([]string, error) { if err != nil || info.Size() == 0 { continue } - names = append(names, e.Name()) + out = append(out, Entry{ + Path: rel, + Host: HostFromFileName(e.Name()), + ModTime: info.ModTime(), + }) } - return names, nil + return out, nil } // ListNonEmptyFilesFS returns non-empty .records files under root within fsys. func ListNonEmptyFilesFS(fsys fs.FS, root string) ([]Entry, error) { - names, err := listRecordsFileNames(fsys, root) - if err != nil { - return nil, err - } - var out []Entry - for _, name := range names { - out = append(out, Entry{ - Path: path.Join(root, name), - Host: HostFromFileName(name), - }) - } - return out, nil + return listRecordsFileNames(fsys, root) } func ListNonEmptyFiles(dir string) ([]Entry, error) { - names, err := listRecordsFileNames(os.DirFS(dir), ".") + entries, err := listRecordsFileNames(os.DirFS(dir), ".") if err != nil { return nil, err } - var out []Entry - for _, name := range names { - out = append(out, Entry{ - Path: filepath.Join(dir, name), - Host: HostFromFileName(name), - }) + for i := range entries { + entries[i].Path = filepath.Join(dir, filepath.Base(entries[i].Path)) } - return out, nil + return entries, nil } diff --git a/internal/recordsdir/recordsdir_test.go b/internal/recordsdir/recordsdir_test.go index 5e72d9d..40028ac 100644 --- a/internal/recordsdir/recordsdir_test.go +++ b/internal/recordsdir/recordsdir_test.go @@ -49,6 +49,9 @@ func TestListNonEmptyFiles(t *testing.T) { if entries[0].Host != "h1" || filepath.Base(entries[0].Path) != "h1.records" { t.Fatalf("unexpected entry: %#v", entries[0]) } + if entries[0].ModTime.IsZero() { + t.Fatal("expected non-zero ModTime") + } } func TestListNonEmptyFiles_ReadError(t *testing.T) { diff --git a/internal/storage/db.go b/internal/storage/db.go index d500509..22e0cd6 100644 --- a/internal/storage/db.go +++ b/internal/storage/db.go @@ -7,6 +7,7 @@ import ( "fmt" "io/fs" "os" + "time" "codeberg.org/snonux/goprecords/internal/recordline" "codeberg.org/snonux/goprecords/internal/recordsdir" @@ -26,6 +27,10 @@ CREATE INDEX IF NOT EXISTS idx_record_host ON record(host); CREATE INDEX IF NOT EXISTS idx_record_os ON record(os); CREATE INDEX IF NOT EXISTS idx_record_os_kernel_name ON record(os_kernel_name); CREATE INDEX IF NOT EXISTS idx_record_os_kernel_major ON record(os_kernel_major); +CREATE TABLE IF NOT EXISTS host_meta ( + host TEXT NOT NULL PRIMARY KEY, + last_updated INTEGER NOT NULL +); CREATE TABLE IF NOT EXISTS excluded_host ( host TEXT NOT NULL PRIMARY KEY, reason TEXT NOT NULL DEFAULT '', @@ -72,6 +77,43 @@ func ResetRecords(ctx context.Context, db *sql.DB) error { return err } +// ResetHostMeta deletes all rows from the host_meta table. +func ResetHostMeta(ctx context.Context, db *sql.DB) error { + _, err := db.ExecContext(ctx, "DELETE FROM host_meta") + return err +} + +// AddHostMeta inserts a host_meta row. +func AddHostMeta(ctx context.Context, tx *sql.Tx, host string, lastUpdated int64) error { + _, err := tx.ExecContext(ctx, "INSERT INTO host_meta (host, last_updated) VALUES (?, ?)", host, lastUpdated) + if err != nil { + return fmt.Errorf("insert host meta: %w", err) + } + return nil +} + +// LoadHostMeta returns a map of host to last-updated time from the host_meta table. +func LoadHostMeta(ctx context.Context, db *sql.DB) (map[string]time.Time, error) { + rows, err := db.QueryContext(ctx, "SELECT host, last_updated FROM host_meta") + if err != nil { + return nil, fmt.Errorf("query host meta: %w", err) + } + defer rows.Close() + out := make(map[string]time.Time) + for rows.Next() { + var host string + var lu int64 + if err := rows.Scan(&host, &lu); err != nil { + return nil, fmt.Errorf("scan host meta: %w", err) + } + out[host] = time.Unix(lu, 0).UTC() + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("rows host meta: %w", err) + } + return out, nil +} + // ImportFromDir imports non-empty .records files from statsDir into the database, // replacing existing rows. It is equivalent to ImportFromFS with os.DirFS(statsDir). func ImportFromDir(ctx context.Context, db *sql.DB, statsDir string) error { @@ -83,6 +125,9 @@ func ImportFromFS(ctx context.Context, db *sql.DB, fsys fs.FS) error { if err := ResetRecords(ctx, db); err != nil { return fmt.Errorf("reset records: %w", err) } + if err := ResetHostMeta(ctx, db); err != nil { + return fmt.Errorf("reset host meta: %w", err) + } files, err := recordsdir.ListNonEmptyFilesFS(fsys, ".") if err != nil { return fmt.Errorf("read dir: %w", err) @@ -101,6 +146,9 @@ func ImportFromFS(ctx context.Context, db *sql.DB, fsys fs.FS) error { if err := importFile(ctx, insert, fsys, f.Path, f.Host); err != nil { return err } + if err := AddHostMeta(ctx, tx, f.Host, f.ModTime.Unix()); err != nil { + return err + } } if err := tx.Commit(); err != nil { return fmt.Errorf("commit transaction: %w", err) diff --git a/internal/storage/db_test.go b/internal/storage/db_test.go index e34f88d..74aa30d 100644 --- a/internal/storage/db_test.go +++ b/internal/storage/db_test.go @@ -7,6 +7,7 @@ import ( "path/filepath" "testing" "testing/fstest" + "time" ) func TestOpen_ContextCanceled(t *testing.T) { @@ -464,3 +465,60 @@ func TestImportFromDir_pathIsFileNotDirectory(t *testing.T) { t.Fatal("expected error") } } + +func TestHostMetaRoundTrip(t *testing.T) { + tmpDir := t.TempDir() + dbPath := filepath.Join(tmpDir, "test.db") + db, err := Open(context.Background(), dbPath) + if err != nil { + t.Fatalf("open DB: %v", err) + } + defer db.Close() + ctx := context.Background() + if err := CreateSchema(ctx, db); err != nil { + t.Fatalf("schema: %v", err) + } + + if err := ResetHostMeta(ctx, db); err != nil { + t.Fatalf("reset: %v", err) + } + + tx, err := db.BeginTx(ctx, nil) + if err != nil { + t.Fatal(err) + } + if err := AddHostMeta(ctx, tx, "host1", 1705312200); err != nil { + t.Fatalf("add: %v", err) + } + if err := AddHostMeta(ctx, tx, "host2", 1705312300); err != nil { + t.Fatalf("add: %v", err) + } + if err := tx.Commit(); err != nil { + t.Fatal(err) + } + + meta, err := LoadHostMeta(ctx, db) + if err != nil { + t.Fatalf("load: %v", err) + } + if len(meta) != 2 { + t.Fatalf("len=%d, want 2", len(meta)) + } + if meta["host1"] != time.Unix(1705312200, 0).UTC() { + t.Fatalf("host1 time mismatch: %v", meta["host1"]) + } + if meta["host2"] != time.Unix(1705312300, 0).UTC() { + t.Fatalf("host2 time mismatch: %v", meta["host2"]) + } + + if err := ResetHostMeta(ctx, db); err != nil { + t.Fatalf("reset2: %v", err) + } + meta, err = LoadHostMeta(ctx, db) + if err != nil { + t.Fatalf("load after reset: %v", err) + } + if len(meta) != 0 { + t.Fatalf("len after reset=%d, want 0", len(meta)) + } +} diff --git a/internal/version/version.go b/internal/version/version.go index c847147..7ee9a7c 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -1,4 +1,4 @@ package version // Tag is the application release version. -const Tag = "0.5.1" +const Tag = "0.5.2" |
