Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 27cab61e89 | |||
| 11df6e0560 |
Vendored
+20
-3
@@ -89,7 +89,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.web2-shell-wide {
|
.web2-shell-wide {
|
||||||
max-width: 1420px;
|
max-width: min(1760px, calc(100vw - 2rem));
|
||||||
}
|
}
|
||||||
|
|
||||||
.web2-page-head {
|
.web2-page-head {
|
||||||
@@ -101,21 +101,26 @@ body {
|
|||||||
.web2-page-head-row {
|
.web2-page-head-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.web2-head-copy {
|
.web2-head-copy {
|
||||||
|
flex: 1 1 740px;
|
||||||
|
min-width: 0;
|
||||||
max-width: 72ch;
|
max-width: 72ch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.web2-page-title {
|
.web2-page-title {
|
||||||
margin-top: 0.6rem;
|
margin-top: 0.6rem;
|
||||||
font-family: var(--theme_font_display);
|
font-family: var(--theme_font_display);
|
||||||
font-size: clamp(1.95rem, 1.2rem + 1.9vw, 2.65rem);
|
font-size: clamp(1.7rem, 1.1rem + 1.6vw, 2.35rem);
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
letter-spacing: -0.325px;
|
letter-spacing: -0.325px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.web2-page-subtitle {
|
.web2-page-subtitle {
|
||||||
@@ -129,6 +134,8 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex: 0 0 auto;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -710,6 +717,16 @@ summary:focus-visible,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1500px) {
|
||||||
|
.web2-shell {
|
||||||
|
padding-left: 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
.web2-shell-wide {
|
||||||
|
max-width: min(1860px, calc(100vw - 1.25rem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 780px) {
|
@media (min-width: 780px) {
|
||||||
.web2-kpi-grid {
|
.web2-kpi-grid {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
|||||||
@@ -0,0 +1,589 @@
|
|||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"math"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
"vctp/db"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tasksTestDatabase struct {
|
||||||
|
dbConn *sqlx.DB
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *tasksTestDatabase) DB() *sqlx.DB { return d.dbConn }
|
||||||
|
func (d *tasksTestDatabase) Queries() db.Querier { return nil }
|
||||||
|
func (d *tasksTestDatabase) Logger() *slog.Logger {
|
||||||
|
if d.logger != nil {
|
||||||
|
return d.logger
|
||||||
|
}
|
||||||
|
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
}
|
||||||
|
func (d *tasksTestDatabase) Close() error { return d.dbConn.Close() }
|
||||||
|
|
||||||
|
type dailySummaryRow struct {
|
||||||
|
Name string `db:"Name"`
|
||||||
|
Vcenter string `db:"Vcenter"`
|
||||||
|
VmId string `db:"VmId"`
|
||||||
|
VmUuid string `db:"VmUuid"`
|
||||||
|
ResourcePool string `db:"ResourcePool"`
|
||||||
|
CreationTime int64 `db:"CreationTime"`
|
||||||
|
DeletionTime int64 `db:"DeletionTime"`
|
||||||
|
SnapshotTime int64 `db:"SnapshotTime"`
|
||||||
|
SamplesPresent int64 `db:"SamplesPresent"`
|
||||||
|
AvgVcpuCount float64 `db:"AvgVcpuCount"`
|
||||||
|
AvgRamGB float64 `db:"AvgRamGB"`
|
||||||
|
AvgProvisionedDisk float64 `db:"AvgProvisionedDisk"`
|
||||||
|
AvgIsPresent float64 `db:"AvgIsPresent"`
|
||||||
|
PoolTinPct float64 `db:"PoolTinPct"`
|
||||||
|
PoolBronzePct float64 `db:"PoolBronzePct"`
|
||||||
|
PoolSilverPct float64 `db:"PoolSilverPct"`
|
||||||
|
PoolGoldPct float64 `db:"PoolGoldPct"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type monthlySummaryRow struct {
|
||||||
|
Name string `db:"Name"`
|
||||||
|
Vcenter string `db:"Vcenter"`
|
||||||
|
VmId string `db:"VmId"`
|
||||||
|
VmUuid string `db:"VmUuid"`
|
||||||
|
ResourcePool string `db:"ResourcePool"`
|
||||||
|
CreationTime int64 `db:"CreationTime"`
|
||||||
|
DeletionTime int64 `db:"DeletionTime"`
|
||||||
|
SamplesPresent int64 `db:"SamplesPresent"`
|
||||||
|
AvgVcpuCount float64 `db:"AvgVcpuCount"`
|
||||||
|
AvgRamGB float64 `db:"AvgRamGB"`
|
||||||
|
AvgProvisionedDisk float64 `db:"AvgProvisionedDisk"`
|
||||||
|
AvgIsPresent float64 `db:"AvgIsPresent"`
|
||||||
|
PoolTinPct float64 `db:"PoolTinPct"`
|
||||||
|
PoolBronzePct float64 `db:"PoolBronzePct"`
|
||||||
|
PoolSilverPct float64 `db:"PoolSilverPct"`
|
||||||
|
PoolGoldPct float64 `db:"PoolGoldPct"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type hourlySeedRow struct {
|
||||||
|
SnapshotTime int64
|
||||||
|
Name string
|
||||||
|
Vcenter string
|
||||||
|
VmID string
|
||||||
|
VmUUID string
|
||||||
|
ResourcePool string
|
||||||
|
Datacenter string
|
||||||
|
Cluster string
|
||||||
|
Folder string
|
||||||
|
ProvisionedDisk float64
|
||||||
|
VcpuCount int64
|
||||||
|
RamGB int64
|
||||||
|
CreationTime int64
|
||||||
|
DeletionTime int64
|
||||||
|
IsTemplate string
|
||||||
|
PoweredOn string
|
||||||
|
SrmPlaceholder string
|
||||||
|
}
|
||||||
|
|
||||||
|
type dailySeedRow struct {
|
||||||
|
SnapshotTime int64
|
||||||
|
Name string
|
||||||
|
Vcenter string
|
||||||
|
VmID string
|
||||||
|
VmUUID string
|
||||||
|
ResourcePool string
|
||||||
|
Datacenter string
|
||||||
|
Cluster string
|
||||||
|
Folder string
|
||||||
|
ProvisionedDisk float64
|
||||||
|
VcpuCount int64
|
||||||
|
RamGB int64
|
||||||
|
CreationTime int64
|
||||||
|
DeletionTime int64
|
||||||
|
IsTemplate string
|
||||||
|
PoweredOn string
|
||||||
|
SrmPlaceholder string
|
||||||
|
SamplesPresent int64
|
||||||
|
AvgVcpuCount float64
|
||||||
|
AvgRamGB float64
|
||||||
|
AvgProvisionedDisk float64
|
||||||
|
AvgIsPresent float64
|
||||||
|
PoolTinPct float64
|
||||||
|
PoolBronzePct float64
|
||||||
|
PoolSilverPct float64
|
||||||
|
PoolGoldPct float64
|
||||||
|
Tin float64
|
||||||
|
Bronze float64
|
||||||
|
Silver float64
|
||||||
|
Gold float64
|
||||||
|
TotalSamples int64
|
||||||
|
SumVcpu int64
|
||||||
|
SumRam int64
|
||||||
|
SumDisk float64
|
||||||
|
TinHits int64
|
||||||
|
BronzeHits int64
|
||||||
|
SilverHits int64
|
||||||
|
GoldHits int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDailyGoldenParity_SQLUnionVsGoCanonical(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.January, 15, 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()
|
||||||
|
|
||||||
|
rows := []hourlySeedRow{
|
||||||
|
{SnapshotTime: t1, Name: "vm-alpha", Vcenter: "vc-a", VmID: "vm-1", VmUUID: "uuid-1", ResourcePool: "Tin", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod", ProvisionedDisk: 100, VcpuCount: 2, RamGB: 8, CreationTime: 0, IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
|
||||||
|
{SnapshotTime: t3, Name: "vm-alpha", Vcenter: "vc-a", VmID: "vm-1", VmUUID: "uuid-1", ResourcePool: "Gold", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod", ProvisionedDisk: 120, VcpuCount: 4, RamGB: 16, CreationTime: dayStart.Add(30 * time.Minute).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
|
||||||
|
{SnapshotTime: t2, Name: "vm-bravo", Vcenter: "vc-a", VmID: "vm-2", VmUUID: "uuid-2", ResourcePool: "Bronze", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod", ProvisionedDisk: 30, VcpuCount: 1, RamGB: 2, CreationTime: dayStart.Add(-2 * time.Hour).Unix(), DeletionTime: dayStart.Add(4 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
|
||||||
|
{SnapshotTime: t1, Name: "vm-charlie", Vcenter: "vc-a", VmID: "vm-3", VmUUID: "uuid-3", ResourcePool: "Silver", Datacenter: "dc-1", Cluster: "cluster-2", Folder: "/prod2", ProvisionedDisk: 50, VcpuCount: 2, RamGB: 4, CreationTime: dayStart.Add(-5 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
|
||||||
|
{SnapshotTime: t3, Name: "vm-charlie", Vcenter: "vc-a", VmID: "vm-3", VmUUID: "uuid-3", ResourcePool: "Silver", Datacenter: "dc-1", Cluster: "cluster-2", Folder: "/prod2", ProvisionedDisk: 50, VcpuCount: 2, RamGB: 4, CreationTime: dayStart.Add(-5 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE"},
|
||||||
|
{SnapshotTime: t3, Name: "vm-template", Vcenter: "vc-a", VmID: "vm-t", VmUUID: "uuid-t", ResourcePool: "Tin", Datacenter: "dc-1", Cluster: "cluster-3", Folder: "/templates", ProvisionedDisk: 500, VcpuCount: 16, RamGB: 64, CreationTime: dayStart.Add(-10 * time.Hour).Unix(), IsTemplate: "TRUE", PoweredOn: "FALSE", SrmPlaceholder: "FALSE"},
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
if err := insertHourlyCacheSeedRow(ctx, dbConn, row); err != nil {
|
||||||
|
t.Fatalf("failed to insert vm_hourly_stats row: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hourlyTableTimes := []int64{t1, t2, t3}
|
||||||
|
hourlyTables := make([]string, 0, len(hourlyTableTimes))
|
||||||
|
for _, ts := range hourlyTableTimes {
|
||||||
|
tableName, err := hourlyInventoryTableName(time.Unix(ts, 0).UTC())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build hourly table name: %v", err)
|
||||||
|
}
|
||||||
|
hourlyTables = append(hourlyTables, tableName)
|
||||||
|
if err := db.EnsureSnapshotTable(ctx, dbConn, tableName); err != nil {
|
||||||
|
t.Fatalf("failed to ensure snapshot table %s: %v", tableName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, row := range rows {
|
||||||
|
tableName, err := hourlyInventoryTableName(time.Unix(row.SnapshotTime, 0).UTC())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build per-row hourly table name: %v", err)
|
||||||
|
}
|
||||||
|
if err := insertHourlySnapshotSeedRow(ctx, dbConn, tableName, row); err != nil {
|
||||||
|
t.Fatalf("failed to insert snapshot row for table %s: %v", tableName, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldSummaryTable, err := db.SafeTableName("test_daily_sql_union_summary")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build old summary table name: %v", err)
|
||||||
|
}
|
||||||
|
newSummaryTable, err := db.SafeTableName("test_daily_go_cache_summary")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build new summary table name: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.EnsureSummaryTable(ctx, dbConn, oldSummaryTable); err != nil {
|
||||||
|
t.Fatalf("failed to ensure old summary table: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.EnsureSummaryTable(ctx, dbConn, newSummaryTable); err != nil {
|
||||||
|
t.Fatalf("failed to ensure new summary table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unionQuery, err := buildUnionQuery(hourlyTables, summaryUnionColumns, templateExclusionFilter())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build union query: %v", err)
|
||||||
|
}
|
||||||
|
insertSQL, err := db.BuildDailySummaryInsert(oldSummaryTable, unionQuery)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build daily sql insert: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, insertSQL); err != nil {
|
||||||
|
t.Fatalf("failed to execute daily sql insert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aggMap, snapTimes, err := task.scanHourlyCache(ctx, dayStart, dayEnd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("scanHourlyCache failed: %v", err)
|
||||||
|
}
|
||||||
|
totalSamplesByVcenter := sampleCountsByVcenter(aggMap)
|
||||||
|
if err := task.insertDailyAggregates(ctx, newSummaryTable, aggMap, len(snapTimes), totalSamplesByVcenter); err != nil {
|
||||||
|
t.Fatalf("insertDailyAggregates failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldRows, err := loadDailySummaryRows(ctx, dbConn, oldSummaryTable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load old daily rows: %v", err)
|
||||||
|
}
|
||||||
|
newRows, err := loadDailySummaryRows(ctx, dbConn, newSummaryTable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load new daily rows: %v", err)
|
||||||
|
}
|
||||||
|
assertDailySummaryParity(t, oldRows, newRows)
|
||||||
|
|
||||||
|
byKey := mapRowsByKeyDaily(newRows)
|
||||||
|
alpha := byKey["vc-a|vm-1|uuid-1|vm-alpha"]
|
||||||
|
if !approxEqual(alpha.AvgIsPresent, 2.0/3.0, 1e-9) {
|
||||||
|
t.Fatalf("unexpected alpha AvgIsPresent: got %.12f want %.12f", alpha.AvgIsPresent, 2.0/3.0)
|
||||||
|
}
|
||||||
|
if alpha.CreationTime != dayStart.Add(30*time.Minute).Unix() {
|
||||||
|
t.Fatalf("unexpected alpha CreationTime: got %d want %d", alpha.CreationTime, dayStart.Add(30*time.Minute).Unix())
|
||||||
|
}
|
||||||
|
if alpha.ResourcePool != "Gold" {
|
||||||
|
t.Fatalf("unexpected alpha ResourcePool: got %q want %q", alpha.ResourcePool, "Gold")
|
||||||
|
}
|
||||||
|
if alpha.SnapshotTime != t3 {
|
||||||
|
t.Fatalf("unexpected alpha SnapshotTime: got %d want %d", alpha.SnapshotTime, t3)
|
||||||
|
}
|
||||||
|
if !approxEqual(alpha.PoolTinPct, 50.0, 1e-9) || !approxEqual(alpha.PoolGoldPct, 50.0, 1e-9) {
|
||||||
|
t.Fatalf("unexpected alpha pool mix: tin=%.6f gold=%.6f", alpha.PoolTinPct, alpha.PoolGoldPct)
|
||||||
|
}
|
||||||
|
|
||||||
|
bravo := byKey["vc-a|vm-2|uuid-2|vm-bravo"]
|
||||||
|
if bravo.DeletionTime != dayStart.Add(4*time.Hour).Unix() {
|
||||||
|
t.Fatalf("unexpected bravo DeletionTime: got %d want %d", bravo.DeletionTime, dayStart.Add(4*time.Hour).Unix())
|
||||||
|
}
|
||||||
|
if !approxEqual(bravo.AvgIsPresent, 1.0/3.0, 1e-9) {
|
||||||
|
t.Fatalf("unexpected bravo AvgIsPresent: got %.12f want %.12f", bravo.AvgIsPresent, 1.0/3.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonthlyGoldenParity_SQLDailyUnionVsGoDailyRollup(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.February, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
monthEnd := monthStart.AddDate(0, 1, 0)
|
||||||
|
day1 := time.Date(2026, time.February, 3, 0, 0, 0, 0, time.UTC)
|
||||||
|
day2 := day1.AddDate(0, 0, 1)
|
||||||
|
|
||||||
|
day1Table, err := dailySummaryTableName(day1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build day1 table name: %v", err)
|
||||||
|
}
|
||||||
|
day2Table, err := dailySummaryTableName(day2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build day2 table name: %v", err)
|
||||||
|
}
|
||||||
|
for _, table := range []string{day1Table, day2Table} {
|
||||||
|
if err := db.EnsureSummaryTable(ctx, dbConn, table); err != nil {
|
||||||
|
t.Fatalf("failed to ensure daily summary table %s: %v", table, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
seeds := []dailySeedRow{
|
||||||
|
{
|
||||||
|
SnapshotTime: day1.Unix(), Name: "vm-alpha", Vcenter: "vc-a", VmID: "vm-1", VmUUID: "uuid-1",
|
||||||
|
ResourcePool: "Bronze", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod",
|
||||||
|
ProvisionedDisk: 100, VcpuCount: 4, RamGB: 8, CreationTime: monthStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE",
|
||||||
|
SamplesPresent: 2, AvgVcpuCount: 3, AvgRamGB: 6, AvgProvisionedDisk: 90, AvgIsPresent: 1.0,
|
||||||
|
PoolBronzePct: 100, Bronze: 100,
|
||||||
|
TotalSamples: 2, SumVcpu: 6, SumRam: 12, SumDisk: 180, BronzeHits: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
SnapshotTime: day2.Unix(), Name: "vm-alpha", Vcenter: "vc-a", VmID: "vm-1", VmUUID: "uuid-1",
|
||||||
|
ResourcePool: "Tin", Datacenter: "dc-1", Cluster: "cluster-1", Folder: "/prod",
|
||||||
|
ProvisionedDisk: 110, VcpuCount: 2, RamGB: 8, CreationTime: monthStart.Add(-24 * time.Hour).Unix(), IsTemplate: "FALSE", PoweredOn: "TRUE", SrmPlaceholder: "FALSE",
|
||||||
|
SamplesPresent: 2, AvgVcpuCount: 2, AvgRamGB: 8, AvgProvisionedDisk: 110, AvgIsPresent: 1.0,
|
||||||
|
PoolTinPct: 100, Tin: 100,
|
||||||
|
TotalSamples: 2, SumVcpu: 4, SumRam: 16, SumDisk: 220, TinHits: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, seed := range seeds {
|
||||||
|
targetTable := day1Table
|
||||||
|
if seed.SnapshotTime == day2.Unix() {
|
||||||
|
targetTable = day2Table
|
||||||
|
}
|
||||||
|
if err := insertDailySummarySeedRow(ctx, dbConn, targetTable, seed); err != nil {
|
||||||
|
t.Fatalf("failed to insert daily summary seed row: %v", err)
|
||||||
|
}
|
||||||
|
if err := insertDailyRollupSeedRow(ctx, dbConn, seed); err != nil {
|
||||||
|
t.Fatalf("failed to insert daily rollup seed row: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
oldMonthlyTable, err := db.SafeTableName("test_monthly_sql_union_summary")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build old monthly table name: %v", err)
|
||||||
|
}
|
||||||
|
newMonthlyTable, err := db.SafeTableName("test_monthly_go_rollup_summary")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build new monthly table name: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.EnsureSummaryTable(ctx, dbConn, oldMonthlyTable); err != nil {
|
||||||
|
t.Fatalf("failed to ensure old monthly table: %v", err)
|
||||||
|
}
|
||||||
|
if err := db.EnsureSummaryTable(ctx, dbConn, newMonthlyTable); err != nil {
|
||||||
|
t.Fatalf("failed to ensure new monthly table: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
unionQuery, err := buildUnionQuery([]string{day1Table, day2Table}, monthlyUnionColumns, templateExclusionFilter())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build monthly union query: %v", err)
|
||||||
|
}
|
||||||
|
insertSQL, err := db.BuildMonthlySummaryInsert(oldMonthlyTable, unionQuery)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to build monthly sql insert: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := dbConn.ExecContext(ctx, insertSQL); err != nil {
|
||||||
|
t.Fatalf("failed to execute monthly sql insert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
aggMap, err := task.scanDailyRollup(ctx, monthStart, monthEnd)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("scanDailyRollup failed: %v", err)
|
||||||
|
}
|
||||||
|
if err := task.insertMonthlyAggregates(ctx, newMonthlyTable, aggMap); err != nil {
|
||||||
|
t.Fatalf("insertMonthlyAggregates failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
oldRows, err := loadMonthlySummaryRows(ctx, dbConn, oldMonthlyTable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load old monthly rows: %v", err)
|
||||||
|
}
|
||||||
|
newRows, err := loadMonthlySummaryRows(ctx, dbConn, newMonthlyTable)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to load new monthly rows: %v", err)
|
||||||
|
}
|
||||||
|
assertMonthlySummaryParity(t, oldRows, newRows)
|
||||||
|
|
||||||
|
byKey := mapRowsByKeyMonthly(newRows)
|
||||||
|
alpha := byKey["vc-a|vm-1|uuid-1|vm-alpha"]
|
||||||
|
if !approxEqual(alpha.AvgVcpuCount, 2.5, 1e-9) {
|
||||||
|
t.Fatalf("unexpected alpha AvgVcpuCount: got %.6f want %.6f", alpha.AvgVcpuCount, 2.5)
|
||||||
|
}
|
||||||
|
if !approxEqual(alpha.AvgIsPresent, 1.0, 1e-9) {
|
||||||
|
t.Fatalf("unexpected alpha AvgIsPresent: got %.6f want %.6f", alpha.AvgIsPresent, 1.0)
|
||||||
|
}
|
||||||
|
if alpha.ResourcePool != "Tin" {
|
||||||
|
t.Fatalf("unexpected alpha ResourcePool: got %q want %q", alpha.ResourcePool, "Tin")
|
||||||
|
}
|
||||||
|
if !approxEqual(alpha.PoolTinPct, 50.0, 1e-9) || !approxEqual(alpha.PoolBronzePct, 50.0, 1e-9) {
|
||||||
|
t.Fatalf("unexpected alpha monthly pool mix: tin=%.6f bronze=%.6f", alpha.PoolTinPct, alpha.PoolBronzePct)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTasksTestCronTask(dbConn *sqlx.DB) *CronTask {
|
||||||
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
return &CronTask{
|
||||||
|
Logger: logger,
|
||||||
|
Database: &tasksTestDatabase{dbConn: dbConn, logger: logger},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertHourlyCacheSeedRow(ctx context.Context, dbConn *sqlx.DB, row hourlySeedRow) error {
|
||||||
|
_, err := dbConn.ExecContext(ctx, `
|
||||||
|
INSERT INTO vm_hourly_stats (
|
||||||
|
"SnapshotTime","Vcenter","VmId","VmUuid","Name","CreationTime","DeletionTime","ResourcePool",
|
||||||
|
"Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder"
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
`,
|
||||||
|
row.SnapshotTime, row.Vcenter, row.VmID, row.VmUUID, row.Name, row.CreationTime, row.DeletionTime, row.ResourcePool,
|
||||||
|
row.Datacenter, row.Cluster, row.Folder, row.ProvisionedDisk, row.VcpuCount, row.RamGB, row.IsTemplate, row.PoweredOn, row.SrmPlaceholder,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertHourlySnapshotSeedRow(ctx context.Context, dbConn *sqlx.DB, table string, row hourlySeedRow) error {
|
||||||
|
sql := fmt.Sprintf(`
|
||||||
|
INSERT INTO %s (
|
||||||
|
"Name","Vcenter","VmId","VmUuid","EventKey","CloudId","CreationTime","DeletionTime","ResourcePool",
|
||||||
|
"Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder","SnapshotTime"
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
`, table)
|
||||||
|
_, err := dbConn.ExecContext(ctx, sql,
|
||||||
|
row.Name, row.Vcenter, row.VmID, row.VmUUID, nil, nil, row.CreationTime, row.DeletionTime, row.ResourcePool,
|
||||||
|
row.Datacenter, row.Cluster, row.Folder, row.ProvisionedDisk, row.VcpuCount, row.RamGB, row.IsTemplate, row.PoweredOn, row.SrmPlaceholder, row.SnapshotTime,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertDailySummarySeedRow(ctx context.Context, dbConn *sqlx.DB, table string, row dailySeedRow) error {
|
||||||
|
sql := fmt.Sprintf(`
|
||||||
|
INSERT INTO %s (
|
||||||
|
"Name","Vcenter","VmId","VmUuid","EventKey","CloudId","CreationTime","DeletionTime","ResourcePool",
|
||||||
|
"Datacenter","Cluster","Folder","ProvisionedDisk","VcpuCount","RamGB","IsTemplate","PoweredOn","SrmPlaceholder",
|
||||||
|
"SnapshotTime","SamplesPresent","AvgVcpuCount","AvgRamGB","AvgProvisionedDisk","AvgIsPresent",
|
||||||
|
"PoolTinPct","PoolBronzePct","PoolSilverPct","PoolGoldPct","Tin","Bronze","Silver","Gold"
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
`, table)
|
||||||
|
_, err := dbConn.ExecContext(ctx, sql,
|
||||||
|
row.Name, row.Vcenter, row.VmID, row.VmUUID, nil, nil, row.CreationTime, row.DeletionTime, row.ResourcePool,
|
||||||
|
row.Datacenter, row.Cluster, row.Folder, row.ProvisionedDisk, row.VcpuCount, row.RamGB, row.IsTemplate, row.PoweredOn, row.SrmPlaceholder,
|
||||||
|
row.SnapshotTime, row.SamplesPresent, row.AvgVcpuCount, row.AvgRamGB, row.AvgProvisionedDisk, row.AvgIsPresent,
|
||||||
|
row.PoolTinPct, row.PoolBronzePct, row.PoolSilverPct, row.PoolGoldPct, row.Tin, row.Bronze, row.Silver, row.Gold,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertDailyRollupSeedRow(ctx context.Context, dbConn *sqlx.DB, row dailySeedRow) error {
|
||||||
|
_, err := dbConn.ExecContext(ctx, `
|
||||||
|
INSERT INTO vm_daily_rollup (
|
||||||
|
"Date","Vcenter","VmId","VmUuid","Name","CreationTime","DeletionTime","SamplesPresent","TotalSamples",
|
||||||
|
"SumVcpu","SumRam","SumDisk","TinHits","BronzeHits","SilverHits","GoldHits",
|
||||||
|
"LastResourcePool","LastDatacenter","LastCluster","LastFolder","LastProvisionedDisk","LastVcpuCount","LastRamGB","IsTemplate","PoweredOn","SrmPlaceholder"
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
|
`,
|
||||||
|
row.SnapshotTime, row.Vcenter, row.VmID, row.VmUUID, row.Name, row.CreationTime, row.DeletionTime, row.SamplesPresent, row.TotalSamples,
|
||||||
|
row.SumVcpu, row.SumRam, row.SumDisk, row.TinHits, row.BronzeHits, row.SilverHits, row.GoldHits,
|
||||||
|
row.ResourcePool, row.Datacenter, row.Cluster, row.Folder, row.ProvisionedDisk, row.VcpuCount, row.RamGB, row.IsTemplate, row.PoweredOn, row.SrmPlaceholder,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadDailySummaryRows(ctx context.Context, dbConn *sqlx.DB, table string) ([]dailySummaryRow, error) {
|
||||||
|
sql := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
COALESCE("Name",'') AS "Name",
|
||||||
|
COALESCE("Vcenter",'') AS "Vcenter",
|
||||||
|
COALESCE("VmId",'') AS "VmId",
|
||||||
|
COALESCE("VmUuid",'') AS "VmUuid",
|
||||||
|
COALESCE("ResourcePool",'') AS "ResourcePool",
|
||||||
|
COALESCE("CreationTime",0) AS "CreationTime",
|
||||||
|
COALESCE("DeletionTime",0) AS "DeletionTime",
|
||||||
|
COALESCE("SnapshotTime",0) AS "SnapshotTime",
|
||||||
|
COALESCE("SamplesPresent",0) AS "SamplesPresent",
|
||||||
|
COALESCE("AvgVcpuCount",0) AS "AvgVcpuCount",
|
||||||
|
COALESCE("AvgRamGB",0) AS "AvgRamGB",
|
||||||
|
COALESCE("AvgProvisionedDisk",0) AS "AvgProvisionedDisk",
|
||||||
|
COALESCE("AvgIsPresent",0) AS "AvgIsPresent",
|
||||||
|
COALESCE("PoolTinPct",0) AS "PoolTinPct",
|
||||||
|
COALESCE("PoolBronzePct",0) AS "PoolBronzePct",
|
||||||
|
COALESCE("PoolSilverPct",0) AS "PoolSilverPct",
|
||||||
|
COALESCE("PoolGoldPct",0) AS "PoolGoldPct"
|
||||||
|
FROM %s
|
||||||
|
ORDER BY "Vcenter", "VmId", "VmUuid", "Name"
|
||||||
|
`, table)
|
||||||
|
var out []dailySummaryRow
|
||||||
|
return out, dbConn.SelectContext(ctx, &out, sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMonthlySummaryRows(ctx context.Context, dbConn *sqlx.DB, table string) ([]monthlySummaryRow, error) {
|
||||||
|
sql := fmt.Sprintf(`
|
||||||
|
SELECT
|
||||||
|
COALESCE("Name",'') AS "Name",
|
||||||
|
COALESCE("Vcenter",'') AS "Vcenter",
|
||||||
|
COALESCE("VmId",'') AS "VmId",
|
||||||
|
COALESCE("VmUuid",'') AS "VmUuid",
|
||||||
|
COALESCE("ResourcePool",'') AS "ResourcePool",
|
||||||
|
COALESCE("CreationTime",0) AS "CreationTime",
|
||||||
|
COALESCE("DeletionTime",0) AS "DeletionTime",
|
||||||
|
COALESCE("SamplesPresent",0) AS "SamplesPresent",
|
||||||
|
COALESCE("AvgVcpuCount",0) AS "AvgVcpuCount",
|
||||||
|
COALESCE("AvgRamGB",0) AS "AvgRamGB",
|
||||||
|
COALESCE("AvgProvisionedDisk",0) AS "AvgProvisionedDisk",
|
||||||
|
COALESCE("AvgIsPresent",0) AS "AvgIsPresent",
|
||||||
|
COALESCE("PoolTinPct",0) AS "PoolTinPct",
|
||||||
|
COALESCE("PoolBronzePct",0) AS "PoolBronzePct",
|
||||||
|
COALESCE("PoolSilverPct",0) AS "PoolSilverPct",
|
||||||
|
COALESCE("PoolGoldPct",0) AS "PoolGoldPct"
|
||||||
|
FROM %s
|
||||||
|
ORDER BY "Vcenter", "VmId", "VmUuid", "Name"
|
||||||
|
`, table)
|
||||||
|
var out []monthlySummaryRow
|
||||||
|
return out, dbConn.SelectContext(ctx, &out, sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapRowsByKeyDaily(rows []dailySummaryRow) map[string]dailySummaryRow {
|
||||||
|
out := make(map[string]dailySummaryRow, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out[dailyRowKey(row)] = row
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapRowsByKeyMonthly(rows []monthlySummaryRow) map[string]monthlySummaryRow {
|
||||||
|
out := make(map[string]monthlySummaryRow, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out[monthlyRowKey(row)] = row
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func dailyRowKey(r dailySummaryRow) string {
|
||||||
|
return fmt.Sprintf("%s|%s|%s|%s", r.Vcenter, r.VmId, r.VmUuid, r.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func monthlyRowKey(r monthlySummaryRow) string {
|
||||||
|
return fmt.Sprintf("%s|%s|%s|%s", r.Vcenter, r.VmId, r.VmUuid, r.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertDailySummaryParity(t *testing.T, oldRows, newRows []dailySummaryRow) {
|
||||||
|
t.Helper()
|
||||||
|
if len(oldRows) != len(newRows) {
|
||||||
|
t.Fatalf("daily row count mismatch: old=%d new=%d", len(oldRows), len(newRows))
|
||||||
|
}
|
||||||
|
oldByKey := mapRowsByKeyDaily(oldRows)
|
||||||
|
newByKey := mapRowsByKeyDaily(newRows)
|
||||||
|
for key, oldRow := range oldByKey {
|
||||||
|
newRow, ok := newByKey[key]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("missing key in new daily output: %s", key)
|
||||||
|
}
|
||||||
|
if oldRow.ResourcePool != newRow.ResourcePool ||
|
||||||
|
oldRow.CreationTime != newRow.CreationTime ||
|
||||||
|
oldRow.DeletionTime != newRow.DeletionTime ||
|
||||||
|
oldRow.SnapshotTime != newRow.SnapshotTime ||
|
||||||
|
oldRow.SamplesPresent != newRow.SamplesPresent {
|
||||||
|
t.Fatalf("daily scalar mismatch key=%s old=%+v new=%+v", key, oldRow, newRow)
|
||||||
|
}
|
||||||
|
assertFloatClose(t, "AvgVcpuCount", key, oldRow.AvgVcpuCount, newRow.AvgVcpuCount, 1e-9)
|
||||||
|
assertFloatClose(t, "AvgRamGB", key, oldRow.AvgRamGB, newRow.AvgRamGB, 1e-9)
|
||||||
|
assertFloatClose(t, "AvgProvisionedDisk", key, oldRow.AvgProvisionedDisk, newRow.AvgProvisionedDisk, 1e-9)
|
||||||
|
assertFloatClose(t, "AvgIsPresent", key, oldRow.AvgIsPresent, newRow.AvgIsPresent, 1e-9)
|
||||||
|
assertFloatClose(t, "PoolTinPct", key, oldRow.PoolTinPct, newRow.PoolTinPct, 1e-9)
|
||||||
|
assertFloatClose(t, "PoolBronzePct", key, oldRow.PoolBronzePct, newRow.PoolBronzePct, 1e-9)
|
||||||
|
assertFloatClose(t, "PoolSilverPct", key, oldRow.PoolSilverPct, newRow.PoolSilverPct, 1e-9)
|
||||||
|
assertFloatClose(t, "PoolGoldPct", key, oldRow.PoolGoldPct, newRow.PoolGoldPct, 1e-9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertMonthlySummaryParity(t *testing.T, oldRows, newRows []monthlySummaryRow) {
|
||||||
|
t.Helper()
|
||||||
|
if len(oldRows) != len(newRows) {
|
||||||
|
t.Fatalf("monthly row count mismatch: old=%d new=%d", len(oldRows), len(newRows))
|
||||||
|
}
|
||||||
|
oldByKey := mapRowsByKeyMonthly(oldRows)
|
||||||
|
newByKey := mapRowsByKeyMonthly(newRows)
|
||||||
|
for key, oldRow := range oldByKey {
|
||||||
|
newRow, ok := newByKey[key]
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("missing key in new monthly output: %s", key)
|
||||||
|
}
|
||||||
|
if oldRow.ResourcePool != newRow.ResourcePool ||
|
||||||
|
oldRow.CreationTime != newRow.CreationTime ||
|
||||||
|
oldRow.DeletionTime != newRow.DeletionTime ||
|
||||||
|
oldRow.SamplesPresent != newRow.SamplesPresent {
|
||||||
|
t.Fatalf("monthly scalar mismatch key=%s old=%+v new=%+v", key, oldRow, newRow)
|
||||||
|
}
|
||||||
|
assertFloatClose(t, "AvgVcpuCount", key, oldRow.AvgVcpuCount, newRow.AvgVcpuCount, 1e-9)
|
||||||
|
assertFloatClose(t, "AvgRamGB", key, oldRow.AvgRamGB, newRow.AvgRamGB, 1e-9)
|
||||||
|
assertFloatClose(t, "AvgProvisionedDisk", key, oldRow.AvgProvisionedDisk, newRow.AvgProvisionedDisk, 1e-9)
|
||||||
|
assertFloatClose(t, "AvgIsPresent", key, oldRow.AvgIsPresent, newRow.AvgIsPresent, 1e-9)
|
||||||
|
assertFloatClose(t, "PoolTinPct", key, oldRow.PoolTinPct, newRow.PoolTinPct, 1e-9)
|
||||||
|
assertFloatClose(t, "PoolBronzePct", key, oldRow.PoolBronzePct, newRow.PoolBronzePct, 1e-9)
|
||||||
|
assertFloatClose(t, "PoolSilverPct", key, oldRow.PoolSilverPct, newRow.PoolSilverPct, 1e-9)
|
||||||
|
assertFloatClose(t, "PoolGoldPct", key, oldRow.PoolGoldPct, newRow.PoolGoldPct, 1e-9)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFloatClose(t *testing.T, field, key string, oldVal, newVal, eps float64) {
|
||||||
|
t.Helper()
|
||||||
|
if !approxEqual(oldVal, newVal, eps) {
|
||||||
|
t.Fatalf("%s mismatch key=%s old=%.12f new=%.12f", field, key, oldVal, newVal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func approxEqual(a, b, eps float64) bool {
|
||||||
|
return math.Abs(a-b) <= eps
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user