master
go 272 lines 7.19 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package program
4
5 import (
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10 )
11
12 type trueMatcher struct{}
13
14 func (trueMatcher) Matches(_ string, _ SelectorLabels) bool { return true }
15
16 func TestNewProgramScenarios(t *testing.T) {
17 tests := map[string]struct {
18 version string
19 metrics []string
20 charts []Chart
21 wantErr bool
22 assert func(t *testing.T, p *Program)
23 }{
24 "builds immutable snapshot and deterministic metric ordering": {
25 version: "v1",
26 metrics: []string{"windows_tx_total", "windows_rx_total", "windows_tx_total"},
27 charts: []Chart{
28 sampleChart("win.nic.traffic"),
29 },
30 assert: func(t *testing.T, p *Program) {
31 t.Helper()
32
33 gotMetrics := p.MetricNames()
34 require.Len(t, gotMetrics, 2)
35 assert.Equal(t, []string{"windows_rx_total", "windows_tx_total"}, gotMetrics)
36
37 gotCharts := p.Charts()
38 require.Len(t, gotCharts, 1)
39
40 // Mutate returned chart copy and ensure Program internals are unaffected.
41 gotCharts[0].Meta.Title = "mutated-title"
42 again, ok := p.Chart("win.nic.traffic")
43 require.True(t, ok, "Chart() did not return existing template id")
44 assert.Equal(t, "Network traffic", again.Meta.Title)
45 },
46 },
47 "rejects duplicate chart template IDs": {
48 version: "v1",
49 metrics: []string{"windows_rx_total"},
50 charts: []Chart{
51 sampleChart("dup"),
52 sampleChart("dup"),
53 },
54 wantErr: true,
55 },
56 "rejects empty metric name": {
57 version: "v1",
58 metrics: []string{"windows_rx_total", ""},
59 charts: []Chart{
60 sampleChart("win.nic.traffic"),
61 },
62 wantErr: true,
63 },
64 "rejects missing selector matcher": {
65 version: "v1",
66 metrics: []string{"windows_rx_total"},
67 charts: []Chart{
68 {
69 TemplateID: "invalid",
70 Meta: ChartMeta{
71 Title: "Invalid",
72 Family: "net",
73 Context: "win.invalid",
74 Units: "bytes/s",
75 Algorithm: AlgorithmIncremental,
76 Type: ChartTypeLine,
77 },
78 Identity: ChartIdentity{
79 IDTemplate: Template{Raw: "invalid"},
80 Static: true,
81 },
82 Labels: LabelPolicy{
83 Mode: PromotionModeAutoIntersection,
84 Precedence: DefaultLabelPrecedence(),
85 },
86 Lifecycle: LifecyclePolicy{},
87 CollisionReduce: ReduceSum,
88 Dimensions: []Dimension{
89 {
90 Selector: SelectorBinding{
91 Expression: "windows_rx_total",
92 },
93 NameTemplate: Template{Raw: "received"},
94 },
95 },
96 },
97 },
98 wantErr: true,
99 },
100 "rejects invalid chart type": {
101 version: "v1",
102 metrics: []string{"windows_rx_total"},
103 charts: []Chart{
104 func() Chart {
105 chart := sampleChart("invalid-type")
106 chart.Meta.Type = ChartType("bars")
107 return chart
108 }(),
109 },
110 wantErr: true,
111 },
112 "rejects missing label promotion mode": {
113 version: "v1",
114 metrics: []string{"windows_rx_total"},
115 charts: []Chart{
116 func() Chart {
117 chart := sampleChart("missing-label-mode")
118 chart.Labels.Mode = ""
119 return chart
120 }(),
121 },
122 wantErr: true,
123 },
124 "rejects negation-only instance selectors": {
125 version: "v1",
126 metrics: []string{"windows_rx_total"},
127 charts: []Chart{
128 func() Chart {
129 chart := sampleChart("negation-only-instance-selectors")
130 chart.Identity.InstanceByLabels = []InstanceLabelSelector{
131 {Exclude: true, Key: "nic"},
132 }
133 return chart
134 }(),
135 },
136 wantErr: true,
137 },
138 "rejects malformed instance selector keys": {
139 version: "v1",
140 metrics: []string{"windows_rx_total"},
141 charts: []Chart{
142 func() Chart {
143 chart := sampleChart("malformed-instance-selector")
144 chart.Identity.InstanceByLabels = []InstanceLabelSelector{
145 {Key: "nic"},
146 {Exclude: true, Key: " host"},
147 }
148 return chart
149 }(),
150 },
151 wantErr: true,
152 },
153 }
154
155 for name, tc := range tests {
156 t.Run(name, func(t *testing.T) {
157 p, err := New(tc.version, 42, tc.metrics, tc.charts)
158 if tc.wantErr {
159 require.Error(t, err)
160 return
161 }
162 require.NoError(t, err)
163 if tc.assert != nil {
164 tc.assert(t, p)
165 }
166 })
167 }
168 }
169
170 func TestValidateInstanceLabelSelectorsReportsJoinedErrors(t *testing.T) {
171 err := validateInstanceLabelSelectors([]InstanceLabelSelector{
172 {Exclude: true, Key: " host"},
173 {},
174 })
175
176 require.Error(t, err)
177 assert.ErrorContains(t, err, "instance selector[0]: exclude selector key must be trimmed")
178 assert.ErrorContains(t, err, "instance selector[1]: selector is empty")
179 assert.ErrorContains(t, err, "instance selectors must include at least one positive selector")
180 }
181
182 func TestValidateDimensionReportsJoinedErrors(t *testing.T) {
183 err := validateDimension(Dimension{})
184
185 require.Error(t, err)
186 assert.ErrorContains(t, err, "selector expression is required")
187 assert.ErrorContains(t, err, "selector matcher is required")
188 assert.ErrorContains(t, err, "dimension name is required")
189 }
190
191 func TestNewProgramReportsJoinedChartErrors(t *testing.T) {
192 chart := sampleChart("invalid-multi")
193 chart.Meta.Context = ""
194 chart.Meta.Units = ""
195 chart.Identity.InstanceByLabels = []InstanceLabelSelector{
196 {Exclude: true, Key: "nic"},
197 }
198 chart.CollisionReduce = ""
199 chart.Dimensions = []Dimension{{}}
200
201 _, err := New("v1", 42, []string{"windows_rx_total"}, []Chart{chart})
202 require.Error(t, err)
203 assert.ErrorContains(t, err, "context is required")
204 assert.ErrorContains(t, err, "units is required")
205 assert.ErrorContains(t, err, "identity: instance selectors must include at least one positive selector")
206 assert.ErrorContains(t, err, "collision reduce op is required")
207 assert.ErrorContains(t, err, "dimension[0]: selector expression is required")
208 assert.ErrorContains(t, err, "selector matcher is required")
209 assert.ErrorContains(t, err, "dimension name is required")
210 }
211
212 func sampleChart(templateID string) Chart {
213 return Chart{
214 TemplateID: templateID,
215 Meta: ChartMeta{
216 Title: "Network traffic",
217 Family: "net",
218 Context: "win.nic_traffic",
219 Units: "bytes/s",
220 Algorithm: AlgorithmIncremental,
221 Type: ChartTypeLine,
222 Priority: 1000,
223 },
224 Identity: ChartIdentity{
225 IDTemplate: Template{Raw: "win_nic_{nic}_traffic"},
226 InstanceByLabels: []InstanceLabelSelector{
227 {Key: "nic"},
228 },
229 },
230 Labels: LabelPolicy{
231 Mode: PromotionModeAutoIntersection,
232 PromoteKeys: []string{"interface_type"},
233 Exclusions: LabelExclusions{
234 SelectorConstrainedKeys: []string{"direction"},
235 },
236 Precedence: DefaultLabelPrecedence(),
237 },
238 Lifecycle: LifecyclePolicy{
239 MaxInstances: 512,
240 ExpireAfterCycles: 10,
241 Dimensions: DimensionLifecyclePolicy{
242 MaxDims: 16,
243 ExpireAfterCycles: 10,
244 },
245 },
246 CollisionReduce: ReduceSum,
247 Dimensions: []Dimension{
248 {
249 Selector: SelectorBinding{
250 Expression: "windows_rx_total",
251 Matcher: trueMatcher{},
252 MetricNames: []string{"windows_rx_total"},
253 ConstrainedLabelKeys: []string{"direction"},
254 },
255 NameTemplate: Template{
256 Raw: "received",
257 },
258 },
259 {
260 Selector: SelectorBinding{
261 Expression: "windows_tx_total",
262 Matcher: trueMatcher{},
263 MetricNames: []string{"windows_tx_total"},
264 ConstrainedLabelKeys: []string{"direction"},
265 },
266 NameTemplate: Template{
267 Raw: "sent",
268 },
269 },
270 },
271 }
272 }