Files
s3/weed/mount/peer_fetcher_test.go
zooqueen dd74d3e6d6 Replace AGPL MinIO fork with Apache-2.0 SeaweedFS fork; rebrand CLI weed->s3
hanzoai/s3 was an AGPL-3.0 MinIO fork whose network-copyleft blocks embedding
and reselling as a managed service. Replace it wholesale with a fork of
SeaweedFS (github.com/seaweedfs/seaweedfs, Apache-2.0, Go) which ships a
built-in S3 gateway + Filer.

- License: AGPL-3.0 -> Apache-2.0 (upstream LICENSE kept verbatim, incl.
  "Copyright 2025 Chris Lu"); NOTICE added crediting SeaweedFS. No AGPL/BUSL/
  SSPL/GPL or enterprise ("ee") files remain.
- Module: go.mod path set to github.com/hanzoai/s3 with a self-replace
  (replace github.com/seaweedfs/seaweedfs => ./) so the ~1800 internal Go files
  keep their upstream import paths with zero churn. Internal identifiers and
  import paths are intentionally NOT renamed.
- CLI rebrand: the binary is built as `s3` (was `weed`); root usage, per-command
  help/usage/flag strings, shell autocomplete program name, and the product name
  in CLI output now read "Hanzo S3" / `s3`. Users never see `weed`.
  (e.g. `s3 server`, `s3 filer`, `s3 master`, `s3 volume`, `s3 s3`, `s3 mount`.)
- Build: `make` (or `go build -o s3 ./weed`) builds the `s3` binary green;
  `cmd/weed-db` and `cmd/weed-sql` also build; `go vet ./weed/command/` clean.
- README/LLM.md rewritten for Hanzo S3; AGENTS.md/CLAUDE.md symlinks preserved.
- Dropped a 33MB prebuilt Mach-O `stress` binary upstream had committed; `.gitignore`
  updated to ignore the `s3` build output.
2026-06-18 23:41:25 -07:00

