master
go 2,193 lines 60.6 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 package ddsnmp
4
5 import (
6 "fmt"
7 "os"
8 "path/filepath"
9 "slices"
10 "strings"
11 "testing"
12
13 "github.com/stretchr/testify/assert"
14 "github.com/stretchr/testify/require"
15 "gopkg.in/yaml.v2"
16
17 "github.com/netdata/netdata/go/plugins/pkg/multipath"
18 "github.com/netdata/netdata/go/plugins/plugin/go.d/collector/snmp/ddsnmp/ddprofiledefinition"
19 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/oldmetrix"
20 )
21
22 func Test_loadDDSnmpProfiles(t *testing.T) {
23 dir, _ := filepath.Abs("../../../config/go.d/snmp.profiles/default")
24
25 f, err := os.Open(dir)
26 require.NoError(t, err)
27 defer func() { _ = f.Close() }()
28
29 profiles, err := loadProfilesFromDir(dir, multipath.New(dir))
30 require.NoError(t, err)
31
32 require.NotEmpty(t, profiles)
33
34 names, err := f.Readdirnames(-1)
35 require.NoError(t, err)
36
37 var want int64
38 for _, name := range names {
39 want += oldmetrix.Bool(!strings.HasPrefix(name, "_"))
40 }
41
42 require.Equal(t, want-1 /*README.md*/, int64(len(profiles)))
43 }
44
45 func Test_FindProfiles(t *testing.T) {
46 test := map[string]struct {
47 sysObjOId string
48 manualProfiles []string
49 wanProfiles []string
50 }{
51 "MikroTik": {
52 sysObjOId: "1.3.6.1.4.1.14988.1",
53 wanProfiles: []string{"mikrotik-router", "generic-device"},
54 },
55 "net-snmp linux": {
56 sysObjOId: "1.3.6.1.4.1.8072.3.2.10",
57 wanProfiles: []string{"net-snmp", "generic-device"},
58 },
59 "Kyocera printer": {
60 sysObjOId: "1.3.6.1.4.1.1347.41",
61 wanProfiles: []string{"kyocera-printer", "generic-device"},
62 },
63 "APC pdu": {
64 sysObjOId: "1.3.6.1.4.1.318.1.3.4.5",
65 wanProfiles: []string{"apc-pdu", "apc", "generic-device"},
66 },
67 "IBM RackSwitch G8052-2 ": {
68 sysObjOId: "1.3.6.1.4.1.26543.1.7.7",
69 wanProfiles: []string{"generic-device"},
70 },
71 "Aruba WLC 7210": {
72 sysObjOId: "1.3.6.1.4.1.14823.1.1.32",
73 wanProfiles: []string{"aruba-wireless-controller", "aruba-switch", "aruba", "generic-device"},
74 },
75 "FortiGate 200": {
76 sysObjOId: "1.3.6.1.4.1.12356.15.200",
77 wanProfiles: []string{"generic-device"},
78 },
79 "Cisco 7204 VXR": {
80 sysObjOId: "1.3.6.1.4.1.9.1.223",
81 wanProfiles: []string{"cisco", "generic-device"},
82 },
83 "NetApp Cluster": {
84 sysObjOId: "1.3.6.1.4.1.789.2.5",
85 wanProfiles: []string{"netapp", "generic-device"},
86 },
87 "Zyxel ZyAIR B-1000": {
88 sysObjOId: "1.3.6.1.4.1.890.1.2",
89 wanProfiles: []string{"generic-device"},
90 },
91 "HP ProCurve 2650-PWR": {
92 sysObjOId: "1.3.6.1.4.1.11.2.3.7.11.35",
93 wanProfiles: []string{"hp-icf-switch", "generic-device"},
94 },
95 "Aruba 2930F-8G-PoE+-2SFP+": {
96 sysObjOId: "1.3.6.1.4.1.11.2.3.7.11.181.16",
97 wanProfiles: []string{"hp-icf-switch", "generic-device"},
98 },
99 "HP LaserJet 4050 Series": {
100 sysObjOId: "1.3.6.1.4.1.11.2.3.9.1",
101 wanProfiles: []string{"generic-device"},
102 },
103 "Huawei H3C s2008-HI": {
104 sysObjOId: "1.3.6.1.4.1.2011.10.1.152",
105 wanProfiles: []string{"generic-device"},
106 },
107 "Avaya AVX571040": {
108 sysObjOId: "1.3.6.1.4.1.6889.1.69.4",
109 wanProfiles: []string{"generic-device"},
110 },
111 "Aruba IAP-225": {
112 sysObjOId: "1.3.6.1.4.1.14823.1.2.59",
113 wanProfiles: []string{"aruba-access-point", "aruba", "generic-device"},
114 },
115 "Brocade MLXe-16 slot": {
116 sysObjOId: "1.3.6.1.4.1.1991.1.3.55.1.2",
117 wanProfiles: []string{"generic-device"},
118 },
119 "FortiGate-100D": {
120 sysObjOId: "1.3.6.1.4.1.12356.101.1.1004",
121 wanProfiles: []string{"fortinet-fortigate", "generic-device"},
122 },
123 "Summit X450-24x": {
124 sysObjOId: "1.3.6.1.4.1.1916.2.65",
125 wanProfiles: []string{"extreme-switching", "generic-device"},
126 },
127 "Meraki MX84": {
128 sysObjOId: "1.3.6.1.4.1.29671.2.109",
129 wanProfiles: []string{"meraki", "generic-device"},
130 },
131 "Palo Alto WF-500": {
132 sysObjOId: "1.3.6.1.4.1.25461.2.3.33",
133 wanProfiles: []string{"palo-alto", "generic-device"},
134 },
135 "Cisco UCS 6120XP": {
136 sysObjOId: "1.3.6.1.4.1.9.12.3.1.3.847",
137 wanProfiles: []string{"cisco-nexus", "cisco", "generic-device"},
138 },
139 "Cisco 5520 Wireless Controller": {
140 sysObjOId: "1.3.6.1.4.1.9.1.2170",
141 wanProfiles: []string{"cisco-legacy-wlc", "cisco", "generic-device"},
142 },
143 "Linksys BEFSX41": {
144 sysObjOId: "1.3.6.1.4.1.3955.1.1",
145 wanProfiles: []string{"linksys", "generic-device"},
146 },
147 "Juniper ERX-705": {
148 sysObjOId: "1.3.6.1.4.1.4874.1.1.1.1.4",
149 wanProfiles: []string{"generic-device"},
150 },
151
152 "no match": {
153 sysObjOId: "0.1.2.3",
154 wanProfiles: nil,
155 },
156 "no sysObjectID, manual profile applied": {
157 sysObjOId: "",
158 manualProfiles: []string{"generic-device"},
159 wanProfiles: []string{"generic-device"},
160 },
161 }
162
163 for name, test := range test {
164 t.Run(name, func(t *testing.T) {
165 profiles := FindProfiles(test.sysObjOId, "", test.manualProfiles)
166
167 var names []string
168 for _, p := range profiles {
169 names = append(names, stripFileNameExt(p.SourceFile))
170 }
171
172 assert.Equal(t, test.wanProfiles, names)
173 })
174 }
175 }
176
177 func TestDefaultCatalogResolveProject_LoadedProfilesSeparateConsumers(t *testing.T) {
178 tests := map[string]struct {
179 sysObjectID string
180 consumer ProfileConsumer
181 consumers []ProfileConsumer
182 wantMetrics []string
183 wantTopologyKinds []ddprofiledefinition.TopologyKind
184 wantLicensingIDs []string
185 wantBGPIDs []string
186 wantNoMetrics bool
187 wantNoTopology bool
188 wantNoLicensing bool
189 wantNoBGP bool
190 }{
191 "metrics_projection": {
192 sysObjectID: "1.3.6.1.4.1.9.1.1",
193 consumer: ConsumerMetrics,
194 wantMetrics: []string{"systemUptime", "tcpCurrEstab", "cpmCPUTotal5minRev"},
195 wantNoTopology: true,
196 wantNoLicensing: true,
197 wantNoBGP: true,
198 },
199 "topology_projection": {
200 sysObjectID: "1.3.6.1.4.1.9.1.1",
201 consumer: ConsumerTopology,
202 wantTopologyKinds: []ddprofiledefinition.TopologyKind{
203 ddprofiledefinition.KindLldpRem,
204 ddprofiledefinition.KindCdpCache,
205 ddprofiledefinition.KindFdbEntry,
206 ddprofiledefinition.KindQbridgeFdbEntry,
207 ddprofiledefinition.KindStpPort,
208 ddprofiledefinition.KindVtpVlan,
209 },
210 wantNoMetrics: true,
211 wantNoLicensing: true,
212 wantNoBGP: true,
213 },
214 "licensing_projection": {
215 sysObjectID: "1.3.6.1.4.1.9.1.1",
216 consumer: ConsumerLicensing,
217 wantLicensingIDs: []string{
218 "cisco_traditional_licenses",
219 "smart_registration",
220 "smart_authorization",
221 "smart_id_certificate",
222 "smart_evaluation_period",
223 "smart_entitlements",
224 },
225 wantNoMetrics: true,
226 wantNoTopology: true,
227 wantNoBGP: true,
228 },
229 "bgp_projection": {
230 sysObjectID: "1.3.6.1.4.1.9.1.923",
231 consumer: ConsumerBGP,
232 wantBGPIDs: []string{"cisco-bgp-peer", "cisco-bgp-peer-family"},
233 wantNoMetrics: true,
234 wantNoTopology: true,
235 wantNoLicensing: true,
236 },
237 "bgp_projection_nokia": {
238 sysObjectID: "1.3.6.1.4.1.6527.1.3.17",
239 consumer: ConsumerBGP,
240 wantBGPIDs: []string{
241 "nokia-timos-bgp-peer",
242 "nokia-timos-bgp-peer-family-ipv4-unicast",
243 "nokia-timos-bgp-peer-family-ipv4-vpn",
244 "nokia-timos-bgp-peer-family-ipv6-unicast",
245 "nokia-timos-bgp-peer-family-ipv6-vpn",
246 "nokia-timos-bgp-peer-family-l2vpn-vpls",
247 "nokia-timos-bgp-peer-family-l2vpn-evpn",
248 },
249 wantNoMetrics: true,
250 wantNoTopology: true,
251 wantNoLicensing: true,
252 },
253 "bgp_projection_huawei": {
254 sysObjectID: "1.3.6.1.4.1.2011.2.224.279",
255 consumer: ConsumerBGP,
256 wantBGPIDs: []string{
257 "huawei-bgp-device-counts",
258 "huawei-bgp-peer-family",
259 "huawei-bgp-peer-family-routes",
260 "huawei-bgp-peer-family-messages",
261 "huawei-bgp-peer-statistics",
262 },
263 wantNoMetrics: true,
264 wantNoTopology: true,
265 wantNoLicensing: true,
266 },
267 "bgp_projection_arista": {
268 sysObjectID: "1.3.6.1.4.1.30065.1.3011.7010.427.48",
269 consumer: ConsumerBGP,
270 wantBGPIDs: []string{
271 "arista-bgp-peer",
272 "arista-bgp-peer-family",
273 },
274 wantNoMetrics: true,
275 wantNoTopology: true,
276 wantNoLicensing: true,
277 },
278 "bgp_projection_dell_os10": {
279 sysObjectID: "1.3.6.1.4.1.674.11000.5000.100.2.1.1",
280 consumer: ConsumerBGP,
281 wantBGPIDs: []string{
282 "dell-os10-bgp-peer",
283 "dell-os10-bgp-peer-family",
284 },
285 wantNoMetrics: true,
286 wantNoTopology: true,
287 wantNoLicensing: true,
288 },
289 "bgp_projection_alcatel": {
290 sysObjectID: "1.3.6.1.4.1.6486.801.1.1.2.1.1",
291 consumer: ConsumerBGP,
292 wantBGPIDs: []string{
293 "alcatel-bgp4-peer",
294 "alcatel-bgp4-peer-family",
295 "alcatel-bgp6-peer",
296 "alcatel-bgp6-peer-family",
297 },
298 wantNoMetrics: true,
299 wantNoTopology: true,
300 wantNoLicensing: true,
301 },
302 "metrics_and_licensing_projection": {
303 sysObjectID: "1.3.6.1.4.1.9.1.1",
304 consumer: ConsumerMetrics,
305 consumers: []ProfileConsumer{ConsumerLicensing},
306 wantMetrics: []string{"systemUptime", "tcpCurrEstab", "cpmCPUTotal5minRev"},
307 wantLicensingIDs: []string{
308 "cisco_traditional_licenses",
309 "smart_registration",
310 "smart_authorization",
311 "smart_id_certificate",
312 "smart_evaluation_period",
313 "smart_entitlements",
314 },
315 wantNoTopology: true,
316 wantNoBGP: true,
317 },
318 }
319
320 for name, tc := range tests {
321 t.Run(name, func(t *testing.T) {
322 profiles := DefaultCatalog().Resolve(ResolveRequest{
323 SysObjectID: tc.sysObjectID,
324 ManualPolicy: ManualProfileFallback,
325 }).Project(tc.consumer, tc.consumers...).Profiles()
326 require.NotEmpty(t, profiles)
327
328 metricNames := make(map[string]bool)
329 topologyKinds := make(map[ddprofiledefinition.TopologyKind]bool)
330 licensingIDs := make(map[string]bool)
331 bgpIDs := make(map[string]bool)
332
333 for _, prof := range profiles {
334 require.NotNil(t, prof.Definition)
335 if tc.wantNoMetrics {
336 assert.Empty(t, prof.Definition.Metrics, prof.SourceFile)
337 assert.Empty(t, prof.Definition.VirtualMetrics, prof.SourceFile)
338 }
339 if tc.wantNoTopology {
340 assert.Empty(t, prof.Definition.Topology, prof.SourceFile)
341 }
342 if tc.wantNoLicensing {
343 assert.Empty(t, prof.Definition.Licensing, prof.SourceFile)
344 }
345 if tc.wantNoBGP {
346 assert.Empty(t, prof.Definition.BGP, prof.SourceFile)
347 }
348
349 for _, metric := range prof.Definition.Metrics {
350 if metric.Symbol.Name != "" {
351 metricNames[metric.Symbol.Name] = true
352 }
353 for _, sym := range metric.Symbols {
354 metricNames[sym.Name] = true
355 }
356 }
357 for _, topo := range prof.Definition.Topology {
358 topologyKinds[topo.Kind] = true
359 }
360 for _, row := range prof.Definition.Licensing {
361 licensingIDs[row.ID] = true
362 }
363 for _, row := range prof.Definition.BGP {
364 bgpIDs[row.ID] = true
365 }
366 }
367
368 for _, metricName := range tc.wantMetrics {
369 assert.True(t, metricNames[metricName], "missing metric %q", metricName)
370 }
371 for _, kind := range tc.wantTopologyKinds {
372 assert.True(t, topologyKinds[kind], "missing topology kind %q", kind)
373 }
374 for _, id := range tc.wantLicensingIDs {
375 assert.True(t, licensingIDs[id], "missing licensing row %q", id)
376 }
377 for _, id := range tc.wantBGPIDs {
378 assert.True(t, bgpIDs[id], "missing BGP row %q", id)
379 }
380 })
381 }
382 }
383
384 func Test_Profile_merge(t *testing.T) {
385 profiles := FindProfiles("1.3.6.1.4.1.9.1.1216", "", nil) // cisco-nexus
386
387 i := slices.IndexFunc(profiles, func(p *Profile) bool {
388 return strings.HasSuffix(p.SourceFile, "cisco-nexus.yaml")
389 })
390
391 require.GreaterOrEqual(t, 1, 0)
392
393 for _, m := range profiles[i].Definition.Metrics {
394 if m.IsColumn() && m.Table.Name == "ciscoMemoryPoolTable" {
395 for _, s := range m.Symbols {
396 assert.NotEqual(t, "memory.used", s.Name)
397 }
398 }
399 }
400 }
401
402 func TestProfileMerge_ColumnSymbolsWithSameNameFromBaseArePreserved(t *testing.T) {
403 child := &Profile{
404 Definition: &ddprofiledefinition.ProfileDefinition{},
405 }
406 base := &Profile{
407 Definition: &ddprofiledefinition.ProfileDefinition{
408 Metrics: []ddprofiledefinition.MetricsConfig{
409 {
410 Table: ddprofiledefinition.SymbolConfig{OID: "1.2.3", Name: "tableA"},
411 Symbols: []ddprofiledefinition.SymbolConfig{
412 {OID: "1.2.3.1", Name: "duplicated.column"},
413 {OID: "1.2.3.2", Name: "duplicated.column"},
414 },
415 },
416 {
417 Table: ddprofiledefinition.SymbolConfig{OID: "1.2.4", Name: "tableB"},
418 Symbols: []ddprofiledefinition.SymbolConfig{
419 {OID: "1.2.4.1", Name: "duplicated.column"},
420 },
421 },
422 },
423 },
424 }
425
426 child.mergeMetrics(base)
427
428 require.Len(t, child.Definition.Metrics, 2)
429 require.Len(t, child.Definition.Metrics[0].Symbols, 2)
430 assert.Equal(t, "1.2.3.1", child.Definition.Metrics[0].Symbols[0].OID)
431 assert.Equal(t, "1.2.3.2", child.Definition.Metrics[0].Symbols[1].OID)
432 require.Len(t, child.Definition.Metrics[1].Symbols, 1)
433 assert.Equal(t, "1.2.4.1", child.Definition.Metrics[1].Symbols[0].OID)
434 }
435
436 func TestProfileMerge_DifferentTablesDoNotOverrideColumnsByName(t *testing.T) {
437 child := &Profile{
438 Definition: &ddprofiledefinition.ProfileDefinition{
439 Metrics: []ddprofiledefinition.MetricsConfig{
440 {
441 Table: ddprofiledefinition.SymbolConfig{OID: "9.9.9", Name: "childTable"},
442 Symbols: []ddprofiledefinition.SymbolConfig{
443 {OID: "9.9.9.1", Name: "memory.used"},
444 },
445 },
446 },
447 },
448 }
449 base := &Profile{
450 Definition: &ddprofiledefinition.ProfileDefinition{
451 Metrics: []ddprofiledefinition.MetricsConfig{
452 {
453 Table: ddprofiledefinition.SymbolConfig{OID: "1.2.3", Name: "baseTable"},
454 Symbols: []ddprofiledefinition.SymbolConfig{
455 {OID: "1.2.3.1", Name: "memory.used"},
456 {OID: "1.2.3.2", Name: "memory.free"},
457 },
458 },
459 },
460 },
461 }
462
463 child.mergeMetrics(base)
464
465 require.Len(t, child.Definition.Metrics, 2)
466 assert.Equal(t, "childTable", child.Definition.Metrics[0].Table.Name)
467 require.Len(t, child.Definition.Metrics[0].Symbols, 1)
468 assert.Equal(t, "memory.used", child.Definition.Metrics[0].Symbols[0].Name)
469 assert.Equal(t, "baseTable", child.Definition.Metrics[1].Table.Name)
470 require.Len(t, child.Definition.Metrics[1].Symbols, 2)
471 assert.Equal(t, "memory.used", child.Definition.Metrics[1].Symbols[0].Name)
472 assert.Equal(t, "memory.free", child.Definition.Metrics[1].Symbols[1].Name)
473 }
474
475 func TestProfileMerge_BaseScalarDuplicateAddedOnce(t *testing.T) {
476 child := &Profile{
477 Definition: &ddprofiledefinition.ProfileDefinition{},
478 }
479 base := &Profile{
480 Definition: &ddprofiledefinition.ProfileDefinition{
481 Metrics: []ddprofiledefinition.MetricsConfig{
482 {
483 Symbol: ddprofiledefinition.SymbolConfig{
484 OID: "1.2.3.0",
485 Name: "license.expiry",
486 },
487 },
488 {
489 Symbol: ddprofiledefinition.SymbolConfig{
490 OID: "1.2.3.0",
491 Name: "license.expiry",
492 },
493 },
494 {
495 Symbol: ddprofiledefinition.SymbolConfig{
496 OID: "1.2.4.0",
497 Name: "license.state",
498 },
499 },
500 },
501 },
502 }
503
504 child.mergeMetrics(base)
505
506 require.Len(t, child.Definition.Metrics, 2)
507 assert.Equal(t, "license.expiry", child.Definition.Metrics[0].Symbol.Name)
508 assert.Equal(t, "1.2.3.0", child.Definition.Metrics[0].Symbol.OID)
509 assert.Equal(t, "license.state", child.Definition.Metrics[1].Symbol.Name)
510 assert.Equal(t, "1.2.4.0", child.Definition.Metrics[1].Symbol.OID)
511 }
512
513 func TestDeduplicateMetricsAcrossProfiles(t *testing.T) {
514 tests := map[string]struct {
515 profiles []*Profile
516 expected []*Profile
517 }{
518 "no profiles": {
519 profiles: []*Profile{},
520 expected: []*Profile{},
521 },
522 "single profile no duplicates": {
523 profiles: []*Profile{
524 {
525 SourceFile: "profile1.yaml",
526 Definition: &ddprofiledefinition.ProfileDefinition{
527 Metrics: []ddprofiledefinition.MetricsConfig{
528 {
529 Symbol: ddprofiledefinition.SymbolConfig{
530 OID: "1.3.6.1.2.1.1.3.0",
531 Name: "sysUpTime",
532 },
533 },
534 {
535 Symbol: ddprofiledefinition.SymbolConfig{
536 OID: "1.3.6.1.2.1.1.5.0",
537 Name: "sysName",
538 },
539 },
540 },
541 },
542 },
543 },
544 expected: []*Profile{
545 {
546 SourceFile: "profile1.yaml",
547 Definition: &ddprofiledefinition.ProfileDefinition{
548 Metrics: []ddprofiledefinition.MetricsConfig{
549 {
550 Symbol: ddprofiledefinition.SymbolConfig{
551 OID: "1.3.6.1.2.1.1.3.0",
552 Name: "sysUpTime",
553 },
554 },
555 {
556 Symbol: ddprofiledefinition.SymbolConfig{
557 OID: "1.3.6.1.2.1.1.5.0",
558 Name: "sysName",
559 },
560 },
561 },
562 },
563 },
564 },
565 },
566 "duplicate scalar metrics across profiles - first wins": {
567 profiles: []*Profile{
568 {
569 SourceFile: "specific-profile.yaml", // Most specific, comes first
570 Definition: &ddprofiledefinition.ProfileDefinition{
571 Metrics: []ddprofiledefinition.MetricsConfig{
572 {
573 Symbol: ddprofiledefinition.SymbolConfig{
574 OID: "1.3.6.1.2.1.1.3.0",
575 Name: "sysUpTime",
576 },
577 },
578 },
579 },
580 },
581 {
582 SourceFile: "generic-profile.yaml", // Less specific, comes second
583 Definition: &ddprofiledefinition.ProfileDefinition{
584 Metrics: []ddprofiledefinition.MetricsConfig{
585 {
586 Symbol: ddprofiledefinition.SymbolConfig{
587 OID: "1.3.6.1.2.1.1.3.0",
588 Name: "sysUpTime",
589 },
590 },
591 {
592 Symbol: ddprofiledefinition.SymbolConfig{
593 OID: "1.3.6.1.2.1.1.5.0",
594 Name: "sysName",
595 },
596 },
597 },
598 },
599 },
600 },
601 expected: []*Profile{
602 {
603 SourceFile: "specific-profile.yaml",
604 Definition: &ddprofiledefinition.ProfileDefinition{
605 Metrics: []ddprofiledefinition.MetricsConfig{
606 {
607 Symbol: ddprofiledefinition.SymbolConfig{
608 OID: "1.3.6.1.2.1.1.3.0",
609 Name: "sysUpTime",
610 },
611 },
612 },
613 },
614 },
615 {
616 SourceFile: "generic-profile.yaml",
617 Definition: &ddprofiledefinition.ProfileDefinition{
618 Metrics: []ddprofiledefinition.MetricsConfig{
619 {
620 Symbol: ddprofiledefinition.SymbolConfig{
621 OID: "1.3.6.1.2.1.1.5.0",
622 Name: "sysName",
623 },
624 },
625 },
626 },
627 },
628 },
629 },
630 "duplicate table metrics - exact same symbols": {
631 profiles: []*Profile{
632 {
633 SourceFile: "specific-profile.yaml",
634 Definition: &ddprofiledefinition.ProfileDefinition{
635 Metrics: []ddprofiledefinition.MetricsConfig{
636 {
637 Table: ddprofiledefinition.SymbolConfig{
638 OID: "1.3.6.1.2.1.2.2",
639 Name: "ifTable",
640 },
641 Symbols: []ddprofiledefinition.SymbolConfig{
642 {
643 OID: "1.3.6.1.2.1.2.2.1.10",
644 Name: "ifInOctets",
645 },
646 {
647 OID: "1.3.6.1.2.1.2.2.1.16",
648 Name: "ifOutOctets",
649 },
650 },
651 MetricTags: []ddprofiledefinition.MetricTagConfig{
652 {
653 Tag: "interface",
654 Symbol: ddprofiledefinition.SymbolConfigCompat{
655 OID: "1.3.6.1.2.1.2.2.1.2",
656 Name: "ifDescr",
657 },
658 },
659 },
660 },
661 },
662 },
663 },
664 {
665 SourceFile: "generic-profile.yaml",
666 Definition: &ddprofiledefinition.ProfileDefinition{
667 Metrics: []ddprofiledefinition.MetricsConfig{
668 {
669 Table: ddprofiledefinition.SymbolConfig{
670 OID: "1.3.6.1.2.1.2.2",
671 Name: "ifTable",
672 },
673 Symbols: []ddprofiledefinition.SymbolConfig{
674 {
675 OID: "1.3.6.1.2.1.2.2.1.10",
676 Name: "ifInOctets",
677 },
678 {
679 OID: "1.3.6.1.2.1.2.2.1.16",
680 Name: "ifOutOctets",
681 },
682 },
683 MetricTags: []ddprofiledefinition.MetricTagConfig{
684 {
685 Tag: "if_name", // Different tag, but ignored in key
686 Symbol: ddprofiledefinition.SymbolConfigCompat{
687 OID: "1.3.6.1.2.1.31.1.1.1.1",
688 Name: "ifName",
689 },
690 Table: "ifXTable",
691 },
692 },
693 },
694 },
695 },
696 },
697 },
698 expected: []*Profile{
699 {
700 SourceFile: "specific-profile.yaml",
701 Definition: &ddprofiledefinition.ProfileDefinition{
702 Metrics: []ddprofiledefinition.MetricsConfig{
703 {
704 Table: ddprofiledefinition.SymbolConfig{
705 OID: "1.3.6.1.2.1.2.2",
706 Name: "ifTable",
707 },
708 Symbols: []ddprofiledefinition.SymbolConfig{
709 {
710 OID: "1.3.6.1.2.1.2.2.1.10",
711 Name: "ifInOctets",
712 },
713 {
714 OID: "1.3.6.1.2.1.2.2.1.16",
715 Name: "ifOutOctets",
716 },
717 },
718 MetricTags: []ddprofiledefinition.MetricTagConfig{
719 {
720 Tag: "interface",
721 Symbol: ddprofiledefinition.SymbolConfigCompat{
722 OID: "1.3.6.1.2.1.2.2.1.2",
723 Name: "ifDescr",
724 },
725 },
726 },
727 },
728 },
729 },
730 },
731 {
732 SourceFile: "generic-profile.yaml",
733 Definition: &ddprofiledefinition.ProfileDefinition{
734 Metrics: []ddprofiledefinition.MetricsConfig{}, // Empty because it's a duplicate
735 },
736 },
737 },
738 },
739 "same table different symbols - keep both": {
740 profiles: []*Profile{
741 {
742 SourceFile: "profile1.yaml",
743 Definition: &ddprofiledefinition.ProfileDefinition{
744 Metrics: []ddprofiledefinition.MetricsConfig{
745 {
746 Table: ddprofiledefinition.SymbolConfig{
747 OID: "1.3.6.1.2.1.2.2",
748 Name: "ifTable",
749 },
750 Symbols: []ddprofiledefinition.SymbolConfig{
751 {
752 OID: "1.3.6.1.2.1.2.2.1.10",
753 Name: "ifInOctets",
754 },
755 },
756 },
757 },
758 },
759 },
760 {
761 SourceFile: "profile2.yaml",
762 Definition: &ddprofiledefinition.ProfileDefinition{
763 Metrics: []ddprofiledefinition.MetricsConfig{
764 {
765 Table: ddprofiledefinition.SymbolConfig{
766 OID: "1.3.6.1.2.1.2.2",
767 Name: "ifTable",
768 },
769 Symbols: []ddprofiledefinition.SymbolConfig{
770 {
771 OID: "1.3.6.1.2.1.2.2.1.16",
772 Name: "ifOutOctets",
773 },
774 },
775 },
776 },
777 },
778 },
779 },
780 expected: []*Profile{
781 {
782 SourceFile: "profile1.yaml",
783 Definition: &ddprofiledefinition.ProfileDefinition{
784 Metrics: []ddprofiledefinition.MetricsConfig{
785 {
786 Table: ddprofiledefinition.SymbolConfig{
787 OID: "1.3.6.1.2.1.2.2",
788 Name: "ifTable",
789 },
790 Symbols: []ddprofiledefinition.SymbolConfig{
791 {
792 OID: "1.3.6.1.2.1.2.2.1.10",
793 Name: "ifInOctets",
794 },
795 },
796 },
797 },
798 },
799 },
800 {
801 SourceFile: "profile2.yaml",
802 Definition: &ddprofiledefinition.ProfileDefinition{
803 Metrics: []ddprofiledefinition.MetricsConfig{
804 {
805 Table: ddprofiledefinition.SymbolConfig{
806 OID: "1.3.6.1.2.1.2.2",
807 Name: "ifTable",
808 },
809 Symbols: []ddprofiledefinition.SymbolConfig{
810 {
811 OID: "1.3.6.1.2.1.2.2.1.16",
812 Name: "ifOutOctets",
813 },
814 },
815 },
816 },
817 },
818 },
819 },
820 },
821 "symbols in different order - still duplicate": {
822 profiles: []*Profile{
823 {
824 SourceFile: "profile1.yaml",
825 Definition: &ddprofiledefinition.ProfileDefinition{
826 Metrics: []ddprofiledefinition.MetricsConfig{
827 {
828 Table: ddprofiledefinition.SymbolConfig{
829 OID: "1.3.6.1.2.1.2.2",
830 Name: "ifTable",
831 },
832 Symbols: []ddprofiledefinition.SymbolConfig{
833 {
834 OID: "1.3.6.1.2.1.2.2.1.10",
835 Name: "ifInOctets",
836 },
837 {
838 OID: "1.3.6.1.2.1.2.2.1.16",
839 Name: "ifOutOctets",
840 },
841 },
842 },
843 },
844 },
845 },
846 {
847 SourceFile: "profile2.yaml",
848 Definition: &ddprofiledefinition.ProfileDefinition{
849 Metrics: []ddprofiledefinition.MetricsConfig{
850 {
851 Table: ddprofiledefinition.SymbolConfig{
852 OID: "1.3.6.1.2.1.2.2",
853 Name: "ifTable",
854 },
855 Symbols: []ddprofiledefinition.SymbolConfig{
856 {
857 OID: "1.3.6.1.2.1.2.2.1.16",
858 Name: "ifOutOctets",
859 },
860 {
861 OID: "1.3.6.1.2.1.2.2.1.10",
862 Name: "ifInOctets",
863 },
864 },
865 },
866 },
867 },
868 },
869 },
870 expected: []*Profile{
871 {
872 SourceFile: "profile1.yaml",
873 Definition: &ddprofiledefinition.ProfileDefinition{
874 Metrics: []ddprofiledefinition.MetricsConfig{
875 {
876 Table: ddprofiledefinition.SymbolConfig{
877 OID: "1.3.6.1.2.1.2.2",
878 Name: "ifTable",
879 },
880 Symbols: []ddprofiledefinition.SymbolConfig{
881 {
882 OID: "1.3.6.1.2.1.2.2.1.10",
883 Name: "ifInOctets",
884 },
885 {
886 OID: "1.3.6.1.2.1.2.2.1.16",
887 Name: "ifOutOctets",
888 },
889 },
890 },
891 },
892 },
893 },
894 {
895 SourceFile: "profile2.yaml",
896 Definition: &ddprofiledefinition.ProfileDefinition{
897 Metrics: []ddprofiledefinition.MetricsConfig{}, // Empty because symbols are sorted in key
898 },
899 },
900 },
901 },
902 "order matters - specific profiles first": {
903 // Profiles are already sorted by FindProfiles, most specific first
904 profiles: []*Profile{
905 {
906 SourceFile: "cisco-nexus-9000.yaml", // Most specific
907 Definition: &ddprofiledefinition.ProfileDefinition{
908 Metrics: []ddprofiledefinition.MetricsConfig{
909 {
910 Symbol: ddprofiledefinition.SymbolConfig{
911 OID: "1.3.6.1.2.1.1.3.0",
912 Name: "sysUpTime",
913 },
914 },
915 {
916 Symbol: ddprofiledefinition.SymbolConfig{
917 OID: "1.3.6.1.4.1.9.9.305.1.1.1.0",
918 Name: "cempMemPoolUsed",
919 },
920 },
921 },
922 },
923 },
924 {
925 SourceFile: "cisco-nexus.yaml", // Less specific
926 Definition: &ddprofiledefinition.ProfileDefinition{
927 Metrics: []ddprofiledefinition.MetricsConfig{
928 {
929 Symbol: ddprofiledefinition.SymbolConfig{
930 OID: "1.3.6.1.2.1.1.3.0",
931 Name: "sysUpTime",
932 },
933 },
934 {
935 Symbol: ddprofiledefinition.SymbolConfig{
936 OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.7",
937 Name: "cpmCPUTotal5minRev",
938 },
939 },
940 },
941 },
942 },
943 {
944 SourceFile: "generic-device.yaml", // Least specific
945 Definition: &ddprofiledefinition.ProfileDefinition{
946 Metrics: []ddprofiledefinition.MetricsConfig{
947 {
948 Symbol: ddprofiledefinition.SymbolConfig{
949 OID: "1.3.6.1.2.1.1.3.0",
950 Name: "sysUpTime",
951 },
952 },
953 {
954 Table: ddprofiledefinition.SymbolConfig{
955 OID: "1.3.6.1.2.1.2.2",
956 Name: "ifTable",
957 },
958 Symbols: []ddprofiledefinition.SymbolConfig{
959 {
960 OID: "1.3.6.1.2.1.2.2.1.10",
961 Name: "ifInOctets",
962 },
963 },
964 },
965 },
966 },
967 },
968 },
969 expected: []*Profile{
970 {
971 SourceFile: "cisco-nexus-9000.yaml",
972 Definition: &ddprofiledefinition.ProfileDefinition{
973 Metrics: []ddprofiledefinition.MetricsConfig{
974 {
975 Symbol: ddprofiledefinition.SymbolConfig{
976 OID: "1.3.6.1.2.1.1.3.0",
977 Name: "sysUpTime",
978 },
979 },
980 {
981 Symbol: ddprofiledefinition.SymbolConfig{
982 OID: "1.3.6.1.4.1.9.9.305.1.1.1.0",
983 Name: "cempMemPoolUsed",
984 },
985 },
986 },
987 },
988 },
989 {
990 SourceFile: "cisco-nexus.yaml",
991 Definition: &ddprofiledefinition.ProfileDefinition{
992 Metrics: []ddprofiledefinition.MetricsConfig{
993 {
994 Symbol: ddprofiledefinition.SymbolConfig{
995 OID: "1.3.6.1.4.1.9.9.109.1.1.1.1.7",
996 Name: "cpmCPUTotal5minRev",
997 },
998 },
999 },
1000 },
1001 },
1002 {
1003 SourceFile: "generic-device.yaml",
1004 Definition: &ddprofiledefinition.ProfileDefinition{
1005 Metrics: []ddprofiledefinition.MetricsConfig{
1006 {
1007 Table: ddprofiledefinition.SymbolConfig{
1008 OID: "1.3.6.1.2.1.2.2",
1009 Name: "ifTable",
1010 },
1011 Symbols: []ddprofiledefinition.SymbolConfig{
1012 {
1013 OID: "1.3.6.1.2.1.2.2.1.10",
1014 Name: "ifInOctets",
1015 },
1016 },
1017 },
1018 },
1019 },
1020 },
1021 },
1022 },
1023 "empty metrics in some profiles": {
1024 profiles: []*Profile{
1025 {
1026 SourceFile: "profile1.yaml",
1027 Definition: &ddprofiledefinition.ProfileDefinition{
1028 Metrics: []ddprofiledefinition.MetricsConfig{
1029 {
1030 Symbol: ddprofiledefinition.SymbolConfig{
1031 OID: "1.3.6.1.2.1.1.3.0",
1032 Name: "sysUpTime",
1033 },
1034 },
1035 },
1036 },
1037 },
1038 {
1039 SourceFile: "profile2.yaml",
1040 Definition: &ddprofiledefinition.ProfileDefinition{
1041 Metrics: []ddprofiledefinition.MetricsConfig{},
1042 },
1043 },
1044 {
1045 SourceFile: "profile3.yaml",
1046 Definition: &ddprofiledefinition.ProfileDefinition{
1047 Metrics: []ddprofiledefinition.MetricsConfig{
1048 {
1049 Symbol: ddprofiledefinition.SymbolConfig{
1050 OID: "1.3.6.1.2.1.1.3.0",
1051 Name: "sysUpTime",
1052 },
1053 },
1054 },
1055 },
1056 },
1057 },
1058 expected: []*Profile{
1059 {
1060 SourceFile: "profile1.yaml",
1061 Definition: &ddprofiledefinition.ProfileDefinition{
1062 Metrics: []ddprofiledefinition.MetricsConfig{
1063 {
1064 Symbol: ddprofiledefinition.SymbolConfig{
1065 OID: "1.3.6.1.2.1.1.3.0",
1066 Name: "sysUpTime",
1067 },
1068 },
1069 },
1070 },
1071 },
1072 {
1073 SourceFile: "profile2.yaml",
1074 Definition: &ddprofiledefinition.ProfileDefinition{
1075 Metrics: []ddprofiledefinition.MetricsConfig{},
1076 },
1077 },
1078 {
1079 SourceFile: "profile3.yaml",
1080 Definition: &ddprofiledefinition.ProfileDefinition{
1081 Metrics: []ddprofiledefinition.MetricsConfig{}, // Removed as duplicate
1082 },
1083 },
1084 },
1085 },
1086 }
1087
1088 for name, tc := range tests {
1089 t.Run(name, func(t *testing.T) {
1090 // Make a deep copy of profiles to avoid modifying test data
1091 profiles := make([]*Profile, len(tc.profiles))
1092 for i, p := range tc.profiles {
1093 profiles[i] = p.clone()
1094 }
1095
1096 deduplicateMetricsAcrossProfiles(profiles)
1097
1098 require.Equal(t, len(tc.expected), len(profiles))
1099
1100 for i, expectedProf := range tc.expected {
1101 assert.Equal(t, expectedProf.SourceFile, profiles[i].SourceFile)
1102 assert.Equal(t, len(expectedProf.Definition.Metrics), len(profiles[i].Definition.Metrics))
1103
1104 // Compare metrics
1105 for j, expectedMetric := range expectedProf.Definition.Metrics {
1106 actualMetric := profiles[i].Definition.Metrics[j]
1107
1108 if expectedMetric.IsScalar() {
1109 assert.Equal(t, expectedMetric.Symbol.OID, actualMetric.Symbol.OID)
1110 assert.Equal(t, expectedMetric.Symbol.Name, actualMetric.Symbol.Name)
1111 } else {
1112 assert.Equal(t, expectedMetric.Table.OID, actualMetric.Table.OID)
1113 assert.Equal(t, expectedMetric.Table.Name, actualMetric.Table.Name)
1114 assert.Equal(t, len(expectedMetric.Symbols), len(actualMetric.Symbols))
1115 }
1116 }
1117 }
1118 })
1119 }
1120 }
1121
1122 func TestSortProfilesBySpecificity(t *testing.T) {
1123 tests := map[string]struct {
1124 profiles []string // Source file names
1125 matchedOIDs []string // OIDs in the same order as profiles
1126 expected []string // Expected order of profile source files
1127 }{
1128 "empty profiles": {
1129 profiles: []string{},
1130 matchedOIDs: []string{},
1131 expected: []string{},
1132 },
1133 "single profile": {
1134 profiles: []string{"cisco.yaml"},
1135 matchedOIDs: []string{"1.3.6.1.4.1.9.1.669"},
1136 expected: []string{"cisco.yaml"},
1137 },
1138 "exact OID vs pattern - same length": {
1139 profiles: []string{"pattern.yaml", "exact.yaml"},
1140 matchedOIDs: []string{"1.3.6.1.4.1.9.1.66*", "1.3.6.1.4.1.9.1.669"},
1141 expected: []string{"exact.yaml", "pattern.yaml"},
1142 },
1143 "longer OID wins": {
1144 profiles: []string{"short.yaml", "long.yaml", "medium.yaml"},
1145 matchedOIDs: []string{"1.3.6.1.4.1.9.*", "1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1.*"},
1146 expected: []string{"long.yaml", "medium.yaml", "short.yaml"},
1147 },
1148 "complex mix - length, exact, and patterns": {
1149 profiles: []string{
1150 "generic-wildcard.yaml",
1151 "specific-exact.yaml",
1152 "specific-pattern.yaml",
1153 "mid-exact.yaml",
1154 "mid-pattern.yaml",
1155 },
1156 matchedOIDs: []string{
1157 "1.3.6.1.4.1.9.*",
1158 "1.3.6.1.4.1.9.1.669",
1159 "1.3.6.1.4.1.9.1.66*",
1160 "1.3.6.1.4.1.9.1",
1161 "1.3.6.1.4.1.9.*",
1162 },
1163 expected: []string{
1164 "specific-exact.yaml", // Longest + exact
1165 "specific-pattern.yaml", // Longest + pattern
1166 "mid-exact.yaml", // Medium + exact
1167 "generic-wildcard.yaml", // Short + pattern (first in input order)
1168 "mid-pattern.yaml", // Short + pattern (second in input order)
1169 },
1170 },
1171 "same OID different profiles - stable sort": {
1172 profiles: []string{"profile-b.yaml", "profile-a.yaml", "profile-c.yaml"},
1173 matchedOIDs: []string{"1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1.669"},
1174 expected: []string{"profile-b.yaml", "profile-a.yaml", "profile-c.yaml"}, // Maintains input order
1175 },
1176 "regex patterns with special characters": {
1177 profiles: []string{"exact.yaml", "regex-dots.yaml", "regex-plus.yaml", "regex-question.yaml"},
1178 matchedOIDs: []string{"1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1...", "1.3.6.1.4.1.9.1.6+", "1.3.6.1.4.1.9.1.66?"},
1179 expected: []string{
1180 "exact.yaml", // Length 21, exact
1181 "regex-question.yaml", // Length 20, pattern
1182 "regex-dots.yaml", // Length 19, pattern ("..." < ".6+" lexicographically)
1183 "regex-plus.yaml", // Length 19, pattern
1184 },
1185 },
1186 "all exact OIDs - sort by OID value": {
1187 profiles: []string{"oid-670.yaml", "oid-669.yaml", "oid-671.yaml"},
1188 matchedOIDs: []string{"1.3.6.1.4.1.9.1.670", "1.3.6.1.4.1.9.1.669", "1.3.6.1.4.1.9.1.671"},
1189 expected: []string{"oid-669.yaml", "oid-670.yaml", "oid-671.yaml"},
1190 },
1191 "all patterns - sort by pattern value": {
1192 profiles: []string{"pattern-b.yaml", "pattern-a.yaml", "pattern-c.yaml"},
1193 matchedOIDs: []string{"1.3.6.1.4.1.9.1.b*", "1.3.6.1.4.1.9.1.a*", "1.3.6.1.4.1.9.1.c*"},
1194 expected: []string{"pattern-a.yaml", "pattern-b.yaml", "pattern-c.yaml"},
1195 },
1196 "real-world scenario": {
1197 profiles: []string{
1198 "generic-device.yaml",
1199 "cisco.yaml",
1200 "cisco-asa.yaml",
1201 "cisco-asa-5510.yaml",
1202 },
1203 matchedOIDs: []string{
1204 "1.3.6.1.4.1.*",
1205 "1.3.6.1.4.1.9.*",
1206 "1.3.6.1.4.1.9.1.*",
1207 "1.3.6.1.4.1.9.1.669",
1208 },
1209 expected: []string{
1210 "cisco-asa-5510.yaml",
1211 "cisco-asa.yaml",
1212 "cisco.yaml",
1213 "generic-device.yaml",
1214 },
1215 },
1216 "mixed vendor OIDs": {
1217 profiles: []string{
1218 "dell.yaml",
1219 "cisco.yaml",
1220 "hp.yaml",
1221 "generic.yaml",
1222 },
1223 matchedOIDs: []string{
1224 "1.3.6.1.4.1.674.10892.5",
1225 "1.3.6.1.4.1.9.1.669",
1226 "1.3.6.1.4.1.11.2.3.7.11",
1227 "1.3.6.1.4.1.*",
1228 },
1229 expected: []string{
1230 "hp.yaml", // Longest OID (24 chars)
1231 "dell.yaml", // Second longest (24 chars, but "11" < "674" lexicographically)
1232 "cisco.yaml", // Third longest (21 chars)
1233 "generic.yaml", // Shortest (pattern, 13 chars)
1234 },
1235 },
1236 "edge case - empty OID": {
1237 profiles: []string{"profile1.yaml", "profile2.yaml"},
1238 matchedOIDs: []string{"", "1.3.6.1.4.1.9.1.669"},
1239 expected: []string{"profile2.yaml", "profile1.yaml"},
1240 },
1241 "patterns with same prefix": {
1242 profiles: []string{
1243 "generic.yaml",
1244 "specific.yaml",
1245 "more-specific.yaml",
1246 },
1247 matchedOIDs: []string{
1248 "1.3.6.1.4.1.9.*",
1249 "1.3.6.1.4.1.9.1.*",
1250 "1.3.6.1.4.1.9.1.6*",
1251 },
1252 expected: []string{
1253 "more-specific.yaml",
1254 "specific.yaml",
1255 "generic.yaml",
1256 },
1257 },
1258 "mix: longer > shorter; OID > descr-only": {
1259 profiles: []string{"a.yaml", "b.yaml", "c.yaml", "d.yaml"},
1260 matchedOIDs: []string{"1.3.6.1", "1.3.*", "", "1.3.6.1.4"},
1261 // d: longest "1.3.6.1.4" > a: "1.3.6.1" > b: "1.3.*" > c: descr-only
1262 expected: []string{"d.yaml", "a.yaml", "b.yaml", "c.yaml"},
1263 },
1264 "oid match outranks descr-only": {
1265 profiles: []string{"descr-only.yaml", "exact.yaml"},
1266 matchedOIDs: []string{"", "1.2.3"},
1267 expected: []string{"exact.yaml", "descr-only.yaml"},
1268 },
1269 "same length: exact before pattern": {
1270 profiles: []string{"exact.yaml", "pattern.yaml"},
1271 matchedOIDs: []string{"1.3.4", "1.3.*"}, // both len == 5
1272 expected: []string{"exact.yaml", "pattern.yaml"},
1273 },
1274 "longer pattern beats shorter exact (by design)": {
1275 profiles: []string{"exact-short.yaml", "pattern-long.yaml"},
1276 matchedOIDs: []string{"1.3.4", "1.3.4.5.*"}, // len(pattern) > len(exact)
1277 expected: []string{"pattern-long.yaml", "exact-short.yaml"},
1278 },
1279 "lexicographic tiebreaker (same type & same length, both exact)": {
1280 profiles: []string{"a.yaml", "b.yaml"},
1281 matchedOIDs: []string{"1.10", "1.20"}, // same len, both exact
1282 expected: []string{"a.yaml", "b.yaml"}, // "1.10" < "1.20"
1283 },
1284 "stability when both descr-only (both empty)": {
1285 profiles: []string{"x.yaml", "y.yaml", "z.yaml"},
1286 matchedOIDs: []string{"", "", ""},
1287 // comparator returns 0 → SortStable preserves original order
1288 expected: []string{"x.yaml", "y.yaml", "z.yaml"},
1289 },
1290 "patterns by length (no exacts)": {
1291 profiles: []string{"p1.yaml", "p2.yaml", "p3.yaml"},
1292 matchedOIDs: []string{"1.*", "1.2.*", "1.2.3.*"},
1293 expected: []string{"p3.yaml", "p2.yaml", "p1.yaml"},
1294 },
1295 "tie: same length patterns → lexicographic": {
1296 profiles: []string{"pA.yaml", "pB.yaml"},
1297 matchedOIDs: []string{"1.9.*", "1.10.*"}, // same length strings
1298 expected: []string{"pB.yaml", "pA.yaml"}, // "1.10.*" < "1.9.*" lexicographically
1299 },
1300 "mixed: two OID matches + one descr-only; ensure empty last": {
1301 profiles: []string{"o1.yaml", "desc.yaml", "o2.yaml"},
1302 matchedOIDs: []string{"1.2.*", "", "1.2.3"},
1303 // longer first among OID matches, then empty
1304 expected: []string{"o2.yaml", "o1.yaml", "desc.yaml"},
1305 },
1306 }
1307
1308 for name, tc := range tests {
1309 t.Run(name, func(t *testing.T) {
1310 // Create profiles and matchedOIDs map
1311 profiles := make([]*Profile, len(tc.profiles))
1312 matchedOIDs := make(map[*Profile]string)
1313
1314 for i, sourceFile := range tc.profiles {
1315 profiles[i] = &Profile{SourceFile: sourceFile}
1316 if i < len(tc.matchedOIDs) {
1317 matchedOIDs[profiles[i]] = tc.matchedOIDs[i]
1318 }
1319 }
1320
1321 sortProfilesBySpecificity(profiles, matchedOIDs)
1322
1323 actual := make([]string, len(profiles))
1324 for i, p := range profiles {
1325 actual[i] = p.SourceFile
1326 }
1327
1328 assert.Equal(t, tc.expected, actual, "Profile order mismatch")
1329 })
1330 }
1331 }
1332
1333 func TestSortProfilesBySpecificity_Stable(t *testing.T) {
1334 // Create many profiles with the same OID
1335 numProfiles := 100
1336 profiles := make([]*Profile, numProfiles)
1337 matchedOIDs := make(map[*Profile]string)
1338
1339 for i := range numProfiles {
1340 profiles[i] = &Profile{
1341 SourceFile: fmt.Sprintf("profile-%03d.yaml", i),
1342 }
1343 matchedOIDs[profiles[i]] = "1.3.6.1.4.1.9.1.669"
1344 }
1345
1346 sortProfilesBySpecificity(profiles, matchedOIDs)
1347
1348 // Verify order is preserved (lexicographic due to same OID)
1349 for i := range numProfiles {
1350 expected := fmt.Sprintf("profile-%03d.yaml", i)
1351 assert.Equal(t, expected, profiles[i].SourceFile)
1352 }
1353 }
1354
1355 func Test_ProfileExtends_CircularReference(t *testing.T) {
1356 tmp := t.TempDir()
1357
1358 a := filepath.Join(tmp, "a.yaml")
1359 b := filepath.Join(tmp, "b.yaml")
1360
1361 writeYAML(t, a, map[string]any{
1362 "extends": []string{"b.yaml"},
1363 })
1364 writeYAML(t, b, map[string]any{
1365 "extends": []string{"a.yaml"},
1366 })
1367
1368 paths := multipath.New(tmp)
1369 _, err := loadProfile(a, paths)
1370 require.Error(t, err)
1371 require.Contains(t, err.Error(), "circular extends")
1372 }
1373
1374 func Test_ProfileExtends_RecursiveChain(t *testing.T) {
1375 tmp := t.TempDir()
1376
1377 base := filepath.Join(tmp, "base.yaml")
1378 mid := filepath.Join(tmp, "mid.yaml")
1379 top := filepath.Join(tmp, "top.yaml")
1380
1381 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{
1382 Metrics: []ddprofiledefinition.MetricsConfig{
1383 {
1384 Symbol: ddprofiledefinition.SymbolConfig{
1385 OID: "1.3.6.1.2.1.1.3.0",
1386 Name: "sysUpTime",
1387 },
1388 },
1389 },
1390 })
1391 writeYAML(t, mid, map[string]any{
1392 "extends": []string{"base.yaml"},
1393 })
1394 writeYAML(t, top, map[string]any{
1395 "extends": []string{"mid.yaml"},
1396 })
1397
1398 paths := multipath.New(tmp)
1399 prof, err := loadProfile(top, paths)
1400 require.NoError(t, err)
1401 require.Len(t, prof.Definition.Metrics, 1)
1402 require.Equal(t, "sysUpTime", prof.Definition.Metrics[0].Symbol.Name)
1403 }
1404
1405 func Test_ProfileExtends_MultipleBases(t *testing.T) {
1406 tmp := t.TempDir()
1407
1408 base1 := filepath.Join(tmp, "base1.yaml")
1409 base2 := filepath.Join(tmp, "base2.yaml")
1410 main := filepath.Join(tmp, "main.yaml")
1411
1412 writeYAML(t, base1, ddprofiledefinition.ProfileDefinition{
1413 Metrics: []ddprofiledefinition.MetricsConfig{
1414 {Symbol: ddprofiledefinition.SymbolConfig{
1415 OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr",
1416 }},
1417 },
1418 })
1419 writeYAML(t, base2, ddprofiledefinition.ProfileDefinition{
1420 Metrics: []ddprofiledefinition.MetricsConfig{
1421 {Symbol: ddprofiledefinition.SymbolConfig{
1422 OID: "1.3.6.1.2.1.1.5.0", Name: "sysName",
1423 }},
1424 },
1425 })
1426 writeYAML(t, main, map[string]any{
1427 "extends": []string{"base1.yaml", "base2.yaml"},
1428 })
1429
1430 paths := multipath.New(tmp)
1431 prof, err := loadProfile(main, paths)
1432 require.NoError(t, err)
1433 require.Len(t, prof.Definition.Metrics, 2)
1434 }
1435
1436 func Test_ProfileExtends_NonexistentFile(t *testing.T) {
1437 tmp := t.TempDir()
1438 main := filepath.Join(tmp, "main.yaml")
1439
1440 writeYAML(t, main, map[string]any{
1441 "extends": []string{"missing.yaml"},
1442 })
1443
1444 paths := multipath.New(tmp)
1445 _, err := loadProfile(main, paths)
1446 require.Error(t, err)
1447 require.Contains(t, err.Error(), "missing.yaml")
1448 }
1449
1450 func Test_ProfileExtends_SharedBase(t *testing.T) {
1451 tmp := t.TempDir()
1452
1453 base := filepath.Join(tmp, "base.yaml")
1454 a := filepath.Join(tmp, "a.yaml")
1455 b := filepath.Join(tmp, "b.yaml")
1456
1457 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{
1458 Metrics: []ddprofiledefinition.MetricsConfig{
1459 {Symbol: ddprofiledefinition.SymbolConfig{
1460 OID: "1.3.6.1.2.1.1.1.0", Name: "sysDescr",
1461 }},
1462 },
1463 })
1464 writeYAML(t, a, map[string]any{"extends": []string{"base.yaml"}})
1465 writeYAML(t, b, map[string]any{"extends": []string{"base.yaml"}})
1466
1467 paths := multipath.New(tmp)
1468
1469 profA, err := loadProfile(a, paths)
1470 require.NoError(t, err)
1471 require.Len(t, profA.Definition.Metrics, 1)
1472
1473 profB, err := loadProfile(b, paths)
1474 require.NoError(t, err)
1475 require.Len(t, profB.Definition.Metrics, 1)
1476 }
1477
1478 func Test_ProfileExtends_OverrideIgnored(t *testing.T) {
1479 tmp := t.TempDir()
1480
1481 base := filepath.Join(tmp, "base.yaml")
1482 main := filepath.Join(tmp, "main.yaml")
1483
1484 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{
1485 Metrics: []ddprofiledefinition.MetricsConfig{
1486 {Symbol: ddprofiledefinition.SymbolConfig{
1487 OID: "1.3.6.1.2.1.1.3.0", Name: "sysUpTime",
1488 }},
1489 },
1490 })
1491 writeYAML(t, main, map[string]any{
1492 "extends": []string{"base.yaml"},
1493 "metrics": []map[string]any{
1494 {
1495 "symbol": map[string]string{
1496 "OID": "1.3.6.1.2.1.1.3.0",
1497 "name": "sysUpTime",
1498 },
1499 },
1500 },
1501 })
1502
1503 paths := multipath.New(tmp)
1504 prof, err := loadProfile(main, paths)
1505 require.NoError(t, err)
1506
1507 deduplicateMetricsAcrossProfiles([]*Profile{prof})
1508
1509 // Should not duplicate
1510 require.Len(t, prof.Definition.Metrics, 1)
1511 }
1512
1513 func Test_ProfileExtends_UserOverride(t *testing.T) {
1514 stockDir := filepath.Join(t.TempDir(), "stock")
1515 userDir := filepath.Join(t.TempDir(), "user")
1516
1517 require.NoError(t, os.MkdirAll(stockDir, 0755))
1518 require.NoError(t, os.MkdirAll(userDir, 0755))
1519
1520 // Stock base profile
1521 writeYAML(t, filepath.Join(stockDir, "_base.yaml"), ddprofiledefinition.ProfileDefinition{
1522 Metrics: []ddprofiledefinition.MetricsConfig{
1523 {Symbol: ddprofiledefinition.SymbolConfig{
1524 OID: "1.3.6.1.2.1.1.3.0", Name: "sysUpTime",
1525 }},
1526 },
1527 })
1528
1529 // User override of base profile
1530 writeYAML(t, filepath.Join(userDir, "_base.yaml"), ddprofiledefinition.ProfileDefinition{
1531 Metrics: []ddprofiledefinition.MetricsConfig{
1532 {Symbol: ddprofiledefinition.SymbolConfig{
1533 OID: "1.3.6.1.2.1.1.3.0", Name: "sysUpTime",
1534 }},
1535 {Symbol: ddprofiledefinition.SymbolConfig{
1536 OID: "1.3.6.1.2.1.1.5.0", Name: "sysName",
1537 }},
1538 },
1539 })
1540
1541 // Main profile in stock
1542 writeYAML(t, filepath.Join(stockDir, "device.yaml"), map[string]any{
1543 "extends": []string{"_base.yaml"},
1544 })
1545
1546 paths := multipath.New(userDir, stockDir)
1547 prof, err := loadProfile(filepath.Join(stockDir, "device.yaml"), paths)
1548 require.NoError(t, err)
1549
1550 // Should use user's _base.yaml, so should have 2 metrics
1551 require.Len(t, prof.Definition.Metrics, 2)
1552 assert.Equal(t, "sysName", prof.Definition.Metrics[1].Symbol.Name)
1553 }
1554
1555 func TestProfile_ExtensionHierarchy(t *testing.T) {
1556 tmp := t.TempDir()
1557
1558 // Create a base profile
1559 base := filepath.Join(tmp, "_base.yaml")
1560 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{
1561 Metrics: []ddprofiledefinition.MetricsConfig{
1562 {
1563 Symbol: ddprofiledefinition.SymbolConfig{
1564 OID: "1.3.6.1.2.1.1.3.0",
1565 Name: "sysUpTime",
1566 },
1567 },
1568 },
1569 })
1570
1571 // Create an intermediate profile
1572 intermediate := filepath.Join(tmp, "_intermediate.yaml")
1573 writeYAML(t, intermediate, map[string]any{
1574 "extends": []string{"_base.yaml"},
1575 "metrics": []map[string]any{
1576 {
1577 "symbol": map[string]string{
1578 "OID": "1.3.6.1.2.1.1.5.0",
1579 "name": "sysName",
1580 },
1581 },
1582 },
1583 })
1584
1585 // Create the main profile
1586 main := filepath.Join(tmp, "device.yaml")
1587 writeYAML(t, main, map[string]any{
1588 "extends": []string{"_intermediate.yaml"},
1589 "metrics": []map[string]any{
1590 {
1591 "symbol": map[string]string{
1592 "OID": "1.3.6.1.2.1.1.1.0",
1593 "name": "sysDescr",
1594 },
1595 },
1596 },
1597 })
1598
1599 paths := multipath.New(tmp)
1600 prof, err := loadProfile(main, paths)
1601 require.NoError(t, err)
1602
1603 // Check that we have the extension hierarchy
1604 require.Len(t, prof.extensionHierarchy, 1)
1605 assert.Equal(t, "_intermediate.yaml", prof.extensionHierarchy[0].name)
1606
1607 // Check nested extensions
1608 require.Len(t, prof.extensionHierarchy[0].extensions, 1)
1609 assert.Equal(t, "_base.yaml", prof.extensionHierarchy[0].extensions[0].name)
1610
1611 // Check that all metrics were merged
1612 require.Len(t, prof.Definition.Metrics, 3)
1613
1614 // Test helper methods
1615 allFiles := prof.getAllExtendedFiles()
1616 fmt.Println(allFiles)
1617 assert.Len(t, allFiles, 2)
1618
1619 depth := prof.getExtensionDepth()
1620 assert.Equal(t, 2, depth)
1621
1622 assert.False(t, prof.hasCircularDependency())
1623 }
1624
1625 func TestProfile_MultipleExtends(t *testing.T) {
1626 tmp := t.TempDir()
1627
1628 // Create base profiles
1629 base1 := filepath.Join(tmp, "_base1.yaml")
1630 writeYAML(t, base1, ddprofiledefinition.ProfileDefinition{
1631 Metrics: []ddprofiledefinition.MetricsConfig{
1632 {
1633 Symbol: ddprofiledefinition.SymbolConfig{
1634 OID: "1.3.6.1.2.1.1.1.0",
1635 Name: "sysDescr",
1636 },
1637 },
1638 },
1639 })
1640
1641 base2 := filepath.Join(tmp, "_base2.yaml")
1642 writeYAML(t, base2, ddprofiledefinition.ProfileDefinition{
1643 Metrics: []ddprofiledefinition.MetricsConfig{
1644 {
1645 Symbol: ddprofiledefinition.SymbolConfig{
1646 OID: "1.3.6.1.2.1.1.5.0",
1647 Name: "sysName",
1648 },
1649 },
1650 },
1651 })
1652
1653 // Main profile extending both
1654 main := filepath.Join(tmp, "device.yaml")
1655 writeYAML(t, main, ddprofiledefinition.ProfileDefinition{
1656 Extends: []string{"_base1.yaml", "_base2.yaml"},
1657 })
1658
1659 paths := multipath.New(tmp)
1660 prof, err := loadProfile(main, paths)
1661 require.NoError(t, err)
1662
1663 // Check that we have both extensions
1664 require.Len(t, prof.extensionHierarchy, 2)
1665 assert.Equal(t, "_base1.yaml", prof.extensionHierarchy[0].name)
1666 assert.Equal(t, "_base2.yaml", prof.extensionHierarchy[1].name)
1667
1668 // Both should have no nested extensions
1669 assert.Len(t, prof.extensionHierarchy[0].extensions, 0)
1670 assert.Len(t, prof.extensionHierarchy[1].extensions, 0)
1671
1672 // Check all files
1673 allFiles := prof.getAllExtendedFiles()
1674 assert.Len(t, allFiles, 2)
1675 }
1676
1677 func TestProfile_MultipleExtends_LaterOverrideEarlier(t *testing.T) {
1678 tmp := t.TempDir()
1679
1680 writeBase := func(path, suffix string) {
1681 writeYAML(t, path, ddprofiledefinition.ProfileDefinition{
1682 Metadata: ddprofiledefinition.MetadataConfig{
1683 "device": {
1684 Fields: map[string]ddprofiledefinition.MetadataField{
1685 "model": {Value: suffix},
1686 },
1687 },
1688 },
1689 StaticTags: []ddprofiledefinition.StaticMetricTagConfig{
1690 {Tag: "source", Value: suffix},
1691 },
1692 Metrics: []ddprofiledefinition.MetricsConfig{
1693 {
1694 Symbol: ddprofiledefinition.SymbolConfig{
1695 OID: "1.3.6.1.2.1.1.5.0",
1696 Name: "sysName",
1697 ChartMeta: ddprofiledefinition.ChartMeta{
1698 Description: suffix,
1699 },
1700 },
1701 },
1702 },
1703 VirtualMetrics: []ddprofiledefinition.VirtualMetricConfig{
1704 {
1705 Name: "ifTraffic",
1706 Sources: []ddprofiledefinition.VirtualMetricSourceConfig{
1707 {Metric: "sysName", Table: ""},
1708 },
1709 ChartMeta: ddprofiledefinition.ChartMeta{
1710 Description: suffix,
1711 },
1712 },
1713 },
1714 })
1715 }
1716
1717 base1 := filepath.Join(tmp, "_base1.yaml")
1718 base2 := filepath.Join(tmp, "_base2.yaml")
1719 writeBase(base1, "base1")
1720 writeBase(base2, "base2")
1721
1722 main := filepath.Join(tmp, "device.yaml")
1723 writeYAML(t, main, map[string]any{
1724 "extends": []string{"_base1.yaml", "_base2.yaml"},
1725 })
1726
1727 prof, err := loadProfile(main, multipath.New(tmp))
1728 require.NoError(t, err)
1729
1730 require.Len(t, prof.Definition.Metrics, 1)
1731 assert.Equal(t, "base2", prof.Definition.Metrics[0].Symbol.ChartMeta.Description)
1732
1733 require.Len(t, prof.Definition.VirtualMetrics, 1)
1734 assert.Equal(t, "base2", prof.Definition.VirtualMetrics[0].ChartMeta.Description)
1735
1736 assert.Equal(t, "base2", prof.Definition.Metadata["device"].Fields["model"].Value)
1737
1738 require.Len(t, prof.Definition.StaticTags, 2)
1739 assert.Equal(t, "base1", prof.Definition.StaticTags[0].Value)
1740 assert.Equal(t, "base2", prof.Definition.StaticTags[1].Value)
1741
1742 mergedStaticTags := make(map[string]string, len(prof.Definition.StaticTags))
1743 for _, tag := range prof.Definition.StaticTags {
1744 if tag.Tag != "" && tag.Value != "" {
1745 mergedStaticTags[tag.Tag] = tag.Value
1746 }
1747 }
1748 assert.Equal(t, "base2", mergedStaticTags["source"])
1749 }
1750
1751 func TestProfile_MultipleExtends_PreservesScalarSameNameFallbackOIDs(t *testing.T) {
1752 tmp := t.TempDir()
1753
1754 writeYAML(t, filepath.Join(tmp, "_base1.yaml"), ddprofiledefinition.ProfileDefinition{
1755 Metrics: []ddprofiledefinition.MetricsConfig{
1756 {
1757 Symbol: ddprofiledefinition.SymbolConfig{
1758 OID: "1.3.6.1.2.1.25.1.1.0",
1759 Name: "systemUptime",
1760 },
1761 },
1762 },
1763 })
1764 writeYAML(t, filepath.Join(tmp, "_base2.yaml"), ddprofiledefinition.ProfileDefinition{
1765 Metrics: []ddprofiledefinition.MetricsConfig{
1766 {
1767 Symbol: ddprofiledefinition.SymbolConfig{
1768 OID: "1.3.6.1.2.1.1.3.0",
1769 Name: "systemUptime",
1770 },
1771 },
1772 },
1773 })
1774 writeYAML(t, filepath.Join(tmp, "device.yaml"), map[string]any{
1775 "extends": []string{"_base1.yaml", "_base2.yaml"},
1776 })
1777
1778 prof, err := loadProfile(filepath.Join(tmp, "device.yaml"), multipath.New(tmp))
1779 require.NoError(t, err)
1780 require.Len(t, prof.Definition.Metrics, 2)
1781 assert.Equal(t, "systemUptime", prof.Definition.Metrics[0].Symbol.Name)
1782 assert.Equal(t, "1.3.6.1.2.1.1.3.0", prof.Definition.Metrics[0].Symbol.OID)
1783 assert.Equal(t, "systemUptime", prof.Definition.Metrics[1].Symbol.Name)
1784 assert.Equal(t, "1.3.6.1.2.1.25.1.1.0", prof.Definition.Metrics[1].Symbol.OID)
1785 }
1786
1787 func TestProfile_ComplexHierarchy(t *testing.T) {
1788 tmp := t.TempDir()
1789
1790 // Create a complex hierarchy:
1791 // device.yaml -> [_vendor.yaml, _generic.yaml]
1792 // _vendor.yaml -> _base.yaml
1793 // _generic.yaml -> _base.yaml
1794
1795 base := filepath.Join(tmp, "_base.yaml")
1796 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{
1797 Metrics: []ddprofiledefinition.MetricsConfig{
1798 {
1799 Symbol: ddprofiledefinition.SymbolConfig{
1800 OID: "1.3.6.1.2.1.1.3.0",
1801 Name: "sysUpTime",
1802 },
1803 },
1804 },
1805 })
1806
1807 vendor := filepath.Join(tmp, "_vendor.yaml")
1808 writeYAML(t, vendor, map[string]any{
1809 "extends": []string{"_base.yaml"},
1810 "metrics": []map[string]any{
1811 {
1812 "symbol": map[string]string{
1813 "OID": "1.3.6.1.4.1.9.9.109.1.1.1.1.7",
1814 "name": "cpmCPUTotal5minRev",
1815 },
1816 },
1817 },
1818 })
1819
1820 generic := filepath.Join(tmp, "_generic.yaml")
1821 writeYAML(t, generic, map[string]any{
1822 "extends": []string{"_base.yaml"},
1823 "metrics": []map[string]any{
1824 {
1825 "table": map[string]string{
1826 "OID": "1.3.6.1.2.1.2.2",
1827 "name": "ifTable",
1828 },
1829 "symbols": []map[string]string{
1830 {
1831 "OID": "1.3.6.1.2.1.2.2.1.10",
1832 "name": "ifInOctets",
1833 },
1834 },
1835 },
1836 },
1837 })
1838
1839 device := filepath.Join(tmp, "device.yaml")
1840 writeYAML(t, device, map[string]any{
1841 "extends": []string{"_vendor.yaml", "_generic.yaml"},
1842 "metrics": []map[string]any{
1843 {
1844 "symbol": map[string]string{
1845 "OID": "1.3.6.1.2.1.1.1.0",
1846 "name": "sysDescr",
1847 },
1848 },
1849 },
1850 })
1851
1852 paths := multipath.New(tmp)
1853 prof, err := loadProfile(device, paths)
1854 require.NoError(t, err)
1855
1856 // Check hierarchy
1857 require.Len(t, prof.extensionHierarchy, 2)
1858
1859 // Both vendor and generic should have base as their extension
1860 require.Len(t, prof.extensionHierarchy[0].extensions, 1)
1861 require.Len(t, prof.extensionHierarchy[1].extensions, 1)
1862 assert.Equal(t, "_base.yaml", prof.extensionHierarchy[0].extensions[0].name)
1863 assert.Equal(t, "_base.yaml", prof.extensionHierarchy[1].extensions[0].name)
1864
1865 // Check depth
1866 assert.Equal(t, 2, prof.getExtensionDepth())
1867
1868 // Check that base.yaml appears only once in the flat list
1869 allFiles := prof.getAllExtendedFiles()
1870 baseCount := 0
1871 for _, f := range allFiles {
1872 if filepath.Base(f) == "_base.yaml" {
1873 baseCount++
1874 }
1875 }
1876 assert.Equal(t, 1, baseCount, "base.yaml should appear only once in the flat list")
1877 }
1878
1879 func TestProfile_Clone(t *testing.T) {
1880 tmp := t.TempDir()
1881
1882 base := filepath.Join(tmp, "_base.yaml")
1883 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{
1884 Metrics: []ddprofiledefinition.MetricsConfig{
1885 {
1886 Symbol: ddprofiledefinition.SymbolConfig{
1887 OID: "1.3.6.1.2.1.1.3.0",
1888 Name: "sysUpTime",
1889 },
1890 },
1891 },
1892 })
1893
1894 main := filepath.Join(tmp, "device.yaml")
1895 writeYAML(t, main, map[string]any{
1896 "extends": []string{"_base.yaml"},
1897 })
1898
1899 paths := multipath.New(tmp)
1900 original, err := loadProfile(main, paths)
1901 require.NoError(t, err)
1902
1903 // Clone the profile
1904 cloned := original.clone()
1905
1906 // Verify the clone is independent
1907 assert.Equal(t, original.SourceFile, cloned.SourceFile)
1908 assert.Len(t, cloned.extensionHierarchy, 1)
1909
1910 // Modify the original
1911 original.extensionHierarchy[0].name = "modified"
1912
1913 // Check that clone wasn't affected
1914 assert.Equal(t, "_base.yaml", cloned.extensionHierarchy[0].name)
1915 }
1916
1917 func TestProfile_SourceTree(t *testing.T) {
1918 tests := map[string]struct {
1919 setup func(t *testing.T, tmp string) string
1920 expected string
1921 }{
1922 "no extensions": {
1923 setup: func(t *testing.T, tmp string) string {
1924 device := filepath.Join(tmp, "device.yaml")
1925 writeYAML(t, device, ddprofiledefinition.ProfileDefinition{
1926 Metrics: []ddprofiledefinition.MetricsConfig{{
1927 Symbol: ddprofiledefinition.SymbolConfig{
1928 OID: "1.3.6.1.2.1.1.1.0",
1929 Name: "sysDescr",
1930 },
1931 }},
1932 })
1933 return device
1934 },
1935 expected: "device",
1936 },
1937 "single extension": {
1938 setup: func(t *testing.T, tmp string) string {
1939 base := filepath.Join(tmp, "_base.yaml")
1940 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{})
1941
1942 device := filepath.Join(tmp, "device.yaml")
1943 writeYAML(t, device, map[string]any{
1944 "extends": []string{"_base.yaml"},
1945 })
1946 return device
1947 },
1948 expected: "device: [_base]",
1949 },
1950 "two direct extensions": {
1951 setup: func(t *testing.T, tmp string) string {
1952 base1 := filepath.Join(tmp, "_base1.yaml")
1953 writeYAML(t, base1, ddprofiledefinition.ProfileDefinition{})
1954
1955 base2 := filepath.Join(tmp, "_base2.yaml")
1956 writeYAML(t, base2, ddprofiledefinition.ProfileDefinition{})
1957
1958 device := filepath.Join(tmp, "device.yaml")
1959 writeYAML(t, device, map[string]any{
1960 "extends": []string{"_base1.yaml", "_base2.yaml"},
1961 })
1962 return device
1963 },
1964 expected: "device: [_base1, _base2]",
1965 },
1966 "nested chain": {
1967 setup: func(t *testing.T, tmp string) string {
1968 base := filepath.Join(tmp, "_base.yaml")
1969 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{})
1970
1971 intermediate := filepath.Join(tmp, "_intermediate.yaml")
1972 writeYAML(t, intermediate, map[string]any{
1973 "extends": []string{"_base.yaml"},
1974 })
1975
1976 device := filepath.Join(tmp, "device.yaml")
1977 writeYAML(t, device, map[string]any{
1978 "extends": []string{"_intermediate.yaml"},
1979 })
1980 return device
1981 },
1982 expected: "device: [_intermediate: [_base]]",
1983 },
1984 "complex diamond pattern": {
1985 setup: func(t *testing.T, tmp string) string {
1986 base := filepath.Join(tmp, "_base.yaml")
1987 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{})
1988
1989 vendor := filepath.Join(tmp, "_vendor.yaml")
1990 writeYAML(t, vendor, map[string]any{
1991 "extends": []string{"_base.yaml"},
1992 })
1993
1994 generic := filepath.Join(tmp, "_generic.yaml")
1995 writeYAML(t, generic, map[string]any{
1996 "extends": []string{"_base.yaml"},
1997 })
1998
1999 device := filepath.Join(tmp, "cisco-nexus.yaml")
2000 writeYAML(t, device, map[string]any{
2001 "extends": []string{"_vendor.yaml", "_generic.yaml"},
2002 })
2003 return device
2004 },
2005 expected: "cisco-nexus: [_vendor: [_base], _generic: [_base]]",
2006 },
2007 "mixed depths": {
2008 setup: func(t *testing.T, tmp string) string {
2009 base := filepath.Join(tmp, "_base.yaml")
2010 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{})
2011
2012 intermediate := filepath.Join(tmp, "_intermediate.yaml")
2013 writeYAML(t, intermediate, map[string]any{
2014 "extends": []string{"_base.yaml"},
2015 })
2016
2017 standalone := filepath.Join(tmp, "_standalone.yaml")
2018 writeYAML(t, standalone, ddprofiledefinition.ProfileDefinition{})
2019
2020 device := filepath.Join(tmp, "device.yaml")
2021 writeYAML(t, device, map[string]any{
2022 "extends": []string{"_intermediate.yaml", "_standalone.yaml"},
2023 })
2024 return device
2025 },
2026 expected: "device: [_intermediate: [_base], _standalone]",
2027 },
2028 "three level nesting": {
2029 setup: func(t *testing.T, tmp string) string {
2030 base := filepath.Join(tmp, "_base.yaml")
2031 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{})
2032
2033 level1 := filepath.Join(tmp, "_level1.yaml")
2034 writeYAML(t, level1, map[string]any{
2035 "extends": []string{"_base.yaml"},
2036 })
2037
2038 level2 := filepath.Join(tmp, "_level2.yaml")
2039 writeYAML(t, level2, map[string]any{
2040 "extends": []string{"_level1.yaml"},
2041 })
2042
2043 device := filepath.Join(tmp, "device.yaml")
2044 writeYAML(t, device, map[string]any{
2045 "extends": []string{"_level2.yaml"},
2046 })
2047 return device
2048 },
2049 expected: "device: [_level2: [_level1: [_base]]]",
2050 },
2051 "yml extension stripped": {
2052 setup: func(t *testing.T, tmp string) string {
2053 base := filepath.Join(tmp, "_base.yml")
2054 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{})
2055
2056 device := filepath.Join(tmp, "device.yml")
2057 writeYAML(t, device, map[string]any{
2058 "extends": []string{"_base.yml"},
2059 })
2060 return device
2061 },
2062 expected: "device: [_base]",
2063 },
2064 "complex real-world example": {
2065 setup: func(t *testing.T, tmp string) string {
2066 base := filepath.Join(tmp, "_base.yaml")
2067 writeYAML(t, base, ddprofiledefinition.ProfileDefinition{})
2068
2069 genericDevice := filepath.Join(tmp, "_generic-device.yaml")
2070 writeYAML(t, genericDevice, map[string]any{
2071 "extends": []string{"_base.yaml"},
2072 })
2073
2074 genericIf := filepath.Join(tmp, "_generic-if.yaml")
2075 writeYAML(t, genericIf, map[string]any{
2076 "extends": []string{"_base.yaml"},
2077 })
2078
2079 cisco := filepath.Join(tmp, "_cisco.yaml")
2080 writeYAML(t, cisco, map[string]any{
2081 "extends": []string{"_generic-device.yaml"},
2082 })
2083
2084 ciscoNexus := filepath.Join(tmp, "cisco-nexus.yaml")
2085 writeYAML(t, ciscoNexus, map[string]any{
2086 "extends": []string{"_cisco.yaml", "_generic-if.yaml"},
2087 })
2088 return ciscoNexus
2089 },
2090 expected: "cisco-nexus: [_cisco: [_generic-device: [_base]], _generic-if: [_base]]",
2091 },
2092 "empty extends list": {
2093 setup: func(t *testing.T, tmp string) string {
2094 device := filepath.Join(tmp, "device.yaml")
2095 writeYAML(t, device, map[string]any{
2096 "extends": []string{},
2097 "metrics": []map[string]any{{
2098 "symbol": map[string]string{
2099 "OID": "1.3.6.1.2.1.1.1.0",
2100 "name": "sysDescr",
2101 },
2102 }},
2103 })
2104 return device
2105 },
2106 expected: "device",
2107 },
2108 }
2109
2110 for name, tc := range tests {
2111 t.Run(name, func(t *testing.T) {
2112 tmp := t.TempDir()
2113 mainFile := tc.setup(t, tmp)
2114
2115 paths := multipath.New(tmp)
2116 prof, err := loadProfile(mainFile, paths)
2117 require.NoError(t, err)
2118
2119 assert.Equal(t, tc.expected, prof.SourceTree())
2120 })
2121 }
2122 }
2123
2124 // getAllExtendedFiles returns a flat list of all files in the extension hierarchy
2125 func (p *Profile) getAllExtendedFiles() []string {
2126 var files []string
2127 seen := make(map[string]bool)
2128
2129 var collect func([]*extensionInfo)
2130 collect = func(extensions []*extensionInfo) {
2131 for _, ext := range extensions {
2132 if !seen[ext.sourceFile] {
2133 seen[ext.sourceFile] = true
2134 files = append(files, ext.sourceFile)
2135 }
2136 collect(ext.extensions)
2137 }
2138 }
2139
2140 collect(p.extensionHierarchy)
2141 return files
2142 }
2143
2144 // getExtensionDepth returns the maximum depth of the extension hierarchy
2145 func (p *Profile) getExtensionDepth() int {
2146 var maxDepth func([]*extensionInfo, int) int
2147 maxDepth = func(extensions []*extensionInfo, depth int) int {
2148 if len(extensions) == 0 {
2149 return depth
2150 }
2151
2152 maximum := depth + 1
2153 for _, ext := range extensions {
2154 d := maxDepth(ext.extensions, depth+1)
2155 if d > maximum {
2156 maximum = d
2157 }
2158 }
2159 return maximum
2160 }
2161
2162 return maxDepth(p.extensionHierarchy, 0)
2163 }
2164
2165 // hasCircularDependency checks if there's a circular dependency in the extension hierarchy
2166 func (p *Profile) hasCircularDependency() bool {
2167 visited := make(map[string]bool)
2168
2169 var hasCircle func([]*extensionInfo) bool
2170 hasCircle = func(extensions []*extensionInfo) bool {
2171 for _, ext := range extensions {
2172 if visited[ext.sourceFile] {
2173 return true
2174 }
2175 visited[ext.sourceFile] = true
2176 if hasCircle(ext.extensions) {
2177 return true
2178 }
2179 delete(visited, ext.sourceFile)
2180 }
2181 return false
2182 }
2183
2184 return hasCircle(p.extensionHierarchy)
2185 }
2186
2187 func writeYAML(t *testing.T, path string, data any) {
2188 t.Helper()
2189
2190 content, err := yaml.Marshal(data)
2191 require.NoError(t, err)
2192 require.NoError(t, os.WriteFile(path, content, 0600))
2193 }