Ctrl K
ComunidadGuías…

internal/budget/refresh_race_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package budget

import (
	"context"
	"sync"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

// gatedBudgetStore holds its next ListBudgets, once armed, after it read the
// budgets and until release is closed.
type gatedBudgetStore struct {
	*fakeStore
	mu      sync.Mutex
	armed   bool
	started chan struct{}
	release chan struct{}
}

func (s *gatedBudgetStore) ListBudgets(context.Context) ([]Budget, error) {
	s.mu.Lock()
	budgets := append([]Budget(nil), s.budgets...)
	hold := s.armed
	s.armed = false
	s.mu.Unlock()
	if hold {
		close(s.started)
		<-s.release
	}
	return budgets, nil
}

func (s *gatedBudgetStore) setBudgets(budgets []Budget) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.budgets = budgets
}

// A refresh that read the store before a write must not install its older
// budgets over those of a refresh that read after it.
func TestServiceRefreshNeverInstallsAnOlderSnapshot(t *testing.T) {
	ctx := context.Background()
	older := Budget{Scope: ScopeUserPath, Subject: "/team", PeriodSeconds: 86400, Amount: 5}
	newer := older
	newer.Amount = 50
	store := &gatedBudgetStore{fakeStore: &fakeStore{budgets: []Budget{older}}}
	service, err := NewService(ctx, store)
	require.NoError(t, err)

	store.started, store.release = make(chan struct{}), make(chan struct{})
	store.armed = true
	slow := make(chan error, 1)
	go func() { slow <- service.Refresh(ctx) }()
	<-store.started

	store.setBudgets([]Budget{newer})
	fast := make(chan error, 1)
	go func() { fast <- service.Refresh(ctx) }()
	select {
	case err := <-fast:
		fast <- err
	case <-time.After(100 * time.Millisecond):
	}
	close(store.release)
	require.NoError(t, <-slow)
	require.NoError(t, <-fast)

	budgets := service.Budgets()
	require.Len(t, budgets, 1)
	assert.Equal(t, 50.0, budgets[0].Amount)
}