Per machine, like every other page. Four tiles — processor, memory, storage, graphics — over a card for each. Memory is the part that had nowhere to live before: every slot, with the module's size, type, form factor, rated and running speed, maker, part number, serial, rank and voltage, and the empty slots marked empty. A module whose maker is only a JEDEC id gets a name when it is one we know and keeps the number when it is not, because a wrong name is worse than a code. Everything is read from what Debian already has: dmidecode for the parts only the firmware knows, lscpu, lspci, sysfs. None of it changes under a running kernel, so it is read once and kept for ten minutes; Re-read is for the one thing that does change, a drive plugged in. Every reader is allowed to come back empty. A machine with no dmidecode, no root to read it with, or no PCI bus at all still gets a page, with a note saying what was missing and why. Checked against the Mac mini, which is all three cases at once: no SMBIOS memory table, a GPU only the device tree knows about, and cores that are not all alike — lscpu counts one cluster as "cores per socket" there, so without SMT the thread count is the honest answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
package hardware
|
|
|
|
import "testing"
|
|
|
|
func TestDMISize(t *testing.T) {
|
|
for in, want := range map[string]uint64{
|
|
"8 GB": 8 << 30, "16384 MB": 16384 << 20, "512 kB": 512 << 10, "1 TB": 1 << 40,
|
|
"No Module Installed": 0, "": 0, "8 GiB": 0,
|
|
} {
|
|
if got := dmiSize(in); got != want {
|
|
t.Errorf("dmiSize(%q) = %d, want %d", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMakerName(t *testing.T) {
|
|
cases := []struct{ v, id, want string }{
|
|
{"Samsung", "", "Samsung"}, // the module says who it is
|
|
{"0x0B45", "Bank 12, Hex 0x45", "Wilk Elektronik"}, // only a JEDEC id, and one we know
|
|
{"0xFFFF", "", "0xFFFF"}, // one we don't: the number, not a guess
|
|
{"Unknown", "", ""},
|
|
{"", "0xCE00", "Samsung"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := makerName(c.v, c.id); got != c.want {
|
|
t.Errorf("makerName(%q, %q) = %q, want %q", c.v, c.id, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestStripIDAndDisplayClass(t *testing.T) {
|
|
if got := stripID("Alder Lake-N [UHD Graphics] [46d1]"); got != "Alder Lake-N [UHD Graphics]" {
|
|
t.Errorf("stripID = %q", got)
|
|
}
|
|
if got := stripID("no brackets here"); got != "no brackets here" {
|
|
t.Errorf("stripID left a plain name alone wrong: %q", got)
|
|
}
|
|
if displayClass(`VGA compatible controller [0300]`) != "VGA" {
|
|
t.Error("a VGA controller should be graphics")
|
|
}
|
|
if displayClass(`Ethernet controller [0200]`) != "" {
|
|
t.Error("a network card is not graphics")
|
|
}
|
|
}
|
|
|
|
func TestLeadingIntAndJoin(t *testing.T) {
|
|
if leadingInt("3200 MT/s") != 3200 || leadingInt("") != 0 || leadingInt("Unknown") != 0 {
|
|
t.Error("leadingInt")
|
|
}
|
|
if got := join("GMKtec", "GMKtec NucBox G3"); got != "GMKtec NucBox G3" {
|
|
t.Errorf("a maker the product already names should not be said twice: %q", got)
|
|
}
|
|
if got := join("Dell Inc.", "OptiPlex 7040"); got != "Dell Inc. OptiPlex 7040" {
|
|
t.Errorf("join = %q", got)
|
|
}
|
|
if got := join("To Be Filled By O.E.M.", "System Product Name"); got != "" {
|
|
t.Errorf("firmware boilerplate is not a model: %q", got)
|
|
}
|
|
}
|
|
|
|
const sample = `# dmidecode 3.6
|
|
Handle 0x0028, DMI type 17, 92 bytes
|
|
Memory Device
|
|
Array Handle: 0x0027
|
|
Size: 8 GB
|
|
Form Factor: SODIMM
|
|
Locator: Controller0-ChannelA-DIMM0
|
|
Type: DDR4
|
|
Speed: 3200 MT/s
|
|
Manufacturer: 0x0B45
|
|
Part Number: WPBH32D408SWD-8G
|
|
Configured Memory Speed: 2933 MT/s
|
|
Rank: 1
|
|
|
|
Handle 0x0029, DMI type 17, 92 bytes
|
|
Memory Device
|
|
Size: No Module Installed
|
|
Locator: Controller1-ChannelA-DIMM0
|
|
Type: Unknown
|
|
`
|
|
|
|
// parseRecords is what readDMI does once dmidecode has answered; this exercises it
|
|
// without needing root or the tool.
|
|
func TestMemoryFromRecords(t *testing.T) {
|
|
recs := parse(sample)
|
|
m := memory(recs)
|
|
if m.Slots != 2 || m.Filled != 1 {
|
|
t.Fatalf("slots %d filled %d, want 2 and 1", m.Slots, m.Filled)
|
|
}
|
|
mod := m.Modules[0]
|
|
if mod.Size != 8<<30 || mod.Type != "DDR4" || mod.Maker != "Wilk Elektronik" || mod.Part != "WPBH32D408SWD-8G" {
|
|
t.Errorf("first module read wrong: %+v", mod)
|
|
}
|
|
if mod.SpeedMTs != 3200 || mod.RunningMTs != 2933 {
|
|
t.Errorf("a module running below its rating should say both: %d / %d", mod.RunningMTs, mod.SpeedMTs)
|
|
}
|
|
if !m.Modules[1].Empty || m.Modules[1].Locator != "Controller1-ChannelA-DIMM0" {
|
|
t.Errorf("empty slot read wrong: %+v", m.Modules[1])
|
|
}
|
|
if m.Installed != 8<<30 {
|
|
t.Errorf("installed = %d", m.Installed)
|
|
}
|
|
}
|