Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

keystore: Property based testing #3538

Closed
wants to merge 4 commits into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
238 changes: 238 additions & 0 deletions keystore/keystore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@ import (
"fmt"
"io/ioutil"
"math/rand"
"reflect"
"sort"
"strings"
"testing"

"github.com/leanovate/gopter"
"github.com/leanovate/gopter/commands"
"github.com/leanovate/gopter/gen"
ci "gx/ipfs/QmfWDLQjGjVe4fr5CoztYW2DYYjRysMJrFe1RCsXLPTf46/go-libp2p-crypto"
)

Expand Down Expand Up @@ -195,3 +200,236 @@ func assertDirContents(dir string, exp []string) error {
}
return nil
}

type fsState struct {
store map[string]ci.PrivKey
}

func (st *fsState) HasKey(key string) bool {
_, e := st.store[key]
return e
}

func (st *fsState) Put(key string, value ci.PrivKey) *fsState {
newstore := map[string]ci.PrivKey{}
for k, v := range st.store {
newstore[k] = v
}
newstore[key] = value
return &fsState{store: newstore}
}

func (st *fsState) Get(key string) ci.PrivKey {
return st.store[key]
}

func (st *fsState) Del(key string) *fsState {
newstore := map[string]ci.PrivKey{}
for k, v := range st.store {
newstore[k] = v
}
delete(newstore, key)
return &fsState{store: newstore}
}

// Using []interface{} as return type due to gopter accepting interfaces in gen.OneOfConst(...)
func (st *fsState) Keys() []interface{} {
keys := make([]interface{}, len(st.store))
i := 0
for k := range st.store {
keys[i] = k
i++
}
return keys
}

func ValidKey(key string) bool {
// XXX: Only disallowing "/" might cause issues on Windows and with weird FSes
return key != "" && !strings.Contains(key, "/") && !strings.HasPrefix(key, ".")
}

type getCommand string

func (cm getCommand) Run(store commands.SystemUnderTest) commands.Result {
val, err := store.(*FSKeystore).Get(string(cm))
if err != nil {
return nil
}
hash, err := val.Hash()
if err != nil {
return nil
}
return hash
}
func (getCommand) NextState(s commands.State) commands.State {
return s
}
func (cm getCommand) PreCondition(state commands.State) bool {
return state.(*fsState).HasKey(string(cm)) && ValidKey(string(cm))
}
func (cm getCommand) PostCondition(state commands.State, result commands.Result) *gopter.PropResult {
expected, err := state.(*fsState).Get(string(cm)).Hash()
if result != nil && err == nil && reflect.DeepEqual(expected, result.([]byte)) {
return &gopter.PropResult{Status: gopter.PropTrue}
}
return gopter.NewPropResult(false, fmt.Sprintf("result is nil: %v, err is nil: %v", result == nil, err == nil))
}
func (cm getCommand) String() string {
return fmt.Sprintf("Get(%q)", string(cm))
}

func getGen(state commands.State) gopter.Gen {
return gen.OneConstOf(state.(*fsState).Keys()...).Map(func(v string) getCommand {
return getCommand(v)
})
}

type delCommand string

func (cm delCommand) Run(store commands.SystemUnderTest) commands.Result {
err := store.(*FSKeystore).Delete(string(cm))
if err != nil {
return false
}
return true
}
func (cm delCommand) NextState(st commands.State) commands.State {
return st.(*fsState).Del(string(cm))
}
func (cm delCommand) PreCondition(state commands.State) bool {
return state.(*fsState).HasKey(string(cm)) && ValidKey(string(cm))
}
func (delCommand) PostCondition(st commands.State, result commands.Result) *gopter.PropResult {
if result.(bool) {
return &gopter.PropResult{Status: gopter.PropTrue}
}
return &gopter.PropResult{Status: gopter.PropFalse}
}
func (cm delCommand) String() string {
return fmt.Sprintf("Del(%q)", string(cm))
}

