From 642ef7838298441e02d5ef04ed17293599e1ced8 Mon Sep 17 00:00:00 2001 From: r0hansaxena Date: Tue, 5 May 2026 14:32:04 +0530 Subject: [PATCH 1/2] test(hypervisors): add unit tests for Firecracker Signed-off-by: r0hansaxena --- .../hypervisors/firecracker_test.go | 173 ++++++++++++++++++ pkg/unikontainers/hypervisors/helpers_test.go | 31 ++++ 2 files changed, 204 insertions(+) create mode 100644 pkg/unikontainers/hypervisors/firecracker_test.go create mode 100644 pkg/unikontainers/hypervisors/helpers_test.go diff --git a/pkg/unikontainers/hypervisors/firecracker_test.go b/pkg/unikontainers/hypervisors/firecracker_test.go new file mode 100644 index 000000000..fe37d4e43 --- /dev/null +++ b/pkg/unikontainers/hypervisors/firecracker_test.go @@ -0,0 +1,173 @@ +// Copyright (c) 2023-2026, Nubificus LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hypervisors + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/urunc-dev/urunc/pkg/unikontainers/types" +) + +func TestFirecrackerSimpleAccessors(t *testing.T) { + fc := &Firecracker{binary: FirecrackerBinary, binaryPath: "/usr/bin/firecracker"} + + assert.Equal(t, "/usr/bin/firecracker", fc.Path()) + assert.True(t, fc.UsesKVM()) + assert.NoError(t, fc.Ok()) + assert.NoError(t, fc.PreExec(types.ExecArgs{})) + + for _, fs := range []string{"virtiofs", "9pfs", ""} { + assert.False(t, fc.SupportsSharedfs(fs), "fs=%q should be unsupported", fs) + } +} + +func TestFirecrackerBuildExecCmd_TypicalRun(t *testing.T) { + fc := &Firecracker{binary: FirecrackerBinary, binaryPath: "/usr/bin/firecracker"} + + uk := &mockUnikernel{ + blockCli: []types.MonitorBlockArgs{ + {ID: "rootfs", Path: "/dev/vda"}, + {ID: "extra", Path: "/dev/vdb"}, + }, + } + cmd, err := fc.BuildExecCmd(types.ExecArgs{ + UnikernelPath: "/kernel", + Command: "console=ttyS0", + MemSizeB: 512 * 1024 * 1024, + VCPUs: 2, + Net: types.NetDevParams{ + TapDev: "tap0", + MAC: "aa:bb:cc:dd:ee:ff", + }, + }, uk) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, []string{ + "/usr/bin/firecracker", + "--no-api", + "--config-file", + "/tmp/fc.json", + "--no-seccomp", + }, cmd) + + raw, err := os.ReadFile(filepath.Join("/tmp", FCJsonFilename)) + if !assert.NoError(t, err) { + return + } + + var cfg FirecrackerConfig + if !assert.NoError(t, json.Unmarshal(raw, &cfg)) { + return + } + + assert.Equal(t, "/kernel", cfg.Source.ImagePath) + assert.Equal(t, "console=ttyS0", cfg.Source.BootArgs) + + assert.Equal(t, uint(2), cfg.Machine.VcpuCount) + assert.Equal(t, uint64(512), cfg.Machine.MemSizeMiB) + assert.False(t, cfg.Machine.Smt) + + if assert.Len(t, cfg.NetIfs, 1) { + assert.Equal(t, "net1", cfg.NetIfs[0].IfaceID) + assert.Equal(t, "tap0", cfg.NetIfs[0].HostIF) + assert.Equal(t, "aa:bb:cc:dd:ee:ff", cfg.NetIfs[0].GuestMAC) + } + + if assert.Len(t, cfg.Drives, 2) { + assert.True(t, cfg.Drives[0].IsRootDev, "rootfs should be flagged root") + assert.False(t, cfg.Drives[1].IsRootDev) + } +} + +func TestFirecrackerSeccompTogglesNoSeccompFlag(t *testing.T) { + fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} + + off, err := fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel"}, &mockUnikernel{}) + assert.NoError(t, err) + assert.Contains(t, off, "--no-seccomp") + + on, err := fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel", Seccomp: true}, &mockUnikernel{}) + assert.NoError(t, err) + assert.NotContains(t, on, "--no-seccomp") +} + +func TestFirecrackerMemoryFallsBackToDefault(t *testing.T) { + fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} + + for _, b := range []uint64{0, 1024, 1024 * 1024 * 0} { + _, err := fc.BuildExecCmd(types.ExecArgs{ + UnikernelPath: "/kernel", + MemSizeB: b, + }, &mockUnikernel{}) + assert.NoError(t, err) + + raw, _ := os.ReadFile("/tmp/fc.json") + var cfg FirecrackerConfig + assert.NoError(t, json.Unmarshal(raw, &cfg)) + assert.Equal(t, DefaultMemory, cfg.Machine.MemSizeMiB, + "sub-MiB or zero MemSizeB should give default, got bytes=%d", b) + } +} + +func TestFirecrackerInitrdArgsBeatsMonitorExtra(t *testing.T) { + fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} + + _, err := fc.BuildExecCmd(types.ExecArgs{ + UnikernelPath: "/kernel", + InitrdPath: "/from-args.img", + }, &mockUnikernel{ + monCli: types.MonitorCliArgs{ExtraInitrd: "/from-monitor.img"}, + }) + assert.NoError(t, err) + + raw, _ := os.ReadFile("/tmp/fc.json") + var cfg FirecrackerConfig + assert.NoError(t, json.Unmarshal(raw, &cfg)) + assert.Equal(t, "/from-args.img", cfg.Source.InitrdPath) + + _, err = fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel"}, &mockUnikernel{ + monCli: types.MonitorCliArgs{ExtraInitrd: "/from-monitor.img"}, + }) + assert.NoError(t, err) + + raw, _ = os.ReadFile("/tmp/fc.json") + assert.NoError(t, json.Unmarshal(raw, &cfg)) + assert.Equal(t, "/from-monitor.img", cfg.Source.InitrdPath) +} + +func TestFirecrackerVSock(t *testing.T) { + fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} + + _, err := fc.BuildExecCmd(types.ExecArgs{ + UnikernelPath: "/kernel", + VAccelType: "vsock", + VSockDevID: 7, + VSockDevPath: "/run/vaccel", + }, &mockUnikernel{}) + assert.NoError(t, err) + + raw, _ := os.ReadFile("/tmp/fc.json") + var cfg FirecrackerConfig + assert.NoError(t, json.Unmarshal(raw, &cfg)) + assert.Equal(t, 7, cfg.VSock.GuestCID) + assert.Equal(t, "/run/vaccel/vaccel.sock", cfg.VSock.UDSPath) + assert.Equal(t, "root", cfg.VSock.VSockID) +} diff --git a/pkg/unikontainers/hypervisors/helpers_test.go b/pkg/unikontainers/hypervisors/helpers_test.go new file mode 100644 index 000000000..b2d6b1400 --- /dev/null +++ b/pkg/unikontainers/hypervisors/helpers_test.go @@ -0,0 +1,31 @@ +// Copyright (c) 2023-2026, Nubificus LTD +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package hypervisors + +import "github.com/urunc-dev/urunc/pkg/unikontainers/types" + +type mockUnikernel struct { + netCli string + blockCli []types.MonitorBlockArgs + monCli types.MonitorCliArgs +} + +func (m *mockUnikernel) Init(_ types.UnikernelParams) error { return nil } +func (m *mockUnikernel) CommandString() (string, error) { return "", nil } +func (m *mockUnikernel) SupportsBlock() bool { return false } +func (m *mockUnikernel) SupportsFS(_ string) bool { return false } +func (m *mockUnikernel) MonitorNetCli(_, _ string) string { return m.netCli } +func (m *mockUnikernel) MonitorBlockCli() []types.MonitorBlockArgs { return m.blockCli } +func (m *mockUnikernel) MonitorCli() types.MonitorCliArgs { return m.monCli } From d44f4747985805b67dc5029429ec3fc0633c74d5 Mon Sep 17 00:00:00 2001 From: r0hansaxena Date: Sun, 31 May 2026 16:33:48 +0530 Subject: [PATCH 2/2] update Firecracker tests Signed-off-by: r0hansaxena --- .../hypervisors/firecracker_test.go | 253 +++++++++--------- pkg/unikontainers/hypervisors/helpers_test.go | 31 --- 2 files changed, 128 insertions(+), 156 deletions(-) delete mode 100644 pkg/unikontainers/hypervisors/helpers_test.go diff --git a/pkg/unikontainers/hypervisors/firecracker_test.go b/pkg/unikontainers/hypervisors/firecracker_test.go index fe37d4e43..f240c9a5d 100644 --- a/pkg/unikontainers/hypervisors/firecracker_test.go +++ b/pkg/unikontainers/hypervisors/firecracker_test.go @@ -24,150 +24,153 @@ import ( "github.com/urunc-dev/urunc/pkg/unikontainers/types" ) -func TestFirecrackerSimpleAccessors(t *testing.T) { - fc := &Firecracker{binary: FirecrackerBinary, binaryPath: "/usr/bin/firecracker"} +type mockUnikernel struct { + netCli string + blockCli []types.MonitorBlockArgs + monCli types.MonitorCliArgs +} - assert.Equal(t, "/usr/bin/firecracker", fc.Path()) - assert.True(t, fc.UsesKVM()) - assert.NoError(t, fc.Ok()) - assert.NoError(t, fc.PreExec(types.ExecArgs{})) +func (m *mockUnikernel) Init(_ types.UnikernelParams) error { return nil } +func (m *mockUnikernel) CommandString() (string, error) { return "", nil } +func (m *mockUnikernel) SupportsBlock() bool { return false } +func (m *mockUnikernel) SupportsFS(_ string) bool { return false } +func (m *mockUnikernel) MonitorNetCli(_, _ string) string { return m.netCli } +func (m *mockUnikernel) MonitorBlockCli() []types.MonitorBlockArgs { return m.blockCli } +func (m *mockUnikernel) MonitorCli() types.MonitorCliArgs { return m.monCli } + +func TestFirecrackerBuildExecCmd(t *testing.T) { + t.Run("typical run", func(t *testing.T) { + fc := &Firecracker{binary: FirecrackerBinary, binaryPath: "/usr/bin/firecracker"} + + uk := &mockUnikernel{ + blockCli: []types.MonitorBlockArgs{ + {ID: "rootfs", Path: "/dev/vda"}, + {ID: "extra", Path: "/dev/vdb"}, + }, + } + cmd, err := fc.BuildExecCmd(types.ExecArgs{ + UnikernelPath: "/kernel", + Command: "console=ttyS0", + MemSizeB: 512 * 1024 * 1024, + VCPUs: 2, + Net: types.NetDevParams{ + TapDev: "tap0", + MAC: "aa:bb:cc:dd:ee:ff", + }, + }, uk) + if !assert.NoError(t, err) { + return + } + + assert.Equal(t, []string{ + "/usr/bin/firecracker", + "--no-api", + "--config-file", + "/tmp/fc.json", + "--no-seccomp", + }, cmd) + + raw, err := os.ReadFile(filepath.Join("/tmp", FCJsonFilename)) + if !assert.NoError(t, err) { + return + } - for _, fs := range []string{"virtiofs", "9pfs", ""} { - assert.False(t, fc.SupportsSharedfs(fs), "fs=%q should be unsupported", fs) - } -} + var cfg FirecrackerConfig + if !assert.NoError(t, json.Unmarshal(raw, &cfg)) { + return + } + + assert.Equal(t, "/kernel", cfg.Source.ImagePath) + assert.Equal(t, "console=ttyS0", cfg.Source.BootArgs) + + assert.Equal(t, uint(2), cfg.Machine.VcpuCount) + assert.Equal(t, uint64(512), cfg.Machine.MemSizeMiB) + assert.False(t, cfg.Machine.Smt) + + if assert.Len(t, cfg.NetIfs, 1) { + assert.Equal(t, "net1", cfg.NetIfs[0].IfaceID) + assert.Equal(t, "tap0", cfg.NetIfs[0].HostIF) + assert.Equal(t, "aa:bb:cc:dd:ee:ff", cfg.NetIfs[0].GuestMAC) + } + + if assert.Len(t, cfg.Drives, 2) { + assert.True(t, cfg.Drives[0].IsRootDev, "rootfs should be flagged root") + assert.False(t, cfg.Drives[1].IsRootDev) + } + }) -func TestFirecrackerBuildExecCmd_TypicalRun(t *testing.T) { - fc := &Firecracker{binary: FirecrackerBinary, binaryPath: "/usr/bin/firecracker"} - - uk := &mockUnikernel{ - blockCli: []types.MonitorBlockArgs{ - {ID: "rootfs", Path: "/dev/vda"}, - {ID: "extra", Path: "/dev/vdb"}, - }, - } - cmd, err := fc.BuildExecCmd(types.ExecArgs{ - UnikernelPath: "/kernel", - Command: "console=ttyS0", - MemSizeB: 512 * 1024 * 1024, - VCPUs: 2, - Net: types.NetDevParams{ - TapDev: "tap0", - MAC: "aa:bb:cc:dd:ee:ff", - }, - }, uk) - if !assert.NoError(t, err) { - return - } - - assert.Equal(t, []string{ - "/usr/bin/firecracker", - "--no-api", - "--config-file", - "/tmp/fc.json", - "--no-seccomp", - }, cmd) - - raw, err := os.ReadFile(filepath.Join("/tmp", FCJsonFilename)) - if !assert.NoError(t, err) { - return - } - - var cfg FirecrackerConfig - if !assert.NoError(t, json.Unmarshal(raw, &cfg)) { - return - } - - assert.Equal(t, "/kernel", cfg.Source.ImagePath) - assert.Equal(t, "console=ttyS0", cfg.Source.BootArgs) - - assert.Equal(t, uint(2), cfg.Machine.VcpuCount) - assert.Equal(t, uint64(512), cfg.Machine.MemSizeMiB) - assert.False(t, cfg.Machine.Smt) - - if assert.Len(t, cfg.NetIfs, 1) { - assert.Equal(t, "net1", cfg.NetIfs[0].IfaceID) - assert.Equal(t, "tap0", cfg.NetIfs[0].HostIF) - assert.Equal(t, "aa:bb:cc:dd:ee:ff", cfg.NetIfs[0].GuestMAC) - } - - if assert.Len(t, cfg.Drives, 2) { - assert.True(t, cfg.Drives[0].IsRootDev, "rootfs should be flagged root") - assert.False(t, cfg.Drives[1].IsRootDev) - } -} + t.Run("seccomp flag", func(t *testing.T) { + fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} -func TestFirecrackerSeccompTogglesNoSeccompFlag(t *testing.T) { - fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} + off, err := fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel"}, &mockUnikernel{}) + assert.NoError(t, err) + assert.Contains(t, off, "--no-seccomp") - off, err := fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel"}, &mockUnikernel{}) - assert.NoError(t, err) - assert.Contains(t, off, "--no-seccomp") + on, err := fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel", Seccomp: true}, &mockUnikernel{}) + assert.NoError(t, err) + assert.NotContains(t, on, "--no-seccomp") + }) - on, err := fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel", Seccomp: true}, &mockUnikernel{}) - assert.NoError(t, err) - assert.NotContains(t, on, "--no-seccomp") -} + t.Run("memory fallback", func(t *testing.T) { + fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} + + for _, b := range []uint64{0, 1024, 1024 * 1024 * 0} { + _, err := fc.BuildExecCmd(types.ExecArgs{ + UnikernelPath: "/kernel", + MemSizeB: b, + }, &mockUnikernel{}) + assert.NoError(t, err) + + raw, _ := os.ReadFile("/tmp/fc.json") + var cfg FirecrackerConfig + assert.NoError(t, json.Unmarshal(raw, &cfg)) + assert.Equal(t, DefaultMemory, cfg.Machine.MemSizeMiB, + "sub-MiB or zero MemSizeB should give default, got bytes=%d", b) + } + }) -func TestFirecrackerMemoryFallsBackToDefault(t *testing.T) { - fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} + t.Run("initrd priority", func(t *testing.T) { + fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} - for _, b := range []uint64{0, 1024, 1024 * 1024 * 0} { _, err := fc.BuildExecCmd(types.ExecArgs{ UnikernelPath: "/kernel", - MemSizeB: b, - }, &mockUnikernel{}) + InitrdPath: "/from-args.img", + }, &mockUnikernel{ + monCli: types.MonitorCliArgs{ExtraInitrd: "/from-monitor.img"}, + }) assert.NoError(t, err) raw, _ := os.ReadFile("/tmp/fc.json") var cfg FirecrackerConfig assert.NoError(t, json.Unmarshal(raw, &cfg)) - assert.Equal(t, DefaultMemory, cfg.Machine.MemSizeMiB, - "sub-MiB or zero MemSizeB should give default, got bytes=%d", b) - } -} + assert.Equal(t, "/from-args.img", cfg.Source.InitrdPath) -func TestFirecrackerInitrdArgsBeatsMonitorExtra(t *testing.T) { - fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} + _, err = fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel"}, &mockUnikernel{ + monCli: types.MonitorCliArgs{ExtraInitrd: "/from-monitor.img"}, + }) + assert.NoError(t, err) - _, err := fc.BuildExecCmd(types.ExecArgs{ - UnikernelPath: "/kernel", - InitrdPath: "/from-args.img", - }, &mockUnikernel{ - monCli: types.MonitorCliArgs{ExtraInitrd: "/from-monitor.img"}, + raw, _ = os.ReadFile("/tmp/fc.json") + assert.NoError(t, json.Unmarshal(raw, &cfg)) + assert.Equal(t, "/from-monitor.img", cfg.Source.InitrdPath) }) - assert.NoError(t, err) - raw, _ := os.ReadFile("/tmp/fc.json") - var cfg FirecrackerConfig - assert.NoError(t, json.Unmarshal(raw, &cfg)) - assert.Equal(t, "/from-args.img", cfg.Source.InitrdPath) - - _, err = fc.BuildExecCmd(types.ExecArgs{UnikernelPath: "/kernel"}, &mockUnikernel{ - monCli: types.MonitorCliArgs{ExtraInitrd: "/from-monitor.img"}, - }) - assert.NoError(t, err) + t.Run("vsock", func(t *testing.T) { + fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} - raw, _ = os.ReadFile("/tmp/fc.json") - assert.NoError(t, json.Unmarshal(raw, &cfg)) - assert.Equal(t, "/from-monitor.img", cfg.Source.InitrdPath) -} + _, err := fc.BuildExecCmd(types.ExecArgs{ + UnikernelPath: "/kernel", + VAccelType: "vsock", + VSockDevID: 7, + VSockDevPath: "/run/vaccel", + }, &mockUnikernel{}) + assert.NoError(t, err) -func TestFirecrackerVSock(t *testing.T) { - fc := &Firecracker{binaryPath: "/usr/bin/firecracker"} - - _, err := fc.BuildExecCmd(types.ExecArgs{ - UnikernelPath: "/kernel", - VAccelType: "vsock", - VSockDevID: 7, - VSockDevPath: "/run/vaccel", - }, &mockUnikernel{}) - assert.NoError(t, err) - - raw, _ := os.ReadFile("/tmp/fc.json") - var cfg FirecrackerConfig - assert.NoError(t, json.Unmarshal(raw, &cfg)) - assert.Equal(t, 7, cfg.VSock.GuestCID) - assert.Equal(t, "/run/vaccel/vaccel.sock", cfg.VSock.UDSPath) - assert.Equal(t, "root", cfg.VSock.VSockID) + raw, _ := os.ReadFile("/tmp/fc.json") + var cfg FirecrackerConfig + assert.NoError(t, json.Unmarshal(raw, &cfg)) + assert.Equal(t, 7, cfg.VSock.GuestCID) + assert.Equal(t, "/run/vaccel/vaccel.sock", cfg.VSock.UDSPath) + assert.Equal(t, "root", cfg.VSock.VSockID) + }) } diff --git a/pkg/unikontainers/hypervisors/helpers_test.go b/pkg/unikontainers/hypervisors/helpers_test.go deleted file mode 100644 index b2d6b1400..000000000 --- a/pkg/unikontainers/hypervisors/helpers_test.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) 2023-2026, Nubificus LTD -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package hypervisors - -import "github.com/urunc-dev/urunc/pkg/unikontainers/types" - -type mockUnikernel struct { - netCli string - blockCli []types.MonitorBlockArgs - monCli types.MonitorCliArgs -} - -func (m *mockUnikernel) Init(_ types.UnikernelParams) error { return nil } -func (m *mockUnikernel) CommandString() (string, error) { return "", nil } -func (m *mockUnikernel) SupportsBlock() bool { return false } -func (m *mockUnikernel) SupportsFS(_ string) bool { return false } -func (m *mockUnikernel) MonitorNetCli(_, _ string) string { return m.netCli } -func (m *mockUnikernel) MonitorBlockCli() []types.MonitorBlockArgs { return m.blockCli } -func (m *mockUnikernel) MonitorCli() types.MonitorCliArgs { return m.monCli }