added dynamic unit tests

This commit is contained in:
Matt Johnstone 2024-06-14 00:13:53 +02:00
parent f5d604d4f1
commit b742be47ef
No known key found for this signature in database
GPG Key ID: 7D96C656728409F9
7 changed files with 553 additions and 201 deletions

View File

@ -0,0 +1,212 @@
package main
import (
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
func TestSolanaCollector_Collect_Dynamic(t *testing.T) {
client := newDynamicRPCClient()
collector := createSolanaCollector(
client,
slotPacerSchedule,
)
prometheus.NewPedanticRegistry().MustRegister(collector)
// start off by testing initial state:
testCases := []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 3
solana_active_validators{state="delinquent"} 0
`,
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 1000000
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 1000000
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
`,
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="v1.0.0"} 1
`,
},
}
runCollectionTests(t, collector, testCases)
// now make some changes:
client.UpdateStake("aaa", 2_000_000)
client.UpdateStake("bbb", 500_000)
client.UpdateDelinquency("ccc", true)
client.UpdateVersion("v1.2.3")
// now test the final state
testCases = []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 2
solana_active_validators{state="delinquent"} 1
`,
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 2000000
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 500000
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 1000000
`,
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 0
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 0
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 0
`,
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 0
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 1
`,
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="v1.2.3"} 1
`,
},
}
runCollectionTests(t, collector, testCases)
}
type slotMetricValues struct {
SlotHeight float64
TotalTransactions float64
EpochNumber float64
EpochFirstSlot float64
EpochLastSlot float64
}
func getSlotMetricValues() slotMetricValues {
return slotMetricValues{
SlotHeight: testutil.ToFloat64(confirmedSlotHeight),
TotalTransactions: testutil.ToFloat64(totalTransactionsTotal),
EpochNumber: testutil.ToFloat64(currentEpochNumber),
EpochFirstSlot: testutil.ToFloat64(epochFirstSlot),
EpochLastSlot: testutil.ToFloat64(epochLastSlot),
}
}
func TestSolanaCollector_WatchSlots_Dynamic(t *testing.T) {
// this test passes, however, it seems to cause the static tests to fail (after this test runs,
// the static tests fail to set their correct values to the prometheus metrics). So, putting this
// here while I debug
if testing.Short() {
t.Skip()
}
// create clients:
client := newDynamicRPCClient()
collector := createSolanaCollector(
client,
300*time.Millisecond,
)
prometheus.NewPedanticRegistry().MustRegister(collector)
// start client/collector and wait a bit:
go client.Run()
time.Sleep(1 * time.Second)
go collector.WatchSlots()
time.Sleep(1 * time.Second)
initial := getSlotMetricValues()
// wait a bit:
var epochChanged bool
for i := 0; i < 5; i++ {
// wait a bit then get new metrics
time.Sleep(1 * time.Second)
final := getSlotMetricValues()
// make sure that things have increased
assert.Greaterf(
t,
final.SlotHeight,
initial.SlotHeight,
"Slot has not increased! (%v -> %v)",
initial.SlotHeight,
final.SlotHeight,
)
assert.Greaterf(
t,
final.TotalTransactions,
initial.TotalTransactions,
"Total transactions have not increased! (%v -> %v)",
initial.TotalTransactions,
final.TotalTransactions,
)
assert.GreaterOrEqualf(
t,
final.EpochNumber,
initial.EpochNumber,
"Epoch number has decreased! (%v -> %v)",
initial.EpochNumber,
final.EpochNumber,
)
// make current final the new initial (for next iteration)
initial = final
}
assert.True(t, epochChanged)
}

View File