func delGen(state commands.State) gopter.Gen {
return gen.OneConstOf(state.(*fsState).Keys()...).Map(func(v string) delCommand {
return delCommand(v)
})
}

type putCommand struct {
key string
val ci.PrivKey
}

func (cm *putCommand) Run(store commands.SystemUnderTest) commands.Result {
err := store.(*FSKeystore).Put(cm.key, cm.val)
if err != nil {
return false
}
return true
}
func (cm *putCommand) NextState(st commands.State) commands.State {
return st.(*fsState).Put(cm.key, cm.val)
}
func (cm *putCommand) PreCondition(st commands.State) bool {
return !st.(*fsState).HasKey(cm.key) && ValidKey(string(cm.key))
}
func (*putCommand) PostCondition(st commands.State, result commands.Result) *gopter.PropResult {
if result.(bool) {
return &gopter.PropResult{Status: gopter.PropTrue}
}
return &gopter.PropResult{Status: gopter.PropFalse}
}
func (cm *putCommand) String() string {
h, _ := cm.val.Hash()
return fmt.Sprintf("Put(%q, %x...)", cm.key, h[:6])
}

func runesToString(v []rune) string {
return string(v)
}

func genString(runeGen gopter.Gen, runeSieve func(ch rune) bool) gopter.Gen {
return gen.SliceOf(runeGen).Map(runesToString).SuchThat(func(v string) bool {
for _, ch := range v {
if !runeSieve(ch) {
return false
}
}
return true
}).WithShrinker(gen.StringShrinker)
}

func putGen() gopter.Gen {
return genString(gen.OneGenOf(gen.AlphaLowerChar(), gen.NumChar(), gen.OneConstOf('_', '-')),
func(_ rune) bool {
return true
}).Map(func(v string) *putCommand {
k, _, _ := ci.GenerateEd25519Key(rr{}) // Unfortunately, can't replicate privk related bugs with this
return &putCommand{
key: v,
val: k,
}
})
}

var listCommand = &commands.ProtoCommand{
Name: "List",
RunFunc: func(store commands.SystemUnderTest) commands.Result {
list, err := store.(*FSKeystore).List()
if err != nil {
return nil
}
return list
},
PostConditionFunc: func(state commands.State, res commands.Result) *gopter.PropResult {
if res != nil && len(res.([]string)) == len(state.(*fsState).store) {
stk := state.(*fsState).Keys()
// Convert []interface{} to []string
expected := make([]string, len(stk))
i := 0
for _, k := range stk {
expected[i] = k.(string)
i++
}
sort.Strings(expected)
actual := res.([]string)
sort.Strings(actual)
if reflect.DeepEqual(expected, actual) {
return &gopter.PropResult{Status: gopter.PropTrue}
}
return gopter.NewPropResult(false, "Failed at deep equal")
}
return gopter.NewPropResult(false, fmt.Sprintf("Failed at first if, is res nil?: %v", res == nil))
},
}

var filestoreCommands = &commands.ProtoCommands{
NewSystemUnderTestFunc: func(initialState commands.State) commands.SystemUnderTest {
tmp, err := ioutil.TempDir("", "keystore-test")
if err != nil {
return nil
}
keystore, err := NewFSKeystore(tmp)
if err != nil {
return nil
}
return keystore
},
InitialStateGen: gen.Const(&fsState{store: map[string]ci.PrivKey{}}),
GenCommandFunc: func(state commands.State) gopter.Gen {
if len(state.(*fsState).Keys()) == 0 {
return gen.OneGenOf(putGen(), gen.Const(listCommand))
}
return gen.OneGenOf(getGen(state), putGen(), delGen(state), gen.Const(listCommand))
},
}

func TestFilestoreCommands(t *testing.T) {
parameters := gopter.DefaultTestParameters()

properties := gopter.NewProperties(parameters)

properties.Property("filestore", commands.Prop(filestoreCommands))

properties.TestingRun(t)
}