kexecme

Easily kexec (on Fedora)

git clone https://code.pdelong.com/kexecme.git

  1package main
  2
  3import (
  4	"errors"
  5	"fmt"
  6	"io"
  7	"os"
  8	"strings"
  9	"testing"
 10)
 11
 12func TestParseGrubEntryFilesSuccess(t *testing.T) {
 13	files := []string{
 14		"testdata/entries/0.conf",
 15		"testdata/entries/1.conf",
 16		"testdata/entries/2.conf",
 17	}
 18	expected := []kernelInfo{
 19		{
 20			title:   "Fedora Linux (5.18.16-200.fc36.x86_64) 36 (Workstation Edition)",
 21			version: "5.18.16-200.fc36.x86_64",
 22			linux:   "/vmlinuz-5.18.16-200.fc36.x86_64",
 23			initrd:  "/initramfs-5.18.16-200.fc36.x86_64.img $tuned_initrd",
 24			options: "root=UUID=random_uuid ro rootflags=subvol=root rd.luks.uuid=luks-some-other-uuid rhgb quiet",
 25		},
 26		{
 27			title:   "Fedora Linux (5.18.17-200.fc36.x86_64) 36 (Workstation Edition)",
 28			version: "5.18.17-200.fc36.x86_64",
 29			linux:   "/vmlinuz-5.18.17-200.fc36.x86_64",
 30			initrd:  "/initramfs-5.18.17-200.fc36.x86_64.img",
 31			options: "root=UUID=random_uuid ro rootflags=subvol=root rd.luks.uuid=luks-some-other-uuid rhgb quiet",
 32		},
 33		{
 34			title:   "Fedora Linux (5.18.18-200.fc36.x86_64) 36 (Workstation Edition)",
 35			version: "5.18.18-200.fc36.x86_64",
 36			linux:   "/vmlinuz-5.18.18-200.fc36.x86_64",
 37			initrd:  "/initramfs-5.18.18-200.fc36.x86_64.img",
 38			options: "root=UUID=random_uuid ro rootflags=subvol=root rd.luks.uuid=luks-some-other-uuid rhgb quiet",
 39		},
 40	}
 41
 42	ks, err := parseGrubEntryFiles(files)
 43	if err != nil {
 44		t.Fatalf("unexpected error: %s", err)
 45	}
 46
 47	if len(ks) != len(expected) {
 48		t.Fatalf("unexpected number of files.  got=%d, want=%d",
 49			len(ks), len(expected))
 50	}
 51
 52	for i := range expected {
 53		if err := testKernelInfo(&expected[i], &ks[i]); err != nil {
 54			t.Errorf("Error in kernel %d: %s", i, err)
 55		}
 56	}
 57}
 58
 59func TestParseGrubEntryFilesFailures(t *testing.T) {
 60	tests := []struct {
 61		Name      string
 62		Filename  string
 63		ErrSubstr string
 64	}{
 65		{
 66			"bad file",
 67			"testdata/entries/failure.conf",
 68			"did not contain options",
 69		},
 70		{
 71			"not found",
 72			"testdata/entries/idonotexist.conf",
 73			"no such file or directory",
 74		},
 75	}
 76
 77	for _, tt := range tests {
 78		t.Run(tt.Name, func(t *testing.T) {
 79			ks, err := parseGrubEntryFiles([]string{tt.Filename})
 80			if err == nil {
 81				t.Fatalf("expected an error")
 82			}
 83
 84			if !strings.Contains(err.Error(), tt.ErrSubstr) {
 85				t.Errorf(
 86					"error message did not contain %q: %q",
 87					tt.ErrSubstr,
 88					err.Error(),
 89				)
 90			}
 91
 92			if ks != nil {
 93				t.Errorf("reterned kernels slice was not nil")
 94			}
 95		})
 96	}
 97}
 98
 99func TestParseGrubEntry(t *testing.T) {
100	tests := []struct {
101		Name         string
102		Contents     string
103		ExpectedInfo *kernelInfo
104		HasErr       bool
105		ErrSubstr    string
106	}{
107		{
108			"success",
109			`
110title Fedora Linux (5.18.16-200.fc36.x86_64) 36 (Workstation Edition)
111version 5.18.16-200.fc36.x86_64
112linux /vmlinuz-5.18.16-200.fc36.x86_64
113initrd /initramfs-5.18.16-200.fc36.x86_64.img
114options root=UUID=random_uuid ro rootflags=subvol=root rd.luks.uuid=luks-some-other-uuid rhgb quiet
115			`,
116			&kernelInfo{
117				title:   "Fedora Linux (5.18.16-200.fc36.x86_64) 36 (Workstation Edition)",
118				version: "5.18.16-200.fc36.x86_64",
119				linux:   "/vmlinuz-5.18.16-200.fc36.x86_64",
120				initrd:  "/initramfs-5.18.16-200.fc36.x86_64.img",
121				options: "root=UUID=random_uuid ro rootflags=subvol=root rd.luks.uuid=luks-some-other-uuid rhgb quiet",
122			},
123			false,
124			"",
125		},
126		{
127			"missing title",
128			``,
129			nil,
130			true,
131			"did not contain title",
132		},
133		{
134			"missing version",
135			`
136title Fedora Linux (5.18.16-200.fc36.x86_64) 36 (Workstation Edition)
137			`,
138			nil,
139			true,
140			"did not contain version",
141		},
142		{
143			"missing linux",
144			`
145title Fedora Linux (5.18.16-200.fc36.x86_64) 36 (Workstation Edition)
146version 5.18.16-200.fc36.x86_64
147			`,
148			nil,
149			true,
150			"did not contain linux",
151		},
152		{
153			"missing initrd",
154			`
155title Fedora Linux (5.18.16-200.fc36.x86_64) 36 (Workstation Edition)
156version 5.18.16-200.fc36.x86_64
157linux /vmlinuz-5.18.16-200.fc36.x86_64
158			`,
159			nil,
160			true,
161			"did not contain initrd",
162		},
163		{
164			"missing options",
165			`
166title Fedora Linux (5.18.16-200.fc36.x86_64) 36 (Workstation Edition)
167version 5.18.16-200.fc36.x86_64
168linux /vmlinuz-5.18.16-200.fc36.x86_64
169initrd /initramfs-5.18.16-200.fc36.x86_64.img
170			`,
171			nil,
172			true,
173			"did not contain options",
174		},
175		{
176			"bad format",
177			`
178			title this is a title
179			thereisnospacehereohno
180			`,
181			nil,
182			true,
183			"file in wrong format",
184		},
185	}
186
187	for _, tt := range tests {
188		t.Run(tt.Name, func(t *testing.T) {
189			k, err := parseGrubEntry(tt.Contents)
190			if tt.HasErr {
191				if err != nil {
192					if !strings.Contains(err.Error(), tt.ErrSubstr) {
193						t.Errorf(
194							"could not find substring %q in error %q",
195							tt.ErrSubstr,
196							err.Error(),
197						)
198					}
199				} else {
200					t.Errorf("no error when expected")
201				}
202			} else {
203				if err != nil {
204					t.Errorf("unexpected error: %s", err)
205				}
206			}
207
208			if err := testKernelInfo(tt.ExpectedInfo, k); err != nil {
209				t.Errorf("kernelInfo mismatch: %s", err.Error())
210			}
211		})
212	}
213}
214
215func TestGetGrubEntryFilesSuccess(t *testing.T) {
216	files := []string{
217		"testdata/entries/0.conf",
218		"testdata/entries/1.conf",
219		"testdata/entries/2.conf",
220		"testdata/entries/failure.conf",
221	}
222
223	fs, err := getGrubEntryFiles("testdata/entries/")
224	if err != nil {
225		t.Fatalf("unexpected error: %s", err)
226	}
227
228	if len(fs) != len(files) {
229		t.Fatalf("wrong number of files. got=%d; want=%d", len(fs), len(files))
230	}
231
232	for i := range files {
233		if files[i] != fs[i] {
234			t.Errorf("name mismatch for file %d: got=%s; want=%s",
235				i, files[i], fs[i])
236		}
237	}
238}
239
240func TestGetGrubEntryFilesFailure(t *testing.T) {
241	_, err := getGrubEntryFiles("testdata/entries/idonotexist")
242	if err != nil {
243		if !errors.Is(err, os.ErrNotExist) {
244			t.Errorf("unexpected error returned: %s", err)
245		}
246	} else {
247		t.Error("no error reported when expected")
248	}
249}
250
251func TestUserSelectKernelSuccess(t *testing.T) {
252	tests := []struct {
253		Name   string
254		Input  string
255		Output int
256	}{
257		{"eof", "", -1},
258		{"empty line", "\n", 0},
259		{"single number", "1\n", 1},
260		{"multiple tries", "-1\nabcd\n2\n", 2},
261		{"too long", "-1\nabcd\n100\n3\n", 3},
262	}
263
264	for _, tt := range tests {
265		t.Run(tt.Name, func(t *testing.T) {
266			// TODO: Is it reasonble to test the output?
267			n, err := userSelectKernel(
268				strings.NewReader(tt.Input),
269				io.Discard,
270				[]kernelInfo{{}, {}, {}, {}},
271			)
272			if err != nil {
273				t.Fatalf("unexpected error: %s", err)
274			}
275
276			if n != tt.Output {
277				t.Fatalf("wrong output: got=%d; want=%d", n, tt.Output)
278			}
279		})
280	}
281}
282
283// A struct that implements io.Reader, but only returns errors.
284type errorReader struct {
285	err error
286}
287
288func (e *errorReader) Read(p []byte) (n int, err error) {
289	return 0, e.err
290}
291
292func TestUserSelectKernelScannerError(t *testing.T) {
293	errStr := "bad bad bad"
294	r := errorReader{fmt.Errorf(errStr)}
295
296	_, err := userSelectKernel(&r, io.Discard, []kernelInfo{{}, {}, {}})
297	if err == nil {
298		t.Fatal("unexpected nil error")
299	}
300
301	if !strings.Contains(err.Error(), errStr) {
302		t.Errorf("error did not contain %q: %q", errStr, err.Error())
303	}
304}
305
306func testKernelInfo(want, got *kernelInfo) error {
307	if (want == nil) != (got == nil) {
308		return fmt.Errorf(
309			"nil mismatch: got=%t; want=%t",
310			want == nil,
311			got == nil,
312		)
313	}
314
315	if want == nil {
316		return nil
317	}
318
319	if want.title != got.title {
320		return fmt.Errorf("title mismatch: want=%s; got=%s",
321			want.title, got.title)
322	}
323
324	if want.version != got.version {
325		return fmt.Errorf("version mismatch: want=%s; got=%s",
326			want.version, got.version)
327	}
328
329	if want.linux != got.linux {
330		return fmt.Errorf("linux mismatch: want=%s; got=%s",
331			want.linux, got.linux)
332	}
333
334	if want.initrd != got.initrd {
335		return fmt.Errorf("initrd mismatch: want=%s; got=%s",
336			want.initrd, got.initrd)
337	}
338
339	if want.options != got.options {
340		return fmt.Errorf("options mismatch: want=%s; got=%s",
341			want.options, got.options)
342	}
343
344	return nil
345}