@ -26,6 +26,7 @@ func init() {
type solanaCollector struct {
rpcClient rpc.Provider
slotPace time.Duration
totalValidatorsDesc *prometheus.Desc
validatorActivatedStake *prometheus.Desc
@ -35,9 +36,10 @@ type solanaCollector struct {
solanaVersion *prometheus.Desc
}
func createSolanaCollector(provider rpc.Provider) *solanaCollector {
func createSolanaCollector(provider rpc.Provider, slotPace time.Duration) *solanaCollector {
return &solanaCollector{
rpcClient: provider,
slotPace: slotPace,
totalValidatorsDesc: prometheus.NewDesc(
"solana_active_validators",
"Total number of active validators by state",
@ -66,7 +68,7 @@ func createSolanaCollector(provider rpc.Provider) *solanaCollector {
}
func NewSolanaCollector(rpcAddr string) *solanaCollector {
return createSolanaCollector(rpc.NewRPCClient(rpcAddr))
return createSolanaCollector(rpc.NewRPCClient(rpcAddr), slotPacerSchedule)
}
func (c *solanaCollector) Describe(ch chan<- *prometheus.Desc) {
@ -127,7 +129,7 @@ func (c *solanaCollector) Collect(ch chan<- prometheus.Metric) {
if err != nil {
ch <- prometheus.NewInvalidMetric(c.solanaVersion, err)
} else {
ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, *version)
ch <- prometheus.MustNewConstMetric(c.solanaVersion, prometheus.GaugeValue, 1, version)
}
}

View File

@ -89,7 +89,7 @@ func (c *solanaCollector) WatchSlots() {
if err != nil {
klog.Error(err)
}
ticker := time.NewTicker(slotPacerSchedule)
ticker := time.NewTicker(c.slotPace)
for {
<-ticker.C

View File

@ -1,7 +1,6 @@
package main
import (
"bytes"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
@ -10,71 +9,83 @@ import (
"time"
)
var staticCollector = createSolanaCollector(&staticRPCClient{})
func TestSolanaCollector_Collect_Static(t *testing.T) {
collector := createSolanaCollector(
&staticRPCClient{},
slotPacerSchedule,
)
prometheus.NewPedanticRegistry().MustRegister(collector)
func TestSolanaCollector_Collect(t *testing.T) {
prometheus.NewPedanticRegistry().MustRegister(staticCollector)
testCases := map[string]string{
"solana_active_validators": `
testCases := []collectionTest{
{
Name: "solana_active_validators",
ExpectedResponse: `
# HELP solana_active_validators Total number of active validators by state
# TYPE solana_active_validators gauge
solana_active_validators{state="current"} 2
solana_active_validators{state="delinquent"} 1
`,
"solana_validator_activated_stake": `
},
{
Name: "solana_validator_activated_stake",
ExpectedResponse: `
# HELP solana_validator_activated_stake Activated stake per validator
# TYPE solana_validator_activated_stake gauge
solana_validator_activated_stake{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 49
solana_validator_activated_stake{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 42
solana_validator_activated_stake{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 43
solana_validator_activated_stake{nodekey="aaa",pubkey="AAA"} 49
solana_validator_activated_stake{nodekey="bbb",pubkey="BBB"} 42
solana_validator_activated_stake{nodekey="ccc",pubkey="CCC"} 43
`,
"solana_validator_last_vote": `
},
{
Name: "solana_validator_last_vote",
ExpectedResponse: `
# HELP solana_validator_last_vote Last voted slot per validator
# TYPE solana_validator_last_vote gauge
solana_validator_last_vote{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 92
solana_validator_last_vote{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 147
solana_validator_last_vote{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 148
solana_validator_last_vote{nodekey="aaa",pubkey="AAA"} 92
solana_validator_last_vote{nodekey="bbb",pubkey="BBB"} 147
solana_validator_last_vote{nodekey="ccc",pubkey="CCC"} 148
`,
"solana_validator_root_slot": `
},
{
Name: "solana_validator_root_slot",
ExpectedResponse: `
# HELP solana_validator_root_slot Root slot per validator
# TYPE solana_validator_root_slot gauge
solana_validator_root_slot{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 3
solana_validator_root_slot{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 18
solana_validator_root_slot{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 19
solana_validator_root_slot{nodekey="aaa",pubkey="AAA"} 3
solana_validator_root_slot{nodekey="bbb",pubkey="BBB"} 18
solana_validator_root_slot{nodekey="ccc",pubkey="CCC"} 19
`,
"solana_validator_delinquent": `
},
{
Name: "solana_validator_delinquent",
ExpectedResponse: `
# HELP solana_validator_delinquent Whether a validator is delinquent
# TYPE solana_validator_delinquent gauge
solana_validator_delinquent{nodekey="4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",pubkey="xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"} 1
solana_validator_delinquent{nodekey="B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 0
solana_validator_delinquent{nodekey="C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",pubkey="4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"} 0
solana_validator_delinquent{nodekey="aaa",pubkey="AAA"} 1
solana_validator_delinquent{nodekey="bbb",pubkey="BBB"} 0
solana_validator_delinquent{nodekey="ccc",pubkey="CCC"} 0
`,
"solana_node_version": `
},
{
Name: "solana_node_version",
ExpectedResponse: `
# HELP solana_node_version Node version of solana
# TYPE solana_node_version gauge
solana_node_version{version="1.16.7"} 1
`,
},
}
for testName, expectedValue := range testCases {
t.Run(
testName,
func(t *testing.T) {
if err := testutil.CollectAndCompare(
staticCollector,
bytes.NewBufferString(expectedValue),
testName,
); err != nil {
t.Errorf("unexpected collecting result for %s: \n%s", testName, err)
}
},
)
}
runCollectionTests(t, collector, testCases)
}
func TestSolanaCollector_WatchSlots(t *testing.T) {
go staticCollector.WatchSlots()
func TestSolanaCollector_WatchSlots_Static(t *testing.T) {
collector := createSolanaCollector(
&staticRPCClient{},
100*time.Millisecond,
)
prometheus.NewPedanticRegistry().MustRegister(collector)
go collector.WatchSlots()
time.Sleep(1 * time.Second)
tests := []struct {
@ -124,8 +135,8 @@ func TestSolanaCollector_WatchSlots(t *testing.T) {
for _, status := range statuses {
// sub subtest for each status (as each one requires a different calc)
t.Run(status, func(t *testing.T) {
for _, testValidator := range testValidators {
testBlockProductionMetric(t, metric, testValidator.identity, status)
for _, identity := range identities {
testBlockProductionMetric(t, metric, identity, status)
}
})
}
@ -154,5 +165,11 @@ func testBlockProductionMetric(
labels = append(labels, fmt.Sprintf("%d", staticEpochInfo.Epoch))
}
// now we can do the assertion:
assert.Equal(t, expectedValue, testutil.ToFloat64(metric.WithLabelValues(labels...)))
assert.Equalf(
t,
expectedValue,
testutil.ToFloat64(metric.WithLabelValues(labels...)),
"wrong value for block-production metric with labels: %s",
labels,
)
}

View File

@ -1,29 +1,55 @@
package main
import (
"bytes"
"context"
"github.com/certusone/solana_exporter/pkg/rpc"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/testutil"
"math/rand"
"regexp"
"testing"
"time"
)
type (
staticRPCClient struct{}
// TODO: create dynamicRPCClient + according tests!
staticRPCClient struct{}
dynamicRPCClient struct {
Slot int
BlockHeight int
Epoch int
EpochSize int
SlotTime time.Duration
TransactionCount int
Version string
SlotInfos map[int]slotInfo
LeaderIndex int
ValidatorInfos map[string]validatorInfo
}
slotInfo struct {
leader string
blockProduced bool
}
validatorInfo struct {
Stake int
LastVote int
Commission int
Delinquent bool
}
)
var (
testValidators = []struct {
identity string
vote string
}{
{"B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", "3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"},
{"C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD", "4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw"},
{"4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae", "xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU"},
identities = []string{
"aaa",
"bbb",
"ccc",
}
n = len(testValidators)
identityVotes = map[string]string{
"aaa": "AAA",
"bbb": "BBB",
"ccc": "CCC",
}
nv = len(identities)
staticEpochInfo = rpc.EpochInfo{
AbsoluteSlot: 166598,
BlockHeight: 166500,
@ -36,44 +62,21 @@ var (
FirstSlot: 1000,
LastSlot: 2000,
Hosts: map[string]rpc.BlockProductionPerHost{
"B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": {
"bbb": {
LeaderSlots: 400,
BlocksProduced: 360,
},
"C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD": {
"ccc": {
LeaderSlots: 300,
BlocksProduced: 296,
},
"4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae": {
"aaa": {
LeaderSlots: 300,
BlocksProduced: 0,
},
},
}
)
//goland:noinspection GoUnusedParameter
func (c *staticRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) {
return &staticEpochInfo, nil
}
//goland:noinspection GoUnusedParameter
func (c *staticRPCClient) GetSlot(ctx context.Context) (int64, error) {
return staticEpochInfo.AbsoluteSlot, nil
}
//goland:noinspection GoUnusedParameter
func (c *staticRPCClient) GetVersion(ctx context.Context) (*string, error) {
version := "1.16.7"
return &version, nil
}
//goland:noinspection GoUnusedParameter
func (c *staticRPCClient) GetVoteAccounts(
ctx context.Context,
params []interface{},
) (*rpc.VoteAccounts, error) {
voteAccounts := rpc.VoteAccounts{
staticVoteAccounts = rpc.VoteAccounts{
Current: []rpc.VoteAccount{
{
ActivatedStake: 42,
@ -84,9 +87,9 @@ func (c *staticRPCClient) GetVoteAccounts(
},
EpochVoteAccount: true,
LastVote: 147,
NodePubkey: "B97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",
NodePubkey: "bbb",
RootSlot: 18,
VotePubkey: "3ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw",
VotePubkey: "BBB",
},
{
ActivatedStake: 43,
@ -97,9 +100,9 @@ func (c *staticRPCClient) GetVoteAccounts(
},
EpochVoteAccount: true,
LastVote: 148,
NodePubkey: "C97CCUW3AEZFGy6uUg6zUdnNYvnVq5VG8PUtb2HayTDD",
NodePubkey: "ccc",
RootSlot: 19,
VotePubkey: "4ZT31jkAGhUaw8jsy4bTknwBMP8i4Eueh52By4zXcsVw",
VotePubkey: "CCC",
},
},
Delinquent: []rpc.VoteAccount{
@ -112,13 +115,40 @@ func (c *staticRPCClient) GetVoteAccounts(
},
EpochVoteAccount: true,
LastVote: 92,
NodePubkey: "4MUdt8D2CadJKeJ8Fv2sz4jXU9xv4t2aBPpTf6TN8bae",
NodePubkey: "aaa",
RootSlot: 3,
VotePubkey: "xKUz6fZ79SXnjGYaYhhYTYQBoRUBoCyuDMkBa1tL3zU",
VotePubkey: "AAA",
},
},
}
return &voteAccounts, nil
)
/*
===== STATIC CLIENT =====:
*/
//goland:noinspection GoUnusedParameter
func (c *staticRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) {
return &staticEpochInfo, nil
}
//goland:noinspection GoUnusedParameter
func (c *staticRPCClient) GetSlot(ctx context.Context) (int64, error) {
return staticEpochInfo.AbsoluteSlot, nil
}
//goland:noinspection GoUnusedParameter
func (c *staticRPCClient) GetVersion(ctx context.Context) (string, error) {
version := "1.16.7"
return version, nil
}
//goland:noinspection GoUnusedParameter
func (c *staticRPCClient) GetVoteAccounts(
ctx context.Context,
params []interface{},
) (*rpc.VoteAccounts, error) {
return &staticVoteAccounts, nil
}
//goland:noinspection GoUnusedParameter
@ -130,6 +160,183 @@ func (c *staticRPCClient) GetBlockProduction(
return staticBlockProduction, nil
}
/*
===== DYNAMIC CLIENT =====:
*/
func newDynamicRPCClient() *dynamicRPCClient {
validatorInfos := make(map[string]validatorInfo)
for identity := range identityVotes {
validatorInfos[identity] = validatorInfo{
Stake: 1_000_000,
LastVote: 0,
Commission: 5,
Delinquent: false,
}
}
return &dynamicRPCClient{
Slot: 0,
BlockHeight: 0,
Epoch: 0,
EpochSize: 20,
SlotTime: 100 * time.Millisecond,
TransactionCount: 0,
Version: "v1.0.0",
SlotInfos: map[int]slotInfo{},
LeaderIndex: 0,
ValidatorInfos: validatorInfos,
}
}
func (c *dynamicRPCClient) Run() {
for {
c.newSlot()
// add 5% noise to the slot time:
noiseRange := float64(c.SlotTime) * 0.05
noise := (rand.Float64()*2 - 1) * noiseRange
time.Sleep(c.SlotTime + time.Duration(noise))
}
}
func (c *dynamicRPCClient) newSlot() {
c.Slot++
// leader changes every 4 slots
if c.Slot%4 == 0 {
c.LeaderIndex = (c.LeaderIndex + 1) % nv
}
if c.Slot%c.EpochSize == 0 {
c.Epoch++
}
// assume 90% chance of block produced:
blockProduced := rand.Intn(100) <= 90
// add slot info:
c.SlotInfos[c.Slot] = slotInfo{
leader: identities[c.LeaderIndex],
blockProduced: blockProduced,
}
if blockProduced {
c.BlockHeight++
// only add some transactions if a block was produced
c.TransactionCount += rand.Intn(10)
// assume both other validators voted
for i := 1; i < 3; i++ {
otherValidatorIndex := (c.LeaderIndex + i) % nv
identity := identities[otherValidatorIndex]
info := c.ValidatorInfos[identity]
info.LastVote = c.Slot
c.ValidatorInfos[identity] = info
}
}
}
func (c *dynamicRPCClient) UpdateVersion(version string) {
c.Version = version
}
func (c *dynamicRPCClient) UpdateStake(validator string, amount int) {
info := c.ValidatorInfos[validator]
info.Stake = amount
c.ValidatorInfos[validator] = info
}
func (c *dynamicRPCClient) UpdateCommission(validator string, newCommission int) {
info := c.ValidatorInfos[validator]
info.Commission = newCommission
c.ValidatorInfos[validator] = info
}
func (c *dynamicRPCClient) UpdateDelinquency(validator string, newDelinquent bool) {
info := c.ValidatorInfos[validator]
info.Delinquent = newDelinquent
c.ValidatorInfos[validator] = info
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) {
return &rpc.EpochInfo{
AbsoluteSlot: int64(c.Slot),
BlockHeight: int64(c.BlockHeight),
Epoch: int64(c.Epoch),
SlotIndex: int64(c.Slot % c.EpochSize),
SlotsInEpoch: int64(c.EpochSize),
TransactionCount: int64(c.TransactionCount),
}, nil
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetSlot(ctx context.Context) (int64, error) {
return int64(c.Slot), nil
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetVersion(ctx context.Context) (string, error) {
return c.Version, nil
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetVoteAccounts(
ctx context.Context,
params []interface{},
) (*rpc.VoteAccounts, error) {
var currentVoteAccounts, delinquentVoteAccounts []rpc.VoteAccount
for identity, vote := range identityVotes {
info := c.ValidatorInfos[identity]
voteAccount := rpc.VoteAccount{
ActivatedStake: int64(info.Stake),
Commission: info.Commission,
EpochCredits: [][]int{},
EpochVoteAccount: true,
LastVote: info.LastVote,
NodePubkey: identity,
RootSlot: 0,
VotePubkey: vote,
}
if info.Delinquent {
delinquentVoteAccounts = append(delinquentVoteAccounts, voteAccount)
} else {
currentVoteAccounts = append(currentVoteAccounts, voteAccount)
}
}
return &rpc.VoteAccounts{
Current: currentVoteAccounts,
Delinquent: delinquentVoteAccounts,
}, nil
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetBlockProduction(
ctx context.Context,
firstSlot *int64,
lastSlot *int64,
) (rpc.BlockProduction, error) {
hostProduction := make(map[string]rpc.BlockProductionPerHost)
for _, identity := range identities {
hostProduction[identity] = rpc.BlockProductionPerHost{LeaderSlots: 0, BlocksProduced: 0}
}
for i := *firstSlot; i <= *lastSlot; i++ {
info := c.SlotInfos[int(i)]
hp := hostProduction[info.leader]
hp.LeaderSlots++
if info.blockProduced {
hp.BlocksProduced++
}
hostProduction[info.leader] = hp
}
return rpc.BlockProduction{
FirstSlot: *firstSlot,
LastSlot: *lastSlot,
Hosts: hostProduction,
}, nil
}
/*
===== OTHER TEST UTILITIES =====:
*/
// extractName takes a Prometheus descriptor and returns its name
func extractName(desc *prometheus.Desc) string {
// Get the string representation of the descriptor
@ -148,110 +355,24 @@ func extractName(desc *prometheus.Desc) string {
return name
}
type (
slotInfo struct {
leader string
blockProduced bool
votes []string
}
dynamicRPCClient struct {
slot int
blockHeight int
epoch int
epochSize int
transactionCount int
version string
slotsInfo map[int]slotInfo
leaderIndex int
}
)
func (c *dynamicRPCClient) run() {
ticker := time.NewTicker(100 * time.Millisecond)
for {
<-ticker.C
c.newSlot()
}
type collectionTest struct {
Name string
ExpectedResponse string
}
func (c *dynamicRPCClient) newSlot() {
c.slot++
// leader changes every 4 slots
if c.slot%4 == 0 {
c.leaderIndex = (c.leaderIndex + 1) % n
}
if c.slot%c.epochSize == 0 {
c.epoch++
}
// assume 90% chance of block produced:
blockProduced := rand.Intn(100) > 90
if blockProduced {
c.blockHeight++
// only add some transactions if a block was produced
c.transactionCount += rand.Intn(10)
}
// add slot info:
c.slotsInfo[c.slot] = slotInfo{
leader: testValidators[c.leaderIndex].identity,
blockProduced: blockProduced,
// assume the other 2 validators voted:
votes: []string{
testValidators[(c.leaderIndex+1)%n].identity,
testValidators[(c.leaderIndex+2)%n].identity,
},
func runCollectionTests(t *testing.T, collector prometheus.Collector, testCases []collectionTest) {
for _, test := range testCases {
t.Run(
test.Name,
func(t *testing.T) {
if err := testutil.CollectAndCompare(
collector,
bytes.NewBufferString(test.ExpectedResponse),
test.Name,
); err != nil {
t.Errorf("unexpected collecting result for %s: \n%s", test.Name, err)
}
},
)
}
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetEpochInfo(ctx context.Context, commitment rpc.Commitment) (*rpc.EpochInfo, error) {
return &rpc.EpochInfo{
AbsoluteSlot: int64(c.slot),
BlockHeight: int64(c.blockHeight),
Epoch: int64(c.epoch),
SlotIndex: int64(c.slot % c.epochSize),
SlotsInEpoch: int64(c.epochSize),
TransactionCount: int64(c.transactionCount),
}, nil
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetSlot(ctx context.Context) (int64, error) {
return int64(c.slot), nil
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetVersion(ctx context.Context) (*string, error) {
return &c.version, nil
}
//goland:noinspection GoUnusedParameter
func (c *dynamicRPCClient) GetBlockProduction(
ctx context.Context,
firstSlot *int64,
lastSlot *int64,
) (rpc.BlockProduction, error) {
hostProduction := map[string]rpc.BlockProductionPerHost{
testValidators[0].identity: {0, 0},
testValidators[1].identity: {0, 0},
testValidators[2].identity: {0, 0},
}
for i := *firstSlot; i <= *lastSlot; i++ {
slotInfo := c.slotsInfo[int(i)]
hp := hostProduction[slotInfo.leader]
hp.LeaderSlots++
if slotInfo.blockProduced {
hp.BlocksProduced++
}
hostProduction[slotInfo.leader] = hp
}
return rpc.BlockProduction{
FirstSlot: *firstSlot,
LastSlot: *lastSlot,
Hosts: hostProduction,
}, nil
}

View File

@ -63,7 +63,7 @@ type Provider interface {
// GetVersion retrieves the version of the Solana node.
// The method takes a context for cancellation.
// It returns a pointer to a string containing the version information, or an error if the operation fails.
GetVersion(ctx context.Context) (*string, error)
GetVersion(ctx context.Context) (string, error)
}
func (c Commitment) MarshalJSON() ([]byte, error) {

View File

@ -17,27 +17,27 @@ type (
}
)
func (c *Client) GetVersion(ctx context.Context) (*string, error) {
func (c *Client) GetVersion(ctx context.Context) (string, error) {
body, err := c.rpcRequest(ctx, formatRPCRequest("getVersion", []interface{}{}))
if body == nil {
return nil, fmt.Errorf("RPC call failed: Body empty")
return "", fmt.Errorf("RPC call failed: Body empty")
}
if err != nil {
return nil, fmt.Errorf("RPC call failed: %w", err)
return "", fmt.Errorf("RPC call failed: %w", err)
}
klog.V(2).Infof("version response: %v", string(body))
var resp GetVersionResponse
if err = json.Unmarshal(body, &resp); err != nil {
return nil, fmt.Errorf("failed to decode response body: %w", err)
return "", fmt.Errorf("failed to decode response body: %w", err)
}
if resp.Error.Code != 0 {
return nil, fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message)
return "", fmt.Errorf("RPC error: %d %v", resp.Error.Code, resp.Error.Message)
}
return &resp.Result.Version, nil
return resp.Result.Version, nil
}