package tasks import ( "context" "database/sql" "fmt" "testing" "time" "vctp/db" "vctp/internal/settings" "github.com/jmoiron/sqlx" ) func TestCanonicalDailyFlow_WritesRollupAndTotalsCache(t *testing.T) { ctx := context.Background() dbConn := newTasksTestDB(t) task := newTasksTestCronTask(dbConn) if err := db.EnsureVmHourlyStats(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_hourly_stats: %v", err) } dayStart := time.Date(2026, time.March, 10, 0, 0, 0, 0, time.UTC) dayEnd := dayStart.AddDate(0, 0, 1) t1 := dayStart.Add(1 * time.Hour).Unix() t2 := dayStart.Add(2 * time.Hour).Unix() t3 := dayStart.Add(3 * time.Hour).Unix() seeds := []hourlySeedRow{ {SnapshotTime: t1, Name: "vm-a1", Vcenter: "vc-a", VmID: "vm-a1", VmUUID: "uuid-a1", ResourcePool: "Tin", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 100, VcpuCount: 2, RamGB: 8, CreationTime: dayStart.Add(-1 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t2, Name: "vm-a1", Vcenter: "vc-a", VmID: "vm-a1", VmUUID: "uuid-a1", ResourcePool: "Gold", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 120, VcpuCount: 4, RamGB: 8, CreationTime: dayStart.Add(-1 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t2, Name: "vm-a2", Vcenter: "vc-a", VmID: "vm-a2", VmUUID: "uuid-a2", ResourcePool: "Bronze", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 40, VcpuCount: 1, RamGB: 4, CreationTime: dayStart.Add(-2 * time.Hour).Unix(), DeletionTime: dayStart.Add(4 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t1, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Silver", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 200, VcpuCount: 8, RamGB: 32, CreationTime: dayStart.Add(-3 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t2, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Silver", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 200, VcpuCount: 8, RamGB: 32, CreationTime: dayStart.Add(-3 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t3, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Silver", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 200, VcpuCount: 8, RamGB: 32, CreationTime: dayStart.Add(-3 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, } for _, seed := range seeds { if err := insertHourlyCacheSeedRow(ctx, dbConn, seed); err != nil { t.Fatalf("failed to insert hourly seed row: %v", err) } } aggMap, snapTimes, err := task.scanHourlyCache(ctx, dayStart, dayEnd) if err != nil { t.Fatalf("scanHourlyCache failed: %v", err) } if len(aggMap) != 3 { t.Fatalf("unexpected daily agg key count: got %d want %d", len(aggMap), 3) } if len(snapTimes) != 3 { t.Fatalf("unexpected snapshot time count: got %d want %d", len(snapTimes), 3) } totalSamplesByVcenter := sampleCountsByVcenter(aggMap) if totalSamplesByVcenter["vc-a"] != 2 || totalSamplesByVcenter["vc-b"] != 3 { t.Fatalf("unexpected per-vcenter sample counts: %#v", totalSamplesByVcenter) } summaryTable, err := db.SafeTableName("test_daily_canonical_integration_summary") if err != nil { t.Fatalf("failed to build summary table name: %v", err) } if err := db.EnsureSummaryTable(ctx, dbConn, summaryTable); err != nil { t.Fatalf("failed to ensure summary table: %v", err) } if err := task.insertDailyAggregates(ctx, summaryTable, aggMap, len(snapTimes), totalSamplesByVcenter); err != nil { t.Fatalf("insertDailyAggregates failed: %v", err) } if err := task.persistDailyRollup(ctx, dayStart.Unix(), aggMap, len(snapTimes), totalSamplesByVcenter); err != nil { t.Fatalf("persistDailyRollup failed: %v", err) } rollupAgg, err := task.scanDailyRollup(ctx, dayStart, dayEnd) if err != nil { t.Fatalf("scanDailyRollup failed: %v", err) } if len(rollupAgg) != len(aggMap) { t.Fatalf("unexpected rollup agg key count: got %d want %d", len(rollupAgg), len(aggMap)) } refreshed, err := db.ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "daily", dayStart.Unix()) if err != nil { t.Fatalf("ReplaceVcenterAggregateTotalsFromSummary(daily) failed: %v", err) } if refreshed != 2 { t.Fatalf("unexpected daily refreshed vcenter rows: got %d want %d", refreshed, 2) } assertSummaryCacheMatchesByVcenter(t, ctx, dbConn, summaryTable, "daily", dayStart.Unix()) assertRollupTotalSamplesForVcenter(t, ctx, dbConn, dayStart.Unix(), "vc-a", 2) assertRollupTotalSamplesForVcenter(t, ctx, dbConn, dayStart.Unix(), "vc-b", 3) } func TestCanonicalMonthlyFlow_WritesSummaryAndTotalsCache(t *testing.T) { ctx := context.Background() dbConn := newTasksTestDB(t) task := newTasksTestCronTask(dbConn) if err := db.EnsureVmDailyRollup(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_daily_rollup: %v", err) } monthStart := time.Date(2026, time.April, 1, 0, 0, 0, 0, time.UTC) monthEnd := monthStart.AddDate(0, 1, 0) day1 := monthStart.AddDate(0, 0, 5).Unix() day2 := monthStart.AddDate(0, 0, 6).Unix() rollupSeeds := []dailySeedRow{ { SnapshotTime: day1, Name: "vm-a1", Vcenter: "vc-a", VmID: "vm-a1", VmUUID: "uuid-a1", ResourcePool: "Bronze", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 120, VcpuCount: 4, RamGB: 8, CreationTime: monthStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE", SamplesPresent: 2, TotalSamples: 2, SumVcpu: 6, SumRam: 12, SumDisk: 240, BronzeHits: 2, }, { SnapshotTime: day2, Name: "vm-a1", Vcenter: "vc-a", VmID: "vm-a1", VmUUID: "uuid-a1", ResourcePool: "Tin", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 110, VcpuCount: 2, RamGB: 8, CreationTime: monthStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE", SamplesPresent: 2, TotalSamples: 2, SumVcpu: 4, SumRam: 16, SumDisk: 220, TinHits: 2, }, { SnapshotTime: day1, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Gold", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 200, VcpuCount: 8, RamGB: 32, CreationTime: monthStart.Add(-10 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE", SamplesPresent: 2, TotalSamples: 2, SumVcpu: 16, SumRam: 64, SumDisk: 400, GoldHits: 2, }, { SnapshotTime: day2, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Gold", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 210, VcpuCount: 8, RamGB: 32, CreationTime: monthStart.Add(-10 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE", SamplesPresent: 2, TotalSamples: 2, SumVcpu: 16, SumRam: 64, SumDisk: 420, GoldHits: 2, }, } for _, seed := range rollupSeeds { if err := insertDailyRollupSeedRow(ctx, dbConn, seed); err != nil { t.Fatalf("failed to insert daily rollup seed row: %v", err) } } aggMap, err := task.scanDailyRollup(ctx, monthStart, monthEnd) if err != nil { t.Fatalf("scanDailyRollup failed: %v", err) } if len(aggMap) != 2 { t.Fatalf("unexpected monthly agg key count: got %d want %d", len(aggMap), 2) } summaryTable, err := db.SafeTableName("test_monthly_canonical_integration_summary") if err != nil { t.Fatalf("failed to build monthly summary table name: %v", err) } if err := db.EnsureSummaryTable(ctx, dbConn, summaryTable); err != nil { t.Fatalf("failed to ensure monthly summary table: %v", err) } if err := task.insertMonthlyAggregates(ctx, summaryTable, aggMap); err != nil { t.Fatalf("insertMonthlyAggregates failed: %v", err) } refreshed, err := db.ReplaceVcenterAggregateTotalsFromSummary(ctx, dbConn, summaryTable, "monthly", monthStart.Unix()) if err != nil { t.Fatalf("ReplaceVcenterAggregateTotalsFromSummary(monthly) failed: %v", err) } if refreshed != 2 { t.Fatalf("unexpected monthly refreshed vcenter rows: got %d want %d", refreshed, 2) } monthlyRows, err := loadMonthlySummaryRows(ctx, dbConn, summaryTable) if err != nil { t.Fatalf("failed to load monthly summary rows: %v", err) } if len(monthlyRows) != 2 { t.Fatalf("unexpected monthly summary row count: got %d want %d", len(monthlyRows), 2) } assertSummaryCacheMatchesByVcenter(t, ctx, dbConn, summaryTable, "monthly", monthStart.Unix()) } func TestScheduledCanonicalDailyTaskFlow_WritesSummaryRollupRegistryAndTotalsCache(t *testing.T) { ctx := context.Background() dbConn := newTasksTestDB(t) task := newTasksTestCronTaskForAggregateFlow(t, dbConn) if err := db.EnsureVmHourlyStats(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_hourly_stats: %v", err) } dayStart := time.Date(2026, time.March, 12, 0, 0, 0, 0, time.UTC) t1 := dayStart.Add(1 * time.Hour).Unix() t2 := dayStart.Add(2 * time.Hour).Unix() t3 := dayStart.Add(3 * time.Hour).Unix() seeds := []hourlySeedRow{ {SnapshotTime: t1, Name: "vm-a1", Vcenter: "vc-a", VmID: "vm-a1", VmUUID: "uuid-a1", ResourcePool: "Tin", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 100, VcpuCount: 2, RamGB: 8, CreationTime: dayStart.Add(-1 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t2, Name: "vm-a1", Vcenter: "vc-a", VmID: "vm-a1", VmUUID: "uuid-a1", ResourcePool: "Gold", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 120, VcpuCount: 4, RamGB: 8, CreationTime: dayStart.Add(-1 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t2, Name: "vm-a2", Vcenter: "vc-a", VmID: "vm-a2", VmUUID: "uuid-a2", ResourcePool: "Bronze", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 40, VcpuCount: 1, RamGB: 4, CreationTime: dayStart.Add(-2 * time.Hour).Unix(), DeletionTime: dayStart.Add(4 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t1, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Silver", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 200, VcpuCount: 8, RamGB: 32, CreationTime: dayStart.Add(-3 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t2, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Silver", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 200, VcpuCount: 8, RamGB: 32, CreationTime: dayStart.Add(-3 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t3, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Silver", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 200, VcpuCount: 8, RamGB: 32, CreationTime: dayStart.Add(-3 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, } for _, seed := range seeds { if err := insertHourlyCacheSeedRow(ctx, dbConn, seed); err != nil { t.Fatalf("failed to insert hourly seed row: %v", err) } } if err := task.aggregateDailySummaryWithMode(ctx, dayStart, true, true); err != nil { t.Fatalf("aggregateDailySummaryWithMode failed: %v", err) } summaryTable, err := dailySummaryTableName(dayStart) if err != nil { t.Fatalf("failed to build summary table name: %v", err) } rows, err := loadDailySummaryRows(ctx, dbConn, summaryTable) if err != nil { t.Fatalf("failed to load daily summary rows: %v", err) } if len(rows) != 3 { t.Fatalf("unexpected daily summary row count: got %d want %d", len(rows), 3) } assertSnapshotRegistryRow(t, ctx, dbConn, "daily", summaryTable, dayStart.Unix(), int64(len(rows))) assertSummaryCacheMatchesByVcenter(t, ctx, dbConn, summaryTable, "daily", dayStart.Unix()) assertRollupTotalSamplesForVcenter(t, ctx, dbConn, dayStart.Unix(), "vc-a", 2) assertRollupTotalSamplesForVcenter(t, ctx, dbConn, dayStart.Unix(), "vc-b", 3) } func TestScheduledCanonicalMonthlyTaskFlow_WritesSummaryRegistryAndTotalsCache(t *testing.T) { ctx := context.Background() dbConn := newTasksTestDB(t) task := newTasksTestCronTaskForAggregateFlow(t, dbConn) if err := db.EnsureVmDailyRollup(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_daily_rollup: %v", err) } targetMonth := time.Date(2026, time.April, 20, 0, 0, 0, 0, time.UTC) monthStart := time.Date(targetMonth.Year(), targetMonth.Month(), 1, 0, 0, 0, 0, targetMonth.Location()) day1 := monthStart.AddDate(0, 0, 5).Unix() day2 := monthStart.AddDate(0, 0, 6).Unix() rollupSeeds := []dailySeedRow{ { SnapshotTime: day1, Name: "vm-a1", Vcenter: "vc-a", VmID: "vm-a1", VmUUID: "uuid-a1", ResourcePool: "Bronze", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 120, VcpuCount: 4, RamGB: 8, CreationTime: monthStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE", SamplesPresent: 2, TotalSamples: 2, SumVcpu: 6, SumRam: 12, SumDisk: 240, BronzeHits: 2, }, { SnapshotTime: day2, Name: "vm-a1", Vcenter: "vc-a", VmID: "vm-a1", VmUUID: "uuid-a1", ResourcePool: "Tin", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 110, VcpuCount: 2, RamGB: 8, CreationTime: monthStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE", SamplesPresent: 2, TotalSamples: 2, SumVcpu: 4, SumRam: 16, SumDisk: 220, TinHits: 2, }, { SnapshotTime: day1, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Gold", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 200, VcpuCount: 8, RamGB: 32, CreationTime: monthStart.Add(-10 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE", SamplesPresent: 2, TotalSamples: 2, SumVcpu: 16, SumRam: 64, SumDisk: 400, GoldHits: 2, }, { SnapshotTime: day2, Name: "vm-b1", Vcenter: "vc-b", VmID: "vm-b1", VmUUID: "uuid-b1", ResourcePool: "Gold", Datacenter: "dc-b", Cluster: "cluster-b", Folder: "/prod", ProvisionedDisk: 210, VcpuCount: 8, RamGB: 32, CreationTime: monthStart.Add(-10 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE", SamplesPresent: 2, TotalSamples: 2, SumVcpu: 16, SumRam: 64, SumDisk: 420, GoldHits: 2, }, } for _, seed := range rollupSeeds { if err := insertDailyRollupSeedRow(ctx, dbConn, seed); err != nil { t.Fatalf("failed to insert daily rollup seed row: %v", err) } } if err := task.aggregateMonthlySummaryWithMode(ctx, targetMonth, true, true); err != nil { t.Fatalf("aggregateMonthlySummaryWithMode failed: %v", err) } summaryTable, err := monthlySummaryTableName(targetMonth) if err != nil { t.Fatalf("failed to build monthly summary table name: %v", err) } rows, err := loadMonthlySummaryRows(ctx, dbConn, summaryTable) if err != nil { t.Fatalf("failed to load monthly summary rows: %v", err) } if len(rows) != 2 { t.Fatalf("unexpected monthly summary row count: got %d want %d", len(rows), 2) } assertSnapshotRegistryRow(t, ctx, dbConn, "monthly", summaryTable, monthStart.Unix(), int64(len(rows))) assertSummaryCacheMatchesByVcenter(t, ctx, dbConn, summaryTable, "monthly", monthStart.Unix()) } func TestScheduledCanonicalDailyTaskFlow_LifecycleEdgeCases(t *testing.T) { ctx := context.Background() dbConn := newTasksTestDB(t) task := newTasksTestCronTaskForAggregateFlow(t, dbConn) if err := db.EnsureVmHourlyStats(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_hourly_stats: %v", err) } if err := db.EnsureVmLifecycleCache(ctx, dbConn); err != nil { t.Fatalf("failed to ensure vm_lifecycle_cache: %v", err) } dayStart := time.Date(2026, time.March, 13, 0, 0, 0, 0, time.UTC) dayEnd := dayStart.AddDate(0, 0, 1) t1 := dayStart.Add(1 * time.Hour).Unix() t2 := dayStart.Add(2 * time.Hour).Unix() t3 := dayStart.Add(3 * time.Hour).Unix() seeds := []hourlySeedRow{ // Deleted VM: appears only once; deletion should be inferred at first missing snapshot (t2). {SnapshotTime: t1, Name: "vm-gone", Vcenter: "vc-a", VmID: "vm-g", VmUUID: "uuid-g", ResourcePool: "Bronze", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 80, VcpuCount: 4, RamGB: 16, CreationTime: dayStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, // Resource-change VM: verify last pool + averaged CPU/RAM/disk + pool mix percentages. {SnapshotTime: t1, Name: "vm-change", Vcenter: "vc-a", VmID: "vm-c", VmUUID: "uuid-c", ResourcePool: "Tin", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 100, VcpuCount: 2, RamGB: 8, CreationTime: dayStart.Add(-48 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t2, Name: "vm-change", Vcenter: "vc-a", VmID: "vm-c", VmUUID: "uuid-c", ResourcePool: "Silver", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 120, VcpuCount: 4, RamGB: 16, CreationTime: dayStart.Add(-48 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t3, Name: "vm-change", Vcenter: "vc-a", VmID: "vm-c", VmUUID: "uuid-c", ResourcePool: "Gold", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 140, VcpuCount: 6, RamGB: 24, CreationTime: dayStart.Add(-48 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, // Missing-creation VM: snapshot rows lack CreationTime; lifecycle cache should backfill FirstSeen (t2). {SnapshotTime: t2, Name: "vm-partial", Vcenter: "vc-a", VmID: "vm-p", VmUUID: "uuid-p", ResourcePool: "Tin", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 60, VcpuCount: 2, RamGB: 8, CreationTime: 0, IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, {SnapshotTime: t3, Name: "vm-partial", Vcenter: "vc-a", VmID: "vm-p", VmUUID: "uuid-p", ResourcePool: "Tin", Datacenter: "dc-a", Cluster: "cluster-a", Folder: "/prod", ProvisionedDisk: 60, VcpuCount: 2, RamGB: 8, CreationTime: 0, IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"}, } for _, seed := range seeds { if err := insertHourlyCacheSeedRow(ctx, dbConn, seed); err != nil { t.Fatalf("failed to insert hourly edge-case seed row: %v", err) } } if err := db.UpsertVmLifecycleCache(ctx, dbConn, "vc-a", "vm-p", "uuid-p", "vm-partial", "cluster-a", time.Unix(t2, 0), sql.NullInt64{}); err != nil { t.Fatalf("failed to upsert lifecycle cache for vm-partial: %v", err) } if err := task.aggregateDailySummaryWithMode(ctx, dayStart, true, true); err != nil { t.Fatalf("aggregateDailySummaryWithMode failed: %v", err) } summaryTable, err := dailySummaryTableName(dayStart) if err != nil { t.Fatalf("failed to build summary table name: %v", err) } rows, err := loadDailySummaryRows(ctx, dbConn, summaryTable) if err != nil { t.Fatalf("failed to load daily summary rows: %v", err) } if len(rows) != 3 { t.Fatalf("unexpected daily summary row count: got %d want %d", len(rows), 3) } byKey := mapRowsByKeyDaily(rows) partial := byKey["vc-a|vm-p|uuid-p|vm-partial"] if partial.CreationTime != t2 { t.Fatalf("expected vm-partial creation to be backfilled from lifecycle FirstSeen: got %d want %d", partial.CreationTime, t2) } wantPartialPresence := float64(dayEnd.Unix()-t2) / float64(dayEnd.Unix()-dayStart.Unix()) if !approxEqual(partial.AvgIsPresent, wantPartialPresence, 1e-9) { t.Fatalf("unexpected vm-partial AvgIsPresent after lifecycle creation backfill: got %.12f want %.12f", partial.AvgIsPresent, wantPartialPresence) } gone := byKey["vc-a|vm-g|uuid-g|vm-gone"] if gone.DeletionTime != t2 { t.Fatalf("expected vm-gone deletion to be inferred from consecutive misses: got %d want %d", gone.DeletionTime, t2) } wantGonePresence := float64(t2-dayStart.Unix()) / float64(dayEnd.Unix()-dayStart.Unix()) if !approxEqual(gone.AvgIsPresent, wantGonePresence, 1e-9) { t.Fatalf("unexpected vm-gone AvgIsPresent after inferred deletion: got %.12f want %.12f", gone.AvgIsPresent, wantGonePresence) } change := byKey["vc-a|vm-c|uuid-c|vm-change"] if change.ResourcePool != "Gold" { t.Fatalf("unexpected vm-change ResourcePool: got %q want %q", change.ResourcePool, "Gold") } if !approxEqual(change.AvgVcpuCount, 4.0, 1e-9) { t.Fatalf("unexpected vm-change AvgVcpuCount: got %.12f want %.12f", change.AvgVcpuCount, 4.0) } if !approxEqual(change.AvgRamGB, 16.0, 1e-9) { t.Fatalf("unexpected vm-change AvgRamGB: got %.12f want %.12f", change.AvgRamGB, 16.0) } if !approxEqual(change.AvgProvisionedDisk, 120.0, 1e-9) { t.Fatalf("unexpected vm-change AvgProvisionedDisk: got %.12f want %.12f", change.AvgProvisionedDisk, 120.0) } if !approxEqual(change.PoolTinPct, 100.0/3.0, 1e-9) || !approxEqual(change.PoolSilverPct, 100.0/3.0, 1e-9) || !approxEqual(change.PoolGoldPct, 100.0/3.0, 1e-9) { t.Fatalf("unexpected vm-change pool percentages: tin=%.12f silver=%.12f gold=%.12f", change.PoolTinPct, change.PoolSilverPct, change.PoolGoldPct) } } type summaryTotalsByVcenter struct { Vcenter string `db:"vcenter"` VmCount int64 `db:"vm_count"` VcpuTotal int64 `db:"vcpu_total"` RamTotal int64 `db:"ram_total"` } func newTasksTestCronTaskForAggregateFlow(t *testing.T, dbConn *sqlx.DB) *CronTask { t.Helper() task := newTasksTestCronTask(dbConn) cfg := &settings.Settings{Values: &settings.SettingsYML{}} asyncReports := false cfg.Values.Settings.AsyncReportGeneration = &asyncReports cfg.Values.Settings.ReportsDir = t.TempDir() cfg.Values.Settings.MonthlyAggregationGranularity = "daily" cfg.Values.Settings.ScheduledAggregationEngine = "go" task.Settings = cfg return task } func assertSummaryCacheMatchesByVcenter(t *testing.T, ctx context.Context, dbConn *sqlx.DB, summaryTable, snapshotType string, snapshotTime int64) { t.Helper() sql := fmt.Sprintf(` SELECT "Vcenter" AS vcenter, COUNT(1) AS vm_count, CAST(COALESCE(SUM(COALESCE("AvgVcpuCount","VcpuCount")),0) AS BIGINT) AS vcpu_total, CAST(COALESCE(SUM(COALESCE("AvgRamGB","RamGB")),0) AS BIGINT) AS ram_total FROM %s GROUP BY "Vcenter" `, summaryTable) var expected []summaryTotalsByVcenter if err := dbConn.SelectContext(ctx, &expected, sql); err != nil { t.Fatalf("failed to load expected summary totals: %v", err) } if len(expected) == 0 { t.Fatal("expected non-empty summary totals") } cacheCountQuery := dbConn.Rebind(` SELECT COUNT(1) FROM vcenter_aggregate_totals WHERE "SnapshotType" = ? AND "SnapshotTime" = ? `) var cacheCount int if err := dbConn.GetContext(ctx, &cacheCount, cacheCountQuery, snapshotType, snapshotTime); err != nil { t.Fatalf("failed to count cache rows: %v", err) } if cacheCount != len(expected) { t.Fatalf("unexpected cache row count: got %d want %d", cacheCount, len(expected)) } for _, exp := range expected { rows, err := db.ListVcenterAggregateTotals(ctx, dbConn, exp.Vcenter, snapshotType, 10) if err != nil { t.Fatalf("ListVcenterAggregateTotals failed for %s/%s: %v", exp.Vcenter, snapshotType, err) } var got *db.VcenterTotalRow for i := range rows { if rows[i].SnapshotTime == snapshotTime { got = &rows[i] break } } if got == nil { t.Fatalf("missing cache row for vcenter=%s snapshot_type=%s snapshot_time=%d", exp.Vcenter, snapshotType, snapshotTime) } if got.VmCount != exp.VmCount || got.VcpuTotal != exp.VcpuTotal || got.RamTotalGB != exp.RamTotal { t.Fatalf( "cache mismatch for vcenter=%s snapshot_type=%s: got(vm=%d vcpu=%d ram=%d) want(vm=%d vcpu=%d ram=%d)", exp.Vcenter, snapshotType, got.VmCount, got.VcpuTotal, got.RamTotalGB, exp.VmCount, exp.VcpuTotal, exp.RamTotal, ) } } } func assertRollupTotalSamplesForVcenter(t *testing.T, ctx context.Context, dbConn *sqlx.DB, dayUnix int64, vcenter string, wantTotalSamples int64) { t.Helper() query := dbConn.Rebind(` SELECT "TotalSamples" FROM vm_daily_rollup WHERE "Date" = ? AND "Vcenter" = ? `) var got []int64 if err := dbConn.SelectContext(ctx, &got, query, dayUnix, vcenter); err != nil { t.Fatalf("failed to read rollup total samples for %s: %v", vcenter, err) } if len(got) == 0 { t.Fatalf("no rollup rows found for vcenter=%s date=%d", vcenter, dayUnix) } for _, value := range got { if value != wantTotalSamples { t.Fatalf("unexpected rollup TotalSamples for vcenter=%s: got %d want %d (rows=%v)", vcenter, value, wantTotalSamples, got) } } } func assertSnapshotRegistryRow(t *testing.T, ctx context.Context, dbConn *sqlx.DB, snapshotType, tableName string, snapshotTime int64, snapshotCount int64) { t.Helper() var row struct { SnapshotType string `db:"snapshot_type"` TableName string `db:"table_name"` SnapshotTime int64 `db:"snapshot_time"` SnapshotCount int64 `db:"snapshot_count"` } query := dbConn.Rebind(` SELECT snapshot_type, table_name, snapshot_time, snapshot_count FROM snapshot_registry WHERE table_name = ? `) if err := dbConn.GetContext(ctx, &row, query, tableName); err != nil { t.Fatalf("failed to load snapshot_registry row for table %s: %v", tableName, err) } if row.SnapshotType != snapshotType { t.Fatalf("unexpected snapshot type for table %s: got %s want %s", tableName, row.SnapshotType, snapshotType) } if row.SnapshotTime != snapshotTime { t.Fatalf("unexpected snapshot time for table %s: got %d want %d", tableName, row.SnapshotTime, snapshotTime) } if row.SnapshotCount != snapshotCount { t.Fatalf("unexpected snapshot count for table %s: got %d want %d", tableName, row.SnapshotCount, snapshotCount) } }