151 lines
4.3 KiB
Go
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())
|
|
}
|
|
}
|