Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions internal/config/case.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,20 @@ type TestCase struct {
// Leave empty to use the registry default (or whatever --image/--version
// specifies). Non-strict YAML decode means older harness binaries silently
// ignore these fields — they fall back to the registry default.
SubjectImage string `yaml:"subject_image"`
SubjectVersion string `yaml:"subject_version"`
SubjectImage string `yaml:"subject_image"`

// SubjectCPULimit and SubjectMemLimit pin the subject container's cgroup
// ceilings for this case, overriding the --cpu-limit / --mem-limit flags.
// Same syntax as those flags ("2", "0.5"; "512m", "4g").
//
// A case needs these when the limits are part of what it asserts rather
// than part of how it is being benchmarked — container-awareness cases
// have to run constrained or they assert nothing, and relying on the
// operator to remember the flags would make a plain run report a product
// failure that is really a missing argument.
SubjectCPULimit string `yaml:"subject_cpu_limit"`
SubjectMemLimit string `yaml:"subject_mem_limit"`
SubjectVersion string `yaml:"subject_version"`

Subjects []string `yaml:"subjects"`
Configurations map[string]Configuration `yaml:"configurations"`
Expand Down Expand Up @@ -1435,6 +1447,19 @@ type FleetConfig struct {
// Example: {route.in: {events_in: 500, dropped_count: 500, events_out: 0}}.
ExpectStats map[string]map[string]int64 `yaml:"expect_stats"`

// ExpectResources (stats scenario only) asserts on the DeviceResource
// (inputtype=4) rows the simulator decodes: CPU, memory, volumes and the
// runtime rows that say which environment the subject measured itself in.
// Keyed "<resource type>.<identifier>" (e.g. "cpu.container",
// "runtime.container") → field ("count" | "total" | "used" | "sockets" |
// "cores" | "threads" | "samples") → bound.
//
// Bounds rather than ExpectStats' exact match, because these are GAUGES:
// a CPU row says how busy the box was during one 250 ms sample, so the
// only honest assertions are "equals the configured ceiling" for a limit
// and "within [0, ceiling]" for a reading.
ExpectResources map[string]map[string]ResourceBound `yaml:"expect_resources"`

// BaselineSeconds (config_update data-plane mode only) is how long the driver
// confirms delivery is suppressed after the BEFORE config is delivered, before
// pushing the AFTER config. It proves the case really starts suppressed
Expand Down Expand Up @@ -2812,3 +2837,26 @@ func ListCases(casesDir string) ([]string, error) {
}
return names, nil
}

// ResourceBound constrains one DeviceResource gauge field. Set Eq for an exact
// value, or Min/Max (either or both) for a range. An empty bound matches
// anything, which is how a case asserts only that the row exists.
type ResourceBound struct {
Eq *int64 `yaml:"eq"`
Min *int64 `yaml:"min"`
Max *int64 `yaml:"max"`
}

// Check reports why v fails the bound, or "" when it passes.
func (b ResourceBound) Check(v int64) string {
if b.Eq != nil && v != *b.Eq {
return fmt.Sprintf("= %d, want exactly %d", v, *b.Eq)
}
if b.Min != nil && v < *b.Min {
return fmt.Sprintf("= %d, want >= %d", v, *b.Min)
}
if b.Max != nil && v > *b.Max {
return fmt.Sprintf("= %d, want <= %d", v, *b.Max)
}
return ""
}
68 changes: 68 additions & 0 deletions internal/config/resource_bound_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package config

import (
"testing"

"gopkg.in/yaml.v3"
)

// TestResourceBoundYAMLDecode guards against a silently vacuous assertion.
//
// ResourceBound's fields are pointers so "unset" is distinguishable from zero.
// If a yaml tag ever stops matching, decoding yields all-nil pointers, Check
// returns "pass" for every value, and a case that looks like it asserts exact
// cgroup ceilings would assert only that the row exists — while still printing
// a tick. This test fails instead.
func TestResourceBoundYAMLDecode(t *testing.T) {
var fc struct {
ExpectResources map[string]map[string]ResourceBound `yaml:"expect_resources"`
}
src := `
expect_resources:
runtime.container:
count: {eq: 1}
total: {eq: 524288}
cpu.container:
used: {min: 0, max: 2000}
`
if err := yaml.Unmarshal([]byte(src), &fc); err != nil {
t.Fatal(err)
}

total := fc.ExpectResources["runtime.container"]["total"]
if total.Eq == nil || *total.Eq != 524288 {
t.Fatalf("total.eq did not decode: %+v", total)
}
if why := total.Check(524288); why != "" {
t.Errorf("Check(524288) = %q, want pass", why)
}
// The exact value a pre-cgroup director reported: host RAM in KB.
if why := total.Check(32528416); why == "" {
t.Error("Check(host-sized value) passed — the bound is vacuous")
}

used := fc.ExpectResources["cpu.container"]["used"]
if used.Min == nil || used.Max == nil {
t.Fatalf("min/max did not decode: %+v", used)
}
if why := used.Check(2000); why != "" {
t.Errorf("Check(2000) = %q, want pass at the ceiling", why)
}
if why := used.Check(2001); why == "" {
t.Error("Check(over max) passed — max is vacuous")
}
if why := used.Check(-1); why == "" {
t.Error("Check(under min) passed — min is vacuous")
}
}

// TestResourceBoundEmptyMatchesAnything pins the documented "row must exist"
// form: a bound with nothing set accepts any value.
func TestResourceBoundEmptyMatchesAnything(t *testing.T) {
var b ResourceBound
for _, v := range []int64{-1, 0, 1 << 40} {
if why := b.Check(v); why != "" {
t.Errorf("empty bound rejected %d: %s", v, why)
}
}
}
Loading
Loading