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()) } }