181 lines
6.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mount
import (
"context"
"crypto/md5"
"encoding/base64"
"encoding/hex"
"testing"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
// newTestFetchServer stands up a minimal MountPeer gRPC server that only
// serves FetchChunk out of a fakeChunkCache. Goes through the same
// PeerGrpcServer.Start path production uses (via pb.NewGrpcServer with
// the standard keepalive + msg-size options) so future default server
// options are exercised by these tests too.
func newTestFetchServer(t *testing.T, cache *fakeChunkCache) (addr string, stop func()) {
t.Helper()
dir := NewPeerDirectory()
srv := NewPeerGrpcServer(cache, dir, nil, "")
if err := srv.Start("127.0.0.1:0"); err != nil {
t.Fatalf("start: %v", err)
}
return srv.Addr(), func() { srv.Stop() }
}
func etagOf(b []byte) string {
sum := md5.Sum(b)
return hex.EncodeToString(sum[:])
}
// etagOfBase64 mirrors how the real upload path produces FileChunk.ETag:
// base64 of the raw 16-byte MD5 digest. Kept alongside the hex form so
// we can test that both encodings work end-to-end.
func etagOfBase64(b []byte) string {
sum := md5.Sum(b)
return base64.StdEncoding.EncodeToString(sum[:])
}
func TestFetchChunkFromPeer_Hit(t *testing.T) {
cache := newFakeChunkCache()
payload := []byte("hello from peer stream")
cache.Put("3,abc", payload)
addr, stop := newTestFetchServer(t, cache)
defer stop()
dial := DefaultMountPeerDialer(grpc.WithTransportCredentials(insecure.NewCredentials()))
got, err := fetchChunkFromPeer(context.Background(), dial, addr, "3,abc", uint64(len(payload)), etagOf(payload))
if err != nil {
t.Fatalf("fetch: %v", err)
}
if string(got) != string(payload) {
t.Errorf("bytes mismatch: got %q want %q", got, payload)
}
}
// TestFetchChunkFromPeer_Base64Etag mirrors the real upload path where
// FileChunk.ETag is base64(md5) rather than hex(md5). A regression
// there would reject every valid peer response in production.
func TestFetchChunkFromPeer_Base64Etag(t *testing.T) {
cache := newFakeChunkCache()
payload := []byte("base64-etag peer bytes")
cache.Put("3,b64", payload)
addr, stop := newTestFetchServer(t, cache)
defer stop()
dial := DefaultMountPeerDialer(grpc.WithTransportCredentials(insecure.NewCredentials()))
got, err := fetchChunkFromPeer(context.Background(), dial, addr, "3,b64", uint64(len(payload)), etagOfBase64(payload))
if err != nil {
t.Fatalf("fetch: %v", err)
}
if string(got) != string(payload) {
t.Errorf("bytes mismatch: got %q want %q", got, payload)
}
}
func TestFetchChunkFromPeer_EtagMismatch(t *testing.T) {
cache := newFakeChunkCache()
payload := []byte("unexpected bytes")
cache.Put("3,abc", payload)
addr, stop := newTestFetchServer(t, cache)
defer stop()
dial := DefaultMountPeerDialer(grpc.WithTransportCredentials(insecure.NewCredentials()))
_, err := fetchChunkFromPeer(context.Background(), dial, addr, "3,abc", 0, "not-the-real-etag")
if err == nil {
t.Fatalf("expected etag mismatch error, got nil")
}
}
func TestFetchChunkFromPeer_NotFound(t *testing.T) {
cache := newFakeChunkCache()
addr, stop := newTestFetchServer(t, cache)
defer stop()
dial := DefaultMountPeerDialer(grpc.WithTransportCredentials(insecure.NewCredentials()))
_, err := fetchChunkFromPeer(context.Background(), dial, addr, "3,missing", 0, "")
if err == nil {
t.Errorf("expected error for missing fid, got nil")
}
}
// TestSortHoldersByLocality verifies that same-rack peers sort ahead of
// same-DC-different-rack peers, which in turn sort ahead of cross-DC
// peers, and that LRU order is preserved within each bucket.
func TestSortHoldersByLocality(t *testing.T) {
selfDC, selfRack := "dc1", "r1"
// Input order mimics a server-side LRU list (newest first).
holders := []peerHolder{
{addr: "far-newest", dc: "dc2", rack: "r9"}, // bucket 2 (diff DC)
{addr: "mid-newer", dc: "dc1", rack: "r2"}, // bucket 1 (same DC, diff rack)
{addr: "local-newer", dc: "dc1", rack: "r1"}, // bucket 0 (same rack)
{addr: "far-older", dc: "dc2", rack: "r9"}, // bucket 2
{addr: "mid-older", dc: "dc1", rack: "r2"}, // bucket 1
{addr: "local-older", dc: "dc1", rack: "r1"}, // bucket 0
{addr: "unlabeled-older", dc: "", rack: ""}, // bucket 2 (unknown)
}
sortHoldersByLocality(holders, selfDC, selfRack)
want := []string{
"local-newer", "local-older",
"mid-newer", "mid-older",
"far-newest", "far-older", "unlabeled-older",
}
if len(holders) != len(want) {
t.Fatalf("len got %d want %d", len(holders), len(want))
}
for i, w := range want {
if holders[i].addr != w {
t.Errorf("pos %d: got %q want %q (full: %+v)", i, holders[i].addr, w, holders)
}
}
}
// TestSortHoldersByLocality_NoSelfLabels — when the caller has no DC/rack
// labels, every peer falls to bucket 2 and the server-returned LRU order
// must pass through unchanged.
func TestSortHoldersByLocality_NoSelfLabels(t *testing.T) {
holders := []peerHolder{
{addr: "a", dc: "dc1", rack: "r1"},
{addr: "b", dc: "dc2", rack: "r2"},
{addr: "c", dc: "", rack: ""},
}
sortHoldersByLocality(holders, "", "")
want := []string{"a", "b", "c"}
for i, w := range want {
if holders[i].addr != w {
t.Errorf("pos %d: got %q want %q", i, holders[i].addr, w)
}
}
}
func TestFetchChunkFromPeer_MultiFrameChunkAssembledCorrectly(t *testing.T) {
cache := newFakeChunkCache()
// Just over 2× the stream frame size so we get at least three frames.
payload := make([]byte, fetchChunkStreamSize*2+42)
for i := range payload {
payload[i] = byte((i * 7) & 0xff)
}
cache.Put("3,large", payload)
addr, stop := newTestFetchServer(t, cache)
defer stop()
dial := DefaultMountPeerDialer(grpc.WithTransportCredentials(insecure.NewCredentials()))
got, err := fetchChunkFromPeer(context.Background(), dial, addr, "3,large", uint64(len(payload)), etagOf(payload))
if err != nil {
t.Fatalf("fetch: %v", err)
}
if len(got) != len(payload) {
t.Fatalf("len got %d want %d", len(got), len(payload))
}
for i := range payload {
if got[i] != payload[i] {
t.Fatalf("mismatch at offset %d", i)
}
}
}