blob: 259f9ca1e07ab4dc836fbf8b2302fa168018fb3b [file] [log] [blame]
// Copyright 2016 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vdltest
import (
"fmt"
"hash"
"hash/fnv"
"math/rand"
"strconv"
"time"
"v.io/v23/vdl"
)
// EntryValue is like Entry, but represents the target and source values as
// *vdl.Value, rather than interface{}.
type EntryValue struct {
Label string
Target *vdl.Value
Source *vdl.Value
}
// IsCanonical returns true iff e.Target == e.Source.
func (e EntryValue) IsCanonical() bool {
return vdl.EqualValue(e.Target, e.Source)
}
// EntryGenerator generates test entries.
type EntryGenerator struct {
// AllMaxMinTargets specifies that all max/min targets should be generated.
// By default max/min targets are only generated for numbers.
AllMaxMinTargets bool
// MaxRandomTargets limits the number of random targets that are generated.
MaxRandomTargets int
// Each of the Num*Entries fields configures, for each unique target, the
// number of non-canonical entries with the given label that are generated.
NumZeroEntries int
NumMaxMinEntries int
NumFullEntries int
NumRandomEntries int
valueGen *ValueGenerator
hasher hash.Hash64
randSeed int64
rng *rand.Rand
// Keep the total set of types separated by kind, for quick filtering.
ttBool, ttStringEnum, ttNumber, ttArrayList, ttSet, ttMap, ttStruct, ttUnion []*vdl.Type
}
// NewEntryGenerator returns a new EntryGenerator, which uses a random number
// generator seeded to the current time.
func NewEntryGenerator(types []*vdl.Type) *EntryGenerator {
now := time.Now().Unix()
g := &EntryGenerator{
MaxRandomTargets: 2,
NumZeroEntries: 2,
NumMaxMinEntries: 2,
NumFullEntries: 1,
NumRandomEntries: 1,
valueGen: NewValueGenerator(types),
hasher: fnv.New64a(),
randSeed: now,
rng: rand.New(rand.NewSource(now)),
}
for _, tt := range types {
kind := tt.NonOptional().Kind()
if kind.IsNumber() {
g.ttNumber = append(g.ttNumber, tt)
}
switch kind {
case vdl.Bool:
g.ttBool = append(g.ttBool, tt)
case vdl.String, vdl.Enum:
g.ttStringEnum = append(g.ttStringEnum, tt)
case vdl.Array, vdl.List:
g.ttArrayList = append(g.ttArrayList, tt)
case vdl.Set:
g.ttSet = append(g.ttSet, tt)
case vdl.Map:
g.ttMap = append(g.ttMap, tt)
case vdl.Struct:
g.ttStruct = append(g.ttStruct, tt)
case vdl.Union:
g.ttUnion = append(g.ttUnion, tt)
}
}
return g
}
// RandSeed sets the seed for the random number generator used by g.
func (g *EntryGenerator) RandSeed(seed int64) {
g.randSeed = seed
}
// candidateTypes returns the types whose values might be convertible to values
// of the tt type. In theory we could run a compatibility check here for fewer
// false positives, but we'll need to check compatibility again when generating
// values anyways, to handle inner types of nested any, so we don't bother.
func (g *EntryGenerator) candidateTypes(tt *vdl.Type, mode entryMode) []*vdl.Type {
var candidates []*vdl.Type
kind := tt.NonOptional().Kind()
if kind.IsNumber() {
candidates = g.ttNumber
}
switch kind {
case vdl.Bool:
candidates = g.ttBool
case vdl.String, vdl.Enum:
candidates = g.ttStringEnum
case vdl.Array, vdl.List:
candidates = g.ttArrayList
case vdl.Set:
candidates = g.ttSet
case vdl.Map:
candidates = g.ttMap
case vdl.Struct:
candidates = g.ttStruct
case vdl.Union:
candidates = g.ttUnion
case vdl.TypeObject:
candidates = []*vdl.Type{vdl.TypeObjectType}
}
if mode == entryAll {
return candidates
}
var filtered []*vdl.Type
for _, c := range candidates {
if (mode == entryUnnamed) == (c.Name() == "") {
filtered = append(filtered, c)
}
}
return filtered
}
// GenAllPass generates a list of passing entries for all types.
func (g *EntryGenerator) GenAllPass() []EntryValue {
var entries []EntryValue
for _, tt := range g.valueGen.Types {
entries = append(entries, g.GenPass(tt)...)
}
return entries
}
// GenPass generates a list of passing entries for the tt type. Each entry has
// a target value of type tt. The source value of each entry is created with
// the property that, if the source value is converted to the target type, the
// result is exactly the target value.
func (g *EntryGenerator) GenPass(tt *vdl.Type) []EntryValue {
// Add entries for zero values.
ev := g.genPass("Zero", vdl.ZeroValue(tt), g.NumZeroEntries, entryAll)
if tt.Kind() == vdl.Optional {
// Add entry to convert from any(nil) to optional(nil).
ev = append(ev, EntryValue{"NilAny", vdl.ZeroValue(tt), vdl.ZeroValue(vdl.AnyType)})
}
// Handle some special-cases.
switch {
case tt == vdl.AnyType:
// Don't create non-nil any values.
return ev
case tt.Kind() == vdl.Enum:
// Test all enum values exhaustively.
for ix := 1; ix < tt.NumEnumLabel(); ix++ {
ev = append(ev, g.genPass("Full", vdl.EnumValue(tt, ix), -1, entryAll)...)
}
return ev
}
// Add entries for max/min number testing.
if needsGenPos(tt) && (g.AllMaxMinTargets || tt.Kind().IsNumber()) {
max := g.makeValue(tt, GenPosMax, 0)
min := g.makeValue(tt, GenPosMin, 0)
ev = append(ev, g.genPassMaxMin("+Max", max)...)
ev = append(ev, g.genPassMaxMin("+Min", min)...)
}
if needsGenNeg(tt) && (g.AllMaxMinTargets || tt.Kind().IsNumber()) {
max := g.makeValue(tt, GenNegMax, 0)
min := g.makeValue(tt, GenNegMin, 0)
ev = append(ev, g.genPassMaxMin("-Max", max)...)
ev = append(ev, g.genPassMaxMin("-Min", min)...)
}
// Add full entries, which are deterministic and recursively non-zero. As a
// special-case, for types that are part of a cycle, the values are still
// deterministic but will contain zero items.
if needsGenFull(tt) {
full := g.makeValue(tt, GenFull, 0)
ev = append(ev, g.genPass("Full", full, g.NumFullEntries, entryAll)...)
}
// Add some random entries.
if needsGenRandom(tt) {
for ix := 0; ix < g.MaxRandomTargets; ix++ {
random := g.makeValue(tt, GenRandom, ix)
ev = append(ev, g.genPass("Random", random, g.NumRandomEntries, entryAll)...)
}
}
return ev
}
func (g *EntryGenerator) genPassMaxMin(label string, target *vdl.Value) []EntryValue {
// We test the boundaries for all unnamed (i.e. built-in) numbers, and limit
// the entries otherwise.
var ev []EntryValue
if tt := target.Type(); tt.Kind().IsNumber() && tt.Name() == "" {
ev = append(ev, g.genPass(label, target, -1, entryUnnamed)...)
ev = append(ev, g.genPass(label, target, g.NumMaxMinEntries, entryNamedNoCanonical)...)
} else {
ev = append(ev, g.genPass(label, target, g.NumMaxMinEntries, entryAll)...)
}
return ev
return nil
}
type entryMode int
const (
entryAll entryMode = iota // Consider all types when generating.
entryUnnamed // Only consider unnamed (anonymous) types.
entryNamedNoCanonical // Only consider named types, no canonical.
)
func (m entryMode) String() string {
switch m {
case entryAll:
return "All"
case entryUnnamed:
return "Unnamed"
case entryNamedNoCanonical:
return "NamedNoCanonical"
}
panic(fmt.Errorf("vdltest: unhandled mode %d", m))
}
// genPass generates a list of passing entries, where each entry has the given
// target value. The given max limits the number of returned entries; -1
// returns all entries.
func (g *EntryGenerator) genPass(label string, target *vdl.Value, max int, mode entryMode) []EntryValue {
var ev []EntryValue
if mode != entryNamedNoCanonical {
// Add the canonical identity conversion for each target value.
ev = append(ev, EntryValue{label, target, target})
}
// Add up to max conversion entries. The general strategy is to add an entry
// for each source type where we can create a value that can convert to the
// target. We filter out all types that cannot possibly be convertible, and
// are left with candidates. The candidates still might not be convertible,
// so we try to mimic values for each type, and add the entry if it succeeds.
candidates := g.candidateTypes(target.Type(), mode)
switch {
case max == 0:
candidates = nil
case max != -1:
// Randomly permute the candidates if we're returning a limited number of
// entries, to cover more cases.
shuffled := make([]*vdl.Type, len(candidates))
for i, p := range g.perm(len(candidates), label, target, mode) {
shuffled[i] = candidates[p]
}
candidates = shuffled
}
num := 0
for _, ttSource := range candidates {
if ttSource == target.Type() {
continue // Skip the canonical case, which was handled above.
}
if source := MimicValue(ttSource, target); source != nil {
if max >= 0 && num >= max {
break
}
num++
ev = append(ev, EntryValue{label, target, source})
}
}
return ev
}
func (g *EntryGenerator) makeValue(tt *vdl.Type, mode GenMode, i int) *vdl.Value {
// ValueGenerator creates random values for us, but we'd like to ensure that
// the values don't change spuriously. I.e. adding new types or generating
// more values shouldn't change any existing values. To this end, we seed the
// random source with a hash of the unique type string, gen mode and iteration
// counter i.
g.hasher.Reset()
g.hasher.Write([]byte(tt.Unique()))
g.hasher.Write([]byte(mode.String()))
g.hasher.Write([]byte(strconv.Itoa(i)))
g.valueGen.RandSeed(g.randSeed + int64(g.hasher.Sum64()))
return g.valueGen.Gen(tt, mode)
}
func (g *EntryGenerator) perm(n int, label string, target *vdl.Value, mode entryMode) []int {
// Similar to makeValue, we'd like to ensure that our choice of random
// candidate permutations don't change our test values spuriously.
g.hasher.Reset()
g.hasher.Write([]byte(label))
// TODO(toddw): The target string changes spuriously because of map ordering.
// Add vdl.Value.UniqueString() or something like that, which will also be
// useful for maintaining sets of all vdl values.
//
//g.hasher.Write([]byte(target.String()))
g.hasher.Write([]byte(mode.String()))
g.rng.Seed(g.randSeed + int64(g.hasher.Sum64()))
return g.rng.Perm(n)
}
// GenAllFail generates a list of failing entries for all types.
func (g *EntryGenerator) GenAllFail() []EntryValue {
var entries []EntryValue
for _, tt := range g.valueGen.Types {
entries = append(entries, g.GenFail(tt)...)
}
return entries
}
// GenFail generates a list of failing entries for the tt type. Each entry has
// a target value of type tt. The source value of each entry is created with
// the property that, if the source value is converted to the target type, the
// conversion fails.
func (g *EntryGenerator) GenFail(tt *vdl.Type) []EntryValue {
// TODO(toddw): Implement this!
return nil
}