master
go 723 lines 18 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package chartemit
4
5 import (
6 "bytes"
7 "math"
8 "testing"
9
10 "github.com/netdata/netdata/go/plugins/plugin/framework/chartengine"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/netdata/netdata/go/plugins/pkg/netdataapi"
15 )
16
17 func TestApplyPlanEmitsNetdataWire(t *testing.T) {
18 var buf bytes.Buffer
19 api := netdataapi.New(&buf)
20
21 meta := chartengine.ChartMeta{
22 Title: "NIC traffic",
23 Family: "Net",
24 Context: "nic_traffic",
25 Units: "bytes/s",
26 Algorithm: chartengine.AlgorithmIncremental,
27 Type: chartengine.ChartTypeLine,
28 Priority: 1,
29 }
30
31 plan := Plan{
32 Actions: []EngineAction{
33 chartengine.CreateChartAction{
34 ChartTemplateID: "g0c0",
35 ChartID: "win_nic_traffic_eth0",
36 Meta: meta,
37 },
38 chartengine.CreateDimensionAction{
39 ChartID: "win_nic_traffic_eth0",
40 ChartMeta: meta,
41 Name: "received",
42 Hidden: false,
43 Float: true,
44 Algorithm: chartengine.AlgorithmIncremental,
45 Multiplier: 1,
46 Divisor: 1,
47 },
48 chartengine.UpdateChartAction{
49 ChartID: "win_nic_traffic_eth0",
50 Values: []chartengine.UpdateDimensionValue{
51 {
52 Name: "received",
53 IsFloat: true,
54 Float64: 123.5,
55 },
56 },
57 },
58 chartengine.RemoveDimensionAction{
59 ChartID: "win_nic_traffic_eth0",
60 ChartMeta: meta,
61 Name: "received",
62 Hidden: false,
63 Float: true,
64 Algorithm: chartengine.AlgorithmIncremental,
65 Multiplier: 1,
66 Divisor: 1,
67 },
68 chartengine.RemoveChartAction{
69 ChartID: "win_nic_traffic_eth0",
70 Meta: meta,
71 },
72 },
73 }
74
75 err := ApplyPlan(api, plan, EmitEnv{
76 TypeID: "collector.job",
77 UpdateEvery: 5,
78 Plugin: "go.d.plugin",
79 Module: "windows",
80 JobName: "job01",
81 JobLabels: map[string]string{
82 "instance": "localhost",
83 },
84 MSSinceLast: 100,
85 })
86 require.NoError(t, err)
87
88 out := buf.String()
89 assert.Equal(t, `HOST ''
90
91 CHART 'collector.job.win_nic_traffic_eth0' '' 'NIC traffic' 'bytes/s' 'Net' 'nic_traffic' 'line' '1' '5' '' 'go.d.plugin' 'windows'
92 CLABEL 'instance' 'localhost' '2'
93 CLABEL '_collect_job' 'job01' '1'
94 CLABEL_COMMIT
95 DIMENSION 'received' 'received' 'incremental' '1' '1' 'type=float'
96 BEGIN 'collector.job.win_nic_traffic_eth0' 100
97 SET 'received' = 123.5
98 END
99
100 CHART 'collector.job.win_nic_traffic_eth0' '' 'NIC traffic' 'bytes/s' 'Net' 'nic_traffic' 'line' '1' '5' '' 'go.d.plugin' 'windows'
101 DIMENSION 'received' 'received' 'incremental' '1' '1' 'obsolete type=float'
102 CHART 'collector.job.win_nic_traffic_eth0' '' 'NIC traffic' 'bytes/s' 'Net' 'nic_traffic' 'line' '1' '5' 'obsolete' 'go.d.plugin' 'windows'
103 `, out)
104 }
105
106 func TestApplyPlanAutogenChartCreateUpdateRemove(t *testing.T) {
107 var buf bytes.Buffer
108 api := netdataapi.New(&buf)
109
110 meta := chartengine.ChartMeta{
111 Title: `Metric "svc.errors_total"`,
112 Family: "svc_errors",
113 Context: "svc.errors_total",
114 Units: "events/s",
115 Algorithm: chartengine.AlgorithmIncremental,
116 Type: chartengine.ChartTypeLine,
117 }
118
119 plan := Plan{
120 Actions: []EngineAction{
121 chartengine.CreateChartAction{
122 ChartTemplateID: "__autogen__:svc.errors_total-method=GET",
123 ChartID: "svc.errors_total-method=GET",
124 Meta: meta,
125 Labels: map[string]string{
126 "method": "GET",
127 },
128 },
129 chartengine.CreateDimensionAction{
130 ChartID: "svc.errors_total-method=GET",
131 ChartMeta: meta,
132 Name: "svc.errors_total",
133 Algorithm: chartengine.AlgorithmIncremental,
134 Multiplier: 1,
135 Divisor: 1,
136 },
137 chartengine.UpdateChartAction{
138 ChartID: "svc.errors_total-method=GET",
139 Values: []chartengine.UpdateDimensionValue{
140 {
141 Name: "svc.errors_total",
142 IsFloat: true,
143 Float64: 10,
144 },
145 },
146 },
147 chartengine.RemoveChartAction{
148 ChartID: "svc.errors_total-method=GET",
149 Meta: meta,
150 },
151 },
152 }
153
154 err := ApplyPlan(api, plan, EmitEnv{
155 TypeID: "collector.job",
156 UpdateEvery: 1,
157 Plugin: "go.d.plugin",
158 Module: "prometheus",
159 JobName: "job01",
160 })
161 require.NoError(t, err)
162
163 out := buf.String()
164 assert.Equal(t, `HOST ''
165
166 CHART 'collector.job.svc.errors_total-method=GET' '' 'Metric "svc.errors_total"' 'events/s' 'svc_errors' 'svc.errors_total' 'line' '0' '1' '' 'go.d.plugin' 'prometheus'
167 CLABEL 'method' 'GET' '1'
168 CLABEL '_collect_job' 'job01' '1'
169 CLABEL_COMMIT
170 DIMENSION 'svc.errors_total' 'svc.errors_total' 'incremental' '1' '1' ''
171 BEGIN 'collector.job.svc.errors_total-method=GET'
172 SET 'svc.errors_total' = 10
173 END
174
175 CHART 'collector.job.svc.errors_total-method=GET' '' 'Metric "svc.errors_total"' 'events/s' 'svc_errors' 'svc.errors_total' 'line' '0' '1' 'obsolete' 'go.d.plugin' 'prometheus'
176 `, out)
177 }
178
179 func TestApplyPlanUsesIntegerSETForNonFloatUpdates(t *testing.T) {
180 var buf bytes.Buffer
181 api := netdataapi.New(&buf)
182
183 meta := chartengine.ChartMeta{
184 Title: "Runtime jobs",
185 Family: "Runtime",
186 Context: "runtime.jobs",
187 Units: "jobs",
188 Algorithm: chartengine.AlgorithmAbsolute,
189 Type: chartengine.ChartTypeLine,
190 }
191
192 plan := Plan{
193 Actions: []EngineAction{
194 chartengine.CreateChartAction{
195 ChartTemplateID: "g0c0",
196 ChartID: "runtime_jobs",
197 Meta: meta,
198 },
199 chartengine.CreateDimensionAction{
200 ChartID: "runtime_jobs",
201 ChartMeta: meta,
202 Name: "total",
203 Algorithm: chartengine.AlgorithmAbsolute,
204 Multiplier: 1,
205 Divisor: 1,
206 },
207 chartengine.UpdateChartAction{
208 ChartID: "runtime_jobs",
209 Values: []chartengine.UpdateDimensionValue{
210 {
211 Name: "total",
212 IsFloat: false,
213 Int64: 7,
214 Float64: 7.9,
215 },
216 },
217 },
218 },
219 }
220
221 err := ApplyPlan(api, plan, EmitEnv{
222 TypeID: "collector.job",
223 UpdateEvery: 1,
224 Plugin: "go.d.plugin",
225 Module: "runtime",
226 JobName: "job01",
227 MSSinceLast: 1,
228 })
229 require.NoError(t, err)
230
231 out := buf.String()
232 assert.Equal(t, `HOST ''
233
234 CHART 'collector.job.runtime_jobs' '' 'Runtime jobs' 'jobs' 'Runtime' 'runtime.jobs' 'line' '0' '1' '' 'go.d.plugin' 'runtime'
235 CLABEL '_collect_job' 'job01' '1'
236 CLABEL_COMMIT
237 DIMENSION 'total' 'total' 'absolute' '1' '1' ''
238 BEGIN 'collector.job.runtime_jobs' 1
239 SET 'total' = 7
240 END
241
242 `, out)
243 assert.NotContains(t, out, "SET 'total' = 7.9")
244 }
245
246 func TestApplyPlanGapsNonFiniteFloatUpdate(t *testing.T) {
247 var buf bytes.Buffer
248 api := netdataapi.New(&buf)
249
250 meta := chartengine.ChartMeta{
251 Title: "Latency",
252 Family: "Latency",
253 Context: "svc.latency",
254 Units: "ms",
255 Algorithm: chartengine.AlgorithmAbsolute,
256 Type: chartengine.ChartTypeLine,
257 }
258
259 plan := Plan{
260 Actions: []EngineAction{
261 chartengine.CreateChartAction{ChartTemplateID: "g0c0", ChartID: "svc_latency", Meta: meta},
262 chartengine.CreateDimensionAction{
263 ChartID: "svc_latency", ChartMeta: meta, Name: "value",
264 Float: true, Algorithm: chartengine.AlgorithmAbsolute, Multiplier: 1, Divisor: 1,
265 },
266 chartengine.UpdateChartAction{
267 ChartID: "svc_latency",
268 Values: []chartengine.UpdateDimensionValue{
269 {Name: "value", IsFloat: true, Float64: math.NaN()},
270 },
271 },
272 },
273 }
274
275 require.NoError(t, ApplyPlan(api, plan, EmitEnv{
276 TypeID: "collector.job", UpdateEvery: 1, Plugin: "go.d.plugin", Module: "prometheus",
277 JobName: "job01", MSSinceLast: 1,
278 }))
279
280 out := buf.String()
281 // A non-finite float dimension is emitted as a gap (empty SET), never "SET = NaN" (which the C
282 // agent would render as 0).
283 assert.NotContains(t, out, "NaN")
284 assert.Contains(t, out, "SET 'value' = \n")
285 }
286
287 func TestApplyPlanDimensionOnlyCreateEmitsLabelsAndCommit(t *testing.T) {
288 var buf bytes.Buffer
289 api := netdataapi.New(&buf)
290
291 meta := chartengine.ChartMeta{
292 Title: "Dimension-only chart",
293 Family: "Runtime",
294 Context: "runtime.dimension_only",
295 Units: "1",
296 Algorithm: chartengine.AlgorithmAbsolute,
297 Type: chartengine.ChartTypeLine,
298 }
299
300 plan := Plan{
301 Actions: []EngineAction{
302 chartengine.CreateDimensionAction{
303 ChartID: "dimension_only_chart",
304 ChartMeta: meta,
305 Name: "value",
306 Algorithm: chartengine.AlgorithmAbsolute,
307 Multiplier: 1,
308 Divisor: 1,
309 },
310 },
311 }
312
313 err := ApplyPlan(api, plan, EmitEnv{
314 TypeID: "collector.job",
315 UpdateEvery: 1,
316 Plugin: "go.d.plugin",
317 Module: "runtime",
318 JobName: "job01",
319 JobLabels: map[string]string{
320 "instance": "localhost",
321 },
322 })
323 require.NoError(t, err)
324
325 out := buf.String()
326 assert.Equal(t, `HOST ''
327
328 CHART 'collector.job.dimension_only_chart' '' 'Dimension-only chart' '1' 'Runtime' 'runtime.dimension_only' 'line' '0' '1' '' 'go.d.plugin' 'runtime'
329 CLABEL 'instance' 'localhost' '2'
330 CLABEL '_collect_job' 'job01' '1'
331 CLABEL_COMMIT
332 DIMENSION 'value' 'value' 'absolute' '1' '1' ''
333 `, out)
334 }
335
336 func TestApplyPlanSanitizesWireValues(t *testing.T) {
337 var buf bytes.Buffer
338 api := netdataapi.New(&buf)
339
340 meta := chartengine.ChartMeta{
341 Title: "Title'\n",
342 Family: "Family'\n",
343 Context: "Context'\n",
344 Units: "units'\n",
345 Algorithm: chartengine.AlgorithmAbsolute,
346 Type: chartengine.ChartTypeLine,
347 }
348
349 plan := Plan{
350 Actions: []EngineAction{
351 chartengine.CreateChartAction{
352 ChartTemplateID: "g0.c0",
353 ChartID: "chart'id\n",
354 Meta: meta,
355 Labels: map[string]string{
356 "la'bel\n": "va'lue\n",
357 },
358 },
359 chartengine.CreateDimensionAction{
360 ChartID: "chart'id\n",
361 ChartMeta: meta,
362 Name: "dim'name\n",
363 Algorithm: chartengine.AlgorithmAbsolute,
364 Multiplier: 1,
365 Divisor: 1,
366 },
367 chartengine.UpdateChartAction{
368 ChartID: "chart'id\n",
369 Values: []chartengine.UpdateDimensionValue{
370 {
371 Name: "dim'name\n",
372 IsFloat: true,
373 Float64: 5,
374 },
375 },
376 },
377 },
378 }
379
380 err := ApplyPlan(api, plan, EmitEnv{
381 TypeID: "collector.'job\n",
382 UpdateEvery: 1,
383 Plugin: "go.d'plugin\n",
384 Module: "mod'ule\n",
385 JobName: "job'01\n",
386 JobLabels: map[string]string{
387 "inst'ance\n": "local'host\n",
388 },
389 MSSinceLast: 1,
390 })
391 require.NoError(t, err)
392
393 out := buf.String()
394 assert.Equal(t, `HOST ''
395
396 CHART 'collector.job.chartid' '' 'Title ' 'units ' 'Family ' 'Context ' 'line' '0' '1' '' 'go.dplugin ' 'module '
397 CLABEL 'instance' 'localhost ' '2'
398 CLABEL 'label' 'value ' '1'
399 CLABEL '_collect_job' 'job01 ' '1'
400 CLABEL_COMMIT
401 DIMENSION 'dimname' 'dimname' 'absolute' '1' '1' ''
402 BEGIN 'collector.job.chartid' 1
403 SET 'dimname' = 5
404 END
405
406 `, out)
407 }
408
409 func TestApplyPlanRejectsEmptyTypeID(t *testing.T) {
410 tests := map[string]struct {
411 typeID string
412 wantErr string
413 }{
414 "rejects empty type id": {
415 typeID: "",
416 wantErr: "type_id is required",
417 },
418 "rejects whitespace type id": {
419 typeID: " \t",
420 wantErr: "type_id is required",
421 },
422 }
423
424 for name, tc := range tests {
425 t.Run(name, func(t *testing.T) {
426 var buf bytes.Buffer
427 api := netdataapi.New(&buf)
428
429 err := ApplyPlan(api, Plan{}, EmitEnv{
430 TypeID: tc.typeID,
431 })
432 require.Error(t, err)
433 assert.ErrorContains(t, err, tc.wantErr)
434 })
435 }
436 }
437
438 func TestApplyPlanDefaultGlobalHostSelection(t *testing.T) {
439 var buf bytes.Buffer
440 api := netdataapi.New(&buf)
441
442 meta := chartengine.ChartMeta{
443 Title: "Requests",
444 Family: "Service",
445 Context: "requests",
446 Units: "req/s",
447 Type: chartengine.ChartTypeLine,
448 }
449 plan := Plan{
450 Actions: []EngineAction{
451 chartengine.CreateChartAction{
452 ChartID: "requests",
453 Meta: meta,
454 },
455 },
456 }
457
458 require.NoError(t, ApplyPlan(api, plan, EmitEnv{
459 TypeID: "collector.job",
460 UpdateEvery: 1,
461 Plugin: "go.d.plugin",
462 Module: "httpcheck",
463 JobName: "job01",
464 }))
465
466 out := buf.String()
467 assert.Equal(t, `HOST ''
468
469 CHART 'collector.job.requests' '' 'Requests' 'req/s' 'Service' 'requests' 'line' '0' '1' '' 'go.d.plugin' 'httpcheck'
470 CLABEL '_collect_job' 'job01' '1'
471 CLABEL_COMMIT
472 `, out)
473 }
474
475 func TestApplyPlanVnodeHostSelectionAndDefine(t *testing.T) {
476 var buf bytes.Buffer
477 api := netdataapi.New(&buf)
478
479 meta := chartengine.ChartMeta{
480 Title: "Workers Busy",
481 Family: "Workers",
482 Context: "workers_busy",
483 Units: "workers",
484 Type: chartengine.ChartTypeLine,
485 }
486 info, err := PrepareHostInfo(netdataapi.HostInfo{
487 GUID: "node-guid",
488 Hostname: "node-host",
489 Labels: map[string]string{
490 "region": "eu'\n",
491 },
492 })
493 require.NoError(t, err)
494
495 plan := Plan{
496 Actions: []EngineAction{
497 chartengine.CreateChartAction{
498 ChartID: "workers_busy",
499 Meta: meta,
500 },
501 },
502 }
503
504 require.NoError(t, ApplyPlan(api, plan, EmitEnv{
505 TypeID: "collector.job",
506 UpdateEvery: 1,
507 Plugin: "go.d.plugin",
508 Module: "apache",
509 JobName: "job01",
510 HostScope: &HostScope{
511 GUID: "node-guid",
512 Define: &info,
513 },
514 }))
515
516 out := buf.String()
517 assert.Equal(t, `HOST_DEFINE 'node-guid' 'node-host'
518 HOST_LABEL '_hostname' 'node-host'
519 HOST_LABEL 'region' 'eu '
520 HOST_DEFINE_END
521
522 HOST 'node-guid'
523
524 CHART 'collector.job.workers_busy' '' 'Workers Busy' 'workers' 'Workers' 'workers_busy' 'line' '0' '1' '' 'go.d.plugin' 'apache'
525 CLABEL '_collect_job' 'job01' '1'
526 CLABEL_COMMIT
527 `, out)
528 }
529
530 func TestApplyPlanSkipsHostSelectionForEmptyPlans(t *testing.T) {
531 var buf bytes.Buffer
532 api := netdataapi.New(&buf)
533
534 require.NoError(t, ApplyPlan(api, Plan{}, EmitEnv{
535 TypeID: "collector.job",
536 UpdateEvery: 1,
537 Plugin: "go.d.plugin",
538 Module: "runtime",
539 JobName: "job01",
540 }))
541 assert.Equal(t, "", buf.String())
542 }
543
544 func TestApplyPlanRejectsMismatchedHostDefine(t *testing.T) {
545 var buf bytes.Buffer
546 api := netdataapi.New(&buf)
547
548 meta := chartengine.ChartMeta{
549 Title: "Requests",
550 Family: "Service",
551 Context: "requests",
552 Units: "req/s",
553 Type: chartengine.ChartTypeLine,
554 }
555 plan := Plan{
556 Actions: []EngineAction{
557 chartengine.CreateChartAction{
558 ChartID: "requests",
559 Meta: meta,
560 },
561 },
562 }
563
564 err := ApplyPlan(api, plan, EmitEnv{
565 TypeID: "collector.job",
566 UpdateEvery: 1,
567 Plugin: "go.d.plugin",
568 Module: "httpcheck",
569 JobName: "job01",
570 HostScope: &HostScope{
571 GUID: "guid-a",
572 Define: &netdataapi.HostInfo{
573 GUID: "guid-b",
574 Hostname: "node-host",
575 },
576 },
577 })
578 require.Error(t, err)
579 assert.ErrorContains(t, err, "does not match")
580 assert.Equal(t, "", buf.String())
581 }
582
583 func TestPrepareHostInfoScenarios(t *testing.T) {
584 cases := map[string]struct {
585 info netdataapi.HostInfo
586 want netdataapi.HostInfo
587 wantErr string
588 }{
589 "rejects unsafe guid": {
590 info: netdataapi.HostInfo{
591 GUID: "node'\nguid",
592 Hostname: "node-host",
593 },
594 wantErr: "unsupported characters",
595 },
596 "rejects unsafe hostname": {
597 info: netdataapi.HostInfo{
598 GUID: "node-guid",
599 Hostname: "node'\nhost",
600 },
601 wantErr: "unsupported characters",
602 },
603 "normalizes labels": {
604 info: netdataapi.HostInfo{
605 GUID: "node-guid",
606 Hostname: "node-host",
607 Labels: map[string]string{
608 "region'\n": "eu'\n",
609 " ": "ignored",
610 },
611 },
612 want: netdataapi.HostInfo{
613 GUID: "node-guid",
614 Hostname: "node-host",
615 Labels: map[string]string{
616 "_hostname": "node-host",
617 "region": "eu ",
618 },
619 },
620 },
621 }
622
623 for name, tc := range cases {
624 t.Run(name, func(t *testing.T) {
625 got, err := PrepareHostInfo(tc.info)
626 if tc.wantErr != "" {
627 require.ErrorContains(t, err, tc.wantErr)
628 return
629 }
630 require.NoError(t, err)
631 assert.Equal(t, tc.want, got)
632 })
633 }
634 }
635
636 func TestApplyPlanRemoveOnlyBatchStillSelectsHost(t *testing.T) {
637 var buf bytes.Buffer
638 api := netdataapi.New(&buf)
639
640 meta := chartengine.ChartMeta{
641 Title: "Requests",
642 Family: "Service",
643 Context: "requests",
644 Units: "req/s",
645 Type: chartengine.ChartTypeLine,
646 }
647 plan := Plan{
648 Actions: []EngineAction{
649 chartengine.RemoveChartAction{
650 ChartID: "requests",
651 Meta: meta,
652 },
653 },
654 }
655
656 require.NoError(t, ApplyPlan(api, plan, EmitEnv{
657 TypeID: "collector.job",
658 UpdateEvery: 1,
659 Plugin: "go.d.plugin",
660 Module: "httpcheck",
661 JobName: "job01",
662 }))
663
664 out := buf.String()
665 assert.Equal(t, `HOST ''
666
667 CHART 'collector.job.requests' '' 'Requests' 'req/s' 'Service' 'requests' 'line' '0' '1' 'obsolete' 'go.d.plugin' 'httpcheck'
668 `, out)
669 }
670
671 func TestNormalizeActionsOrderingDeterminism(t *testing.T) {
672 meta := chartengine.ChartMeta{
673 Title: "Requests",
674 Family: "Service",
675 Context: "requests",
676 Units: "requests/s",
677 Algorithm: chartengine.AlgorithmIncremental,
678 Type: chartengine.ChartTypeLine,
679 }
680
681 tests := map[string]struct {
682 actions []EngineAction
683 }{
684 "preserves update/remove order and groups create actions by chart id": {
685 actions: []EngineAction{
686 chartengine.UpdateChartAction{ChartID: "chart_b"},
687 chartengine.UpdateChartAction{ChartID: "chart_a"},
688 chartengine.RemoveDimensionAction{ChartID: "chart_b", Name: "dim_b", ChartMeta: meta},
689 chartengine.RemoveDimensionAction{ChartID: "chart_a", Name: "dim_a", ChartMeta: meta},
690 chartengine.RemoveChartAction{ChartID: "chart_b", Meta: meta},
691 chartengine.RemoveChartAction{ChartID: "chart_a", Meta: meta},
692 chartengine.CreateDimensionAction{ChartID: "chart_b", Name: "dim_b", ChartMeta: meta},
693 chartengine.CreateChartAction{ChartID: "chart_b", Meta: meta},
694 chartengine.CreateDimensionAction{ChartID: "chart_a", Name: "dim_a", ChartMeta: meta},
695 chartengine.CreateChartAction{ChartID: "chart_a", Meta: meta},
696 },
697 },
698 }
699
700 for name, tc := range tests {
701 t.Run(name, func(t *testing.T) {
702 normalized := normalizeActions(tc.actions)
703
704 require.Len(t, normalized.updateCharts, 2)
705 assert.Equal(t, "chart_b", normalized.updateCharts[0].ChartID)
706 assert.Equal(t, "chart_a", normalized.updateCharts[1].ChartID)
707
708 require.Len(t, normalized.removeDimensions, 2)
709 assert.Equal(t, "chart_b", normalized.removeDimensions[0].ChartID)
710 assert.Equal(t, "chart_a", normalized.removeDimensions[1].ChartID)
711
712 require.Len(t, normalized.removeCharts, 2)
713 assert.Equal(t, "chart_b", normalized.removeCharts[0].ChartID)
714 assert.Equal(t, "chart_a", normalized.removeCharts[1].ChartID)
715
716 require.Len(t, normalized.createCharts, 2)
717 assert.Contains(t, normalized.createCharts, "chart_a")
718 assert.Contains(t, normalized.createCharts, "chart_b")
719 require.Len(t, normalized.createDimsByID["chart_a"], 1)
720 require.Len(t, normalized.createDimsByID["chart_b"], 1)
721 })
722 }
723 }