//go:build linux && amd64 package intel_pmt import ( _ "embed" "os" "testing" "time" "github.com/stretchr/testify/require" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/testutil" ) func createTempFile(t *testing.T, dir, pattern string, data []byte) (*os.File, os.FileInfo) { tempFile, err := os.CreateTemp(dir, pattern) if err != nil { t.Fatalf("error creating a temporary file %v: %v", tempFile.Name(), err) } _, err = tempFile.Write(data) if err != nil { t.Fatalf("error writing buffer to file %v: %v", tempFile.Name(), err) } fileInfo, err := tempFile.Stat() if err != nil { t.Fatalf("failed to stat a temporary file %v: %v", tempFile.Name(), err) } return tempFile, fileInfo } func TestTransformEquation(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "No changes", input: "abc", expected: "abc", }, { name: "Remove $ sign", input: "a$b$c", expected: "abc", }, { name: "Decode HTML entities", input: "a&b", expected: "a&b", }, { name: "Remove $ and decode HTML entities", input: "$a&b$c", expected: "a&bc", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { output := transformEquation(tt.input) require.Equal(t, tt.expected, output) }) } } func TestEval(t *testing.T) { tests := []struct { name string eq string params map[string]interface{} expected interface{} err bool }{ { name: "empty equation", eq: "", params: nil, expected: nil, err: true, }, { name: "Valid equation", eq: "2 + 2", params: nil, expected: float64(4), err: false, }, { name: "Valid equation with params, valid params", eq: "a + b", params: map[string]interface{}{ "a": 2, "b": 3, }, expected: float64(5), err: false, }, { name: "Valid equation with params, invalid params", eq: "a + b", params: map[string]interface{}{ "a": 2, // "b" is missing }, expected: nil, err: true, }, { name: "Invalid equation", eq: "2 +", params: nil, expected: nil, err: true, }, { name: "Real equation from PMT - temperature of unused core", eq: "( ( parameter_0 >> 8 ) & 0xff ) + ( ( parameter_0 & 0xff ) / ( 2 ** 8 ) ) - 64", params: map[string]interface{}{ "parameter_0": 0, }, expected: float64(-64), err: false, }, { name: "Real equation from PMT - temperature of working core", eq: "( ( parameter_0 >> 8 ) & 0xff ) + ( ( parameter_0 & 0xff ) / ( 2 ** 8 ) ) - 64", params: map[string]interface{}{ "parameter_0": 23600, }, expected: float64(28.1875), err: false, }, { name: "Badly parsed real equation from PMT - temperature of working core", eq: "( ( parameter_0 >> 8 ) & 0xff ) + ( ( parameter_0 & 0xff ) / ( 2 ** 8 ) ) - 64", params: map[string]interface{}{ "parameter_0": 23600, }, expected: nil, err: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := eval(tt.eq, tt.params) if tt.err { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tt.expected, result) } }) } } func TestGetTelemSample(t *testing.T) { tests := []struct { name string s sample buf []byte offset uint64 expected uint64 err bool }{ { name: "All bits set", s: sample{Msb: 7, Lsb: 0, mask: 255}, buf: []byte{0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, offset: 0, expected: 255, }, { name: "Middle bits set", s: sample{Msb: 5, Lsb: 2, mask: 60}, buf: []byte{0x3c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x3c = 00111100 in binary offset: 0, expected: 15, }, { name: "Non-zero offset", s: sample{Msb: 7, Lsb: 0, mask: 255}, buf: []byte{0x00, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, offset: 3, expected: 255, }, { name: "Single bit set", s: sample{Msb: 4, Lsb: 4, mask: 16}, buf: []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x10 = 00010000 in binary offset: 0, expected: 1, }, { name: "Two bytes set", s: sample{Msb: 14, Lsb: 0, mask: 32767}, buf: []byte{0x30, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 0x5c30 = 23600 in decimal offset: 0, expected: 23600, }, { name: "Offset larger than buffer size", s: sample{Msb: 7, Lsb: 0, mask: 255}, buf: []byte{0x00}, offset: 5, expected: 0, err: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := getTelemSample(tt.s, tt.buf, tt.offset) if tt.err { require.Error(t, err) } else { require.NoError(t, err) require.Equal(t, tt.expected, result) } }) } } func TestInit(t *testing.T) { t.Run("No PmtSpec", func(t *testing.T) { p := &IntelPMT{ PmtSpec: "", } err := p.Init() require.ErrorContains(t, err, "pmt spec is empty") }) t.Run("Incorrect filepath PmtSpec", func(t *testing.T) { p := &IntelPMT{ PmtSpec: "/this/path/doesntexist", } err := p.Init() require.ErrorContains(t, err, "provided pmt spec is not readable") }) t.Run("Incorrect PmtSpec, random letters", func(t *testing.T) { p := &IntelPMT{ PmtSpec: "loremipsum", } err := p.Init() require.ErrorContains(t, err, "provided pmt spec is not readable") }) t.Run("Correct filepath PmtSpec, no pmt/can't read pmt in sysfs", func(t *testing.T) { tmp := t.TempDir() testFile, _ := createTempFile(t, tmp, "test-file", []byte("")) defer testFile.Close() p := &IntelPMT{ PmtSpec: testFile.Name(), Log: testutil.Logger{}, } err := p.Init() require.ErrorContains(t, err, "error while exploring pmt sysfs") }) } func TestGather(t *testing.T) { type fields struct { PmtSpec string Log telegraf.Logger pmtTelemetryFiles map[string]pmtFileInfo pmtAggregator map[string]aggregator pmtAggregatorInterface map[string]aggregatorInterface pmtTransformations map[string]map[string]transformation } type testFile struct { guid string content []byte numaNode string pciBdf string } tests := []struct { name string fields fields files []testFile expected []telegraf.Metric wantErr bool }{ { name: "Incorrect gather, results map has no value for sample", fields: fields{ pmtAggregator: map[string]aggregator{ "test-guid": { SampleGroup: []sampleGroup{ { SampleID: uint64(0), Sample: []sample{ { DatatypeIDRef: "test-datatype", Msb: 4, Lsb: 4, mask: 16, SampleID: "test-sample-ref", }, }, }, }, }, }, pmtAggregatorInterface: map[string]aggregatorInterface{ "test-guid": { AggregatorSamples: aggregatorSamples{ AggregatorSample: []aggregatorSample{ { SampleName: "test-sample", SampleGroup: "test-group", DatatypeIDRef: "test-datatype", TransformREF: "test-transform-ref", TransformInputs: transformInputs{ TransformInput: []transformInput{ { VarName: "testvar", // missing sampleIDREF }, }, }, }, }, }, }, }, }, files: []testFile{ {guid: "test-guid", content: []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, numaNode: "0"}, }, wantErr: true, }, { name: "Failed Gather, no equation for gathered sample", fields: fields{ pmtAggregatorInterface: map[string]aggregatorInterface{ "test-guid": { AggregatorSamples: aggregatorSamples{ AggregatorSample: []aggregatorSample{ {SampleName: "test-sample"}, }, }, }, }, }, files: []testFile{ {guid: "test-guid"}, }, wantErr: true, }, { name: "Correct gather, 2 guids, 2 metrics returned", fields: fields{ pmtAggregator: map[string]aggregator{ "test-guid": { SampleGroup: []sampleGroup{ { SampleID: uint64(0), Sample: []sample{ { DatatypeIDRef: "test-datatype", Msb: 4, Lsb: 4, mask: 16, SampleID: "test-sample-ref", }, }, }, }, }, "test-guid2": { SampleGroup: []sampleGroup{ { SampleID: uint64(0), Sample: []sample{ { DatatypeIDRef: "test-datatype2", Msb: 14, Lsb: 0, mask: 32767, SampleID: "test-sample-ref2", }, }, }, }, }, }, pmtAggregatorInterface: map[string]aggregatorInterface{ "test-guid": { AggregatorSamples: aggregatorSamples{ AggregatorSample: []aggregatorSample{ { SampleName: "test-sample", SampleGroup: "test-group", DatatypeIDRef: "test-datatype", TransformREF: "test-transform-ref", TransformInputs: transformInputs{ TransformInput: []transformInput{ { VarName: "testvar", SampleIDREF: "test-sample-ref", }, }, }, }, }, }, }, "test-guid2": { AggregatorSamples: aggregatorSamples{ AggregatorSample: []aggregatorSample{ { SampleName: "test-sample2", SampleGroup: "test-group2", DatatypeIDRef: "test-datatype2", TransformREF: "test-transform-ref2", TransformInputs: transformInputs{ TransformInput: []transformInput{ { VarName: "testv", SampleIDREF: "test-sample-ref2", }, }, }, }, }, }, }, }, pmtTransformations: map[string]map[string]transformation{ "test-guid": { "test-transform-ref": { Transform: "testvar + 2", }, }, "test-guid2": { "test-transform-ref2": { Transform: "( ( $testv >> 8 ) & 0xff ) + ( ( $testv & 0xff ) / ( 2 ** 8 ) ) - 64", }, }, }, }, expected: []telegraf.Metric{ testutil.MustMetric( "intel_pmt", map[string]string{ "guid": "test-guid", "numa_node": "0", "pci_bdf": "0000:00:0a.0", "sample_name": "test-sample", "sample_group": "test-group", "datatype_idref": "test-datatype", }, map[string]interface{}{ // 1 from buffer, 2 from equation "value": float64(3), }, time.Time{}, ), testutil.MustMetric( "intel_pmt", map[string]string{ "guid": "test-guid2", "numa_node": "1", "pci_bdf": "0001:00:0a.0", "sample_name": "test-sample2", "sample_group": "test-group2", "datatype_idref": "test-datatype2", }, map[string]interface{}{ "value": float64(28.1875), }, time.Time{}, ), }, files: []testFile{ {guid: "test-guid", content: []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, numaNode: "0", pciBdf: "0000:00:0a.0"}, {guid: "test-guid2", content: []byte{0x30, 0x5c, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, numaNode: "1", pciBdf: "0001:00:0a.0"}, }, wantErr: false, }, { name: "Correct gather, 1 value returned", fields: fields{ pmtAggregator: map[string]aggregator{ "test-guid": { SampleGroup: []sampleGroup{ { SampleID: uint64(0), Sample: []sample{ { DatatypeIDRef: "test-datatype", Msb: 4, Lsb: 4, mask: 16, SampleID: "test-sample-ref", }, }, }, }, }, }, pmtAggregatorInterface: map[string]aggregatorInterface{ "test-guid": { AggregatorSamples: aggregatorSamples{ AggregatorSample: []aggregatorSample{ { SampleName: "test-sample", SampleGroup: "test-group", DatatypeIDRef: "test-datatype", TransformREF: "test-transform-ref", TransformInputs: transformInputs{ TransformInput: []transformInput{ { VarName: "testvar", SampleIDREF: "test-sample-ref", }, }, }, }, }, }, }, }, pmtTransformations: map[string]map[string]transformation{ "test-guid": { "test-transform-ref": { Transform: "testvar + 2", }, }, }, }, expected: []telegraf.Metric{ testutil.MustMetric( "intel_pmt", map[string]string{ "guid": "test-guid", "numa_node": "0", "pci_bdf": "0000:00:0a.0", "sample_name": "test-sample", "sample_group": "test-group", "datatype_idref": "test-datatype", }, map[string]interface{}{ // 1 from buffer, 2 from equation "value": float64(3), }, time.Time{}, ), }, files: []testFile{ {guid: "test-guid", content: []byte{0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, numaNode: "0", pciBdf: "0000:00:0a.0"}, }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &IntelPMT{ PmtSpec: tt.fields.PmtSpec, Log: testutil.Logger{}, pmtAggregator: tt.fields.pmtAggregator, pmtTelemetryFiles: tt.fields.pmtTelemetryFiles, pmtAggregatorInterface: tt.fields.pmtAggregatorInterface, pmtTransformations: tt.fields.pmtTransformations, } var acc testutil.Accumulator telemetryFiles := make(map[string]pmtFileInfo) tmp := t.TempDir() for _, file := range tt.files { testFile, _ := createTempFile(t, tmp, "test-file", file.content) telemetryFiles[file.guid] = append(telemetryFiles[file.guid], fileInfo{ path: testFile.Name(), numaNode: file.numaNode, pciBdf: file.pciBdf, }) } p.pmtTelemetryFiles = telemetryFiles if tt.wantErr { require.Error(t, acc.GatherError(p.Gather)) } else { require.NoError(t, acc.GatherError(p.Gather)) testutil.RequireMetricsEqual(t, tt.expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime(), testutil.SortMetrics()) } }) } }