Files

151 lines
4.3 KiB
Go

package main
import (
"bytes"
"context"
"fmt"
mathrand "math/rand"
"strconv"
"strings"
"sync"
"testing"
"time"
basev1 "gl/gen/base/v1"
ledgerv1 "gl/gen/ledger/v1"
"google.golang.org/grpc"
)
type memoryClient struct {
mu sync.Mutex
balances map[string]int64
journals int
}
func newMemoryClient() *memoryClient {
return &memoryClient{balances: make(map[string]int64)}
}
func (c *memoryClient) Health(context.Context, *basev1.Empty, ...grpc.CallOption) (*ledgerv1.HealthResponse, error) {
return &ledgerv1.HealthResponse{Serving: true, DatabaseReady: true}, nil
}
func (c *memoryClient) AppendJournal(_ context.Context, request *ledgerv1.AppendJournalRequest, _ ...grpc.CallOption) (*ledgerv1.AppendJournalResponse, error) {
c.mu.Lock()
defer c.mu.Unlock()
for _, entry := range request.GetEntries() {
amount, err := strconv.ParseInt(entry.GetAmount(), 10, 64)
if err != nil {
return nil, err
}
c.balances[accountKey(entry.GetAccount())] += amount
}
c.journals++
return &ledgerv1.AppendJournalResponse{Journal: &ledgerv1.Journal{JournalId: fmt.Sprintf("journal-%d", c.journals)}}, nil
}
func (c *memoryClient) GetBalance(_ context.Context, request *ledgerv1.GetBalanceRequest, _ ...grpc.CallOption) (*ledgerv1.GetBalanceResponse, error) {
c.mu.Lock()
defer c.mu.Unlock()
return &ledgerv1.GetBalanceResponse{Account: request.GetAccount(), Balance: strconv.FormatInt(c.balances[accountKey(request.GetAccount())], 10)}, nil
}
func accountKey(account *ledgerv1.AccountReference) string {
return fmt.Sprintf("%d/%s/%s/%d", account.GetAccountClass(), account.GetOwnerType(), account.GetOwnerId(), account.GetAssetId())
}
func TestResolveAssetIDPromptsWhenMissing(t *testing.T) {
var output bytes.Buffer
assetID, err := resolveAssetID(0, strings.NewReader("42\n"), &output)
if err != nil {
t.Fatal(err)
}
if assetID != 42 || output.String() != "Asset number: " {
t.Fatalf("asset=%d prompt=%q", assetID, output.String())
}
}
func TestResolveAssetIDUsesFlagWithoutPrompt(t *testing.T) {
var output bytes.Buffer
assetID, err := resolveAssetID(7, strings.NewReader(""), &output)
if err != nil {
t.Fatal(err)
}
if assetID != 7 || output.Len() != 0 {
t.Fatalf("asset=%d prompt=%q", assetID, output.String())
}
}
func TestRandomReservationsPreserveSupplyAndMinimum(t *testing.T) {
state := &balanceState{balances: [personCount]int64{9595, 45, 45, 45, 45, 45, 45, 45, 45, 45}}
random := mathrand.New(mathrand.NewSource(1234))
distinctAmounts := make(map[int64]struct{})
for range 100_000 {
value := state.reserve(random)
distinctAmounts[value.Amount] = struct{}{}
if value.Sender == value.Receiver || value.Amount <= 0 {
t.Fatalf("invalid transfer: %+v", value)
}
}
var total int64
for index, balance := range state.balances {
if balance < minimumBalance {
t.Fatalf("person %d balance %d is below minimum", index+1, balance)
}
total += balance
}
if total != initialSupply {
t.Fatalf("total=%d, want %d", total, initialSupply)
}
if len(distinctAmounts) < 2 {
t.Fatalf("expected random values, got %v", distinctAmounts)
}
}
func TestVerifyInitialAndFinalBalances(t *testing.T) {
initial := [personCount]int64{10_000}
if err := verifyInitial(initial); err != nil {
t.Fatal(err)
}
final := [personCount]int64{9595, 45, 45, 45, 45, 45, 45, 45, 45, 45}
if err := verifyFinal(final); err != nil {
t.Fatal(err)
}
final[9] = 44
if err := verifyFinal(final); err == nil {
t.Fatal("expected minimum-balance failure")
}
}
func TestPersonIDsAreIsolatedByRun(t *testing.T) {
first := personIDs("run-a")
second := personIDs("run-b")
if first[0] == second[0] || first[0] == first[1] {
t.Fatalf("IDs are not isolated: %q %q %q", first[0], second[0], first[1])
}
}
func TestExecuteFundsRedistributesAndVerifiesConservation(t *testing.T) {
ledger := newMemoryClient()
var output bytes.Buffer
err := execute(context.Background(), ledger, config{
RunID: "test-run",
AssetID: 17,
Transactions: 1000,
Concurrency: 4,
RequestTimeout: time.Second,
RandomSeed: 99,
}, &output)
if err != nil {
t.Fatal(err)
}
if ledger.journals != 1001 {
t.Fatalf("journals=%d, want one funding journal and 1000 transfers", ledger.journals)
}
if !strings.Contains(output.String(), "PASS: all 10 people have at least 45; total=10000") {
t.Fatalf("missing successful verification in output:\n%s", output.String())
}
}