diff --git a/fs/copy_linux_test.go b/fs/copy_linux_test.go new file mode 100644 index 00000000..b5dec42c --- /dev/null +++ b/fs/copy_linux_test.go @@ -0,0 +1,97 @@ +// +build linux + +/* + Copyright The containerd 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. +*/ + +package fs + +import ( + "io" + "path/filepath" + "math/rand" + "io/ioutil" + "os" + "os/exec" + "testing" + + "github.com/containerd/continuity/testutil" + "github.com/containerd/continuity/testutil/loopback" +) + +func TestCopyReflinkWithXFS(t *testing.T) { + testutil.RequiresRoot(t) + mnt, err := ioutil.TempDir("", "containerd-test-copy-reflink-with-xfs") + if err != nil { + t.Fatal(err) + } + defer os.RemoveAll(mnt) + + loop, err := loopback.New(1 << 30) // sparse file (max=1GB) + if err != nil { + t.Fatal(err) + } + mkfs := []string{"mkfs.xfs", "-m", "crc=1", "-n", "ftype=1", "-m", "reflink=1"} + if out, err := exec.Command(mkfs[0], append(mkfs[1:], loop.Device)...).CombinedOutput(); err != nil { + // not fatal + t.Skipf("could not mkfs (%v) %s: %v (out: %q)", mkfs, loop.Device, err, string(out)) + } + loopbackSize, err := loop.HardSize() + if err != nil { + t.Fatal(err) + } + t.Logf("Loopback file size (after mkfs (%v)): %d", mkfs, loopbackSize) + if out, err := exec.Command("mount", loop.Device, mnt).CombinedOutput(); err != nil { + // not fatal + t.Skipf("could not mount %s: %v (out: %q)", loop.Device, err, string(out)) + } + unmounted := false + defer func() { + if !unmounted{ + testutil.Unmount(t, mnt) + } + loop.Close() + }() + + aPath := filepath.Join(mnt, "a") + aSize := int64(100 << 20) // 100MB + a, err := os.Create(aPath) + if err != nil { + t.Fatal(err) + } + randReader := rand.New(rand.NewSource(42)) + if _, err := io.CopyN(a, randReader, aSize); err != nil { + a.Close() + t.Fatal(err) + } + if err := a.Close(); err != nil { + t.Fatal(err) + } + bPath := filepath.Join(mnt, "b") + if err := CopyFile(bPath, aPath); err != nil { + t.Fatal(err) + } + testutil.Unmount(t, mnt) + unmounted = true + loopbackSize, err = loop.HardSize() + if err != nil { + t.Fatal(err) + } + t.Logf("Loopback file size (after copying a %d-byte file): %d", aSize, loopbackSize) + allowedSize := int64(120 << 20) // 120MB + if loopbackSize > allowedSize { + t.Fatalf("expected <= %d, got %d", allowedSize, loopbackSize) + } +} diff --git a/fs/dtype_linux_test.go b/fs/dtype_linux_test.go index 891a324d..403b185d 100644 --- a/fs/dtype_linux_test.go +++ b/fs/dtype_linux_test.go @@ -36,21 +36,21 @@ func testSupportsDType(t *testing.T, expected bool, mkfs ...string) { } defer os.RemoveAll(mnt) - deviceName, cleanupDevice, err := loopback.New(100 << 20) // 100 MB + loop, err := loopback.New(100 << 20) // 100 MB if err != nil { t.Fatal(err) } - if out, err := exec.Command(mkfs[0], append(mkfs[1:], deviceName)...).CombinedOutput(); err != nil { + if out, err := exec.Command(mkfs[0], append(mkfs[1:], loop.Device)...).CombinedOutput(); err != nil { // not fatal - t.Skipf("could not mkfs (%v) %s: %v (out: %q)", mkfs, deviceName, err, string(out)) + t.Skipf("could not mkfs (%v) %s: %v (out: %q)", mkfs, loop.Device, err, string(out)) } - if out, err := exec.Command("mount", deviceName, mnt).CombinedOutput(); err != nil { + if out, err := exec.Command("mount", loop.Device, mnt).CombinedOutput(); err != nil { // not fatal - t.Skipf("could not mount %s: %v (out: %q)", deviceName, err, string(out)) + t.Skipf("could not mount %s: %v (out: %q)", loop.Device, err, string(out)) } defer func() { testutil.Unmount(t, mnt) - cleanupDevice() + loop.Close() }() // check whether it supports d_type result, err := SupportsDType(mnt) diff --git a/testutil/loopback/loopback_linux.go b/testutil/loopback/loopback_linux.go index b1736117..c404c269 100644 --- a/testutil/loopback/loopback_linux.go +++ b/testutil/loopback/loopback_linux.go @@ -22,24 +22,25 @@ import ( "io/ioutil" "os" "os/exec" + "syscall" "strings" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -// New creates a loopback device, and returns its device name (/dev/loopX), and its clean-up function. -func New(size int64) (string, func() error, error) { +// New creates a loopback device +func New(size int64) (*Loopback, error) { // create temporary file for the disk image file, err := ioutil.TempFile("", "containerd-test-loopback") if err != nil { - return "", nil, errors.Wrap(err, "could not create temporary file for loopback") + return nil, errors.Wrap(err, "could not create temporary file for loopback") } if err := file.Truncate(size); err != nil { file.Close() os.Remove(file.Name()) - return "", nil, errors.Wrap(err, "failed to resize temp file") + return nil, errors.Wrap(err, "failed to resize temp file") } file.Close() @@ -48,7 +49,7 @@ func New(size int64) (string, func() error, error) { p, err := losetup.Output() if err != nil { os.Remove(file.Name()) - return "", nil, errors.Wrap(err, "loopback setup failed") + return nil, errors.Wrap(err, "loopback setup failed") } deviceName := strings.TrimSpace(string(p)) @@ -68,5 +69,47 @@ func New(size int64) (string, func() error, error) { return os.Remove(file.Name()) } - return deviceName, cleanup, nil + l := Loopback{ + File: file.Name(), + Device: deviceName, + close: cleanup, + } + return &l, nil +} + +// Loopback device +type Loopback struct { + // File is the underlying sparse file + File string + // Device is /dev/loopX + Device string + close func() error +} + +// SoftSize returns st_size +func (l *Loopback) SoftSize() (int64, error) { + st, err := os.Stat(l.File) + if err != nil { + return 0, err + } + return st.Size(), nil +} + +// HardSize returns st_blocks * 512; see stat(2) +func (l *Loopback) HardSize() (int64, error) { + st, err := os.Stat(l.File) + if err != nil { + return 0, err + } + st2, ok := st.Sys().(*syscall.Stat_t) + if !ok { + return 0, errors.New("st.Sys() is not a *syscall.Stat_t") + } + // NOTE: st_blocks has nothing to do with st_blksize; see stat(2) + return st2.Blocks * 512, nil +} + +// Close detaches the device and removes the underlying file +func (l *Loopback) Close() error { + return l.close() }