116 lines
3.1 KiB
Go
116 lines
3.1 KiB
Go
//go:build unix
|
|
|
|
package cas
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
"testing"
|
|
)
|
|
|
|
func nlink(t *testing.T, path string) uint64 {
|
|
t.Helper()
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
st, ok := fi.Sys().(*syscall.Stat_t)
|
|
if !ok {
|
|
t.Skip("no stat_t on this platform")
|
|
}
|
|
return uint64(st.Nlink)
|
|
}
|
|
|
|
// The deduplication claim, stated as a filesystem fact: a deployed file and its
|
|
// blob are the same inode, so a second deployment of the same content costs a
|
|
// directory entry and nothing more.
|
|
func TestLinkIntoSharesTheInode(t *testing.T) {
|
|
s, deployDir := newStore(t, LinkHard)
|
|
const content = "shared across deployments\n"
|
|
d := put(t, s, content)
|
|
|
|
if got := nlink(t, s.Path(d)); got != 1 {
|
|
t.Fatalf("a fresh blob has %d links, want 1", got)
|
|
}
|
|
|
|
first := filepath.Join(deployDir, "dpl_one")
|
|
second := filepath.Join(deployDir, "dpl_two")
|
|
for _, dir := range []string{first, second} {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.LinkInto(d, dir, "index.html"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
if got := nlink(t, s.Path(d)); got != 3 {
|
|
t.Errorf("blob has %d links after two deployments, want 3 (blob + 2)", got)
|
|
}
|
|
blob, err := os.Stat(s.Path(d))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, dir := range []string{first, second} {
|
|
deployed, err := os.Stat(filepath.Join(dir, "index.html"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !os.SameFile(blob, deployed) {
|
|
t.Errorf("%s is a copy, not a hardlink to the blob", dir)
|
|
}
|
|
if got, err := os.ReadFile(filepath.Join(dir, "index.html")); err != nil || string(got) != content {
|
|
t.Errorf("%s: content = %q, %v", dir, got, err)
|
|
}
|
|
}
|
|
|
|
// Removing a deployment tree must not take the blob with it: the other
|
|
// deployment still references it, and so may other projects.
|
|
if err := os.RemoveAll(first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := nlink(t, s.Path(d)); got != 2 {
|
|
t.Errorf("blob has %d links after one tree was removed, want 2", got)
|
|
}
|
|
}
|
|
|
|
// The mode is what stops a deployed file from being written through into the
|
|
// blob every other project shares.
|
|
func TestDeployedFilesAreReadOnly(t *testing.T) {
|
|
for _, mode := range []LinkMode{LinkHard, LinkCopy} {
|
|
t.Run(string(mode), func(t *testing.T) {
|
|
s, deployDir := newStore(t, mode)
|
|
d := put(t, s, "content")
|
|
if err := os.MkdirAll(deployDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.LinkInto(d, deployDir, "index.html"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
fi, err := os.Stat(filepath.Join(deployDir, "index.html"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if perm := fi.Mode().Perm(); perm != blobMode {
|
|
t.Errorf("deployed file mode = %04o, want %04o", perm, blobMode)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A copy must be a copy: writing through it may not reach the blob.
|
|
func TestCopyModeDoesNotShareTheInode(t *testing.T) {
|
|
s, deployDir := newStore(t, LinkCopy)
|
|
d := put(t, s, "content")
|
|
if err := os.MkdirAll(deployDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := s.LinkInto(d, deployDir, "index.html"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := nlink(t, s.Path(d)); got != 1 {
|
|
t.Errorf("blob has %d links in copy mode, want 1", got)
|
|
}
|
|
}
|