Skip to content

Commit 8a131ba

Browse files
authored
fix lscpu parsing for older versions of lscpu (#533)
Signed-off-by: Harper, Jason M <[email protected]>
1 parent c0aaefd commit 8a131ba

File tree

2 files changed

+261
-2
lines changed

2 files changed

+261
-2
lines changed

internal/report/table_helpers_cache.go

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,19 @@ type lscpuCacheEntry struct {
186186
CoherencySize int `json:"coherency-size"`
187187
}
188188

189+
// for older lscpu versions that return numbers as strings
190+
type lscpuCacheEntryLegacy struct {
191+
Name string `json:"name"`
192+
OneSize string `json:"one-size"`
193+
AllSize string `json:"all-size"`
194+
Ways string `json:"ways"`
195+
Type string `json:"type"`
196+
Level string `json:"level"`
197+
Sets string `json:"sets"`
198+
PhyLine string `json:"phy-line"`
199+
CoherencySize string `json:"coherency-size"`
200+
}
201+
189202
// parseLscpuCacheOutput parses the output of the `lscpu -C -J` command to extract cache information.
190203
// lscpu returns JSON output with cache details, which this function processes to create a map.
191204
// Example:
@@ -242,13 +255,63 @@ func parseLscpuCacheOutput(LscpuCacheOutput string) (map[string]lscpuCacheEntry,
242255
return nil, fmt.Errorf("lscpu cache output is empty")
243256
}
244257
output := make(map[string]lscpuCacheEntry)
258+
259+
// Try modern format first (numbers as integers)
245260
parsed := make(map[string][]lscpuCacheEntry)
246261
err := json.Unmarshal([]byte(LscpuCacheOutput), &parsed)
262+
if err == nil {
263+
// Modern format worked
264+
for _, entry := range parsed["caches"] {
265+
output[entry.Name] = entry
266+
}
267+
return output, nil
268+
}
269+
270+
// Fall back to legacy format (numbers as strings)
271+
slog.Debug("Failed to parse lscpu cache output with modern format, trying legacy format", slog.String("error", err.Error()))
272+
parsedLegacy := make(map[string][]lscpuCacheEntryLegacy)
273+
err = json.Unmarshal([]byte(LscpuCacheOutput), &parsedLegacy)
247274
if err != nil {
248-
slog.Error("Failed to parse lscpu cache JSON output", slog.String("error", err.Error()))
275+
slog.Error("Failed to parse lscpu cache JSON output with both modern and legacy formats", slog.String("error", err.Error()))
249276
return nil, err
250277
}
251-
for _, entry := range parsed["caches"] {
278+
279+
// Convert legacy entries to modern format
280+
for _, legacyEntry := range parsedLegacy["caches"] {
281+
entry := lscpuCacheEntry{
282+
Name: legacyEntry.Name,
283+
OneSize: legacyEntry.OneSize,
284+
AllSize: legacyEntry.AllSize,
285+
Type: legacyEntry.Type,
286+
}
287+
288+
// Convert string fields to integers
289+
if legacyEntry.Ways != "" {
290+
if ways, err := strconv.Atoi(legacyEntry.Ways); err == nil {
291+
entry.Ways = ways
292+
}
293+
}
294+
if legacyEntry.Level != "" {
295+
if level, err := strconv.Atoi(legacyEntry.Level); err == nil {
296+
entry.Level = level
297+
}
298+
}
299+
if legacyEntry.Sets != "" {
300+
if sets, err := strconv.Atoi(legacyEntry.Sets); err == nil {
301+
entry.Sets = sets
302+
}
303+
}
304+
if legacyEntry.PhyLine != "" {
305+
if phyLine, err := strconv.Atoi(legacyEntry.PhyLine); err == nil {
306+
entry.PhyLine = phyLine
307+
}
308+
}
309+
if legacyEntry.CoherencySize != "" {
310+
if coherencySize, err := strconv.Atoi(legacyEntry.CoherencySize); err == nil {
311+
entry.CoherencySize = coherencySize
312+
}
313+
}
314+
252315
output[entry.Name] = entry
253316
}
254317
return output, nil

internal/report/table_helpers_cache_test.go

Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ package report
55

66
import (
77
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
811
)
912

1013
func TestParseCacheSizeToMB(t *testing.T) {
@@ -42,3 +45,196 @@ func TestParseCacheSizeToMB(t *testing.T) {
4245
}
4346
}
4447
}
48+
49+
func TestParseLscpuCacheOutput(t *testing.T) {
50+
tests := []struct {
51+
name string
52+
input string
53+
expectedError bool
54+
expectedLength int
55+
expectedL3 lscpuCacheEntry
56+
}{
57+
{
58+
name: "Modern format with integer fields",
59+
input: `{
60+
"caches": [
61+
{
62+
"name": "L1d",
63+
"one-size": "48K",
64+
"all-size": "6M",
65+
"ways": 12,
66+
"type": "Data",
67+
"level": 1,
68+
"sets": 64,
69+
"phy-line": 1,
70+
"coherency-size": 64
71+
},
72+
{
73+
"name": "L3",
74+
"one-size": "320M",
75+
"all-size": "640M",
76+
"ways": 20,
77+
"type": "Unified",
78+
"level": 3,
79+
"sets": 262144,
80+
"phy-line": 1,
81+
"coherency-size": 64
82+
}
83+
]
84+
}`,
85+
expectedError: false,
86+
expectedLength: 2,
87+
expectedL3: lscpuCacheEntry{
88+
Name: "L3",
89+
OneSize: "320M",
90+
AllSize: "640M",
91+
Ways: 20,
92+
Type: "Unified",
93+
Level: 3,
94+
Sets: 262144,
95+
PhyLine: 1,
96+
CoherencySize: 64,
97+
},
98+
},
99+
{
100+
name: "Legacy format with string fields",
101+
input: `{
102+
"caches": [
103+
{
104+
"name": "L1d",
105+
"one-size": "48K",
106+
"all-size": "6M",
107+
"ways": "12",
108+
"type": "Data",
109+
"level": "1",
110+
"sets": "64",
111+
"phy-line": "1",
112+
"coherency-size": "64"
113+
},
114+
{
115+
"name": "L3",
116+
"one-size": "320M",
117+
"all-size": "640M",
118+
"ways": "20",
119+
"type": "Unified",
120+
"level": "3",
121+
"sets": "262144",
122+
"phy-line": "1",
123+
"coherency-size": "64"
124+
}
125+
]
126+
}`,
127+
expectedError: false,
128+
expectedLength: 2,
129+
expectedL3: lscpuCacheEntry{
130+
Name: "L3",
131+
OneSize: "320M",
132+
AllSize: "640M",
133+
Ways: 20,
134+
Type: "Unified",
135+
Level: 3,
136+
Sets: 262144,
137+
PhyLine: 1,
138+
CoherencySize: 64,
139+
},
140+
},
141+
{
142+
name: "Legacy format with some empty string fields",
143+
input: `{
144+
"caches": [
145+
{
146+
"name": "L3",
147+
"one-size": "320M",
148+
"all-size": "640M",
149+
"ways": "20",
150+
"type": "Unified",
151+
"level": "3",
152+
"sets": "",
153+
"phy-line": "",
154+
"coherency-size": "64"
155+
}
156+
]
157+
}`,
158+
expectedError: false,
159+
expectedLength: 1,
160+
expectedL3: lscpuCacheEntry{
161+
Name: "L3",
162+
OneSize: "320M",
163+
AllSize: "640M",
164+
Ways: 20,
165+
Type: "Unified",
166+
Level: 3,
167+
Sets: 0, // empty string converts to 0
168+
PhyLine: 0, // empty string converts to 0
169+
CoherencySize: 64,
170+
},
171+
},
172+
{
173+
name: "Legacy format with invalid number strings",
174+
input: `{
175+
"caches": [
176+
{
177+
"name": "L3",
178+
"one-size": "320M",
179+
"all-size": "640M",
180+
"ways": "invalid",
181+
"type": "Unified",
182+
"level": "3",
183+
"sets": "not_a_number",
184+
"phy-line": "1",
185+
"coherency-size": "64"
186+
}
187+
]
188+
}`,
189+
expectedError: false,
190+
expectedLength: 1,
191+
expectedL3: lscpuCacheEntry{
192+
Name: "L3",
193+
OneSize: "320M",
194+
AllSize: "640M",
195+
Ways: 0, // invalid string converts to 0
196+
Type: "Unified",
197+
Level: 3,
198+
Sets: 0, // invalid string converts to 0
199+
PhyLine: 1,
200+
CoherencySize: 64,
201+
},
202+
},
203+
{
204+
name: "Empty input",
205+
input: "",
206+
expectedError: true,
207+
},
208+
{
209+
name: "Invalid JSON",
210+
input: `{"caches": [invalid json}`,
211+
expectedError: true,
212+
},
213+
{
214+
name: "Empty caches array",
215+
input: `{"caches": []}`,
216+
expectedError: false,
217+
expectedLength: 0,
218+
},
219+
}
220+
221+
for _, tt := range tests {
222+
t.Run(tt.name, func(t *testing.T) {
223+
result, err := parseLscpuCacheOutput(tt.input)
224+
225+
if tt.expectedError {
226+
require.Error(t, err)
227+
assert.Nil(t, result)
228+
} else {
229+
require.NoError(t, err)
230+
assert.Len(t, result, tt.expectedLength)
231+
232+
if tt.expectedLength > 0 && tt.expectedL3.Name != "" {
233+
l3Cache, exists := result["L3"]
234+
require.True(t, exists, "L3 cache should exist in result")
235+
assert.Equal(t, tt.expectedL3, l3Cache)
236+
}
237+
}
238+
})
239+
}
240+
}

0 commit comments

Comments
 (0)