|  | // 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{})) | 
|  | } |