Compare commits

...

9 Commits

Author SHA1 Message Date
Anton Kaliaev
a2e7494b4b [docs] update state spec 2018-10-16 16:22:04 +04:00
Anton Kaliaev
56f943e890 add a test for DurationPretty 2018-10-16 13:38:27 +04:00
Anton Kaliaev
e9f30e1f22 make evidence age (time.Duration) pretty 2018-10-16 13:19:00 +04:00
Anton Kaliaev
7482113547 do not check peer's last block time
when deciding if we should send evidence to it
2018-10-16 11:28:25 +04:00
Anton Kaliaev
42b84972f5 use block.Time instead of fetching it from state 2018-10-16 11:01:26 +04:00
Anton Kaliaev
5481c6b150 ensure we record LastBlockTime in PeerState 2018-10-16 10:27:00 +04:00
Anton Kaliaev
f7e0cd1360 update changelog and spec 2018-10-16 10:27:00 +04:00
Anton Kaliaev
166dc01ab5 make MaxAge nonnullable 2018-10-16 10:27:00 +04:00
Anton Kaliaev
7e7e4c74ca make ConsensusParams.EvidenceParams.MaxAge time
Refs #2565
2018-10-16 10:26:59 +04:00
22 changed files with 645 additions and 361 deletions

View File

@@ -12,10 +12,17 @@ BREAKING CHANGES:
* [rpc] \#2298 `/abci_query` takes `prove` argument instead of `trusted` and switches the default
behaviour to `prove=false`
* [privval] \#2459 Split `SocketPVMsg`s implementations into Request and Response, where the Response may contain a error message (returned by the remote signer)
* [genesis] \#2565 `consensus_params.evidence_params.max_age` is now `time.Duration` (nanosecond count)
```json
"evidence_params": {
"max_age": "48h0m0s"
}
```
* Apps
* [abci] \#2298 ResponseQuery.Proof is now a structured merkle.Proof, not just
arbitrary bytes
* [abci] \#2565 InitChain `ConsensusParams.EvidenceParams.MaxAge` is now `google.protobuf.Duration` (see https://developers.google.com/protocol-buffers/docs/reference/csharp/class/google/protobuf/well-known-types/duration)
* Go API
* [node] Remove node.RunForever

1
Gopkg.lock generated
View File

@@ -517,6 +517,7 @@
"github.com/gogo/protobuf/proto",
"github.com/gogo/protobuf/types",
"github.com/golang/protobuf/proto",
"github.com/golang/protobuf/ptypes/duration",
"github.com/golang/protobuf/ptypes/timestamp",
"github.com/gorilla/websocket",
"github.com/jmhodges/levigo",

View File

