//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) } }