Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/silkd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,4 @@ jobs:

- name: root-only tests
working-directory: silkd
run: sudo -E env "PATH=$PATH" "HOME=$HOME" cargo test --locked --test exec_e2e --test git_e2e --test fs_e2e --test find_e2e --test tun_e2e -- --ignored --skip throughput
run: sudo -E env "PATH=$PATH" "HOME=$HOME" cargo test --locked --test exec_e2e --test git_e2e --test fs_e2e --test find_e2e --test tun_e2e -- --ignored
41 changes: 22 additions & 19 deletions e2e/internal/harness/harness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"net/http"
"strings"
"testing"
"testing/synctest"
"time"
)

Expand Down Expand Up @@ -53,29 +54,31 @@ func TestPortRelayPreservesGreetingAndHalfClose(t *testing.T) {
}
}

func TestPoll(t *testing.T) {
errTry := errors.New("not yet")
func TestPollRetriesUntilSuccessThenReturnsLastErrorOrContextError(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
errTry := errors.New("not yet")

calls := 0
if err := Poll(t.Context(), time.Second, time.Millisecond, func() error {
calls++
if calls < 3 {
return errTry
calls := 0
if err := Poll(t.Context(), time.Second, time.Millisecond, func() error {
calls++
if calls < 3 {
return errTry
}
return nil
}); err != nil || calls != 3 {
t.Errorf("success: err %v calls %d, want nil and 3", err, calls)
}
return nil
}); err != nil || calls != 3 {
t.Errorf("success: err %v calls %d, want nil and 3", err, calls)
}

if err := Poll(t.Context(), 5*time.Millisecond, time.Millisecond, func() error { return errTry }); !errors.Is(err, errTry) {
t.Errorf("exhausted: err %v, want the last try error", err)
}
if err := Poll(t.Context(), 5*time.Millisecond, time.Millisecond, func() error { return errTry }); !errors.Is(err, errTry) {
t.Errorf("exhausted: err %v, want the last try error", err)
}

ctx, cancel := context.WithCancel(t.Context())
cancel()
if err := Poll(ctx, time.Minute, time.Minute, func() error { return errTry }); !errors.Is(err, context.Canceled) {
t.Errorf("canceled: err %v, want context.Canceled", err)
}
ctx, cancel := context.WithCancel(t.Context())
cancel()
if err := Poll(ctx, time.Minute, time.Minute, func() error { return errTry }); !errors.Is(err, context.Canceled) {
t.Errorf("canceled: err %v, want context.Canceled", err)
}
})
}

func serveGuestFirstHalfClose(listener net.Listener) error {
Expand Down
2 changes: 2 additions & 0 deletions e2e/volume_mutation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ func TestLiveVolumesEndToEnd(t *testing.T) {
}
if err := sb.Hibernate(t.Context()); err == nil {
t.Fatal("captured an attached volume")
} else if refusal, ok := errors.AsType[*sandbox.APIError](err); !ok || refusal.Status != http.StatusConflict {
t.Fatalf("attached capture refusal: %v", err)
}
for range 2 {
got, err := sb.SetVolumes(t.Context(), []sandbox.Volume{})
Expand Down
7 changes: 7 additions & 0 deletions e2e/wire_parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ func TestSDKStructsMatchServerWireShape(t *testing.T) {
t.Errorf("%s: field %q is in the SDK but not on the server", tt.sdk, name)
}
}
for _, name := range tt.allow {
_, onServer := want[name]
_, inSDK := got[name]
if !onServer && !inSDK {
t.Errorf("%s: allowlisted field %q is on neither side", tt.sdk, name)
}
}
})
}
}
Expand Down
17 changes: 11 additions & 6 deletions mcp/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var infoFrame = func() string {
return string(frame) + "\n"
}()