@@ -44,7 +44,7 @@ protoc_all: protoc_libs protoc_merkle protoc_abci protoc_grpc
## See https://stackoverflow.com/a/25518702
## Note the $< here is substituted for the %.proto
## Note the $@ here is substituted for the %.pb.go
protoc $(INCLUDE) $< --gogo_out=Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp,plugins=grpc:.
protoc $(INCLUDE) $< --gogo_out=Mgoogle/protobuf/timestamp.proto=github.com/golang/protobuf/ptypes/timestamp,Mgoogle/protobuf/duration.proto=github.com/golang/protobuf/ptypes/duration,plugins=grpc:.
########################################
### Build ABCI

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,7 @@ package types;
// https://github.com/gogo/protobuf/blob/master/extensions.md
import "github.com/gogo/protobuf/gogoproto/gogo.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "github.com/tendermint/tendermint/libs/common/types.proto";
import "github.com/tendermint/tendermint/crypto/merkle/merkle.proto";
@@ -217,8 +218,8 @@ message BlockSize {
// EvidenceParams contains limits on the evidence.
message EvidenceParams {
// Note: must be greater than 0
int64 max_age = 1;
// Note: must be greater than 0 if provided
google.protobuf.Duration max_age = 1 [(gogoproto.nullable)=false, (gogoproto.stdduration)=true];
}
message LastCommitInfo {

View File

@@ -13,6 +13,7 @@ import golang_proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
import _ "github.com/gogo/protobuf/gogoproto"
import _ "github.com/golang/protobuf/ptypes/duration"
import _ "github.com/golang/protobuf/ptypes/timestamp"
import _ "github.com/tendermint/tendermint/crypto/merkle"
import _ "github.com/tendermint/tendermint/libs/common"

View File

@@ -8,8 +8,7 @@ import (
"github.com/pkg/errors"
"github.com/tendermint/go-amino"
amino "github.com/tendermint/go-amino"
cstypes "github.com/tendermint/tendermint/consensus/types"
cmn "github.com/tendermint/tendermint/libs/common"
tmevents "github.com/tendermint/tendermint/libs/events"
@@ -948,8 +947,8 @@ func (ps *PeerState) ToJSON() ([]byte, error) {
return cdc.MarshalJSON(ps)
}
// GetHeight returns an atomic snapshot of the PeerRoundState's height
// used by the mempool to ensure peers are caught up before broadcasting new txs
// GetHeight returns an atomic snapshot of the PeerRoundState's height used by
// the mempool to ensure peers are caught up before broadcasting new txs.
func (ps *PeerState) GetHeight() int64 {
ps.mtx.Lock()
defer ps.mtx.Unlock()

View File

@@ -443,11 +443,10 @@ Commit are included in the header of the next block.
### EvidenceParams
- **Fields**:
- `MaxAge (int64)`: Max age of evidence, in blocks. Evidence older than this
is considered stale and ignored.
- `MaxAge (google.protobuf.Duration)`: Max age of evidence. Evidence older
than this is considered stale and ignored.
- This should correspond with an app's "unbonding period" or other
similar mechanism for handling Nothing-At-Stake attacks.
- NOTE: this should change to time (instead of blocks)!
### Proof

View File

@@ -101,7 +101,7 @@ type BlockGossip struct {
}
type EvidenceParams struct {
MaxAge int64
MaxAge time.Duration
}
```
@@ -129,5 +129,5 @@ size of each part is `ConsensusParams.BlockGossip.BlockPartSizeBytes`.
For evidence in a block to be valid, it must satisfy:
```
block.Header.Height - evidence.Height < ConsensusParams.EvidenceParams.MaxAge
block.Header.Time - evidence.Time < ConsensusParams.EvidenceParams.MaxAge
```

View File

@@ -3,6 +3,7 @@ package evidence
import (
"fmt"
"sync"
"time"
clist "github.com/tendermint/tendermint/libs/clist"
dbm "github.com/tendermint/tendermint/libs/db"
@@ -84,7 +85,7 @@ func (evpool *EvidencePool) Update(block *types.Block, state sm.State) {
evpool.mtx.Unlock()
// remove evidence from pending and mark committed
evpool.MarkEvidenceAsCommitted(block.Height, block.Evidence.Evidence)
evpool.MarkEvidenceAsCommitted(block.Evidence.Evidence, block.Time)
}
// AddEvidence checks the evidence is valid and adds it to the pool.
@@ -117,8 +118,9 @@ func (evpool *EvidencePool) AddEvidence(evidence types.Evidence) (err error) {
return nil
}
// MarkEvidenceAsCommitted marks all the evidence as committed and removes it from the queue.
func (evpool *EvidencePool) MarkEvidenceAsCommitted(height int64, evidence []types.Evidence) {
// MarkEvidenceAsCommitted marks all the evidence as committed and removes it
// from the queue.
func (evpool *EvidencePool) MarkEvidenceAsCommitted(evidence []types.Evidence, lastBlockTime time.Time) {
// make a map of committed evidence to remove from the clist
blockEvidenceMap := make(map[string]struct{})
for _, ev := range evidence {
@@ -127,20 +129,22 @@ func (evpool *EvidencePool) MarkEvidenceAsCommitted(height int64, evidence []typ
}
// remove committed evidence from the clist
maxAge := evpool.State().ConsensusParams.EvidenceParams.MaxAge
evpool.removeEvidence(height, maxAge, blockEvidenceMap)
maxAge := evpool.State().ConsensusParams.EvidenceParams.MaxAge.Duration
evpool.removeEvidence(blockEvidenceMap, lastBlockTime, maxAge)
}
func (evpool *EvidencePool) removeEvidence(height, maxAge int64, blockEvidenceMap map[string]struct{}) {
func (evpool *EvidencePool) removeEvidence(
blockEvidenceMap map[string]struct{},
lastBlockTime time.Time,
maxAge time.Duration,
) {
for e := evpool.evidenceList.Front(); e != nil; e = e.Next() {
ev := e.Value.(types.Evidence)
evAge := lastBlockTime.Sub(ev.Time())
// Remove the evidence if it's already in a block
// or if it's now too old.
if _, ok := blockEvidenceMap[evMapKey(ev)]; ok ||
ev.Height() < height-maxAge {
if _, ok := blockEvidenceMap[evMapKey(ev)]; ok || evAge > maxAge {
// remove from clist
evpool.evidenceList.Remove(e)
e.DetachPrev()

View File

@@ -39,7 +39,7 @@ func initializeValidatorState(valAddr []byte, height int64) dbm.DB {
LastHeightValidatorsChanged: 1,
ConsensusParams: types.ConsensusParams{
EvidenceParams: types.EvidenceParams{
MaxAge: 1000000,
MaxAge: tmtime.DurationPretty{1000000},
},
},
}

View File

@@ -91,7 +91,7 @@ func (evR *EvidenceReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) {
}
}
// SetEventSwitch implements events.Eventable.
// SetEventBus implements events.Eventable.
func (evR *EvidenceReactor) SetEventBus(b *types.EventBus) {
evR.eventBus = b
}
@@ -153,28 +153,17 @@ func (evR *EvidenceReactor) broadcastEvidenceRoutine(peer p2p.Peer) {
// Returns the message to send the peer, or nil if the evidence is invalid for the peer.
// If message is nil, return true if we should sleep and try again.
func (evR EvidenceReactor) checkSendEvidenceMessage(peer p2p.Peer, ev types.Evidence) (msg EvidenceMessage, retry bool) {
// make sure the peer is up to date
evHeight := ev.Height()
peerState, ok := peer.Get(types.PeerStateKey).(PeerState)
if !ok {
evR.Logger.Info("Found peer without PeerState", "peer", peer)
return nil, true
}
// NOTE: We only send evidence to peers where
// peerHeight - maxAge < evidenceHeight < peerHeight
maxAge := evR.evpool.State().ConsensusParams.EvidenceParams.MaxAge
// NOTE: We only send evidence to peers where evidenceHeight < peerHeight
peerHeight := peerState.GetHeight()
if peerHeight < evHeight {
if peerHeight < ev.Height() {
// peer is behind. sleep while he catches up
return nil, true
} else if peerHeight > evHeight+maxAge {
// evidence is too old, skip
// NOTE: if evidence is too old for an honest peer,
// then we're behind and either it already got committed or it never will!
evR.Logger.Info("Not sending peer old evidence", "peerHeight", peerHeight, "evHeight", evHeight, "maxAge", maxAge, "peer", peer)
return nil, false
}
// send evidence

View File

@@ -132,7 +132,7 @@ func TestReactorBroadcastEvidence(t *testing.T) {
// set the peer height on each reactor
for _, r := range reactors {
for _, peer := range r.Switch.Peers().List() {
ps := peerState{height}
ps := testPeerState{height}
peer.Set(types.PeerStateKey, ps)
}
}
@@ -143,11 +143,13 @@ func TestReactorBroadcastEvidence(t *testing.T) {
waitForEvidence(t, evList, reactors)
}
type peerState struct {
type testPeerState struct {
height int64
}
func (ps peerState) GetHeight() int64 {
var _ PeerState = (*testPeerState)(nil)
func (ps testPeerState) GetHeight() int64 {
return ps.height
}
@@ -165,7 +167,7 @@ func TestReactorSelectiveBroadcast(t *testing.T) {
// make reactors from statedb
reactors := makeAndConnectEvidenceReactors(config, []dbm.DB{stateDB1, stateDB2})
peer := reactors[0].Switch.Peers().List()[0]
ps := peerState{height2}
ps := testPeerState{height2}
peer.Set(types.PeerStateKey, ps)
// send a bunch of valid evidence to the first reactor's evpool

View File

@@ -4,6 +4,7 @@ import (
"bytes"
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -12,6 +13,7 @@ import (
"github.com/tendermint/tendermint/crypto/ed25519"
cmn "github.com/tendermint/tendermint/libs/common"
dbm "github.com/tendermint/tendermint/libs/db"
tmtime "github.com/tendermint/tendermint/types/time"
cfg "github.com/tendermint/tendermint/config"
"github.com/tendermint/tendermint/types"
@@ -386,14 +388,14 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) {
}
}
func makeParams(blockBytes, blockGas, evidenceAge int64) types.ConsensusParams {
func makeParams(blockBytes, blockGas int64, evidenceAge time.Duration) types.ConsensusParams {
return types.ConsensusParams{
BlockSize: types.BlockSize{
MaxBytes: blockBytes,
MaxGas: blockGas,
},
EvidenceParams: types.EvidenceParams{
MaxAge: evidenceAge,
MaxAge: tmtime.DurationPretty{evidenceAge},
},
}
}
@@ -403,7 +405,7 @@ func pk() []byte {
}
func TestApplyUpdates(t *testing.T) {
initParams := makeParams(1, 2, 3)
initParams := makeParams(1, 2, 3*time.Second)
cases := [...]struct {
init types.ConsensusParams
@@ -419,14 +421,14 @@ func TestApplyUpdates(t *testing.T) {
MaxGas: 55,
},
},
makeParams(44, 55, 3)},
makeParams(44, 55, 3*time.Second)},
3: {initParams,
abci.ConsensusParams{
EvidenceParams: &abci.EvidenceParams{
MaxAge: 66,
MaxAge: 66 * time.Second,
},
},
makeParams(1, 2, 66)},
makeParams(1, 2, 66*time.Second)},
}
for i, tc := range cases {

View File

@@ -168,13 +168,11 @@ func validateBlock(stateDB dbm.DB, state State, block *types.Block) error {
// - it is internally consistent
// - it was properly signed by the alleged equivocator
func VerifyEvidence(stateDB dbm.DB, state State, evidence types.Evidence) error {
height := state.LastBlockHeight
evidenceAge := height - evidence.Height()
maxAge := state.ConsensusParams.EvidenceParams.MaxAge
evidenceAge := state.LastBlockTime.Sub(evidence.Time())
maxAge := state.ConsensusParams.EvidenceParams.MaxAge.Duration
if evidenceAge > maxAge {
return fmt.Errorf("Evidence from height %d is too old. Min height is %d",
evidence.Height(), height-maxAge)
return fmt.Errorf("Evidence from %v is too old. Expecting evidence no older than %v",
evidence.Time(), state.LastBlockTime.Add(-maxAge))
}
valset, err := LoadValidators(stateDB, evidence.Height())

View File

@@ -3,8 +3,10 @@ package types
import (
"bytes"
"fmt"
"time"
"github.com/tendermint/tendermint/crypto/tmhash"
tmtime "github.com/tendermint/tendermint/types/time"
amino "github.com/tendermint/go-amino"
@@ -54,6 +56,7 @@ func (err *ErrEvidenceOverflow) Error() string {
// Evidence represents any provable malicious activity by a validator
type Evidence interface {
Height() int64 // height of the equivocation
Time() time.Time // when the evidence was created
Address() []byte // address of the equivocating validator
Bytes() []byte // bytes which compromise the evidence
Hash() []byte // hash of the evidence
@@ -102,6 +105,11 @@ func (dve *DuplicateVoteEvidence) Height() int64 {
return dve.VoteA.Height
}
// Time returns the time when the evidence was created.
func (dve *DuplicateVoteEvidence) Time() time.Time {
return dve.VoteA.Timestamp
}
// Address returns the address of the validator.
func (dve *DuplicateVoteEvidence) Address() []byte {
return dve.PubKey.Address()
@@ -188,6 +196,7 @@ func NewMockGoodEvidence(height int64, idx int, address []byte) MockGoodEvidence
}
func (e MockGoodEvidence) Height() int64 { return e.Height_ }
func (e MockGoodEvidence) Time() time.Time { return tmtime.Now() }
func (e MockGoodEvidence) Address() []byte { return e.Address_ }
func (e MockGoodEvidence) Hash() []byte {
return []byte(fmt.Sprintf("%d-%x", e.Height_, e.Address_))

View File

@@ -1,9 +1,12 @@
package types
import (
"time"
abci "github.com/tendermint/tendermint/abci/types"
"github.com/tendermint/tendermint/crypto/tmhash"
cmn "github.com/tendermint/tendermint/libs/common"
tmtime "github.com/tendermint/tendermint/types/time"
)
const (
@@ -29,7 +32,7 @@ type BlockSize struct {
// EvidenceParams determine how we handle evidence of malfeasance
type EvidenceParams struct {
MaxAge int64 `json:"max_age"` // only accept new evidence more recent than this
MaxAge tmtime.DurationPretty `json:"max_age"` // only accept new evidence more recent than this
}
// DefaultConsensusParams returns a default ConsensusParams.
@@ -51,7 +54,7 @@ func DefaultBlockSize() BlockSize {
// DefaultEvidenceParams Params returns a default EvidenceParams.
func DefaultEvidenceParams() EvidenceParams {
return EvidenceParams{
MaxAge: 100000, // 27.8 hrs at 1block/s
MaxAge: tmtime.DurationPretty{48 * time.Hour},
}
}
@@ -72,9 +75,9 @@ func (params *ConsensusParams) Validate() error {
params.BlockSize.MaxGas)
}
if params.EvidenceParams.MaxAge <= 0 {
if params.EvidenceParams.MaxAge.Duration <= 0 {
return cmn.NewError("EvidenceParams.MaxAge must be greater than 0. Got %d",
params.EvidenceParams.MaxAge)
params.EvidenceParams.MaxAge.Duration)
}
return nil
@@ -109,7 +112,7 @@ func (params ConsensusParams) Update(params2 *abci.ConsensusParams) ConsensusPar
res.BlockSize.MaxGas = params2.BlockSize.MaxGas
}
if params2.EvidenceParams != nil {
res.EvidenceParams.MaxAge = params2.EvidenceParams.MaxAge
res.EvidenceParams.MaxAge = tmtime.DurationPretty{params2.EvidenceParams.MaxAge}
}
return res
}

View File

@@ -4,9 +4,11 @@ import (
"bytes"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
abci "github.com/tendermint/tendermint/abci/types"
tmtime "github.com/tendermint/tendermint/types/time"
)
func TestConsensusParamsValidation(t *testing.T) {
@@ -15,17 +17,17 @@ func TestConsensusParamsValidation(t *testing.T) {
valid bool
}{
// test block size
0: {makeParams(1, 0, 1), true},
1: {makeParams(0, 0, 1), false},
2: {makeParams(47*1024*1024, 0, 1), true},
3: {makeParams(10, 0, 1), true},
4: {makeParams(100*1024*1024, 0, 1), true},
5: {makeParams(101*1024*1024, 0, 1), false},
6: {makeParams(1024*1024*1024, 0, 1), false},
7: {makeParams(1024*1024*1024, 0, -1), false},
0: {makeParams(1, 0, 10*time.Second), true},
1: {makeParams(0, 0, 10*time.Second), false},
2: {makeParams(47*1024*1024, 0, 10*time.Second), true},
3: {makeParams(10, 0, 10*time.Second), true},
4: {makeParams(100*1024*1024, 0, 10*time.Second), true},
5: {makeParams(101*1024*1024, 0, 10*time.Second), false},
6: {makeParams(1024*1024*1024, 0, 10*time.Second), false},
7: {makeParams(1024*1024*1024, 0, -10*time.Second), false},
// test evidence age
8: {makeParams(1, 0, 0), false},
9: {makeParams(1, 0, -1), false},
9: {makeParams(1, 0, -1*time.Millisecond), false},
}
for i, tc := range testCases {
if tc.valid {
@@ -36,28 +38,23 @@ func TestConsensusParamsValidation(t *testing.T) {
}
}
func makeParams(blockBytes, blockGas, evidenceAge int64) ConsensusParams {
func makeParams(blockBytes, blockGas int64, evidenceAge time.Duration) ConsensusParams {
return ConsensusParams{
BlockSize: BlockSize{
MaxBytes: blockBytes,
MaxGas: blockGas,
},
EvidenceParams: EvidenceParams{
MaxAge: evidenceAge,
MaxAge: tmtime.DurationPretty{evidenceAge},
},
}
}
func TestConsensusParamsHash(t *testing.T) {
params := []ConsensusParams{
makeParams(4, 2, 3),
makeParams(1, 4, 3),
makeParams(1, 2, 4),
makeParams(2, 5, 7),
makeParams(1, 7, 6),
makeParams(9, 5, 4),
makeParams(7, 8, 9),
makeParams(4, 6, 5),
makeParams(4, 2, 3*time.Second),
makeParams(1, 4, 3*time.Second),
makeParams(1, 2, 4*time.Second),
}
hashes := make([][]byte, len(params))
@@ -83,23 +80,23 @@ func TestConsensusParamsUpdate(t *testing.T) {
}{
// empty updates
{
makeParams(1, 2, 3),
makeParams(1, 2, 3*time.Second),
&abci.ConsensusParams{},
makeParams(1, 2, 3),
makeParams(1, 2, 3*time.Second),
},
// fine updates
{
makeParams(1, 2, 3),
makeParams(1, 2, 3*time.Second),
&abci.ConsensusParams{
BlockSize: &abci.BlockSize{
MaxBytes: 100,
MaxGas: 200,
},
EvidenceParams: &abci.EvidenceParams{
MaxAge: 300,
MaxAge: 300 * time.Second,
},
},
makeParams(100, 200, 300),
makeParams(100, 200, 300*time.Second),
},
}
for _, tc := range testCases {

View File

@@ -9,6 +9,7 @@ import (
"github.com/tendermint/tendermint/crypto"
"github.com/tendermint/tendermint/crypto/ed25519"
"github.com/tendermint/tendermint/crypto/secp256k1"
tmtime "github.com/tendermint/tendermint/types/time"
)
//-------------------------------------------------------
@@ -119,7 +120,7 @@ func (tm2pb) ConsensusParams(params *ConsensusParams) *abci.ConsensusParams {
MaxGas: params.BlockSize.MaxGas,
},
EvidenceParams: &abci.EvidenceParams{
MaxAge: params.EvidenceParams.MaxAge,
MaxAge: params.EvidenceParams.MaxAge.Duration,
},
}
}
@@ -209,13 +210,18 @@ func (pb2tm) ValidatorUpdates(vals []abci.ValidatorUpdate) ([]*Validator, error)
}
func (pb2tm) ConsensusParams(csp *abci.ConsensusParams) ConsensusParams {
return ConsensusParams{
BlockSize: BlockSize{
params := ConsensusParams{}
// we must defensively consider any structs may be nil
if csp.BlockSize != nil {
params.BlockSize = BlockSize{
MaxBytes: csp.BlockSize.MaxBytes,
MaxGas: csp.BlockSize.MaxGas,
},
EvidenceParams: EvidenceParams{
MaxAge: csp.EvidenceParams.MaxAge,
},
}
}
if csp.EvidenceParams != nil {
params.EvidenceParams.MaxAge = tmtime.DurationPretty{csp.EvidenceParams.MaxAge}
}
return params
}

View File

@@ -68,7 +68,7 @@ func TestABCIValidators(t *testing.T) {
func TestABCIConsensusParams(t *testing.T) {
cp := DefaultConsensusParams()
cp.EvidenceParams.MaxAge = 0 // TODO add this to ABCI
cp.EvidenceParams.MaxAge.Duration = 0 // TODO add this to ABCI
abciCP := TM2PB.ConsensusParams(cp)
cp2 := PB2TM.ConsensusParams(abciCP)

41
types/time/duration.go Normal file
View File

@@ -0,0 +1,41 @@
package time
import (
"encoding/json"
"errors"
"time"
)
// DurationPretty is a wrapper around time.Duration implementing custom
// marshaller/unmarshaller which make it pretty (e.g. "10s", not
// "10000000000").
type DurationPretty struct {
time.Duration
}
// MarshalJSON implements json.Marshaller.
func (d DurationPretty) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
// UnmarshalJSON implements json.Unmarshaller.
func (d *DurationPretty) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
d.Duration = time.Duration(value)
return nil
case string:
var err error
d.Duration, err = time.ParseDuration(value)
if err != nil {
return err
}
return nil
default:
return errors.New("invalid duration")
}
}

View File

@@ -0,0 +1,31 @@
package time
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestDurationPretty(t *testing.T) {
testCases := []struct {
jsonBytes []byte
expErr bool
expValue time.Duration
}{
{[]byte(`"10s"`), false, 10 * time.Second},
{[]byte(`"48h0m0s"`), false, 48 * time.Hour},
{[]byte(`"10kkk"`), true, 0},
{[]byte(`"kkk"`), true, 0},
}
for i, tc := range testCases {
var d DurationPretty
if tc.expErr {
assert.Error(t, d.UnmarshalJSON(tc.jsonBytes), "#%d", i)
} else {
assert.NoError(t, d.UnmarshalJSON(tc.jsonBytes), "#%d", i)
assert.Equal(t, tc.expValue, d.Duration, "#%d", i)
}
}
}