Skip to content

Commit

Permalink
scheduler: add unit tests (#150)
Browse files Browse the repository at this point in the history
Adds unit tests to the scheduler package.

Also adds some helper test utils:
 - app/golden: for golden file testing
 - beaconmock: beacon client mock
 
category: testing
ticket: #145
  • Loading branch information
corverroos committed Mar 1, 2022
1 parent a63ca8a commit 1f93dc2
Show file tree
Hide file tree
Showing 10 changed files with 735 additions and 21 deletions.
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ repos:
rev: v4.1.0
hooks:
- id: trailing-whitespace # trims trailing whitespace
exclude: "testdata"
- id: end-of-file-fixer # ensures that a file is either empty, or ends with one newline
exclude: "testdata"
- id: mixed-line-ending # ensures that a file doesn't contain a mix of LF and CRLF
- id: no-commit-to-branch # Protect specific branches (default: main/master) from direct checkins

Expand Down
76 changes: 76 additions & 0 deletions app/golden/golden.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright © 2021 Obol Technologies Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package golden provides a test utility for asserting blobs against golden files in a testdata folder.
// This is heavily inspired from https://github.com/sebdah/goldie.
package golden

import (
"encoding/json"
"flag"
"os"
"path"
"strings"
"sync"
"testing"

"github.com/stretchr/testify/require"
)

var (
update = flag.Bool("update", false, "Create or update golden files, instead of comparing them")
clean = flag.Bool("clean", false, "Deletes the testdata folder before updating (noop of update==false)")
)

var cleanOnce sync.Once

// RequireBytes asserts that a golden testdata file exists containing the exact data.
func RequireBytes(t *testing.T, data []byte) {
t.Helper()

filename := path.Join("testdata", strings.ReplaceAll(t.Name(), "/", "_"))

if *update {
if *clean {
cleanOnce.Do(func() {
_ = os.RemoveAll("testdata")
})
}

require.NoError(t, os.MkdirAll("testdata", 0o755))

_ = os.Remove(filename)
require.NoError(t, os.WriteFile(filename, data, 0o644)) //nolint:gosec

return
}

expected, err := os.ReadFile(filename)
if os.IsNotExist(err) {
t.Fatalf("golden file does not exist, %s, generate by running with -update", filename)
return
}

require.Equalf(t, expected, data, "Golden file mismatch, %s", filename)
}

// RequireJSON asserts that a golden testdata file exists containing the JSON serialised form of the data object.
func RequireJSON(t *testing.T, data interface{}) {
t.Helper()

b, err := json.MarshalIndent(data, "", " ")
require.NoError(t, err)

RequireBytes(t, b)
}
99 changes: 99 additions & 0 deletions beaconmock/beaconmock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright © 2021 Obol Technologies Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package beaconmock provides a mock beacon client primarily for testing.
package beaconmock

import (
"context"
"time"

eth2client "github.com/attestantio/go-eth2-client"
eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
)

// Interface assertions.
var (
_ eth2client.Service = (*Mock)(nil)
_ eth2client.NodeSyncingProvider = (*Mock)(nil)
_ eth2client.GenesisTimeProvider = (*Mock)(nil)
_ eth2client.ValidatorsProvider = (*Mock)(nil)
_ eth2client.SlotsPerEpochProvider = (*Mock)(nil)
_ eth2client.SlotDurationProvider = (*Mock)(nil)
_ eth2client.AttesterDutiesProvider = (*Mock)(nil)
_ eth2client.ProposerDutiesProvider = (*Mock)(nil)
)

// NewMock returns a new beacon client mock configured with the default and provided options.
func NewMock(opts ...Option) Mock {
mock := defaultMock()
for _, opt := range opts {
opt(&mock)
}

return mock
}

// Mock provides a mock beacon client and implements eth2client.Service and many of the erh2client Providers.
type Mock struct {
ProposerDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error)
AttesterDutiesFunc func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error)
SlotDurationFunc func(context.Context) (time.Duration, error)
SlotsPerEpochFunc func(context.Context) (uint64, error)
ValidatorsFunc func(context.Context, string, []eth2p0.ValidatorIndex) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error)
ValidatorsByPubKeyFunc func(context.Context, string, []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error)
GenesisTimeFunc func(context.Context) (time.Time, error)
NodeSyncingFunc func(context.Context) (*eth2v1.SyncState, error)
}

func (m Mock) ProposerDuties(ctx context.Context, epoch eth2p0.Epoch, validatorIndices []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) {
return m.ProposerDutiesFunc(ctx, epoch, validatorIndices)
}

func (m Mock) AttesterDuties(ctx context.Context, epoch eth2p0.Epoch, validatorIndices []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) {
return m.AttesterDutiesFunc(ctx, epoch, validatorIndices)
}

func (m Mock) SlotDuration(ctx context.Context) (time.Duration, error) {
return m.SlotDurationFunc(ctx)
}

func (m Mock) SlotsPerEpoch(ctx context.Context) (uint64, error) {
return m.SlotsPerEpochFunc(ctx)
}

func (m Mock) Validators(ctx context.Context, stateID string, validatorIndices []eth2p0.ValidatorIndex) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
return m.ValidatorsFunc(ctx, stateID, validatorIndices)
}

func (m Mock) ValidatorsByPubKey(ctx context.Context, stateID string, validatorPubKeys []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
return m.ValidatorsByPubKeyFunc(ctx, stateID, validatorPubKeys)
}

func (m Mock) GenesisTime(ctx context.Context) (time.Time, error) {
return m.GenesisTimeFunc(ctx)
}

func (m Mock) NodeSyncing(ctx context.Context) (*eth2v1.SyncState, error) {
return m.NodeSyncingFunc(ctx)
}

func (Mock) Name() string {
return "beacon-mock"
}

func (Mock) Address() string {
return "mock-address"
}
Loading

0 comments on commit 1f93dc2

Please sign in to comment.