// Copyright 2024 The Prometheus Authors // 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. //go:build linux package fileutil import ( "io" "os" "path" "testing" "github.com/stretchr/testify/require" ) func directIORqmtsForTest(tb testing.TB) *directIORqmts { f, err := os.OpenFile(path.Join(tb.TempDir(), "foo"), os.O_CREATE|os.O_WRONLY, 0o666) require.NoError(tb, err) alignmentRqmts, err := fetchDirectIORqmts(f.Fd()) require.NoError(tb, err) return alignmentRqmts } func TestDirectIOFile(t *testing.T) { tmpDir := t.TempDir() f, err := os.OpenFile(path.Join(tmpDir, "test"), os.O_CREATE|os.O_WRONLY, 0o666) require.NoError(t, err) require.NoError(t, enableDirectIO(f.Fd())) } func TestAlignedBlockEarlyPanic(t *testing.T) { alignRqmts := directIORqmtsForTest(t) cases := []struct { desc string size int }{ {"Zero size", 0}, {"Size not multiple of offset alignment", 9973}, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { require.Panics(t, func() { alignedBlock(tc.size, alignRqmts) }) }) } } func TestAlignedBloc(t *testing.T) { alignRqmts := directIORqmtsForTest(t) block := alignedBlock(5*alignRqmts.offsetAlign, alignRqmts) require.True(t, isAligned(block, alignRqmts)) require.Len(t, block, 5*alignRqmts.offsetAlign) require.False(t, isAligned(block[1:], alignRqmts)) } func TestDirectIOWriter(t *testing.T) { alignRqmts := directIORqmtsForTest(t) cases := []struct { name string initialOffset int bufferSize int dataSize int // writtenBytes should also consider needed zero padding. writtenBytes int shouldInvalidate bool }{ { name: "data equal to buffer", bufferSize: 8 * alignRqmts.offsetAlign, dataSize: 8 * alignRqmts.offsetAlign, writtenBytes: 8 * alignRqmts.offsetAlign, }, { name: "data exceeds buffer", bufferSize: 4 * alignRqmts.offsetAlign, dataSize: 64 * alignRqmts.offsetAlign, writtenBytes: 64 * alignRqmts.offsetAlign, }, { name: "data exceeds buffer + final offset unaligned", bufferSize: 2 * alignRqmts.offsetAlign, dataSize: 4*alignRqmts.offsetAlign + 33, writtenBytes: 4*alignRqmts.offsetAlign + alignRqmts.offsetAlign, shouldInvalidate: true, }, { name: "data smaller than buffer", bufferSize: 8 * alignRqmts.offsetAlign, dataSize: 3 * alignRqmts.offsetAlign, writtenBytes: 3 * alignRqmts.offsetAlign, }, { name: "data smaller than buffer + final offset unaligned", bufferSize: 4 * alignRqmts.offsetAlign, dataSize: alignRqmts.offsetAlign + 70, writtenBytes: alignRqmts.offsetAlign + alignRqmts.offsetAlign, shouldInvalidate: true, }, { name: "offset aligned", initialOffset: alignRqmts.offsetAlign, bufferSize: 8 * alignRqmts.offsetAlign, dataSize: alignRqmts.offsetAlign, writtenBytes: alignRqmts.offsetAlign, }, { name: "initial offset unaligned + final offset unaligned", initialOffset: 8, bufferSize: 8 * alignRqmts.offsetAlign, dataSize: 64 * alignRqmts.offsetAlign, writtenBytes: 64*alignRqmts.offsetAlign + (alignRqmts.offsetAlign - 8), shouldInvalidate: true, }, { name: "offset unaligned + final offset aligned", initialOffset: 8, bufferSize: 4 * alignRqmts.offsetAlign, dataSize: 4*alignRqmts.offsetAlign + (alignRqmts.offsetAlign - 8), writtenBytes: 4*alignRqmts.offsetAlign + (alignRqmts.offsetAlign - 8), }, { name: "empty data", bufferSize: 4 * alignRqmts.offsetAlign, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { fileName := path.Join(t.TempDir(), "test") data := make([]byte, tc.dataSize) for i := 0; i < len(data); i++ { // Do not use 256 as it may be a divider of requiredAlignment. To avoid patterns. data[i] = byte(i % 251) } f, err := os.OpenFile(fileName, os.O_CREATE|os.O_WRONLY, 0o666) require.NoError(t, err) if tc.initialOffset != 0 { _, err = f.Seek(int64(tc.initialOffset), io.SeekStart) require.NoError(t, err) } w, err := newDirectIOWriter(f, tc.bufferSize) require.NoError(t, err) n, err := w.Write(data) require.NoError(t, err) require.Equal(t, tc.dataSize, n) require.NoError(t, w.Flush()) // Check the file's final offset. currOffset, err := currentFileOffset(f) require.NoError(t, err) require.Equal(t, tc.dataSize+tc.initialOffset, currOffset) // Check the written data. fileBytes, err := os.ReadFile(fileName) require.NoError(t, err) if tc.dataSize > 0 { require.Len(t, fileBytes, tc.writtenBytes+tc.initialOffset) require.Equal(t, data, fileBytes[tc.initialOffset:tc.dataSize+tc.initialOffset]) } else { require.Empty(t, fileBytes) } // Check the writer state. if tc.shouldInvalidate { require.True(t, w.invalid) require.Error(t, w.Flush()) _, err = w.Write([]byte{}) require.Error(t, err) } else { require.False(t, w.invalid) require.NoError(t, w.Flush()) _, err = w.Write([]byte{}) require.NoError(t, err) } }) } }