summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/goprecords/db.go6
-rw-r--r--internal/goprecords/db_test.go50
-rw-r--r--internal/goprecords/types.go15
-rw-r--r--internal/goprecords/types_test.go34
4 files changed, 100 insertions, 5 deletions
diff --git a/internal/goprecords/db.go b/internal/goprecords/db.go
index 035c13d..904923e 100644
--- a/internal/goprecords/db.go
+++ b/internal/goprecords/db.go
@@ -42,12 +42,12 @@ func LoadAggregates(ctx context.Context, db *sql.DB) (*Aggregates, error) {
KernelMajor: make(map[string]*Aggregate),
KernelName: make(map[string]*Aggregate),
}
- hostMaxBoot := make(map[string]int64)
+ hostMaxBoot := make(map[string]uint64)
hostLastKernel := make(map[string]string)
for _, rec := range records {
- if rec.BootTime >= uint64(hostMaxBoot[rec.Host]) {
- hostMaxBoot[rec.Host] = int64(rec.BootTime)
+ if rec.BootTime >= hostMaxBoot[rec.Host] {
+ hostMaxBoot[rec.Host] = rec.BootTime
hostLastKernel[rec.Host] = rec.OS
}
if _, ok := out.Host[rec.Host]; !ok {
diff --git a/internal/goprecords/db_test.go b/internal/goprecords/db_test.go
index acfe2ca..204d0ca 100644
--- a/internal/goprecords/db_test.go
+++ b/internal/goprecords/db_test.go
@@ -2,6 +2,7 @@ package goprecords
import (
"context"
+ "math"
"os"
"path/filepath"
"strings"
@@ -205,6 +206,55 @@ func TestLoadAggregates(t *testing.T) {
if host.Uptime != 3000 {
t.Errorf("expected uptime 3000, got %d", host.Uptime)
}
+ if host.LastKernel != "Linux 5.11" {
+ t.Errorf("LastKernel = %q, want %q (latest boot_time row)", host.LastKernel, "Linux 5.11")
+ }
+ }
+}
+
+func TestLoadAggregatesLastKernelMaxBootNearInt64(t *testing.T) {
+ tmpDir := t.TempDir()
+ dbPath := filepath.Join(tmpDir, "test.db")
+
+ db, err := OpenDB(context.Background(), dbPath)
+ if err != nil {
+ t.Fatalf("failed to open DB: %v", err)
+ }
+ defer db.Close()
+
+ ctx := context.Background()
+ if err := CreateSchema(ctx, db); err != nil {
+ t.Fatalf("failed to create schema: %v", err)
+ }
+
+ early := math.MaxInt64 - 1
+ late := math.MaxInt64
+
+ _, err = db.ExecContext(ctx,
+ "INSERT INTO record (host, uptime_sec, boot_time, os, os_kernel_name, os_kernel_major) VALUES (?, ?, ?, ?, ?, ?)",
+ "host1", 100, early, "Linux early", "Linux", "Linux 5...")
+ if err != nil {
+ t.Fatalf("failed to insert: %v", err)
+ }
+
+ _, err = db.ExecContext(ctx,
+ "INSERT INTO record (host, uptime_sec, boot_time, os, os_kernel_name, os_kernel_major) VALUES (?, ?, ?, ?, ?, ?)",
+ "host1", 100, late, "Linux late", "Linux", "Linux 5...")
+ if err != nil {
+ t.Fatalf("failed to insert: %v", err)
+ }
+
+ aggs, err := LoadAggregates(ctx, db)
+ if err != nil {
+ t.Fatalf("failed to load aggregates: %v", err)
+ }
+
+ host := aggs.Host["host1"]
+ if host == nil {
+ t.Fatal("expected host1 aggregate")
+ }
+ if host.LastKernel != "Linux late" {
+ t.Errorf("LastKernel = %q, want %q", host.LastKernel, "Linux late")
}
}
diff --git a/internal/goprecords/types.go b/internal/goprecords/types.go
index 6e5bf10..0f9c3a3 100644
--- a/internal/goprecords/types.go
+++ b/internal/goprecords/types.go
@@ -177,10 +177,21 @@ func (a *Aggregate) MetaScore() uint64 {
}
// Lifespan returns last-seen minus first-boot.
-func (h *HostAggregate) Lifespan() uint64 { return h.LastSeen - h.FirstBoot }
+func (h *HostAggregate) Lifespan() uint64 {
+ if h.LastSeen < h.FirstBoot {
+ return 0
+ }
+ return h.LastSeen - h.FirstBoot
+}
// Downtime returns lifespan minus uptime.
-func (h *HostAggregate) Downtime() uint64 { return h.Lifespan() - h.Uptime }
+func (h *HostAggregate) Downtime() uint64 {
+ life := h.Lifespan()
+ if h.Uptime > life {
+ return 0
+ }
+ return life - h.Uptime
+}
// MetaScore returns the host-specific score (includes downtime component).
func (h *HostAggregate) MetaScore() uint64 {
diff --git a/internal/goprecords/types_test.go b/internal/goprecords/types_test.go
index a315784..245cead 100644
--- a/internal/goprecords/types_test.go
+++ b/internal/goprecords/types_test.go
@@ -113,6 +113,40 @@ func TestHostAggregateDowntime(t *testing.T) {
}
}
+func TestHostAggregateLifespanUnderflow(t *testing.T) {
+ hagg := NewHostAggregate("host1", "Linux 5.10")
+ hagg.FirstBoot = 5000
+ hagg.LastSeen = 1000
+
+ if got := hagg.Lifespan(); got != 0 {
+ t.Errorf("Lifespan() = %d, want 0 when LastSeen < FirstBoot", got)
+ }
+}
+
+func TestHostAggregateDowntimeUnderflow(t *testing.T) {
+ tests := []struct {
+ name string
+ firstBoot uint64
+ lastSeen uint64
+ uptime uint64
+ }{
+ {"uptime equals lifespan", 1000, 5000, 4000},
+ {"uptime exceeds lifespan", 1000, 5000, 5000},
+ {"uptime exceeds short span", 0, 100, 200},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ hagg := NewHostAggregate("host1", "Linux 5.10")
+ hagg.FirstBoot = tt.firstBoot
+ hagg.LastSeen = tt.lastSeen
+ hagg.Uptime = tt.uptime
+ if got := hagg.Downtime(); got != 0 {
+ t.Errorf("Downtime() = %d, want 0", got)
+ }
+ })
+ }
+}
+
func TestEpochHumanDuration(t *testing.T) {
// Unix epoch + 1 year + 2 months + 3 days
epoch := Epoch(31536000 + (60 * 24 * 3600) + (3 * 24 * 3600))