func TestServeSpeaksMCP(t *testing.T) {
func TestServeAnswersEveryRequestAndSkipsNotifications(t *testing.T) {
srv := newTestServer(t, func(w http.ResponseWriter, r *http.Request) bool {
if !strings.HasSuffix(r.URL.Path, "/checkpoint") {
return false
Expand Down Expand Up @@ -177,13 +177,13 @@ func TestToolRepliesReplaceInvalidUTF8(t *testing.T) {
}

func TestCloseBoxesWaitsForEveryReleaseOnce(t *testing.T) {
var releases atomic.Int32
var arrived sync.WaitGroup
arrived.Add(3)
var requests, releases atomic.Int32
arrived := make(chan struct{}, 8)
gate := make(chan struct{})
node := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/release") {
arrived.Done()
requests.Add(1)
arrived <- struct{}{}
<-gate
releases.Add(1)
}
Expand Down Expand Up @@ -211,9 +211,14 @@ func TestCloseBoxesWaitsForEveryReleaseOnce(t *testing.T) {
}
<-called
<-called
arrived.Wait()
for range 3 {
<-arrived
}
close(gate)
wg.Wait()
if got := requests.Load(); got != 3 {
t.Errorf("release requests = %d, want 3 (one per box across both callers)", got)
}
}

func TestExecRejectsAnEmptyCommand(t *testing.T) {
Expand Down
11 changes: 9 additions & 2 deletions sandboxd/archives/archivestest/archivestest.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
const (
racers = 8

liveLease = 30 * time.Second
shortLease = 300 * time.Millisecond
leaseWait = 10 * time.Second
)
Expand Down Expand Up @@ -518,7 +519,7 @@ func testReplace(t *testing.T, r archives.Registry) {
func testTakeLease(t *testing.T, r archives.Registry) {
ctx := t.Context()
first := mustPut(t, r, record("sb_1", "a", time.Time{}))
if err := r.Renew(ctx, "a", shortLease); err != nil {
if err := r.Renew(ctx, "a", liveLease); err != nil {
t.Fatalf("renew: %v", err)
}
if _, err := r.Take(ctx, "sb_1", first, "b"); !errors.Is(err, archives.ErrLost) {
Expand All @@ -531,6 +532,9 @@ func testTakeLease(t *testing.T, r archives.Registry) {
if err != nil || own <= first {
t.Fatalf("the owner's own take with a live lease: %d %v, want a new epoch", own, err)
}
if err = r.Renew(ctx, "a", shortLease); err != nil {
t.Fatalf("renew short: %v", err)
}
eventually(t, "the owner's lease to expire", func() bool {
_, err = r.Take(ctx, "sb_1", own, "b")
return err == nil
Expand Down Expand Up @@ -597,13 +601,16 @@ func testSweepableLease(t *testing.T, r archives.Registry) {
past := time.Now().Add(-time.Minute)
mustPut(t, r, record("sb_live", "a", past))
mustPut(t, r, record("sb_dead", "z", past))
if err := r.Renew(ctx, "a", shortLease); err != nil {
if err := r.Renew(ctx, "a", liveLease); err != nil {
t.Fatalf("renew: %v", err)
}
got, err := r.Sweepable(ctx, time.Now(), "me")
if err != nil || len(got) != 1 || got[0].ID != "sb_dead" {
t.Fatalf("sweepable while a's lease is live: %+v %v, want sb_dead only", got, err)
}
if err = r.Renew(ctx, "a", shortLease); err != nil {
t.Fatalf("renew short: %v", err)
}
eventually(t, "a's lease to expire", func() bool {
got, err = r.Sweepable(ctx, time.Now(), "me")
return err == nil && len(got) == 2
Expand Down
3 changes: 3 additions & 0 deletions sandboxd/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ func TestWriteCAFilesRefusesOverwrite(t *testing.T) {
if err := writeCAFiles(dir, "root", []byte("cert2"), []byte("key2"), false); err == nil {
t.Error("second write without -force overwrote an existing key")
}
if got, err := os.ReadFile(filepath.Join(dir, "root.key")); err != nil || string(got) != "key" {
t.Errorf("root.key after the refused write = %q, %v; want the original key", got, err)
}
if err := writeCAFiles(dir, "root", []byte("cert2"), []byte("key2"), true); err != nil {
t.Errorf("-force write: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/config/client_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package config

import "testing"

func TestClientAdvertise(t *testing.T) {
func TestClientAdvertiseAcceptsAbsoluteHTTPOriginsOnly(t *testing.T) {
for _, addr := range []string{"", "https://node.example", "https://node.example:8443/", "http://[::1]:7777"} {
t.Run(addr, func(t *testing.T) {
cfg := Config{ClientAdvertise: addr}
Expand Down
7 changes: 7 additions & 0 deletions sandboxd/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,13 @@ func TestLoadAcceptsAShardedNetworkList(t *testing.T) {
}
}

func TestPoolSpecRejectsEmptyWarmupArgument(t *testing.T) {
spec := PoolSpec{Template: "rt:24.04", Net: types.NetNone, Size: types.SizeSmall, Warm: 1, Warmup: []string{"node", ""}}
if err := spec.ValidateLimits(); err == nil || !strings.Contains(err.Error(), "warmup") {
t.Errorf("ValidateLimits error = %v, want an empty-argument rejection", err)
}
}

func writeConfig(t *testing.T, body string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "config.json")
Expand Down
2 changes: 1 addition & 1 deletion sandboxd/egress/ca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ func TestLoadCARejectsNonCertBlockInRoot(t *testing.T) {
}
}

func TestFingerprintIsRootAndStable(t *testing.T) {
func TestFingerprintTracksOnlyTheRoot(t *testing.T) {
rootCert, rootKey := testRoot(t, "root")
c1, k1 := testIntermediate(t, rootCert, rootKey, "n1")
c2, k2 := testIntermediate(t, rootCert, rootKey, "n2")
Expand Down
76 changes: 49 additions & 27 deletions sandboxd/egress/limit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import (
"net"
"net/http"
"net/http/httptest"
"runtime"
"sync"
"sync/atomic"
"testing"
"time"

"golang.org/x/time/rate"

"github.com/cocoonstack/sandbox/sandboxd/types"
)

Expand Down Expand Up @@ -43,33 +46,31 @@ func TestALimitCapsOneDirectionOfATunnel(t *testing.T) {
if got.took < 700*time.Millisecond {
t.Errorf("upload of two seconds' worth took %v under a limit with a one-second burst", got.took)
}
if down > 500*time.Millisecond {
if down > 5*time.Second {
t.Errorf("download took %v with only tx limited", down)
}
})
}
}

func TestChangingTheLimitMovesAnOpenTunnel(t *testing.T) {
sink, received := sinkServer(t)
p, _, conn := openConnect(t, sink)
go writeUntilClosed(conn)
waitBytes(t, received, 1<<20)
addr, s := sinkServer(t)
p, _, conn := openConnect(t, addr)
if _, err := conn.Write(make([]byte, 1<<20)); err != nil {
t.Fatalf("write: %v", err)
}
waitBytes(t, s, 1<<20)

p.SetLimit(types.EgressLimit{TxBytesPerSecond: testRate / 2})
time.Sleep(500 * time.Millisecond)
before := received.Load()
time.Sleep(time.Second)
if moved := received.Load() - before; moved < testRate/4 || moved > testRate {
t.Errorf("moved %d bytes in the second after the limit dropped to %d/s", moved, testRate/2)
start := time.Now()
go writeUntilClosed(conn)
waitBytes(t, s, 1<<20+limitBurst+testRate/2)
if took := time.Since(start); took < 700*time.Millisecond {
t.Errorf("a burst and a second's worth past it took %v after the limit dropped to %d/s", took, testRate/2)
}

p.SetLimit(types.EgressLimit{})
before = received.Load()
time.Sleep(300 * time.Millisecond)
if moved := received.Load() - before; moved < 4<<20 {
t.Errorf("moved %d bytes in 300ms after the limit was lifted", moved)
}
waitBytes(t, s, s.received.Load()+4<<20)
}

func TestChangingOneDirectionKeepsTheOtherBucket(t *testing.T) {
Expand All @@ -92,15 +93,15 @@ func TestChangingOneDirectionKeepsTheOtherBucket(t *testing.T) {
}

func TestCloseReleasesAReadWaitingForTokens(t *testing.T) {
sink, received := sinkServer(t)
addr, s := sinkServer(t)
rows := make(chan int64, 4)
p, _, conn := openConnect(t, sink, func(p *Proxy) {
p, _, conn := openConnect(t, addr, func(p *Proxy) {
p.OnTransfer(func(_ Event, sent, _ int64) { rows <- sent })
})
p.SetLimit(types.EgressLimit{TxBytesPerSecond: 64})
go writeUntilClosed(conn)
waitBytes(t, received, limitBurst)
time.Sleep(100 * time.Millisecond)
waitBytes(t, s, limitBurst)
waitDebt(t, p.limits.Load().tx)

start := time.Now()
p.Close()
Expand Down Expand Up @@ -287,6 +288,11 @@ type bulk struct {
uploads chan upload
}

type sink struct {
received atomic.Int64
reads chan struct{}
}

func openConnect(t *testing.T, target string, edits ...proxyEdit) (*Proxy, io.Reader, net.Conn) {
t.Helper()
p := New(Policy{Allow: []Rule{{Host: "bulk.internal"}}}, nil, nil, fixedDial(target), nil, nil)
Expand Down Expand Up @@ -382,14 +388,14 @@ func bulkServer(t *testing.T, down int) *bulk {
return b
}

func sinkServer(t *testing.T) (string, *atomic.Int64) {
func sinkServer(t *testing.T) (string, *sink) {
t.Helper()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen sink: %v", err)
}
t.Cleanup(func() { _ = ln.Close() })
received := new(atomic.Int64)
s := &sink{reads: make(chan struct{}, 1)}
go func() {
conn, err := ln.Accept()
if err != nil {
Expand All @@ -399,22 +405,38 @@ func sinkServer(t *testing.T) (string, *atomic.Int64) {
buf := make([]byte, 64<<10)
for {
n, err := conn.Read(buf)
received.Add(int64(n))
s.received.Add(int64(n))
select {
case s.reads <- struct{}{}:
default:
}
if err != nil {
return
}
}
}()
return ln.Addr().String(), received
return ln.Addr().String(), s
}

func waitBytes(t *testing.T, s *sink, want int64) {
t.Helper()
deadline := time.After(5 * time.Second)
for s.received.Load() < want {
select {
case <-s.reads:
case <-deadline:
t.Fatalf("sink got %d bytes, want %d", s.received.Load(), want)
}
}
}

func waitBytes(t *testing.T, received *atomic.Int64, want int64) {
func waitDebt(t *testing.T, lim *rate.Limiter) {
t.Helper()
deadline := time.Now().Add(5 * time.Second)
for received.Load() < want {
for lim.Tokens() >= 0 {
if time.Now().After(deadline) {
t.Fatalf("sink got %d bytes, want %d", received.Load(), want)
t.Fatalf("the bucket kept %v tokens", lim.Tokens())
}
time.Sleep(10 * time.Millisecond)
runtime.Gosched()
}
}
Loading
Loading