m/pkg/pstore: add package to interface with pstore
This adds a package for interfacing with the Linux kernel's pstore
(persistent storage) system. Currently only handles kmsg/dmesg-type logs
as mce has an unknown format and I have no examples.
Change-Id: I3089a53cdca224c7e6e04dd51a94035d7b2b880b
Reviewed-on: https://review.monogon.dev/c/monogon/+/769
Reviewed-by: Sergiusz Bazanski <serge@monogon.tech>
Tested-by: Jenkins CI
diff --git a/metropolis/pkg/pstore/pstore_test.go b/metropolis/pkg/pstore/pstore_test.go
new file mode 100644
index 0000000..0190f04
--- /dev/null
+++ b/metropolis/pkg/pstore/pstore_test.go
@@ -0,0 +1,144 @@
+package pstore
+
+import (
+ "fmt"
+ "testing"
+ "testing/fstest"
+ "time"
+)
+
+func TestParseHeader(t *testing.T) {
+ var cases = []struct {
+ input string
+ expectedOut *pstoreDmesgHeader
+ }{
+ {"Panic#2 Part30", &pstoreDmesgHeader{"Panic", 2, 30}},
+ {"Oops#1 Part5", &pstoreDmesgHeader{"Oops", 1, 5}},
+ // Random kernel output that is similar, but definitely not a dump header
+ {"<4>[2501503.489317] Oops: 0010 [#1] SMP NOPTI", nil},
+ }
+ for i, c := range cases {
+ t.Run(fmt.Sprintf("Test#%d", i+1), func(t *testing.T) {
+ out, err := parseDmesgHeader(c.input)
+ switch {
+ case err != nil && c.expectedOut != nil:
+ t.Errorf("Failed parsing %q: %v", c.input, err)
+ case err == nil && c.expectedOut == nil:
+ t.Errorf("Successfully parsed %q, expected error", c.input)
+ case err != nil && c.expectedOut == nil:
+ case err == nil && c.expectedOut != nil:
+ if out.Part != c.expectedOut.Part {
+ t.Errorf("Expected part to be %d, got %d", c.expectedOut.Part, out.Part)
+ }
+ if out.Counter != c.expectedOut.Counter {
+ t.Errorf("Expected counter to be %d, got %d", c.expectedOut.Counter, out.Counter)
+ }
+ if out.Reason != c.expectedOut.Reason {
+ t.Errorf("Expected reason to be %q, got %q", c.expectedOut.Reason, out.Reason)
+ }
+ }
+ })
+ }
+}
+
+func TestGetKmsgDumps(t *testing.T) {
+ testTime1 := time.Date(2022, 06, 13, 1, 2, 3, 4, time.UTC)
+ testTime2 := time.Date(2020, 06, 13, 1, 2, 3, 4, time.UTC)
+ testTime3 := time.Date(2010, 06, 13, 1, 2, 3, 4, time.UTC)
+ cases := []struct {
+ name string
+ inputFS fstest.MapFS
+ expectErr bool
+ expectedDumps []KmsgDump
+ }{
+ {"EmptyPstore", map[string]*fstest.MapFile{}, false, []KmsgDump{}},
+ {"SingleDumpSingleFile", map[string]*fstest.MapFile{
+ "dmesg-efi-165467917816002": {ModTime: testTime1, Data: []byte("Panic#2 Part1\ntest1\ntest2")},
+ "yolo-efi-165467917816002": {ModTime: testTime1, Data: []byte("something totally unrelated")},
+ }, false, []KmsgDump{{
+ Reason: "Panic",
+ OccurredAt: testTime1,
+ Counter: 2,
+ Lines: []string{
+ "test1",
+ "test2",
+ },
+ }}},
+ {"SingleDumpMultipleFiles", map[string]*fstest.MapFile{
+ "dmesg-efi-165467917816002": {ModTime: testTime1, Data: []byte("Panic#2 Part1\ntest2\ntest3")},
+ "dmesg-efi-165467917817002": {ModTime: testTime2, Data: []byte("Panic#2 Part2\ntest1")},
+ }, false, []KmsgDump{{
+ Reason: "Panic",
+ OccurredAt: testTime1,
+ Counter: 2,
+ Lines: []string{
+ "test1",
+ "test2",
+ "test3",
+ },
+ }}},
+ {"MultipleDumpsMultipleFiles", map[string]*fstest.MapFile{
+ "dmesg-efi-165467917816002": {ModTime: testTime1, Data: []byte("Panic#2 Part1\ntest2\ntest3")},
+ "dmesg-efi-165467917817002": {ModTime: testTime2, Data: []byte("Panic#2 Part2\ntest1")},
+ "dmesg-efi-265467917816002": {ModTime: testTime3, Data: []byte("Oops#1 Part1\noops3")},
+ "dmesg-efi-265467917817002": {ModTime: testTime2, Data: []byte("Oops#1 Part2\noops1\noops2")},
+ }, false, []KmsgDump{{
+ Reason: "Panic",
+ OccurredAt: testTime1,
+ Counter: 2,
+ Lines: []string{
+ "test1",
+ "test2",
+ "test3",
+ },
+ }, {
+ Reason: "Oops",
+ OccurredAt: testTime3,
+ Counter: 1,
+ Lines: []string{
+ "oops1",
+ "oops2",
+ "oops3",
+ },
+ }}},
+ }
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ dumps, err := getKmsgDumpsFromFS(c.inputFS)
+ switch {
+ case err == nil && c.expectErr:
+ t.Error("Expected error, but got none")
+ return
+ case err != nil && !c.expectErr:
+ t.Errorf("Got unexpected error: %v", err)
+ return
+ case err != nil && c.expectErr:
+ // Got expected error
+ return
+ case err == nil && !c.expectErr:
+ if len(dumps) != len(c.expectedDumps) {
+ t.Fatalf("Expected %d dumps, got %d", len(c.expectedDumps), len(dumps))
+ }
+ for i, dump := range dumps {
+ if dump.OccurredAt != c.expectedDumps[i].OccurredAt {
+ t.Errorf("Dump %d expected to have occurred at %v, got %v", i, c.expectedDumps[i].OccurredAt, dump.OccurredAt)
+ }
+ if dump.Reason != c.expectedDumps[i].Reason {
+ t.Errorf("Expected reason in dump %d to be %v, got %v", i, c.expectedDumps[i].Reason, dump.Reason)
+ }
+ if dump.Counter != c.expectedDumps[i].Counter {
+ t.Errorf("Expected counter in dump %d to be %d, got %d", i, c.expectedDumps[i].Counter, dump.Counter)
+ }
+ if len(dump.Lines) != len(c.expectedDumps[i].Lines) {
+ t.Errorf("Expected number of lines in dump %d to be %d, got %d", i, len(c.expectedDumps[i].Lines), len(dump.Lines))
+ }
+ for j := range dump.Lines {
+ if dump.Lines[j] != c.expectedDumps[i].Lines[j] {
+ t.Errorf("Expected line %d in dump %d to be %q, got %q", i, j, c.expectedDumps[i].Lines[j], dump.Lines[j])
+ }
+ }
+ }
+ }
+ })
+ }
+}