From b742be47effd8629b8e4bd83d57e5da5c013ca04 Mon Sep 17 00:00:00 2001 From: Matt Johnstone Date: Fri, 14 Jun 2024 00:13:53 +0200 Subject: [PATCH] added dynamic unit tests --- cmd/solana_exporter/dynamic_test.go | 212 +++++++++ cmd/solana_exporter/exporter.go | 8 +- cmd/solana_exporter/slots.go | 2 +- .../{exporter_test.go => static_test.go} | 103 +++-- cmd/solana_exporter/utils_test.go | 415 +++++++++++------- pkg/rpc/client.go | 2 +- pkg/rpc/version.go | 12 +- 7 files changed, 553 insertions(+), 201 deletions(-) create mode 100644 cmd/solana_exporter/dynamic_test.go rename cmd/solana_exporter/{exporter_test.go => static_test.go} (52%) diff --git a/cmd/solana_exporter/dynamic_test.go b/cmd/solana_exporter/dynamic_test.go new file mode 100644 index 0000000..5204a3c --- /dev/null +++ b/cmd/solana_exporter/dynamic_test.go @@ -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) +} diff --git a/cmd/solana_exporter/exporter.go b/cmd/solana_exporter/exporter.go index 5884b89..88fd3cb 100644 --- a/cmd/solana_exporter/exporter.go +++ b/cmd/solana_exporter/exporter.go @@ -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) } } diff --git a/cmd/solana_exporter/slots.go b/cmd/solana_exporter/slots.go index 448db6c..eda74ff 100644 --- a/cmd/solana_exporter/slots.go +++ b/cmd/solana_exporter/slots.go @@ -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 diff --git a/cmd/solana_exporter/exporter_test.go b/cmd/solana_exporter/static_test.go similarity index 52% rename from cmd/solana_exporter/exporter_test.go rename to cmd/solana_exporter/static_test.go index 7cb19c4..72adcdc 100644 --- a/cmd/solana_exporter/exporter_test.go +++ b/cmd/solana_exporter/static_test.go @@ -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, + ) } diff --git a/cmd/solana_exporter/utils_test.go b/cmd/solana_exporter/utils_test.go index 0b3b670..d47249d 100644 --- a/cmd/solana_exporter/utils_test.go +++ b/cmd/solana_exporter/utils_test.go @@ -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 -} diff --git a/pkg/rpc/client.go b/pkg/rpc/client.go index ba2ed70..0aa9d9a 100644 --- a/pkg/rpc/client.go +++ b/pkg/rpc/client.go @@ -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) { diff --git a/pkg/rpc/version.go b/pkg/rpc/version.go index abfe1b9..048c233 100644 --- a/pkg/rpc/version.go +++ b/pkg/rpc/version.go @@ -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 }