lnwire: New API for feature vectors.

This is a rewrite of feature vectors in lnwire. This has a few
benefits:

1) a simpler interface
2) separate structs for a plain set of feature bits and a feature
vector with associated feature names
their respective feature sets
3) loosened requirements that bits MUST be assigned in pairs
4) fix endianness of encoding/decoding
This commit is contained in:
Jim Posen 2017-10-11 11:25:37 -07:00 committed by Olaoluwa Osuntokun
parent b4273d1eaa
commit 1633ab180f
2 changed files with 384 additions and 347 deletions

View File

@ -2,49 +2,24 @@ package lnwire
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
"io" "io"
"math"
"github.com/go-errors/errors"
) )
// featureFlag represent the status of the feature optional/required and needed // FeatureBit represents a feature that can be enabled in either a local or
// to allow future incompatible changes, or backward compatible changes. // global feature vector at a specific bit position. Feature bits follow the
type featureFlag uint8 // "it's OK to be odd" rule, where features at even bit positions must be known
// to a node receiving them from a peer while odd bits do not. In accordance,
// String returns the string representation for the featureFlag. // feature bits are usually assigned in pairs, first being assigned an odd bit
func (f featureFlag) String() string { // position which may later be changed to the preceding even position once
switch f { // knowledge of the feature becomes required on the network.
case OptionalFlag: type FeatureBit uint16
return "optional"
case RequiredFlag:
return "required"
default:
return "<unknown>"
}
}
// featureName represent the name of the feature and needed in order to have
// the compile errors if we specify wrong feature name.
type featureName string
const ( const (
// OptionalFlag represent the feature which we already have but it // InitialRoutingSync is a local feature bit meaning that the receiving node
// isn't required yet, and if remote peer doesn't have this feature we // should send a complete dump of routing information when a new connection
// may turn it off without disconnecting with peer. // is established.
OptionalFlag featureFlag = 2 // 0b10 InitialRoutingSync FeatureBit = 3
// RequiredFlag represent the features which is required for proper
// peer interaction, we disconnect with peer if it doesn't have this
// particular feature.
RequiredFlag featureFlag = 1 // 0b01
// flagMask is a mask which is needed to extract feature flag value.
flagMask = 3 // 0b11
// flagBitsSize represent the size of the feature flag in bits. For
// more information read the init message specification.
flagBitsSize = 2
// maxAllowedSize is a maximum allowed size of feature vector. // maxAllowedSize is a maximum allowed size of feature vector.
// //
@ -61,61 +36,62 @@ const (
maxAllowedSize = 32781 maxAllowedSize = 32781
) )
// Feature represent the feature which is used on stage of initialization of // LocalFeatures is a mapping of known connection-local feature bits to a
// feature vector. Initial feature flags might be changed dynamically later. // descriptive name. All known local feature bits must be assigned a name in
type Feature struct { // this mapping. Local features are those which are only sent to the peer and
Name featureName // not advertised to the entire network. A full description of these feature
Flag featureFlag // bits is provided in the BOLT-09 specification.
var LocalFeatures = map[FeatureBit]string{
InitialRoutingSync: "initial-routing-sync",
} }
// FeatureVector represents the global/local feature vector. With this // GlobalFeatures is a mapping of known global feature bits to a descriptive
// structure you may set/get the feature by name and compare feature vector // name. All known global feature bits must be assigned a name in this mapping.
// with remote one. // Global features are those which are advertised to the entire network. A full
type FeatureVector struct { // description of these feature bits is provided in the BOLT-09 specification.
// featuresMap is the map which stores the correspondence between var GlobalFeatures map[FeatureBit]string
// feature name and its index within feature vector. Index within
// feature vector and actual binary position of feature are different
// things)
featuresMap map[featureName]int // name -> index
// flags is the map which stores the correspondence between feature // RawFeatureVector represents a set of feature bits as defined in BOLT-09.
// index and its flag. // A RawFeatureVector itself just stores a set of bit flags but can be used to
flags map[int]featureFlag // index -> flag // construct a FeatureVector which binds meaning to each bit. Feature vectors
//can be serialized and deserialized to/from a byte representation that is
// transmitted in Lightning network messages.
type RawFeatureVector struct {
features map[FeatureBit]bool
} }
// NewFeatureVector creates new instance of feature vector. // NewRawFeatureVector creates a feature vector with all of the feature bits
func NewFeatureVector(features []Feature) *FeatureVector { // given as arguments enabled.
featuresMap := make(map[featureName]int) func NewRawFeatureVector(bits ...FeatureBit) *RawFeatureVector {
flags := make(map[int]featureFlag) fv := &RawFeatureVector{features: make(map[FeatureBit]bool)}
for _, bit := range bits {
for index, feature := range features { fv.Set(bit)
featuresMap[feature.Name] = index
flags[index] = feature.Flag
}
return &FeatureVector{
featuresMap: featuresMap,
flags: flags,
} }
return fv
} }
// SetFeatureFlag assign flag to the feature. // IsSet returns whether a particular feature bit is enabled in the vector.
func (f *FeatureVector) SetFeatureFlag(name featureName, flag featureFlag) error { func (fv *RawFeatureVector) IsSet(feature FeatureBit) bool {
position, ok := f.featuresMap[name] return fv.features[feature]
if !ok {
return errors.Errorf("can't find feature with name: %v", name)
}
f.flags[position] = flag
return nil
} }
// serializedSize returns the number of bytes which is needed to represent // Set marks a feature as enabled in the vector.
// feature vector in byte format. func (fv *RawFeatureVector) Set(feature FeatureBit) {
func (f *FeatureVector) serializedSize() uint16 { fv.features[feature] = true
// Find the largest index in f.flags }
// Unset marks a feature as disabled in the vector.
func (fv *RawFeatureVector) Unset(feature FeatureBit) {
delete(fv.features, feature)
}
// SerializeSize returns the number of bytes needed to represent feature vector
// in byte format.
func (fv *RawFeatureVector) SerializeSize() int {
// Find the largest feature bit index
max := -1 max := -1
for index := range f.flags { for feature := range fv.features {
index := int(feature)
if index > max { if index > max {
max = index max = index
} }
@ -123,181 +99,134 @@ func (f *FeatureVector) serializedSize() uint16 {
if max == -1 { if max == -1 {
return 0 return 0
} }
// We calculate length via the largest index in f.flags so as to not
// get an index out of bounds in Encode's setFlag function. // We calculate byte-length via the largest bit index
return uint16(math.Ceil(float64(flagBitsSize*(max+1)) / 8)) return max/8 + 1
} }
// NewFeatureVectorFromReader decodes the feature vector from binary // Encode writes the feature vector in byte representation. Every feature
// representation and creates the instance of it. Every feature decoded as 2 // encoded as a bit, and the bit vector is serialized using the least number of
// bits where odd bit determine whether the feature is "optional" and even bit // bytes. Since the bit vector length is variable, the first two bytes of the
// told us whether the feature is "required". The even/odd semantic allows // serialization represent the length.
// future incompatible changes, or backward compatible changes. Bits generally func (fv *RawFeatureVector) Encode(w io.Writer) error {
// assigned in pairs, so that optional features can later become compulsory.
func NewFeatureVectorFromReader(r io.Reader) (*FeatureVector, error) {
f := &FeatureVector{
flags: make(map[int]featureFlag),
}
getFlag := func(data []byte, position int) featureFlag {
byteNumber := uint(position / 8)
bitNumber := uint(position % 8)
return featureFlag((data[byteNumber] >> bitNumber) & flagMask)
}
// Read the length of the feature vector.
var l [2]byte
if _, err := io.ReadFull(r, l[:]); err != nil {
return nil, err
}
length := binary.BigEndian.Uint16(l[:])
// Read the feature vector data.
data := make([]byte, length)
if _, err := io.ReadFull(r, data[:]); err != nil {
return nil, err
}
// Initialize feature vector.
bitsNumber := len(data) * 8
for position := 0; position <= bitsNumber-flagBitsSize; position += flagBitsSize {
flag := getFlag(data, position)
switch flag {
case OptionalFlag, RequiredFlag:
// Every feature/flag takes 2 bits, so in order to get
// the feature/flag index we should divide position
// on 2.
index := position / flagBitsSize
f.flags[index] = flag
default:
continue
}
}
return f, nil
}
// Encode encodes the features vector into bytes representation, every feature
// encoded as 2 bits where odd bit determine whether the feature is "optional"
// and even bit told us whether the feature is "required". The even/odd
// semantic allows future incompatible changes, or backward compatible changes.
// Bits generally assigned in pairs, so that optional features can later become
// compulsory.
func (f *FeatureVector) Encode(w io.Writer) error {
setFlag := func(data []byte, position int, flag featureFlag) {
byteNumber := uint(position / 8)
bitNumber := uint(position % 8)
data[byteNumber] |= (byte(flag) << bitNumber)
}
// Write length of feature vector. // Write length of feature vector.
var l [2]byte var l [2]byte
length := f.serializedSize() length := fv.SerializeSize()
binary.BigEndian.PutUint16(l[:], length) binary.BigEndian.PutUint16(l[:], uint16(length))
if _, err := w.Write(l[:]); err != nil { if _, err := w.Write(l[:]); err != nil {
return err return err
} }
// Generate the data and write it. // Generate the data and write it.
data := make([]byte, length) data := make([]byte, length)
for index, flag := range f.flags { for feature := range fv.features {
// Every feature takes 2 bits, so in order to get the feature byteIndex := int(feature / 8)
// bits position we should multiply index by 2. bitIndex := feature % 8
position := index * flagBitsSize data[length-byteIndex-1] |= 1 << bitIndex
setFlag(data, position, flag)
} }
_, err := w.Write(data) _, err := w.Write(data)
return err return err
} }
// Compare checks that features are compatible and returns the features which // Decode reads the feature vector from its byte representation. Every feature
// were present in both remote and local feature vectors. If remote/local node // encoded as a bit, and the bit vector is serialized using the least number of
// doesn't have the feature and local/remote node require it than such vectors // bytes. Since the bit vector length is variable, the first two bytes of the
// are incompatible. // serialization represent the length.
func (f *FeatureVector) Compare(f2 *FeatureVector) (*SharedFeatures, error) { func (fv *RawFeatureVector) Decode(r io.Reader) error {
shared := newSharedFeatures(f.Copy()) // Read the length of the feature vector.
var l [2]byte
if _, err := io.ReadFull(r, l[:]); err != nil {
return err
}
length := binary.BigEndian.Uint16(l[:])
for index, flag := range f.flags { // Read the feature vector data.
if _, exist := f2.flags[index]; !exist { data := make([]byte, length)
switch flag { if _, err := io.ReadFull(r, data); err != nil {
case RequiredFlag: return err
return nil, errors.New("Remote node hasn't " +
"locally required feature")
case OptionalFlag:
// If feature is optional and remote side
// haven't it than it might be safely disabled.
delete(shared.flags, index)
continue
}
}
// If feature exists on both sides than such feature might be
// considered as active.
shared.flags[index] = flag
} }
for index, flag := range f2.flags { // Set feature bits from parsed data.
if _, exist := f.flags[index]; !exist { bitsNumber := len(data) * 8
switch flag { for i := 0; i < bitsNumber; i++ {
case RequiredFlag: byteIndex := uint16(i / 8)
return nil, errors.New("Local node hasn't " + bitIndex := uint(i % 8)
"locally required feature") if (data[length-byteIndex-1]>>bitIndex)&1 == 1 {
case OptionalFlag: fv.Set(FeatureBit(i))
// If feature is optional and local side
// haven't it than it might be safely disabled.
delete(shared.flags, index)
continue
}
}
// If feature exists on both sides than such feature might be
// considered as active.
shared.flags[index] = flag
}
return shared, nil
}
// Copy generate new distinct instance of the feature vector.
func (f *FeatureVector) Copy() *FeatureVector {
features := make([]Feature, len(f.featuresMap))
for name, index := range f.featuresMap {
features[index] = Feature{
Name: name,
Flag: f.flags[index],
} }
} }
return NewFeatureVector(features) return nil
} }
// SharedFeatures is a product of comparison of two features vector which // FeatureVector represents a set of enabled features. The set stores
// consist of features which are present in both local and remote features // information on enabled flags and metadata about the feature names. A feature
// vectors. // vector is serializable to a compact byte representation that is included in
type SharedFeatures struct { // Lightning network messages.
*FeatureVector type FeatureVector struct {
*RawFeatureVector
featureNames map[FeatureBit]string
} }
// newSharedFeatures creates new shared features instance. // NewFeatureVector constructs a new FeatureVector from a raw feature vector and
func newSharedFeatures(f *FeatureVector) *SharedFeatures { // mapping of feature definitions.
return &SharedFeatures{f} func NewFeatureVector(featureVector *RawFeatureVector,
} featureNames map[FeatureBit]string) *FeatureVector {
// IsActive checks is feature active or not, it might be disabled during return &FeatureVector{
// comparision with remote feature vector if it was optional and remote peer RawFeatureVector: featureVector,
// doesn't support it. featureNames: featureNames,
func (f *SharedFeatures) IsActive(name featureName) bool {
index, ok := f.featuresMap[name]
if !ok {
// If we even have no such feature in feature map, than it
// can't be active in any circumstances.
return false
} }
}
_, exist := f.flags[index]
return exist // HasFeature returns whether a particular feature is included in the set. The
// feature can be seen as set either if the bit is set directly OR the queried
// bit has the same meaning as its corresponding even/odd bit, which is set
// instead. The second case is because feature bits are generally assigned in
// pairs where both the even and odd position represent the same feature.
func (fv *FeatureVector) HasFeature(feature FeatureBit) bool {
return fv.IsSet(feature) ||
(fv.isFeatureBitPair(feature) && fv.IsSet(feature^1))
}
// UnknownRequiredFeatures returns a list of feature bits set in the vector that
// are unknown and in an even bit position. Feature bits with an even index must
// be known to a node receiving the feature vector in a message.
func (fv *FeatureVector) UnknownRequiredFeatures() []FeatureBit {
var unknown []FeatureBit
for feature := range fv.features {
if feature%2 == 0 && !fv.IsKnown(feature) {
unknown = append(unknown, feature)
}
}
return unknown
}
// Name returns a string identifier for the feature represented by this bit. If
// the bit does not represent a known feature, this returns a string indicating
// as much.
func (fv *FeatureVector) Name(bit FeatureBit) string {
name, known := fv.featureNames[bit]
if !known {
name = "unknown"
}
return fmt.Sprintf("%s(%d)", name, bit)
}
// IsKnown returns whether this feature bit represents a known feature.
func (fv *FeatureVector) IsKnown(bit FeatureBit) bool {
_, known := fv.featureNames[bit]
return known
}
// isFeatureBitPair returns whether this feature bit and its corresponding
// even/odd bit both represent the same feature. This may often be the case as
// bits are generally assigned in pairs, first being assigned an odd bit
// position then being promoted to an even bit position once the network is
// ready.
func (fv *FeatureVector) isFeatureBitPair(bit FeatureBit) bool {
name1, known1 := fv.featureNames[bit]
name2, known2 := fv.featureNames[bit^1]
return known1 && known2 && name1 == name2
} }

View File

@ -3,152 +3,260 @@ package lnwire
import ( import (
"bytes" "bytes"
"reflect" "reflect"
"sort"
"testing" "testing"
"github.com/davecgh/go-spew/spew"
) )
// TestFeaturesRemoteRequireError checks that we throw an error if remote peer var testFeatureNames = map[FeatureBit]string{
// has required feature which we don't support. 0: "feature1",
func TestFeaturesRemoteRequireError(t *testing.T) { 3: "feature2",
4: "feature3",
5: "feature3",
}
func TestFeatureVectorSetUnset(t *testing.T) {
t.Parallel() t.Parallel()
const ( tests := []struct {
first = "first" bits []FeatureBit
second = "second" expectedFeatures []bool
) }{
// No features are enabled if no bits are set.
{
bits: nil,
expectedFeatures: []bool{false, false, false, false, false, false, false, false},
},
// Test setting an even bit for an even-only bit feature. The
// corresponding odd bit should not be seen as set.
{
bits: []FeatureBit{0},
expectedFeatures: []bool{true, false, false, false, false, false, false, false},
},
// Test setting an odd bit for an even-only bit feature. The
// corresponding even bit should not be seen as set.
{
bits: []FeatureBit{1},
expectedFeatures: []bool{false, true, false, false, false, false, false, false},
},
// Test setting an even bit for an odd-only bit feature. The bit should
// be seen as set and the odd bit should not.
{
bits: []FeatureBit{2},
expectedFeatures: []bool{false, false, true, false, false, false, false, false},
},
// Test setting an odd bit for an odd-only bit feature. The bit should
// be seen as set and the even bit should not.
{
bits: []FeatureBit{3},
expectedFeatures: []bool{false, false, false, true, false, false, false, false},
},
// Test setting an even bit for even-odd pair feature. Both bits in the
// pair should be seen as set.
{
bits: []FeatureBit{4},
expectedFeatures: []bool{false, false, false, false, true, true, false, false},
},
// Test setting an odd bit for even-odd pair feature. Both bits in the
// pair should be seen as set.
{
bits: []FeatureBit{5},
expectedFeatures: []bool{false, false, false, false, true, true, false, false},
},
// Test setting an even bit for an unknown feature. The bit should be
// seen as set and the odd bit should not.
{
bits: []FeatureBit{6},
expectedFeatures: []bool{false, false, false, false, false, false, true, false},
},
// Test setting an odd bit for an unknown feature. The bit should be
// seen as set and the odd bit should not.
{
bits: []FeatureBit{7},
expectedFeatures: []bool{false, false, false, false, false, false, false, true},
},
}
localFeatures := NewFeatureVector([]Feature{ fv := NewFeatureVector(nil, testFeatureNames)
{first, OptionalFlag}, for i, test := range tests {
}) for _, bit := range test.bits {
fv.Set(bit)
}
remoteFeatures := NewFeatureVector([]Feature{ for j, expectedSet := range test.expectedFeatures {
{first, OptionalFlag}, if fv.HasFeature(FeatureBit(j)) != expectedSet {
{second, RequiredFlag}, t.Errorf("Expection failed in case %d, bit %d", i, j)
}) break
}
}
if _, err := localFeatures.Compare(remoteFeatures); err == nil { for _, bit := range test.bits {
t.Fatal("error wasn't received") fv.Unset(bit)
}
} }
} }
// TestFeaturesLocalRequireError checks that we throw an error if local peer has func TestFeatureVectorEncodeDecode(t *testing.T) {
// required feature which remote peer don't support.
func TestFeaturesLocalRequireError(t *testing.T) {
t.Parallel() t.Parallel()
const ( tests := []struct {
first = "first" bits []FeatureBit
second = "second" expectedEncoded []byte
) }{
{
bits: nil,
expectedEncoded: []byte{0x00, 0x00},
},
{
bits: []FeatureBit{2, 3, 7},
expectedEncoded: []byte{0x00, 0x01, 0x8C},
},
{
bits: []FeatureBit{2, 3, 8},
expectedEncoded: []byte{0x00, 0x02, 0x01, 0x0C},
},
}
localFeatures := NewFeatureVector([]Feature{ for i, test := range tests {
{first, OptionalFlag}, fv := NewRawFeatureVector(test.bits...)
{second, RequiredFlag},
})
remoteFeatures := NewFeatureVector([]Feature{ // Test that Encode produces the correct serialization.
{first, OptionalFlag}, buffer := new(bytes.Buffer)
}) err := fv.Encode(buffer)
if err != nil {
t.Errorf("Failed to encode feature vector in case %d: %v", i, err)
continue
}
if _, err := localFeatures.Compare(remoteFeatures); err == nil { encoded := buffer.Bytes()
t.Fatal("error wasn't received") if !bytes.Equal(encoded, test.expectedEncoded) {
t.Errorf("Wrong encoding in case %d: got %v, expected %v",
i, encoded, test.expectedEncoded)
continue
}
// Test that decoding then re-encoding produces the same result.
fv2 := NewRawFeatureVector()
err = fv2.Decode(bytes.NewReader(encoded))
if err != nil {
t.Errorf("Failed to decode feature vector in case %d: %v", i, err)
continue
}
buffer2 := new(bytes.Buffer)
err = fv2.Encode(buffer2)
if err != nil {
t.Errorf("Failed to re-encode feature vector in case %d: %v",
i, err)
continue
}
reencoded := buffer2.Bytes()
if !bytes.Equal(reencoded, test.expectedEncoded) {
t.Errorf("Wrong re-encoding in case %d: got %v, expected %v",
i, reencoded, test.expectedEncoded)
}
} }
} }
// TestOptionalFeature checks that if remote peer don't have the feature but func TestFeatureVectorUnknownFeatures(t *testing.T) {
// on our side this feature is optional than we mark this feature as disabled.
func TestOptionalFeature(t *testing.T) {
t.Parallel() t.Parallel()
const first = "first" tests := []struct {
bits []FeatureBit
localFeatures := NewFeatureVector([]Feature{ expectedUnknown []FeatureBit
{first, OptionalFlag}, }{
}) {
bits: nil,
remoteFeatures := NewFeatureVector([]Feature{}) expectedUnknown: nil,
},
shared, err := localFeatures.Compare(remoteFeatures) // Since bits {0, 3, 4, 5} are known, and only even bits are considered
if err != nil { // required (according to the "it's OK to be odd rule"), that leaves
t.Fatalf("error while feature vector compare: %v", err) // {2, 6} as both unknown and required.
{
bits: []FeatureBit{0, 1, 2, 3, 4, 5, 6, 7},
expectedUnknown: []FeatureBit{2, 6},
},
} }
if shared.IsActive(first) { for i, test := range tests {
t.Fatal("locally feature was set but remote peer notified us" + rawVector := NewRawFeatureVector(test.bits...)
" that it don't have it") fv := NewFeatureVector(rawVector, testFeatureNames)
}
// A feature with a non-existent name shouldn't be active. unknown := fv.UnknownRequiredFeatures()
if shared.IsActive("nothere") {
t.Fatal("non-existent feature shouldn't be active") // Sort to make comparison independent of order
sort.Slice(unknown, func(i, j int) bool {
return unknown[i] < unknown[j]
})
if !reflect.DeepEqual(unknown, test.expectedUnknown) {
t.Errorf("Wrong unknown features in case %d: got %v, expected %v",
i, unknown, test.expectedUnknown)
}
} }
} }
// TestSetRequireAfterInit checks that we can change the feature flag after func TestFeatureNames(t *testing.T) {
// initialization.
func TestSetRequireAfterInit(t *testing.T) {
t.Parallel() t.Parallel()
const first = "first" tests := []struct {
bit FeatureBit
expectedName string
expectedKnown bool
}{
{
bit: 0,
expectedName: "feature1(0)",
expectedKnown: true,
},
{
bit: 1,
expectedName: "unknown(1)",
expectedKnown: false,
},
{
bit: 2,
expectedName: "unknown(2)",
expectedKnown: false,
},
{
bit: 3,
expectedName: "feature2(3)",
expectedKnown: true,
},
{
bit: 4,
expectedName: "feature3(4)",
expectedKnown: true,
},
{
bit: 5,
expectedName: "feature3(5)",
expectedKnown: true,
},
{
bit: 6,
expectedName: "unknown(6)",
expectedKnown: false,
},
{
bit: 7,
expectedName: "unknown(7)",
expectedKnown: false,
},
}
localFeatures := NewFeatureVector([]Feature{ fv := NewFeatureVector(nil, testFeatureNames)
{first, OptionalFlag}, for _, test := range tests {
}) name := fv.Name(test.bit)
localFeatures.SetFeatureFlag(first, RequiredFlag) if name != test.expectedName {
remoteFeatures := NewFeatureVector([]Feature{}) t.Errorf("Name for feature bit %d is incorrect: "+
"expected %s, got %s", test.bit, name, test.expectedName)
}
_, err := localFeatures.Compare(remoteFeatures) known := fv.IsKnown(test.bit)
if err == nil { if known != test.expectedKnown {
t.Fatalf("feature was set as required but error wasn't "+ t.Errorf("IsKnown for feature bit %d is incorrect: "+
"returned: %v", err) "expected %v, got %v", test.bit, known, test.expectedKnown)
} }
}
// TestDecodeEncodeFeaturesVector checks that feature vector might be
// successfully encoded and decoded.
func TestDecodeEncodeFeaturesVector(t *testing.T) {
t.Parallel()
const first = "first"
f := NewFeatureVector([]Feature{
{first, OptionalFlag},
})
var b bytes.Buffer
if err := f.Encode(&b); err != nil {
t.Fatalf("error while encoding feature vector: %v", err)
}
nf, err := NewFeatureVectorFromReader(&b)
if err != nil {
t.Fatalf("error while decoding feature vector: %v", err)
}
// Assert equality of the two instances.
if !reflect.DeepEqual(f.flags, nf.flags) {
t.Fatalf("encode/decode feature vector don't match %v vs "+
"%v", spew.Sdump(f), spew.Sdump(nf))
}
}
func TestFeatureFlagString(t *testing.T) {
t.Parallel()
if OptionalFlag.String() != "optional" {
t.Fatalf("incorrect string, expected optional got %v",
OptionalFlag.String())
}
if RequiredFlag.String() != "required" {
t.Fatalf("incorrect string, expected required got %v",
OptionalFlag.String())
}
fakeFlag := featureFlag(9)
if fakeFlag.String() != "<unknown>" {
t.Fatalf("incorrect string, expected <unknown> got %v",
fakeFlag.String())
} }
} }