512 lines
26 KiB
Go
512 lines
26 KiB
Go
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)
|
|
}
|
|
}
|