blob: 16ead648161163981f06f0f36861f47092a06c8d [file] [log] [blame]
// 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"
"io/ioutil"
"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 := ioutil.TempFile("/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 := ioutil.TempFile("/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{}))
}