Mining/pkg/mining/manager_race_test.go
snider 2a30744a08 test: Add race condition tests and fix AVG float64 scan bug
- Add pkg/mining/manager_race_test.go with concurrent miner tests
- Add pkg/database/database_race_test.go with concurrent DB tests
- Add TestCleanupRetention, TestGetHashrateHistoryTimeRange tests
- Add TestMultipleMinerStats, TestIsInitialized tests
- Fix AVG() float64 to int scan error in GetHashrateStats
- Fix AVG() float64 to int scan error in GetAllMinerStats
- Fix throttle tests to use NewManagerForSimulation to avoid
  autostart conflicts

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 11:07:29 +00:00

314 lines
6.9 KiB
Go

package mining
import (
"context"
"sync"
"testing"
"time"
)
// TestConcurrentStartMultipleMiners verifies that concurrent StartMiner calls
// with different algorithms create unique miners without race conditions
func TestConcurrentStartMultipleMiners(t *testing.T) {
m := setupTestManager(t)
defer m.Stop()
var wg sync.WaitGroup
errors := make(chan error, 10)
// Try to start 10 miners concurrently with different algos
for i := 0; i < 10; i++ {
wg.Add(1)
go func(index int) {
defer wg.Done()
config := &Config{
HTTPPort: 10000 + index,
Pool: "test:1234",
Wallet: "testwallet",
Algo: "algo" + string(rune('A'+index)), // algoA, algoB, etc.
}
_, err := m.StartMiner(context.Background(), "xmrig", config)
if err != nil {
errors <- err
}
}(i)
}
wg.Wait()
close(errors)
// Collect errors
var errCount int
for err := range errors {
t.Logf("Concurrent start error: %v", err)
errCount++
}
// Some failures are expected due to port conflicts, but shouldn't crash
t.Logf("Started miners with %d errors out of 10 attempts", errCount)
// Verify no data races occurred (test passes if no race detector warnings)
}
// TestConcurrentStartDuplicateMiner verifies that starting the same miner
// concurrently results in only one success
func TestConcurrentStartDuplicateMiner(t *testing.T) {
m := setupTestManager(t)
defer m.Stop()
var wg sync.WaitGroup
successes := make(chan struct{}, 10)
failures := make(chan error, 10)
// Try to start the same miner 10 times concurrently
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
config := &Config{
HTTPPort: 11000,
Pool: "test:1234",
Wallet: "testwallet",
Algo: "duplicate_test", // Same algo = same instance name
}
_, err := m.StartMiner(context.Background(), "xmrig", config)
if err != nil {
failures <- err
} else {
successes <- struct{}{}
}
}()
}
wg.Wait()
close(successes)
close(failures)
successCount := len(successes)
failureCount := len(failures)
t.Logf("Duplicate miner test: %d successes, %d failures", successCount, failureCount)
// Only one should succeed (or zero if there's a timing issue)
if successCount > 1 {
t.Errorf("Expected at most 1 success for duplicate miner, got %d", successCount)
}
}
// TestConcurrentStartStop verifies that starting and stopping miners
// concurrently doesn't cause race conditions
func TestConcurrentStartStop(t *testing.T) {
m := setupTestManager(t)
defer m.Stop()
var wg sync.WaitGroup
// Start some miners
for i := 0; i < 5; i++ {
config := &Config{
HTTPPort: 12000 + i,
Pool: "test:1234",
Wallet: "testwallet",
Algo: "startstop" + string(rune('A'+i)),
}
_, err := m.StartMiner(context.Background(), "xmrig", config)
if err != nil {
t.Logf("Setup error (may be expected): %v", err)
}
}
// Give miners time to start
time.Sleep(100 * time.Millisecond)
// Now concurrently start new ones and stop existing ones
for i := 0; i < 10; i++ {
wg.Add(2)
// Start a new miner
go func(index int) {
defer wg.Done()
config := &Config{
HTTPPort: 12100 + index,
Pool: "test:1234",
Wallet: "testwallet",
Algo: "new" + string(rune('A'+index)),
}
m.StartMiner(context.Background(), "xmrig", config)
}(i)
// Stop a miner
go func(index int) {
defer wg.Done()
minerName := "xmrig-startstop" + string(rune('A'+index%5))
m.StopMiner(context.Background(), minerName)
}(i)
}
wg.Wait()
// Test passes if no race detector warnings
}
// TestConcurrentListMiners verifies that listing miners while modifying
// the miner map doesn't cause race conditions
func TestConcurrentListMiners(t *testing.T) {
m := setupTestManager(t)
defer m.Stop()
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Continuously list miners
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
miners := m.ListMiners()
_ = len(miners) // Use the result
}
}
}()
// Continuously start miners
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 20; i++ {
select {
case <-ctx.Done():
return
default:
config := &Config{
HTTPPort: 13000 + i,
Pool: "test:1234",
Wallet: "testwallet",
Algo: "list" + string(rune('A'+i%26)),
}
m.StartMiner(context.Background(), "xmrig", config)
time.Sleep(10 * time.Millisecond)
}
}
}()
wg.Wait()
// Test passes if no race detector warnings
}
// TestConcurrentGetMiner verifies that getting a miner while others
// are being started/stopped doesn't cause race conditions
func TestConcurrentGetMiner(t *testing.T) {
m := setupTestManager(t)
defer m.Stop()
// Start a miner first
config := &Config{
HTTPPort: 14000,
Pool: "test:1234",
Wallet: "testwallet",
Algo: "gettest",
}
miner, err := m.StartMiner(context.Background(), "xmrig", config)
if err != nil {
t.Skipf("Could not start test miner: %v", err)
}
minerName := miner.GetName()
var wg sync.WaitGroup
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
// Continuously get the miner
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
default:
m.GetMiner(minerName)
time.Sleep(time.Millisecond)
}
}
}()
}
// Start more miners in parallel
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
return
default:
config := &Config{
HTTPPort: 14100 + i,
Pool: "test:1234",
Wallet: "testwallet",
Algo: "parallel" + string(rune('A'+i)),
}
m.StartMiner(context.Background(), "xmrig", config)
}
}
}()
wg.Wait()
// Test passes if no race detector warnings
}
// TestConcurrentStatsCollection verifies that stats collection
// doesn't race with miner operations
func TestConcurrentStatsCollection(t *testing.T) {
m := setupTestManager(t)
defer m.Stop()
// Start some miners
for i := 0; i < 3; i++ {
config := &Config{
HTTPPort: 15000 + i,
Pool: "test:1234",
Wallet: "testwallet",
Algo: "stats" + string(rune('A'+i)),
}
m.StartMiner(context.Background(), "xmrig", config)
}
var wg sync.WaitGroup
// Simulate stats collection (normally done by background goroutine)
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 50; i++ {
miners := m.ListMiners()
for _, miner := range miners {
miner.GetStats(context.Background())
}
time.Sleep(10 * time.Millisecond)
}
}()
// Concurrently stop miners
wg.Add(1)
go func() {
defer wg.Done()
time.Sleep(100 * time.Millisecond) // Let stats collection start
for _, name := range []string{"xmrig-statsA", "xmrig-statsB", "xmrig-statsC"} {
m.StopMiner(context.Background(), name)
time.Sleep(50 * time.Millisecond)
}
}()
wg.Wait()
// Test passes if no race detector warnings
}