| package fat32 |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "math/rand" |
| "os" |
| "strings" |
| "testing" |
| "time" |
| |
| "github.com/stretchr/testify/assert" |
| "github.com/stretchr/testify/require" |
| "golang.org/x/mod/semver" |
| "golang.org/x/sys/unix" |
| ) |
| |
| func TestKernelInterop(t *testing.T) { |
| if os.Getenv("IN_KTEST") != "true" { |
| t.Skip("Not in ktest") |
| } |
| |
| // ONCHANGE(//third_party/linux): Drop this once we move to a Kernel version |
| // newer than 5.19 which will have FAT btime support. |
| kernelVersion, err := os.ReadFile("/proc/sys/kernel/osrelease") |
| if err != nil { |
| t.Fatalf("unable to determine kernel version: %v", err) |
| } |
| haveBtime := semver.Compare("v"+string(kernelVersion), "v5.19.0") >= 0 |
| |
| type testCase struct { |
| name string |
| setup func(root *Inode) error |
| validate func(t *testing.T) error |
| } |
| |
| // Random timestamp in UTC, divisible by 10ms |
| testTimestamp1 := time.Date(2022, 03, 04, 5, 6, 7, 10, time.UTC) |
| // Random timestamp in UTC, divisible by 2s |
| testTimestamp2 := time.Date(2022, 03, 04, 5, 6, 8, 0, time.UTC) |
| // Random timestamp in UTC, divisible by 10ms |
| testTimestamp3 := time.Date(2052, 03, 02, 5, 6, 7, 10, time.UTC) |
| // Random timestamp in UTC, divisible by 2s |
| testTimestamp4 := time.Date(2052, 10, 04, 5, 3, 4, 0, time.UTC) |
| |
| testContent1 := "testcontent1" |
| |
| tests := []testCase{ |
| { |
| name: "SimpleFolder", |
| setup: func(root *Inode) error { |
| root.Children = []*Inode{{ |
| Name: "testdir", |
| Attrs: AttrDirectory, |
| CreateTime: testTimestamp1, |
| ModTime: testTimestamp2, |
| }} |
| return nil |
| }, |
| validate: func(t *testing.T) error { |
| var stat unix.Statx_t |
| if err := unix.Statx(0, "/dut/testdir", 0, unix.STATX_TYPE|unix.STATX_MTIME|unix.STATX_BTIME, &stat); err != nil { |
| availableFiles, err := os.ReadDir("/dut") |
| var availableFileNames []string |
| for _, f := range availableFiles { |
| availableFileNames = append(availableFileNames, f.Name()) |
| } |
| if err != nil { |
| t.Fatalf("Failed to list filesystem root directory: %v", err) |
| } |
| t.Fatalf("Failed to stat output: %v (available: %v)", err, strings.Join(availableFileNames, ", ")) |
| } |
| if stat.Mode&unix.S_IFDIR == 0 { |
| t.Errorf("testdir is expected to be a directory, but has mode %v", stat.Mode) |
| } |
| btime := time.Unix(stat.Btime.Sec, int64(stat.Btime.Nsec)) |
| if !btime.Equal(testTimestamp1) && haveBtime { |
| t.Errorf("testdir btime expected %v, got %v", testTimestamp1, btime) |
| } |
| mtime := time.Unix(stat.Mtime.Sec, int64(stat.Mtime.Nsec)) |
| if !mtime.Equal(testTimestamp2) { |
| t.Errorf("testdir mtime expected %v, got %v", testTimestamp2, mtime) |
| } |
| return nil |
| }, |
| }, |
| { |
| name: "SimpleFile", |
| setup: func(root *Inode) error { |
| root.Children = []*Inode{{ |
| Name: "testfile", |
| CreateTime: testTimestamp3, |
| ModTime: testTimestamp4, |
| Content: strings.NewReader(testContent1), |
| }} |
| return nil |
| }, |
| validate: func(t *testing.T) error { |
| var stat unix.Statx_t |
| if err := unix.Statx(0, "/dut/testfile", 0, unix.STATX_TYPE|unix.STATX_MTIME|unix.STATX_BTIME, &stat); err != nil { |
| t.Fatalf("failed to stat output: %v", err) |
| } |
| if stat.Mode&unix.S_IFREG == 0 { |
| t.Errorf("testfile is expected to be a file, but has mode %v", stat.Mode) |
| } |
| btime := time.Unix(stat.Btime.Sec, int64(stat.Btime.Nsec)) |
| if !btime.Equal(testTimestamp3) && haveBtime { |
| t.Errorf("testfile ctime expected %v, got %v", testTimestamp3, btime) |
| } |
| mtime := time.Unix(stat.Mtime.Sec, int64(stat.Mtime.Nsec)) |
| if !mtime.Equal(testTimestamp4) { |
| t.Errorf("testfile mtime expected %v, got %v", testTimestamp3, mtime) |
| } |
| contents, err := os.ReadFile("/dut/testfile") |
| if err != nil { |
| t.Fatalf("failed to read back test file: %v", err) |
| } |
| if string(contents) != testContent1 { |
| t.Errorf("testfile contains %x, got %x", contents, []byte(testContent1)) |
| } |
| return nil |
| }, |
| }, |
| { |
| name: "FolderHierarchy", |
| setup: func(i *Inode) error { |
| i.Children = []*Inode{{ |
| Name: "l1", |
| Attrs: AttrDirectory, |
| CreateTime: testTimestamp1, |
| ModTime: testTimestamp2, |
| Children: []*Inode{{ |
| Name: "l2", |
| Attrs: AttrDirectory, |
| CreateTime: testTimestamp1, |
| ModTime: testTimestamp2, |
| }}, |
| }} |
| return nil |
| }, |
| validate: func(t *testing.T) error { |
| dirInfo, err := os.ReadDir("/dut/l1") |
| if err != nil { |
| t.Fatalf("Failed to read top-level directory: %v", err) |
| } |
| require.Len(t, dirInfo, 1, "more subdirs than expected") |
| require.Equal(t, "l2", dirInfo[0].Name(), "unexpected subdir") |
| require.True(t, dirInfo[0].IsDir(), "l1 not a directory") |
| subdirInfo, err := os.ReadDir("/dut/l1/l2") |
| assert.NoError(t, err, "cannot read empty subdir") |
| require.Len(t, subdirInfo, 0, "unexpected subdirs in empty directory") |
| return nil |
| }, |
| }, |
| { |
| name: "LargeFile", |
| setup: func(i *Inode) error { |
| content := make([]byte, 6500) |
| io.ReadFull(rand.New(rand.NewSource(1)), content) |
| i.Children = []*Inode{{ |
| Name: "test.bin", |
| Content: bytes.NewReader(content), |
| }} |
| return nil |
| }, |
| validate: func(t *testing.T) error { |
| var stat unix.Stat_t |
| err := unix.Stat("/dut/test.bin", &stat) |
| assert.NoError(t, err, "failed to stat file") |
| require.EqualValues(t, 6500, stat.Size, "wrong size") |
| file, err := os.Open("/dut/test.bin") |
| assert.NoError(t, err, "failed to open test file") |
| defer file.Close() |
| r := io.LimitReader(rand.New(rand.NewSource(1)), 6500) // Random but deterministic data |
| expected, _ := io.ReadAll(r) |
| actual, err := io.ReadAll(file) |
| assert.NoError(t, err, "failed to read test file") |
| assert.Equal(t, expected, actual, "content not identical") |
| return nil |
| }, |
| }, |
| { |
| name: "Unicode", |
| setup: func(i *Inode) error { |
| i.Children = []*Inode{{ |
| Name: "✨😂", // Really exercise that UTF-16 conversion |
| Content: strings.NewReader("😂"), |
| }} |
| return nil |
| }, |
| validate: func(t *testing.T) error { |
| file, err := os.Open("/dut/✨😂") |
| if err != nil { |
| availableFiles, err := os.ReadDir("/dut") |
| var availableFileNames []string |
| for _, f := range availableFiles { |
| availableFileNames = append(availableFileNames, f.Name()) |
| } |
| if err != nil { |
| t.Fatalf("Failed to list filesystem root directory: %v", err) |
| } |
| t.Fatalf("Failed to open unicode file: %v (available files: %v)", err, strings.Join(availableFileNames, ", ")) |
| } |
| defer file.Close() |
| contents, err := io.ReadAll(file) |
| if err != nil { |
| t.Errorf("Wrong content: expected %x, got %x", []byte("😂"), contents) |
| } |
| return nil |
| }, |
| }, |
| { |
| name: "MultipleMetaClusters", |
| setup: func(root *Inode) error { |
| // Only test up to 2048 files as Linux gets VERY slow if going |
| // up to the maximum of approximately 32K |
| for i := 0; i < 2048; i++ { |
| root.Children = append(root.Children, &Inode{ |
| Name: fmt.Sprintf("verylongtestfilename%d", i), |
| Content: strings.NewReader("random test content"), |
| }) |
| } |
| return nil |
| }, |
| validate: func(t *testing.T) error { |
| files, err := os.ReadDir("/dut") |
| if err != nil { |
| t.Errorf("failed to list directory: %v", err) |
| } |
| if len(files) != 2048 { |
| t.Errorf("wrong number of files: expected %d, got %d", 2048, len(files)) |
| } |
| return nil |
| }, |
| }, |
| } |
| |
| for _, test := range tests { |
| t.Run(test.name, func(t *testing.T) { |
| file, err := os.OpenFile("/dev/ram0", os.O_WRONLY|os.O_TRUNC, 0644) |
| if err != nil { |
| t.Fatalf("failed to create test image: %v", err) |
| } |
| size, err := unix.IoctlGetInt(int(file.Fd()), unix.BLKGETSIZE64) |
| if err != nil { |
| t.Fatalf("failed to get ramdisk size: %v", err) |
| } |
| blockSize, err := unix.IoctlGetInt(int(file.Fd()), unix.BLKBSZGET) |
| if err != nil { |
| t.Fatalf("failed to get ramdisk block size: %v", err) |
| } |
| defer file.Close() |
| rootInode := Inode{ |
| Attrs: AttrDirectory, |
| } |
| if err := test.setup(&rootInode); err != nil { |
| t.Fatalf("setup failed: %v", err) |
| } |
| if err := WriteFS(file, rootInode, Options{ |
| ID: 1234, |
| Label: "KTEST", |
| BlockSize: uint16(blockSize), |
| BlockCount: uint32(size / blockSize), |
| }); err != nil { |
| t.Fatalf("failed to write fileystem: %v", err) |
| } |
| _ = file.Close() |
| if err := os.MkdirAll("/dut", 0755); err != nil { |
| t.Error(err) |
| } |
| // TODO(lorenz): Set CONFIG_FAT_DEFAULT_UTF8 for Monogon Kernel |
| if err := unix.Mount("/dev/ram0", "/dut", "vfat", unix.MS_NOEXEC|unix.MS_NODEV, "utf8=1"); err != nil { |
| t.Fatal(err) |
| } |
| defer unix.Unmount("/dut", 0) |
| test.validate(t) |
| }) |
| |
| } |
| } |