mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-08-19 20:36:30 +00:00
149 lines
4.0 KiB
Go
149 lines
4.0 KiB
Go
package content
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/opencontainers/go-digest"
|
|
)
|
|
|
|
// writeTestBlob writes content through WriteBlob and returns its digest.
|
|
func writeTestBlob(t *testing.T, o *OCI, body string) digest.Digest {
|
|
t.Helper()
|
|
dg := digest.FromString(body)
|
|
err := o.WriteBlob(context.Background(), dg, int64(len(body)), func() (io.ReadCloser, error) {
|
|
return io.NopCloser(strings.NewReader(body)), nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("WriteBlob: %v", err)
|
|
}
|
|
return dg
|
|
}
|
|
|
|
func TestIOStatsCountsWrittenAndCached(t *testing.T) {
|
|
o, err := NewOCI(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewOCI: %v", err)
|
|
}
|
|
|
|
writeTestBlob(t, o, "hello world")
|
|
|
|
st := o.Stats().Snapshot()
|
|
if st.BlobsWritten != 1 {
|
|
t.Fatalf("BlobsWritten = %d, want 1", st.BlobsWritten)
|
|
}
|
|
if st.BlobsCached != 0 {
|
|
t.Fatalf("BlobsCached = %d, want 0", st.BlobsCached)
|
|
}
|
|
if st.BlobBytesWritten != int64(len("hello world")) {
|
|
t.Fatalf("BlobBytesWritten = %d, want %d", st.BlobBytesWritten, len("hello world"))
|
|
}
|
|
|
|
// Same digest again: must hit the os.Stat fast path, not rewrite.
|
|
writeTestBlob(t, o, "hello world")
|
|
|
|
st = o.Stats().Snapshot()
|
|
if st.BlobsWritten != 1 {
|
|
t.Fatalf("BlobsWritten = %d after rewrite, want 1", st.BlobsWritten)
|
|
}
|
|
if st.BlobsCached != 1 {
|
|
t.Fatalf("BlobsCached = %d, want 1", st.BlobsCached)
|
|
}
|
|
if st.BlobBytesWritten != int64(len("hello world")) {
|
|
t.Fatalf("BlobBytesWritten = %d after cache hit, want unchanged", st.BlobBytesWritten)
|
|
}
|
|
}
|
|
|
|
func TestIOStatsPeakInFlightNeverExceedsCeiling(t *testing.T) {
|
|
const ceiling = 3
|
|
o, err := NewOCI(t.TempDir(), WithBlobConcurrency(ceiling))
|
|
if err != nil {
|
|
t.Fatalf("NewOCI: %v", err)
|
|
}
|
|
if got := o.BlobConcurrency(); got != ceiling {
|
|
t.Fatalf("BlobConcurrency() = %d, want %d", got, ceiling)
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 40; i++ {
|
|
wg.Add(1)
|
|
go func(i int) {
|
|
defer wg.Done()
|
|
body := strings.Repeat("x", 1024) + string(rune('a'+i%26)) + strings.Repeat("y", i)
|
|
dg := digest.FromString(body)
|
|
_ = o.WriteBlob(context.Background(), dg, int64(len(body)), func() (io.ReadCloser, error) {
|
|
return io.NopCloser(strings.NewReader(body)), nil
|
|
})
|
|
}(i)
|
|
}
|
|
wg.Wait()
|
|
|
|
st := o.Stats().Snapshot()
|
|
if st.BlobPeakInFlight > ceiling {
|
|
t.Fatalf("BlobPeakInFlight = %d, must never exceed the ceiling of %d", st.BlobPeakInFlight, ceiling)
|
|
}
|
|
if st.BlobPeakInFlight < 1 {
|
|
t.Fatalf("BlobPeakInFlight = %d, expected at least 1 concurrent write to be observed", st.BlobPeakInFlight)
|
|
}
|
|
}
|
|
|
|
func TestIOStatsCountsIndexWrites(t *testing.T) {
|
|
o, err := NewOCI(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewOCI: %v", err)
|
|
}
|
|
// o.index is only populated by LoadIndex; SaveIndex on a bare OCI (no
|
|
// prior LoadIndex) is a nil-pointer panic by design -- see newTestOCI's
|
|
// doc comment in oci_concurrency_test.go. Mirror that same pattern here.
|
|
if err := o.LoadIndex(); err != nil {
|
|
t.Fatalf("LoadIndex: %v", err)
|
|
}
|
|
|
|
if err := o.SaveIndex(); err != nil {
|
|
t.Fatalf("SaveIndex: %v", err)
|
|
}
|
|
|
|
st := o.Stats().Snapshot()
|
|
if st.IndexWrites != 1 {
|
|
t.Fatalf("IndexWrites = %d, want 1", st.IndexWrites)
|
|
}
|
|
if st.IndexBytesWritten <= 0 {
|
|
t.Fatalf("IndexBytesWritten = %d, want > 0", st.IndexBytesWritten)
|
|
}
|
|
}
|
|
|
|
func TestIOStatsRecordsLockWait(t *testing.T) {
|
|
o, err := NewOCI(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewOCI: %v", err)
|
|
}
|
|
// See TestIOStatsCountsIndexWrites: o.index must be populated before
|
|
// SaveIndex can run, so load it before we grab o.mu below.
|
|
if err := o.LoadIndex(); err != nil {
|
|
t.Fatalf("LoadIndex: %v", err)
|
|
}
|
|
|
|
// Hold the mutex directly so the next lock() call must block on it.
|
|
o.mu.Lock()
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
_ = o.SaveIndex()
|
|
}()
|
|
|
|
// Give the goroutine time to reach lock() and block there.
|
|
time.Sleep(50 * time.Millisecond)
|
|
o.mu.Unlock()
|
|
<-done
|
|
|
|
st := o.Stats().Snapshot()
|
|
if st.IndexLockWait < 10*time.Millisecond {
|
|
t.Fatalf("IndexLockWait = %v, want at least 10ms of recorded contention", st.IndexLockWait)
|
|
}
|
|
}
|