Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
ahrav committed Sep 26, 2024
1 parent 470e873 commit 6e9449b
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 1 deletion.
1 change: 1 addition & 0 deletions pkg/hasher/hasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type Hasher interface {
// It uses the hash.Hash interface from the standard library to perform the actual hashing.
// This implementation is not safe for concurrent use. Each goroutine/worker should
// use its own instance of baseHasher for concurrent operations.
// Implementations that require concurrent access should wrap baseHasher with a mutex. (e.g., MutexHasher)
type baseHasher struct{ hash hash.Hash }

// InputTooLargeError is returned when the input data exceeds the maximum allowed size.
Expand Down
128 changes: 127 additions & 1 deletion pkg/hasher/hasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bytes"
"encoding/hex"
"errors"
"fmt"
"sync"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -101,7 +103,7 @@ func TestBaseHasherHashIdempotency(t *testing.T) {
t.Parallel()

hasher := NewFNVHasher()
input := []byte("Idempotency test")
input := bytes.Repeat([]byte("a"), maxInputSize)

hash1, err1 := hasher.Hash(input)
assert.NoError(t, err1, "unexpected error on first hash")
Expand All @@ -113,3 +115,127 @@ func TestBaseHasherHashIdempotency(t *testing.T) {
t.Errorf("hash results are not identical.\nFirst: %x\nSecond: %x", hash1, hash2)
}
}

// TestMutexHasherConcurrentHash verifies that MutexHasher is thread-safe
// and produces consistent hash results when used concurrently.
func TestMutexHasherConcurrentHash(t *testing.T) {
t.Parallel()

mutexHasher := NewMutexHasher(NewSHA256Hasher())

input := []byte("Concurrent Hashing Test")

const (
numGoroutines = 512
numIterations = 10_000
)

// Compute the expected hash once for comparison.
expectedHash, err := mutexHasher.Hash(input)
assert.NoError(t, err, "unexpected error computing expected hash")

// Channel to collect errors from goroutines.
// Buffered to prevent goroutines from blocking if the main thread is slow.
errs := make(chan error, numGoroutines*numIterations)

// WaitGroup to synchronize all goroutines.
var wg sync.WaitGroup
wg.Add(numGoroutines)

// Launch multiple goroutines to perform hashing concurrently.
for i := range numGoroutines {
go func(goroutineID int) {
defer wg.Done()
for j := range numIterations {
hash, err := mutexHasher.Hash(input)
if err != nil {
errs <- fmt.Errorf("goroutine %d: hash error: %v", goroutineID, err)
continue
}
if !bytes.Equal(hash, expectedHash) {
errs <- fmt.Errorf("goroutine %d: hash mismatch on iteration %d", goroutineID, j)
}
}
}(i)
}

wg.Wait()
close(errs)

// Collect and report any errors.
for err := range errs {
t.Error(err)
}
}

// BenchmarkHasherWithMutex benchmarks hashing using a single SHA-256 Hasher instance
// protected by a sync.Mutex across multiple goroutines.
func BenchmarkHasherWithMutex_SHA256(b *testing.B) {
sampleData := []byte("The quick brown fox jumps over the lazy dog")

// Initialize a single Hasher instance wrapped with a mutex.
mutexHasher := NewMutexHasher(NewSHA256Hasher())

b.ReportAllocs() // Report memory allocations.
b.ResetTimer() // Reset the timer to exclude setup time.

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := mutexHasher.Hash(sampleData)
assert.NoError(b, err)
}
})
}

// BenchmarkHasherPerGoroutine benchmarks hashing using separate SHA-256 Hasher instances
// for each goroutine, eliminating the need for synchronization.
func BenchmarkHasherPerGoroutine_SHA256(b *testing.B) {
sampleData := []byte("The quick brown fox jumps over the lazy dog")

b.ReportAllocs() // Report memory allocations.
b.ResetTimer() // Reset the timer to exclude setup time.

b.RunParallel(func(pb *testing.PB) {
// Each goroutine maintains its own Hasher instance.
hasher := NewSHA256Hasher()
for pb.Next() {
_, err := hasher.Hash(sampleData)
assert.NoError(b, err)
}
})
}

// BenchmarkHasherWithMutex benchmarks hashing using a single FNV-64a Hasher instance
// protected by a sync.Mutex across multiple goroutines.
func BenchmarkHasherWithMutex_FNV(b *testing.B) {
sampleData := []byte("The quick brown fox jumps over the lazy dog")

mutexHasher := NewMutexHasher(NewFNVHasher())

b.ReportAllocs()
b.ResetTimer()

b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, err := mutexHasher.Hash(sampleData)
assert.NoError(b, err)
}
})
}

// BenchmarkHasherPerGoroutine benchmarks hashing using separate FNV-64a Hasher instances
// for each goroutine, eliminating the need for synchronization.
func BenchmarkHasherPerGoroutine_FNV(b *testing.B) {
sampleData := []byte("The quick brown fox jumps over the lazy dog")

b.ReportAllocs()
b.ResetTimer()

b.RunParallel(func(pb *testing.PB) {
hasher := NewFNVHasher()
for pb.Next() {
_, err := hasher.Hash(sampleData)
assert.NoError(b, err)
}
})
}
21 changes: 21 additions & 0 deletions pkg/hasher/mutex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package hasher

import "sync"

// MutexHasher wraps a Hasher with a sync.Mutex to ensure thread-safe access.
type MutexHasher struct {
hasher Hasher
mu sync.Mutex
}

// NewMutexHasher creates a new MutexHasher wrapping the provided Hasher.
func NewMutexHasher(hasher Hasher) *MutexHasher {
return &MutexHasher{hasher: hasher}
}

// Hash synchronizes access to the underlying Hasher using a mutex.
func (m *MutexHasher) Hash(data []byte) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
return m.hasher.Hash(data)
}

0 comments on commit 6e9449b

Please sign in to comment.