Skip to content

Commit 33bb4c3

Browse files
matthewlouisbrockmanValentaTomasbchalios
authored
unit and integration tests for filesystem pause/resume integrity (#2252)
Co-authored-by: ValentaTomas <valenta.and.thomas@gmail.com> Co-authored-by: Babis Chalios <babis.chalios@e2b.dev>
1 parent 98e0431 commit 33bb4c3

File tree

2 files changed

+442
-0
lines changed

2 files changed

+442
-0
lines changed

packages/orchestrator/pkg/sandbox/block/cache_test.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import (
44
"bytes"
55
"crypto/rand"
66
"fmt"
7+
"io"
78
"math"
89
"os"
910
"syscall"
1011
"testing"
1112
"unsafe"
1213

14+
"github.com/google/uuid"
1315
"github.com/stretchr/testify/require"
1416
"golang.org/x/sys/unix"
1517

@@ -218,6 +220,183 @@ func TestEmptyRanges(t *testing.T) {
218220
})
219221
}
220222

223+
func TestCacheExportToDiff_ZeroDirtyBlockEmittedAsDirtyPayload(t *testing.T) {
224+
t.Parallel()
225+
226+
const blockSize = header.RootfsBlockSize
227+
cache, err := NewCache(blockSize, blockSize, t.TempDir()+"/cache", false)
228+
require.NoError(t, err)
229+
230+
t.Cleanup(func() {
231+
require.NoError(t, cache.Close())
232+
})
233+
234+
zeroBlock := make([]byte, blockSize)
235+
n, err := cache.WriteAt(zeroBlock, 0)
236+
require.NoError(t, err)
237+
require.Equal(t, int(blockSize), n)
238+
239+
out, err := os.CreateTemp(t.TempDir(), "diff-*")
240+
require.NoError(t, err)
241+
defer out.Close()
242+
243+
diffMetadata, err := cache.ExportToDiff(t.Context(), out)
244+
require.NoError(t, err)
245+
246+
require.EqualValues(t, 1, diffMetadata.Dirty.Count(), "zero-filled dirty block should be emitted as dirty payload")
247+
require.EqualValues(t, 0, diffMetadata.Empty.Count(), "zero-filled dirty block should not be tracked in empty metadata")
248+
249+
stat, err := out.Stat()
250+
require.NoError(t, err)
251+
require.EqualValues(t, blockSize, stat.Size(), "zero-filled dirty block should write block payload bytes")
252+
}
253+
254+
func TestCacheExportToDiff_ZeroDirtyBlockMapsToSnapshotBuild(t *testing.T) {
255+
t.Parallel()
256+
257+
const blockSize = header.RootfsBlockSize
258+
cache, err := NewCache(blockSize, blockSize, t.TempDir()+"/cache", false)
259+
require.NoError(t, err)
260+
261+
t.Cleanup(func() {
262+
require.NoError(t, cache.Close())
263+
})
264+
265+
zeroBlock := make([]byte, blockSize)
266+
n, err := cache.WriteAt(zeroBlock, 0)
267+
require.NoError(t, err)
268+
require.Equal(t, int(blockSize), n)
269+
270+
out, err := os.CreateTemp(t.TempDir(), "diff-*")
271+
require.NoError(t, err)
272+
defer out.Close()
273+
274+
diffMetadata, err := cache.ExportToDiff(t.Context(), out)
275+
require.NoError(t, err)
276+
277+
baseBuildID := uuid.New()
278+
originalHeader, err := header.NewHeader(
279+
header.NewTemplateMetadata(baseBuildID, uint64(blockSize), uint64(blockSize)),
280+
nil,
281+
)
282+
require.NoError(t, err)
283+
284+
snapshotBuildID := uuid.New()
285+
diffHeader, err := diffMetadata.ToDiffHeader(t.Context(), originalHeader, snapshotBuildID)
286+
require.NoError(t, err)
287+
288+
_, _, mappedBuildID, err := diffHeader.GetShiftedMapping(t.Context(), 0)
289+
require.NoError(t, err)
290+
291+
require.NotNil(t, mappedBuildID)
292+
require.Equal(t, snapshotBuildID, *mappedBuildID, "zero-filled dirty block should map to the snapshot diff when empty detection is skipped")
293+
require.NotEqual(t, uuid.Nil, *mappedBuildID, "zero-filled dirty block should no longer be represented as an empty mapping")
294+
}
295+
296+
func TestCacheExportToDiff_MixedDirtyBlocksKeepsZeroBlockInDiff(t *testing.T) {
297+
t.Parallel()
298+
299+
const blockSize = header.RootfsBlockSize
300+
const size = blockSize * 3
301+
302+
cache, err := NewCache(size, blockSize, t.TempDir()+"/cache", false)
303+
require.NoError(t, err)
304+
t.Cleanup(func() {
305+
require.NoError(t, cache.Close())
306+
})
307+
308+
zeroBlock := make([]byte, blockSize)
309+
nonZeroBlock := bytes.Repeat([]byte{0xAB}, int(blockSize))
310+
311+
_, err = cache.WriteAt(zeroBlock, 0)
312+
require.NoError(t, err)
313+
314+
_, err = cache.WriteAt(nonZeroBlock, blockSize)
315+
require.NoError(t, err)
316+
317+
out, err := os.CreateTemp(t.TempDir(), "diff-*")
318+
require.NoError(t, err)
319+
defer out.Close()
320+
321+
diffMetadata, err := cache.ExportToDiff(t.Context(), out)
322+
require.NoError(t, err)
323+
324+
require.EqualValues(t, 2, diffMetadata.Dirty.Count())
325+
require.EqualValues(t, 0, diffMetadata.Empty.Count(), "mixed export should still skip empty tracking for zero-filled dirty blocks")
326+
327+
_, err = out.Seek(0, io.SeekStart)
328+
require.NoError(t, err)
329+
exported, err := io.ReadAll(out)
330+
require.NoError(t, err)
331+
expected := make([]byte, 0, len(zeroBlock)+len(nonZeroBlock))
332+
expected = append(expected, zeroBlock...)
333+
expected = append(expected, nonZeroBlock...)
334+
require.Equal(t, expected, exported)
335+
336+
baseBuildID := uuid.New()
337+
originalHeader, err := header.NewHeader(
338+
header.NewTemplateMetadata(baseBuildID, uint64(blockSize), uint64(size)),
339+
nil,
340+
)
341+
require.NoError(t, err)
342+
343+
snapshotBuildID := uuid.New()
344+
diffHeader, err := diffMetadata.ToDiffHeader(t.Context(), originalHeader, snapshotBuildID)
345+
require.NoError(t, err)
346+
347+
_, _, firstBlockBuildID, err := diffHeader.GetShiftedMapping(t.Context(), 0)
348+
require.NoError(t, err)
349+
require.Equal(t, snapshotBuildID, *firstBlockBuildID, "zero-filled dirty block should still map to the snapshot diff")
350+
351+
_, _, secondBlockBuildID, err := diffHeader.GetShiftedMapping(t.Context(), blockSize)
352+
require.NoError(t, err)
353+
require.Equal(t, snapshotBuildID, *secondBlockBuildID)
354+
355+
_, _, thirdBlockBuildID, err := diffHeader.GetShiftedMapping(t.Context(), 2*blockSize)
356+
require.NoError(t, err)
357+
require.Equal(t, baseBuildID, *thirdBlockBuildID, "clean blocks should keep the base mapping")
358+
}
359+
360+
func TestCacheExportToDiff_NonContiguousDirtyBlocksPreserveRangeOrder(t *testing.T) {
361+
t.Parallel()
362+
363+
const blockSize = header.RootfsBlockSize
364+
const size = blockSize * 5
365+
366+
cache, err := NewCache(size, blockSize, t.TempDir()+"/cache", false)
367+
require.NoError(t, err)
368+
t.Cleanup(func() {
369+
require.NoError(t, cache.Close())
370+
})
371+
372+
firstBlock := bytes.Repeat([]byte{0x11}, int(blockSize))
373+
secondBlock := bytes.Repeat([]byte{0x22}, int(blockSize))
374+
375+
_, err = cache.WriteAt(firstBlock, 0)
376+
require.NoError(t, err)
377+
378+
_, err = cache.WriteAt(secondBlock, 3*blockSize)
379+
require.NoError(t, err)
380+
381+
out, err := os.CreateTemp(t.TempDir(), "diff-*")
382+
require.NoError(t, err)
383+
defer out.Close()
384+
385+
diffMetadata, err := cache.ExportToDiff(t.Context(), out)
386+
require.NoError(t, err)
387+
388+
require.EqualValues(t, 2, diffMetadata.Dirty.Count())
389+
require.True(t, diffMetadata.Dirty.Test(0))
390+
require.True(t, diffMetadata.Dirty.Test(3))
391+
require.EqualValues(t, 0, diffMetadata.Empty.Count())
392+
393+
_, err = out.Seek(0, io.SeekStart)
394+
require.NoError(t, err)
395+
exported, err := io.ReadAll(out)
396+
require.NoError(t, err)
397+
require.Equal(t, append(firstBlock, secondBlock...), exported)
398+
}
399+
221400
func compareData(readBytes []byte, expectedBytes []byte) error {
222401
// The bytes.Equal is the first place in this flow that actually touches the uffd managed memory and triggers the pagefault, so any deadlocks will manifest here.
223402
if !bytes.Equal(readBytes, expectedBytes) {

0 commit comments

Comments
 (0)