| // Copyright 2020 The Monogon Project Authors. |
| // |
| // SPDX-License-Identifier: Apache-2.0 |
| // |
| // 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 loop |
| |
| import ( |
| "encoding/binary" |
| "io" |
| "math" |
| "os" |
| "runtime" |
| "syscall" |
| "testing" |
| "unsafe" |
| |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| "golang.org/x/sys/unix" |
| ) |
| |
| // Write a test file with a very specific pattern (increasing little-endian 16 |
| // bit unsigned integers) to detect offset correctness. File is always 128KiB |
| // large (2^16 * 2 bytes). |
| func makeTestFile() *os.File { |
| f, err := os.CreateTemp("/tmp", "") |
| if err != nil { |
| panic(err) |
| } |
| for i := 0; i <= math.MaxUint16; i++ { |
| if err := binary.Write(f, binary.LittleEndian, uint16(i)); err != nil { |
| panic(err) |
| } |
| } |
| if _, err := f.Seek(0, io.SeekStart); err != nil { |
| panic(err) |
| } |
| return f |
| } |
| |
| func getBlkdevSize(f *os.File) (size uint64) { |
| if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, f.Fd(), unix.BLKGETSIZE64, uintptr(unsafe.Pointer(&size))); err != 0 { |
| panic(err) |
| } |
| return |
| } |
| |
| func getOffsetFromContent(dev *Device) (firstIndex uint16) { |
| if err := binary.Read(dev.dev, binary.LittleEndian, &firstIndex); err != nil { |
| panic(err) |
| } |
| firstIndex *= 2 // 2 bytes per index |
| return |
| } |
| |
| func setupCreate(t *testing.T, config Config) *Device { |
| f := makeTestFile() |
| dev, err := Create(f, config) |
| defer f.Close() |
| assert.NoError(t, err) |
| t.Cleanup(func() { |
| if dev != nil { |
| dev.Remove() |
| } |
| os.Remove(f.Name()) |
| }) |
| if dev == nil { |
| t.FailNow() |
| } |
| return dev |
| } |
| |
| func TestDeviceAccessors(t *testing.T) { |
| if os.Getenv("IN_KTEST") != "true" { |
| t.Skip("Not in ktest") |
| } |
| dev := setupCreate(t, Config{}) |
| |
| devPath, err := dev.DevPath() |
| assert.NoError(t, err) |
| require.Equal(t, "/dev/loop0", devPath) |
| |
| var stat unix.Stat_t |
| assert.NoError(t, unix.Stat("/dev/loop0", &stat)) |
| devNum, err := dev.Dev() |
| assert.NoError(t, err) |
| require.Equal(t, stat.Rdev, devNum) |
| |
| backingFile, err := dev.BackingFilePath() |
| assert.NoError(t, err) |
| // The filename of the temporary file is not available in this context, but |
| // we know that the file needs to be in /tmp, which should be a good-enough |
| // test. |
| assert.Contains(t, backingFile, "/tmp/") |
| } |
| |
| func TestCreate(t *testing.T) { |
| if os.Getenv("IN_KTEST") != "true" { |
| t.Skip("Not in ktest") |
| } |
| t.Parallel() |
| tests := []struct { |
| name string |
| config Config |
| validate func(t *testing.T, dev *Device) |
| }{ |
| {"NoOpts", Config{}, func(t *testing.T, dev *Device) { |
| require.Equal(t, uint64(128*1024), getBlkdevSize(dev.dev)) |
| require.Equal(t, uint16(0), getOffsetFromContent(dev)) |
| |
| _, err := dev.dev.WriteString("test") |
| assert.NoError(t, err) |
| }}, |
| {"DirectIO", Config{Flags: FlagDirectIO}, func(t *testing.T, dev *Device) { |
| require.Equal(t, uint64(128*1024), getBlkdevSize(dev.dev)) |
| |
| _, err := dev.dev.WriteString("test") |
| assert.NoError(t, err) |
| }}, |
| {"ReadOnly", Config{Flags: FlagReadOnly}, func(t *testing.T, dev *Device) { |
| _, err := dev.dev.WriteString("test") |
| assert.Error(t, err) |
| }}, |
| {"Mapping", Config{BlockSize: 512, SizeLimit: 2048, Offset: 4096}, func(t *testing.T, dev *Device) { |
| assert.Equal(t, uint16(4096), getOffsetFromContent(dev)) |
| assert.Equal(t, uint64(2048), getBlkdevSize(dev.dev)) |
| }}, |
| } |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| dev := setupCreate(t, test.config) |
| test.validate(t, dev) |
| assert.NoError(t, dev.Remove()) |
| }) |
| } |
| } |
| |
| func TestOpenBadDevice(t *testing.T) { |
| if os.Getenv("IN_KTEST") != "true" { |
| t.Skip("Not in ktest") |
| } |
| dev, err := Open("/dev/null") |
| require.Error(t, err) |
| if dev != nil { // Prevent leaks in case this test fails |
| dev.Close() |
| } |
| } |
| |
| func TestOpen(t *testing.T) { |
| if os.Getenv("IN_KTEST") != "true" { |
| t.Skip("Not in ktest") |
| } |
| f := makeTestFile() |
| defer os.Remove(f.Name()) |
| defer f.Close() |
| dev, err := Create(f, Config{}) |
| assert.NoError(t, err) |
| path, err := dev.DevPath() |
| assert.NoError(t, err) |
| assert.NoError(t, dev.Close()) |
| reopenedDev, err := Open(path) |
| assert.NoError(t, err) |
| defer reopenedDev.Remove() |
| reopenedDevPath, err := reopenedDev.DevPath() |
| assert.NoError(t, err) |
| require.Equal(t, path, reopenedDevPath) // Still needs to be the same device |
| } |
| |
| func TestResize(t *testing.T) { |
| if os.Getenv("IN_KTEST") != "true" { |
| t.Skip("Not in ktest") |
| } |
| f, err := os.CreateTemp("/tmp", "") |
| assert.NoError(t, err) |
| empty1K := make([]byte, 1024) |
| for i := 0; i < 64; i++ { |
| _, err := f.Write(empty1K) |
| assert.NoError(t, err) |
| } |
| dev, err := Create(f, Config{}) |
| assert.NoError(t, err) |
| require.Equal(t, uint64(64*1024), getBlkdevSize(dev.dev)) |
| for i := 0; i < 32; i++ { |
| _, err := f.Write(empty1K) |
| assert.NoError(t, err) |
| } |
| assert.NoError(t, f.Sync()) |
| assert.NoError(t, dev.RefreshSize()) |
| require.Equal(t, uint64(96*1024), getBlkdevSize(dev.dev)) |
| } |
| |
| func TestStructSize(t *testing.T) { |
| if runtime.GOOS != "linux" && runtime.GOARCH != "amd64" { |
| t.Skip("Reference value not available") |
| } |
| require.Equal(t, uintptr(304), unsafe.Sizeof(loopConfig{})) |
| } |