Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
62 changes: 60 additions & 2 deletions internal/config/case.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,8 +132,30 @@ 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. Same syntax as the flags ("2", "0.5"; "512m",
// "4g").
//
// PRECEDENCE IS THE INVERSE of SubjectImage above — case pin > CLI flag,
// not CLI flag > case pin — and the inversion is deliberate. An image pin
// says "this case was written against this build", which an operator
// testing a different build should be able to override. A limits pin says
// "these ceilings are part of what this case ASSERTS", which an operator
// flag must not silently change.
//
// Concretely: director_container_resource_stats_correctness asserts the
// director reports exactly 2000 millicores. If --cpu-limit 8 won, a routine
// suite run would fail that case and report a product bug that is really a
// harness argument — the exact failure the pin exists to prevent.
//
// The rule for a new pinnable field: if a case sets it to make an assertion
// true, the case wins; if it only describes the environment the case was
// developed in, the flag wins.
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 +1457,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 +2847,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 nil when it passes.
func (b ResourceBound) Check(v int64) error {
if b.Eq != nil && v != *b.Eq {
return fmt.Errorf("= %d, want exactly %d", v, *b.Eq)
}
if b.Min != nil && v < *b.Min {
return fmt.Errorf("= %d, want >= %d", v, *b.Min)
}
if b.Max != nil && v > *b.Max {
return fmt.Errorf("= %d, want <= %d", v, *b.Max)
}
return nil
}
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 err := total.Check(524288); err != nil {
t.Errorf("Check(524288) = %v, want pass", err)
}
// The exact value a pre-cgroup director reported: host RAM in KB.
if err := total.Check(32528416); err == nil {
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 err := used.Check(2000); err != nil {
t.Errorf("Check(2000) = %v, want pass at the ceiling", err)
}
if err := used.Check(2001); err == nil {
t.Error("Check(over max) passed — max is vacuous")
}
if err := used.Check(-1); err == nil {
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 err := b.Check(v); err != nil {
t.Errorf("empty bound rejected %d: %v", v, err)
}
}
}
Loading
Loading