From f6ccdad187587784dcbebdf3765cdd23e49d179c Mon Sep 17 00:00:00 2001 From: noah-philip Date: Mon, 22 Jun 2026 09:30:31 -0500 Subject: [PATCH 1/2] Add allocation window equivalence test Signed-off-by: noah-philip --- .../allocation_window_equivalence_test.go | 184 ++++++++++++++++++ test/integration/api/allocation/test.bats | 4 + 2 files changed, 188 insertions(+) create mode 100644 test/integration/api/allocation/allocation_window_equivalence_test.go diff --git a/test/integration/api/allocation/allocation_window_equivalence_test.go b/test/integration/api/allocation/allocation_window_equivalence_test.go new file mode 100644 index 0000000..5b054ae --- /dev/null +++ b/test/integration/api/allocation/allocation_window_equivalence_test.go @@ -0,0 +1,184 @@ +package allocation + +// Description - Verify allocation window parsing behavior for keyword windows +// and equivalent explicit RFC3339 ranges. +// +// Implementation Details +// - Each keyword window is queried first. +// - The response window from that keyword query is then used to build an +// equivalent explicit RFC3339 range. +// - The explicit range query should return valid allocation JSON and a +// consistent total cost for the same window. +// - A completed historical window is inherently more stable, but "today" is +// still tested because it is a supported keyword window. +// +// Passing Criteria +// - Each keyword window returns a successful Allocation API response. +// - Each equivalent explicit RFC3339 range returns a successful Allocation API response. +// - Keyword and explicit range totals match within the documented tolerance. +// - Failures identify the window form and the observed difference. + +import ( + "fmt" + "math" + "testing" + "time" + + "github.com/opencost/opencost-integration-tests/pkg/api" +) + +const ( + windowEquivalenceAbsoluteTolerance = 0.01 + windowEquivalenceRelativeTolerance = 0.001 +) + +type windowEquivalenceCase struct { + name string + window string +} + +var windowEquivalenceCases = []windowEquivalenceCase{ + { + name: "Today", + window: "today", + }, + { + name: "Yesterday", + window: "yesterday", + }, + { + name: "Week", + window: "week", + }, + { + name: "14 Days", + window: "14d", + }, +} + +func TestAllocationKeywordAndExplicitWindowsEquivalent(t *testing.T) { + apiClient := api.NewAPI() + + for _, tc := range windowEquivalenceCases { + t.Run(tc.name, func(t *testing.T) { + keywordResponse := fetchAllocationForWindow(t, apiClient, tc.window) + keywordTotal := sumAllocationWindowTotalCost(t, keywordResponse) + keywordWindow := firstAllocationWindow(t, keywordResponse) + + explicitWindow := formatExplicitRFC3339Window(keywordWindow) + explicitResponse := fetchAllocationForWindow(t, apiClient, explicitWindow) + explicitTotal := sumAllocationWindowTotalCost(t, explicitResponse) + + if !withinWindowEquivalenceTolerance(keywordTotal, explicitTotal) { + absoluteDiff := math.Abs(keywordTotal - explicitTotal) + relativeDiff := relativeWindowDiff(keywordTotal, explicitTotal) + + t.Errorf( + "window=%q explicitWindow=%q totals diverged: keywordTotal=%.6f explicitTotal=%.6f diff=%.6f relativeDiff=%.4f%%", + tc.window, + explicitWindow, + keywordTotal, + explicitTotal, + absoluteDiff, + relativeDiff*100, + ) + } + + t.Logf( + "window=%q explicitWindow=%q keywordTotal=%.6f explicitTotal=%.6f", + tc.window, + explicitWindow, + keywordTotal, + explicitTotal, + ) + }) + } +} + +func fetchAllocationForWindow(t *testing.T, apiClient *api.API, window string) *api.AllocationResponse { + t.Helper() + + response, err := apiClient.GetAllocation(api.AllocationRequest{ + Window: window, + Aggregate: "namespace", + Accumulate: "true", + IncludeIdle: "true", + }) + if err != nil { + t.Fatalf("allocation request failed for window %q: %v", window, err) + } + + if response.Code != 200 { + t.Fatalf("allocation request for window %q returned non-200 code: %d", window, response.Code) + } + + return response +} + +func sumAllocationWindowTotalCost(t *testing.T, response *api.AllocationResponse) float64 { + t.Helper() + + total := 0.0 + itemCount := 0 + + for _, allocationSet := range response.Data { + for _, item := range allocationSet { + total += item.TotalCost + itemCount++ + } + } + + if itemCount == 0 { + t.Fatal("allocation response did not contain any allocation items") + } + + return total +} + +func firstAllocationWindow(t *testing.T, response *api.AllocationResponse) api.Window { + t.Helper() + + for _, allocationSet := range response.Data { + for _, item := range allocationSet { + if item.Window.Start.IsZero() || item.Window.End.IsZero() { + t.Fatalf("allocation item returned invalid window: start=%v end=%v", item.Window.Start, item.Window.End) + } + + return item.Window + } + } + + t.Fatal("allocation response did not contain a window") + return api.Window{} +} + +func formatExplicitRFC3339Window(window api.Window) string { + return fmt.Sprintf( + "%s,%s", + window.Start.UTC().Format(time.RFC3339), + window.End.UTC().Format(time.RFC3339), + ) +} + +func withinWindowEquivalenceTolerance(expected, actual float64) bool { + absoluteDiff := math.Abs(expected - actual) + if absoluteDiff <= windowEquivalenceAbsoluteTolerance { + return true + } + + denominator := math.Max(math.Abs(expected), math.Abs(actual)) + if denominator == 0 { + return true + } + + return absoluteDiff/denominator <= windowEquivalenceRelativeTolerance +} + +func relativeWindowDiff(expected, actual float64) float64 { + denominator := math.Max(math.Abs(expected), math.Abs(actual)) + if denominator == 0 { + return 0 + } + + return math.Abs(expected-actual) / denominator +} \ No newline at end of file diff --git a/test/integration/api/allocation/test.bats b/test/integration/api/allocation/test.bats index 2d41bd7..4b1a2b2 100644 --- a/test/integration/api/allocation/test.bats +++ b/test/integration/api/allocation/test.bats @@ -26,6 +26,10 @@ teardown() { go test namespace_annotations_test.go } +@test "allocation: Keyword and Explicit Windows Are Equivalent" { + go test allocation_window_equivalence_test.go +} + @test "validate_api: negative idle cost values" { go test idle_cost_negative_test.go } From cb2004ecd95042ce214bb1296fd5ebcd3f425404 Mon Sep 17 00:00:00 2001 From: noah-philip Date: Tue, 23 Jun 2026 09:31:11 -0500 Subject: [PATCH 2/2] Add allocation window equivalence test Signed-off-by: noah-philip --- .../api/allocation/allocation_window_equivalence_test.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/integration/api/allocation/allocation_window_equivalence_test.go b/test/integration/api/allocation/allocation_window_equivalence_test.go index 5b054ae..baf48be 100644 --- a/test/integration/api/allocation/allocation_window_equivalence_test.go +++ b/test/integration/api/allocation/allocation_window_equivalence_test.go @@ -50,10 +50,6 @@ var windowEquivalenceCases = []windowEquivalenceCase{ name: "Week", window: "week", }, - { - name: "14 Days", - window: "14d", - }, } func TestAllocationKeywordAndExplicitWindowsEquivalent(t *testing.T) {