diff --git a/internal/report/table_helpers_cache.go b/internal/report/table_helpers_cache.go index 16f3bf5c..65cb032a 100644 --- a/internal/report/table_helpers_cache.go +++ b/internal/report/table_helpers_cache.go @@ -186,6 +186,19 @@ type lscpuCacheEntry struct { CoherencySize int `json:"coherency-size"` } +// for older lscpu versions that return numbers as strings +type lscpuCacheEntryLegacy struct { + Name string `json:"name"` + OneSize string `json:"one-size"` + AllSize string `json:"all-size"` + Ways string `json:"ways"` + Type string `json:"type"` + Level string `json:"level"` + Sets string `json:"sets"` + PhyLine string `json:"phy-line"` + CoherencySize string `json:"coherency-size"` +} + // parseLscpuCacheOutput parses the output of the `lscpu -C -J` command to extract cache information. // lscpu returns JSON output with cache details, which this function processes to create a map. // Example: @@ -242,13 +255,63 @@ func parseLscpuCacheOutput(LscpuCacheOutput string) (map[string]lscpuCacheEntry, return nil, fmt.Errorf("lscpu cache output is empty") } output := make(map[string]lscpuCacheEntry) + + // Try modern format first (numbers as integers) parsed := make(map[string][]lscpuCacheEntry) err := json.Unmarshal([]byte(LscpuCacheOutput), &parsed) + if err == nil { + // Modern format worked + for _, entry := range parsed["caches"] { + output[entry.Name] = entry + } + return output, nil + } + + // Fall back to legacy format (numbers as strings) + slog.Debug("Failed to parse lscpu cache output with modern format, trying legacy format", slog.String("error", err.Error())) + parsedLegacy := make(map[string][]lscpuCacheEntryLegacy) + err = json.Unmarshal([]byte(LscpuCacheOutput), &parsedLegacy) if err != nil { - slog.Error("Failed to parse lscpu cache JSON output", slog.String("error", err.Error())) + slog.Error("Failed to parse lscpu cache JSON output with both modern and legacy formats", slog.String("error", err.Error())) return nil, err } - for _, entry := range parsed["caches"] { + + // Convert legacy entries to modern format + for _, legacyEntry := range parsedLegacy["caches"] { + entry := lscpuCacheEntry{ + Name: legacyEntry.Name, + OneSize: legacyEntry.OneSize, + AllSize: legacyEntry.AllSize, + Type: legacyEntry.Type, + } + + // Convert string fields to integers + if legacyEntry.Ways != "" { + if ways, err := strconv.Atoi(legacyEntry.Ways); err == nil { + entry.Ways = ways + } + } + if legacyEntry.Level != "" { + if level, err := strconv.Atoi(legacyEntry.Level); err == nil { + entry.Level = level + } + } + if legacyEntry.Sets != "" { + if sets, err := strconv.Atoi(legacyEntry.Sets); err == nil { + entry.Sets = sets + } + } + if legacyEntry.PhyLine != "" { + if phyLine, err := strconv.Atoi(legacyEntry.PhyLine); err == nil { + entry.PhyLine = phyLine + } + } + if legacyEntry.CoherencySize != "" { + if coherencySize, err := strconv.Atoi(legacyEntry.CoherencySize); err == nil { + entry.CoherencySize = coherencySize + } + } + output[entry.Name] = entry } return output, nil diff --git a/internal/report/table_helpers_cache_test.go b/internal/report/table_helpers_cache_test.go index 0a169048..95c30451 100644 --- a/internal/report/table_helpers_cache_test.go +++ b/internal/report/table_helpers_cache_test.go @@ -5,6 +5,9 @@ package report import ( "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestParseCacheSizeToMB(t *testing.T) { @@ -42,3 +45,196 @@ func TestParseCacheSizeToMB(t *testing.T) { } } } + +func TestParseLscpuCacheOutput(t *testing.T) { + tests := []struct { + name string + input string + expectedError bool + expectedLength int + expectedL3 lscpuCacheEntry + }{ + { + name: "Modern format with integer fields", + input: `{ + "caches": [ + { + "name": "L1d", + "one-size": "48K", + "all-size": "6M", + "ways": 12, + "type": "Data", + "level": 1, + "sets": 64, + "phy-line": 1, + "coherency-size": 64 + }, + { + "name": "L3", + "one-size": "320M", + "all-size": "640M", + "ways": 20, + "type": "Unified", + "level": 3, + "sets": 262144, + "phy-line": 1, + "coherency-size": 64 + } + ] +}`, + expectedError: false, + expectedLength: 2, + expectedL3: lscpuCacheEntry{ + Name: "L3", + OneSize: "320M", + AllSize: "640M", + Ways: 20, + Type: "Unified", + Level: 3, + Sets: 262144, + PhyLine: 1, + CoherencySize: 64, + }, + }, + { + name: "Legacy format with string fields", + input: `{ + "caches": [ + { + "name": "L1d", + "one-size": "48K", + "all-size": "6M", + "ways": "12", + "type": "Data", + "level": "1", + "sets": "64", + "phy-line": "1", + "coherency-size": "64" + }, + { + "name": "L3", + "one-size": "320M", + "all-size": "640M", + "ways": "20", + "type": "Unified", + "level": "3", + "sets": "262144", + "phy-line": "1", + "coherency-size": "64" + } + ] +}`, + expectedError: false, + expectedLength: 2, + expectedL3: lscpuCacheEntry{ + Name: "L3", + OneSize: "320M", + AllSize: "640M", + Ways: 20, + Type: "Unified", + Level: 3, + Sets: 262144, + PhyLine: 1, + CoherencySize: 64, + }, + }, + { + name: "Legacy format with some empty string fields", + input: `{ + "caches": [ + { + "name": "L3", + "one-size": "320M", + "all-size": "640M", + "ways": "20", + "type": "Unified", + "level": "3", + "sets": "", + "phy-line": "", + "coherency-size": "64" + } + ] +}`, + expectedError: false, + expectedLength: 1, + expectedL3: lscpuCacheEntry{ + Name: "L3", + OneSize: "320M", + AllSize: "640M", + Ways: 20, + Type: "Unified", + Level: 3, + Sets: 0, // empty string converts to 0 + PhyLine: 0, // empty string converts to 0 + CoherencySize: 64, + }, + }, + { + name: "Legacy format with invalid number strings", + input: `{ + "caches": [ + { + "name": "L3", + "one-size": "320M", + "all-size": "640M", + "ways": "invalid", + "type": "Unified", + "level": "3", + "sets": "not_a_number", + "phy-line": "1", + "coherency-size": "64" + } + ] +}`, + expectedError: false, + expectedLength: 1, + expectedL3: lscpuCacheEntry{ + Name: "L3", + OneSize: "320M", + AllSize: "640M", + Ways: 0, // invalid string converts to 0 + Type: "Unified", + Level: 3, + Sets: 0, // invalid string converts to 0 + PhyLine: 1, + CoherencySize: 64, + }, + }, + { + name: "Empty input", + input: "", + expectedError: true, + }, + { + name: "Invalid JSON", + input: `{"caches": [invalid json}`, + expectedError: true, + }, + { + name: "Empty caches array", + input: `{"caches": []}`, + expectedError: false, + expectedLength: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseLscpuCacheOutput(tt.input) + + if tt.expectedError { + require.Error(t, err) + assert.Nil(t, result) + } else { + require.NoError(t, err) + assert.Len(t, result, tt.expectedLength) + + if tt.expectedLength > 0 && tt.expectedL3.Name != "" { + l3Cache, exists := result["L3"] + require.True(t, exists, "L3 cache should exist in result") + assert.Equal(t, tt.expectedL3, l3Cache) + } + } + }) + } +}