Fix several kapacitor validation failures
parent
97cae4e788
commit
4f7710454a
|
@ -49,7 +49,7 @@
|
|||
|
||||
[[projects]]
|
||||
name = "github.com/google/go-cmp"
|
||||
packages = ["cmp"]
|
||||
packages = ["cmp","cmp/cmpopts"]
|
||||
revision = "79b2d888f100ec053545168aa94bcfb322e8bfc8"
|
||||
|
||||
[[projects]]
|
||||
|
@ -140,6 +140,6 @@
|
|||
[solve-meta]
|
||||
analyzer-name = "dep"
|
||||
analyzer-version = 1
|
||||
inputs-digest = "f34fb88755292baba8b52c14bf5b9a028daff96a763368a7cf1de90004d33695"
|
||||
inputs-digest = "85a5451fc9e0596e486a676204eb2de0b12900522341ee0804cf9ec86fb2765e"
|
||||
solver-name = "gps-cdcl"
|
||||
solver-version = 1
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/influxdata/chronograf"
|
||||
client "github.com/influxdata/kapacitor/client/v1"
|
||||
)
|
||||
|
@ -945,10 +946,22 @@ func TestClient_Update(t *testing.T) {
|
|||
ctx: context.Background(),
|
||||
href: "/kapacitor/v1/tasks/howdy",
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "howdy",
|
||||
ID: "howdy",
|
||||
Name: "myname",
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
Measurement: "meas",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Type: "field",
|
||||
Value: "usage_user",
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: "threshold",
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: greaterThan,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1009,10 +1022,22 @@ func TestClient_Update(t *testing.T) {
|
|||
ctx: context.Background(),
|
||||
href: "/kapacitor/v1/tasks/howdy",
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "howdy",
|
||||
ID: "howdy",
|
||||
Name: "myname",
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
Measurement: "meas",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Type: "field",
|
||||
Value: "usage_user",
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: "threshold",
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: greaterThan,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1061,6 +1086,135 @@ func TestClient_Update(t *testing.T) {
|
|||
},
|
||||
wantStatus: client.Disabled,
|
||||
},
|
||||
{
|
||||
name: "error because relative cannot have inside range",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
|
||||
return kapa, nil
|
||||
},
|
||||
Ticker: &Alert{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
href: "/kapacitor/v1/tasks/error",
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "error",
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
Fields: []chronograf.Field{
|
||||
{
|
||||
Value: "usage_user",
|
||||
Type: "field",
|
||||
},
|
||||
},
|
||||
},
|
||||
Trigger: Relative,
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Operator: InsideRange,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error because rule has an unknown trigger mechanism",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
|
||||
return kapa, nil
|
||||
},
|
||||
Ticker: &Alert{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
href: "/kapacitor/v1/tasks/error",
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "error",
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error because query has no fields",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
|
||||
return kapa, nil
|
||||
},
|
||||
Ticker: &Alert{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
href: "/kapacitor/v1/tasks/error",
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "error",
|
||||
Trigger: Threshold,
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Period: "1d",
|
||||
},
|
||||
Name: "myname",
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
Measurement: "meas",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error because alert has no name",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
|
||||
return kapa, nil
|
||||
},
|
||||
Ticker: &Alert{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
href: "/kapacitor/v1/tasks/error",
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "error",
|
||||
Trigger: Deadman,
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Period: "1d",
|
||||
},
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
Measurement: "meas",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error because alert period cannot be an empty string in deadman alert",
|
||||
wantErr: true,
|
||||
fields: fields{
|
||||
kapaClient: func(url, username, password string, insecureSkipVerify bool) (KapaClient, error) {
|
||||
return kapa, nil
|
||||
},
|
||||
Ticker: &Alert{},
|
||||
},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
href: "/kapacitor/v1/tasks/error",
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "error",
|
||||
Name: "myname",
|
||||
Trigger: Deadman,
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
Measurement: "meas",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
kapa.ResTask = tt.resTask
|
||||
|
@ -1079,11 +1233,17 @@ func TestClient_Update(t *testing.T) {
|
|||
t.Errorf("Client.Update() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
if !cmp.Equal(got, tt.want) {
|
||||
t.Errorf("%q. Client.Update() = -got/+want %s", tt.name, cmp.Diff(got, tt.want))
|
||||
}
|
||||
if !reflect.DeepEqual(kapa.UpdateTaskOptions, tt.updateTaskOptions) {
|
||||
t.Errorf("Client.Update() = %v, want %v", kapa.UpdateTaskOptions, tt.updateTaskOptions)
|
||||
var cmpOptions = cmp.Options{
|
||||
cmpopts.IgnoreFields(client.UpdateTaskOptions{}, "TICKscript"),
|
||||
}
|
||||
if !cmp.Equal(kapa.UpdateTaskOptions, tt.updateTaskOptions, cmpOptions...) {
|
||||
t.Errorf("Client.Update() = %s", cmp.Diff(got, tt.updateTaskOptions, cmpOptions...))
|
||||
}
|
||||
if tt.wantStatus != kapa.LastStatus {
|
||||
t.Errorf("Client.Update() = %v, want %v", kapa.LastStatus, tt.wantStatus)
|
||||
|
@ -1130,10 +1290,16 @@ func TestClient_Create(t *testing.T) {
|
|||
args: args{
|
||||
ctx: context.Background(),
|
||||
rule: chronograf.AlertRule{
|
||||
ID: "howdy",
|
||||
ID: "howdy",
|
||||
Name: "mynames",
|
||||
Query: &chronograf.QueryConfig{
|
||||
Database: "db",
|
||||
RetentionPolicy: "rp",
|
||||
Measurement: "meas",
|
||||
},
|
||||
Trigger: Deadman,
|
||||
TriggerValues: chronograf.TriggerValues{
|
||||
Period: "1d",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -1152,10 +1318,79 @@ func TestClient_Create(t *testing.T) {
|
|||
},
|
||||
},
|
||||
createTaskOptions: &client.CreateTaskOptions{
|
||||
TICKscript: "",
|
||||
ID: "chronograf-v1-howdy",
|
||||
Type: client.StreamTask,
|
||||
Status: client.Enabled,
|
||||
TICKscript: `var db = 'db'
|
||||
|
||||
var rp = 'rp'
|
||||
|
||||
var measurement = 'meas'
|
||||
|
||||
var groupBy = []
|
||||
|
||||
var whereFilter = lambda: TRUE
|
||||
|
||||
var period = 1d
|
||||
|
||||
var name = 'mynames'
|
||||
|
||||
var idVar = name + ':{{.Group}}'
|
||||
|
||||
var message = ''
|
||||
|
||||
var idTag = 'alertID'
|
||||
|
||||
var levelTag = 'level'
|
||||
|
||||
var messageField = 'message'
|
||||
|
||||
var durationField = 'duration'
|
||||
|
||||
var outputDB = 'chronograf'
|
||||
|
||||
var outputRP = 'autogen'
|
||||
|
||||
var outputMeasurement = 'alerts'
|
||||
|
||||
var triggerType = 'deadman'
|
||||
|
||||
var threshold = 0.0
|
||||
|
||||
var data = stream
|
||||
|from()
|
||||
.database(db)
|
||||
.retentionPolicy(rp)
|
||||
.measurement(measurement)
|
||||
.groupBy(groupBy)
|
||||
.where(whereFilter)
|
||||
|
||||
var trigger = data
|
||||
|deadman(threshold, period)
|
||||
.stateChangesOnly()
|
||||
.message(message)
|
||||
.id(idVar)
|
||||
.idTag(idTag)
|
||||
.levelTag(levelTag)
|
||||
.messageField(messageField)
|
||||
.durationField(durationField)
|
||||
|
||||
trigger
|
||||
|eval(lambda: "emitted")
|
||||
.as('value')
|
||||
.keep('value', messageField, durationField)
|
||||
|influxDBOut()
|
||||
.create()
|
||||
.database(outputDB)
|
||||
.retentionPolicy(outputRP)
|
||||
.measurement(outputMeasurement)
|
||||
.tag('alertName', name)
|
||||
.tag('triggerType', triggerType)
|
||||
|
||||
trigger
|
||||
|httpOut('output')
|
||||
`,
|
||||
|
||||
ID: "chronograf-v1-howdy",
|
||||
Type: client.StreamTask,
|
||||
Status: client.Enabled,
|
||||
DBRPs: []client.DBRP{
|
||||
{
|
||||
Database: "db",
|
||||
|
@ -1205,10 +1440,9 @@ func TestClient_Create(t *testing.T) {
|
|||
},
|
||||
resError: fmt.Errorf("error"),
|
||||
createTaskOptions: &client.CreateTaskOptions{
|
||||
TICKscript: "",
|
||||
ID: "chronograf-v1-howdy",
|
||||
Type: client.StreamTask,
|
||||
Status: client.Enabled,
|
||||
ID: "chronograf-v1-howdy",
|
||||
Type: client.StreamTask,
|
||||
Status: client.Enabled,
|
||||
DBRPs: []client.DBRP{
|
||||
{
|
||||
Database: "db",
|
||||
|
@ -1236,6 +1470,9 @@ func TestClient_Create(t *testing.T) {
|
|||
t.Errorf("Client.Create() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if tt.wantErr {
|
||||
return
|
||||
}
|
||||
if !cmp.Equal(got, tt.want) {
|
||||
t.Errorf("%q. Client.Create() = -got/+want %s", tt.name, cmp.Diff(got, tt.want))
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package kapacitor
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
greaterThan = "greater than"
|
||||
|
|
|
@ -15,11 +15,11 @@ type Alert struct{}
|
|||
func (a *Alert) Generate(rule chronograf.AlertRule) (chronograf.TICKScript, error) {
|
||||
vars, err := Vars(rule)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
return "", err
|
||||
}
|
||||
data, err := Data(rule)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
return "", err
|
||||
}
|
||||
trigger, err := Trigger(rule)
|
||||
if err != nil {
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package kapacitor
|
||||
|
||||
import "github.com/influxdata/chronograf"
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/influxdata/chronograf"
|
||||
)
|
||||
|
||||
const (
|
||||
// Deadman triggers when data is missing for a period of time
|
||||
|
|
|
@ -76,7 +76,37 @@ func Vars(rule chronograf.AlertRule) (string, error) {
|
|||
}
|
||||
}
|
||||
|
||||
type NotEmpty struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (n *NotEmpty) Valid(name, s string) error {
|
||||
if n.Err != nil {
|
||||
return n.Err
|
||||
|
||||
}
|
||||
if s == "" {
|
||||
n.Err = fmt.Errorf("%s cannot be an empty string", name)
|
||||
}
|
||||
return n.Err
|
||||
}
|
||||
|
||||
func commonVars(rule chronograf.AlertRule) (string, error) {
|
||||
n := new(NotEmpty)
|
||||
n.Valid("database", rule.Query.Database)
|
||||
n.Valid("retention policy", rule.Query.RetentionPolicy)
|
||||
n.Valid("measurement", rule.Query.Measurement)
|
||||
n.Valid("alert name", rule.Name)
|
||||
n.Valid("trigger type", rule.Trigger)
|
||||
if n.Err != nil {
|
||||
return "", n.Err
|
||||
}
|
||||
|
||||
wind, err := window(rule)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
common := `
|
||||
var db = '%s'
|
||||
var rp = '%s'
|
||||
|
@ -104,7 +134,7 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
|
|||
rule.Query.Measurement,
|
||||
groupBy(rule.Query),
|
||||
whereFilter(rule.Query),
|
||||
window(rule),
|
||||
wind,
|
||||
rule.Name,
|
||||
rule.Message,
|
||||
IDTag,
|
||||
|
@ -127,17 +157,27 @@ func commonVars(rule chronograf.AlertRule) (string, error) {
|
|||
|
||||
// window is only used if deadman or threshold/relative with aggregate. Will return empty
|
||||
// if no period.
|
||||
func window(rule chronograf.AlertRule) string {
|
||||
func window(rule chronograf.AlertRule) (string, error) {
|
||||
if rule.Trigger == Deadman {
|
||||
return fmt.Sprintf("var period = %s", rule.TriggerValues.Period)
|
||||
if rule.TriggerValues.Period == "" {
|
||||
return "", fmt.Errorf("period cannot be an empty string in deadman alert")
|
||||
}
|
||||
return fmt.Sprintf("var period = %s", rule.TriggerValues.Period), nil
|
||||
|
||||
}
|
||||
// Period only makes sense if the field has a been grouped via a time duration.
|
||||
for _, field := range rule.Query.Fields {
|
||||
if field.Type == "func" {
|
||||
return fmt.Sprintf("var period = %s\nvar every = %s", rule.Query.GroupBy.Time, rule.Every)
|
||||
n := new(NotEmpty)
|
||||
n.Valid("group by time", rule.Query.GroupBy.Time)
|
||||
n.Valid("every", rule.Every)
|
||||
if n.Err != nil {
|
||||
return "", n.Err
|
||||
}
|
||||
return fmt.Sprintf("var period = %s\nvar every = %s", rule.Query.GroupBy.Time, rule.Every), nil
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func groupBy(q *chronograf.QueryConfig) string {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {CHANGES, OPERATORS, SHIFTS} from 'src/kapacitor/constants'
|
||||
import {CHANGES, RELATIVE_OPERATORS, SHIFTS} from 'src/kapacitor/constants'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
|
||||
const mapToItems = (arr, type) => arr.map(text => ({text, type}))
|
||||
const changes = mapToItems(CHANGES, 'change')
|
||||
const shifts = mapToItems(SHIFTS, 'shift')
|
||||
const operators = mapToItems(OPERATORS, 'operator')
|
||||
const operators = mapToItems(RELATIVE_OPERATORS, 'operator')
|
||||
|
||||
const Relative = ({
|
||||
onRuleTypeInputChange,
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import {OPERATORS} from 'src/kapacitor/constants'
|
||||
import {THRESHOLD_OPERATORS} from 'src/kapacitor/constants'
|
||||
import Dropdown from 'shared/components/Dropdown'
|
||||
import _ from 'lodash'
|
||||
|
||||
const mapToItems = (arr, type) => arr.map(text => ({text, type}))
|
||||
const operators = mapToItems(OPERATORS, 'operator')
|
||||
const operators = mapToItems(THRESHOLD_OPERATORS, 'operator')
|
||||
const noopSubmit = e => e.preventDefault()
|
||||
const getField = ({fields}) => {
|
||||
const alias = _.get(fields, ['0', 'alias'], false)
|
||||
|
|
|
@ -31,7 +31,7 @@ export const OUTSIDE_RANGE = 'outside range'
|
|||
export const EQUAL_TO_OR_GREATER_THAN = 'equal to or greater'
|
||||
export const EQUAL_TO_OR_LESS_THAN = 'equal to or less than'
|
||||
|
||||
export const OPERATORS = [
|
||||
export const THRESHOLD_OPERATORS = [
|
||||
GREATER_THAN,
|
||||
EQUAL_TO_OR_GREATER_THAN,
|
||||
EQUAL_TO_OR_LESS_THAN,
|
||||
|
@ -42,6 +42,15 @@ export const OPERATORS = [
|
|||
OUTSIDE_RANGE,
|
||||
]
|
||||
|
||||
export const RELATIVE_OPERATORS = [
|
||||
GREATER_THAN,
|
||||
EQUAL_TO_OR_GREATER_THAN,
|
||||
EQUAL_TO_OR_LESS_THAN,
|
||||
LESS_THAN,
|
||||
EQUAL_TO,
|
||||
NOT_EQUAL_TO,
|
||||
]
|
||||
|
||||
// export const RELATIONS = ['once', 'more than ', 'less than'];
|
||||
export const PERIODS = ['1m', '5m', '10m', '30m', '1h', '2h', '24h']
|
||||
export const CHANGES = ['change', '% change']
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// Package cmpopts provides common options for the cmp package.
|
||||
package cmpopts
|
||||
|
||||
import (
|
||||
"math"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func equateAlways(_, _ interface{}) bool { return true }
|
||||
|
||||
// EquateEmpty returns a Comparer option that determines all maps and slices
|
||||
// with a length of zero to be equal, regardless of whether they are nil.
|
||||
//
|
||||
// EquateEmpty can be used in conjuction with SortSlices and SortMaps.
|
||||
func EquateEmpty() cmp.Option {
|
||||
return cmp.FilterValues(isEmpty, cmp.Comparer(equateAlways))
|
||||
}
|
||||
|
||||
func isEmpty(x, y interface{}) bool {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
return (x != nil && y != nil && vx.Type() == vy.Type()) &&
|
||||
(vx.Kind() == reflect.Slice || vx.Kind() == reflect.Map) &&
|
||||
(vx.Len() == 0 && vy.Len() == 0)
|
||||
}
|
||||
|
||||
// EquateApprox returns a Comparer option that determines float32 or float64
|
||||
// values to be equal if they are within a relative fraction or absolute margin.
|
||||
// This option is not used when either x or y is NaN or infinite.
|
||||
//
|
||||
// The fraction determines that the difference of two values must be within the
|
||||
// smaller fraction of the two values, while the margin determines that the two
|
||||
// values must be within some absolute margin.
|
||||
// To express only a fraction or only a margin, use 0 for the other parameter.
|
||||
// The fraction and margin must be non-negative.
|
||||
//
|
||||
// The mathematical expression used is equivalent to:
|
||||
// |x-y| ≤ max(fraction*min(|x|, |y|), margin)
|
||||
//
|
||||
// EquateApprox can be used in conjuction with EquateNaNs.
|
||||
func EquateApprox(fraction, margin float64) cmp.Option {
|
||||
if margin < 0 || fraction < 0 || math.IsNaN(margin) || math.IsNaN(fraction) {
|
||||
panic("margin or fraction must be a non-negative number")
|
||||
}
|
||||
a := approximator{fraction, margin}
|
||||
return cmp.Options{
|
||||
cmp.FilterValues(areRealF64s, cmp.Comparer(a.compareF64)),
|
||||
cmp.FilterValues(areRealF32s, cmp.Comparer(a.compareF32)),
|
||||
}
|
||||
}
|
||||
|
||||
type approximator struct{ frac, marg float64 }
|
||||
|
||||
func areRealF64s(x, y float64) bool {
|
||||
return !math.IsNaN(x) && !math.IsNaN(y) && !math.IsInf(x, 0) && !math.IsInf(y, 0)
|
||||
}
|
||||
func areRealF32s(x, y float32) bool {
|
||||
return areRealF64s(float64(x), float64(y))
|
||||
}
|
||||
func (a approximator) compareF64(x, y float64) bool {
|
||||
relMarg := a.frac * math.Min(math.Abs(x), math.Abs(y))
|
||||
return math.Abs(x-y) <= math.Max(a.marg, relMarg)
|
||||
}
|
||||
func (a approximator) compareF32(x, y float32) bool {
|
||||
return a.compareF64(float64(x), float64(y))
|
||||
}
|
||||
|
||||
// EquateNaNs returns a Comparer option that determines float32 and float64
|
||||
// NaN values to be equal.
|
||||
//
|
||||
// EquateNaNs can be used in conjuction with EquateApprox.
|
||||
func EquateNaNs() cmp.Option {
|
||||
return cmp.Options{
|
||||
cmp.FilterValues(areNaNsF64s, cmp.Comparer(equateAlways)),
|
||||
cmp.FilterValues(areNaNsF32s, cmp.Comparer(equateAlways)),
|
||||
}
|
||||
}
|
||||
|
||||
func areNaNsF64s(x, y float64) bool {
|
||||
return math.IsNaN(x) && math.IsNaN(y)
|
||||
}
|
||||
func areNaNsF32s(x, y float32) bool {
|
||||
return areNaNsF64s(float64(x), float64(y))
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
package cmpopts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// IgnoreFields returns an Option that ignores exported fields of the
|
||||
// given names on a single struct type.
|
||||
// The struct type is specified by passing in a value of that type.
|
||||
//
|
||||
// The name may be a dot-delimited string (e.g., "Foo.Bar") to ignore a
|
||||
// specific sub-field that is embedded or nested within the parent struct.
|
||||
//
|
||||
// This does not handle unexported fields; use IgnoreUnexported instead.
|
||||
func IgnoreFields(typ interface{}, names ...string) cmp.Option {
|
||||
sf := newStructFilter(typ, names...)
|
||||
return cmp.FilterPath(sf.filter, cmp.Ignore())
|
||||
}
|
||||
|
||||
// IgnoreTypes returns an Option that ignores all values assignable to
|
||||
// certain types, which are specified by passing in a value of each type.
|
||||
func IgnoreTypes(typs ...interface{}) cmp.Option {
|
||||
tf := newTypeFilter(typs...)
|
||||
return cmp.FilterPath(tf.filter, cmp.Ignore())
|
||||
}
|
||||
|
||||
type typeFilter []reflect.Type
|
||||
|
||||
func newTypeFilter(typs ...interface{}) (tf typeFilter) {
|
||||
for _, typ := range typs {
|
||||
t := reflect.TypeOf(typ)
|
||||
if t == nil {
|
||||
// This occurs if someone tries to pass in sync.Locker(nil)
|
||||
panic("cannot determine type; consider using IgnoreInterfaces")
|
||||
}
|
||||
tf = append(tf, t)
|
||||
}
|
||||
return tf
|
||||
}
|
||||
func (tf typeFilter) filter(p cmp.Path) bool {
|
||||
if len(p) < 1 {
|
||||
return false
|
||||
}
|
||||
t := p[len(p)-1].Type()
|
||||
for _, ti := range tf {
|
||||
if t.AssignableTo(ti) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IgnoreInterfaces returns an Option that ignores all values or references of
|
||||
// values assignable to certain interface types. These interfaces are specified
|
||||
// by passing in an anonymous struct with the interface types embedded in it.
|
||||
// For example, to ignore sync.Locker, pass in struct{sync.Locker}{}.
|
||||
func IgnoreInterfaces(ifaces interface{}) cmp.Option {
|
||||
tf := newIfaceFilter(ifaces)
|
||||
return cmp.FilterPath(tf.filter, cmp.Ignore())
|
||||
}
|
||||
|
||||
type ifaceFilter []reflect.Type
|
||||
|
||||
func newIfaceFilter(ifaces interface{}) (tf ifaceFilter) {
|
||||
t := reflect.TypeOf(ifaces)
|
||||
if ifaces == nil || t.Name() != "" || t.Kind() != reflect.Struct {
|
||||
panic("input must be an anonymous struct")
|
||||
}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
fi := t.Field(i)
|
||||
switch {
|
||||
case !fi.Anonymous:
|
||||
panic("struct cannot have named fields")
|
||||
case fi.Type.Kind() != reflect.Interface:
|
||||
panic("embedded field must be an interface type")
|
||||
case fi.Type.NumMethod() == 0:
|
||||
// This matches everything; why would you ever want this?
|
||||
panic("cannot ignore empty interface")
|
||||
default:
|
||||
tf = append(tf, fi.Type)
|
||||
}
|
||||
}
|
||||
return tf
|
||||
}
|
||||
func (tf ifaceFilter) filter(p cmp.Path) bool {
|
||||
if len(p) < 1 {
|
||||
return false
|
||||
}
|
||||
t := p[len(p)-1].Type()
|
||||
for _, ti := range tf {
|
||||
if t.AssignableTo(ti) {
|
||||
return true
|
||||
}
|
||||
if t.Kind() != reflect.Ptr && reflect.PtrTo(t).AssignableTo(ti) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IgnoreUnexported returns an Option that only ignores the immediate unexported
|
||||
// fields of a struct, including anonymous fields of unexported types.
|
||||
// In particular, unexported fields within the struct's exported fields
|
||||
// of struct types, including anonymous fields, will not be ignored unless the
|
||||
// type of the field itself is also passed to IgnoreUnexported.
|
||||
func IgnoreUnexported(typs ...interface{}) cmp.Option {
|
||||
ux := newUnexportedFilter(typs...)
|
||||
return cmp.FilterPath(ux.filter, cmp.Ignore())
|
||||
}
|
||||
|
||||
type unexportedFilter struct{ m map[reflect.Type]bool }
|
||||
|
||||
func newUnexportedFilter(typs ...interface{}) unexportedFilter {
|
||||
ux := unexportedFilter{m: make(map[reflect.Type]bool)}
|
||||
for _, typ := range typs {
|
||||
t := reflect.TypeOf(typ)
|
||||
if t == nil || t.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("invalid struct type: %T", typ))
|
||||
}
|
||||
ux.m[t] = true
|
||||
}
|
||||
return ux
|
||||
}
|
||||
func (xf unexportedFilter) filter(p cmp.Path) bool {
|
||||
if len(p) < 2 {
|
||||
return false
|
||||
}
|
||||
sf, ok := p[len(p)-1].(cmp.StructField)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return xf.m[p[len(p)-2].Type()] && !isExported(sf.Name())
|
||||
}
|
||||
|
||||
// isExported reports whether the identifier is exported.
|
||||
func isExported(id string) bool {
|
||||
r, _ := utf8.DecodeRuneInString(id)
|
||||
return unicode.IsUpper(r)
|
||||
}
|
|
@ -0,0 +1,155 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
package cmpopts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// SortSlices returns a Transformer option that sorts all []V.
|
||||
// The less function must be of the form "func(T, T) bool" which is used to
|
||||
// sort any slice with element type V that is assignable to T.
|
||||
//
|
||||
// The less function must be:
|
||||
// • Deterministic: less(x, y) == less(x, y)
|
||||
// • Irreflexive: !less(x, x)
|
||||
// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
|
||||
//
|
||||
// The less function does not have to be "total". That is, if !less(x, y) and
|
||||
// !less(y, x) for two elements x and y, their relative order is maintained.
|
||||
//
|
||||
// SortSlices can be used in conjuction with EquateEmpty.
|
||||
func SortSlices(less interface{}) cmp.Option {
|
||||
vf := reflect.ValueOf(less)
|
||||
if !isTTBoolFunc(vf.Type()) || vf.IsNil() {
|
||||
panic(fmt.Sprintf("invalid less function: %T", less))
|
||||
}
|
||||
ss := sliceSorter{vf.Type().In(0), vf}
|
||||
return cmp.FilterValues(ss.filter, cmp.Transformer("Sort", ss.sort))
|
||||
}
|
||||
|
||||
type sliceSorter struct {
|
||||
in reflect.Type // T
|
||||
fnc reflect.Value // func(T, T) bool
|
||||
}
|
||||
|
||||
func (ss sliceSorter) filter(x, y interface{}) bool {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
if !(x != nil && y != nil && vx.Type() == vy.Type()) ||
|
||||
!(vx.Kind() == reflect.Slice && vx.Type().Elem().AssignableTo(ss.in)) ||
|
||||
(vx.Len() <= 1 && vy.Len() <= 1) {
|
||||
return false
|
||||
}
|
||||
// Check whether the slices are already sorted to avoid an infinite
|
||||
// recursion cycle applying the same transform to itself.
|
||||
ok1 := sliceIsSorted(x, func(i, j int) bool { return ss.less(vx, i, j) })
|
||||
ok2 := sliceIsSorted(y, func(i, j int) bool { return ss.less(vy, i, j) })
|
||||
return !ok1 || !ok2
|
||||
}
|
||||
func (ss sliceSorter) sort(x interface{}) interface{} {
|
||||
src := reflect.ValueOf(x)
|
||||
dst := reflect.MakeSlice(src.Type(), src.Len(), src.Len())
|
||||
for i := 0; i < src.Len(); i++ {
|
||||
dst.Index(i).Set(src.Index(i))
|
||||
}
|
||||
sortSliceStable(dst.Interface(), func(i, j int) bool { return ss.less(dst, i, j) })
|
||||
ss.checkSort(dst)
|
||||
return dst.Interface()
|
||||
}
|
||||
func (ss sliceSorter) checkSort(v reflect.Value) {
|
||||
start := -1 // Start of a sequence of equal elements.
|
||||
for i := 1; i < v.Len(); i++ {
|
||||
if ss.less(v, i-1, i) {
|
||||
// Check that first and last elements in v[start:i] are equal.
|
||||
if start >= 0 && (ss.less(v, start, i-1) || ss.less(v, i-1, start)) {
|
||||
panic(fmt.Sprintf("incomparable values detected: want equal elements: %v", v.Slice(start, i)))
|
||||
}
|
||||
start = -1
|
||||
} else if start == -1 {
|
||||
start = i
|
||||
}
|
||||
}
|
||||
}
|
||||
func (ss sliceSorter) less(v reflect.Value, i, j int) bool {
|
||||
vx, vy := v.Index(i), v.Index(j)
|
||||
return ss.fnc.Call([]reflect.Value{vx, vy})[0].Bool()
|
||||
}
|
||||
|
||||
// SortMaps returns a Transformer option that flattens map[K]V types to be a
|
||||
// sorted []struct{K, V}. The less function must be of the form
|
||||
// "func(T, T) bool" which is used to sort any map with key K that is
|
||||
// assignable to T.
|
||||
//
|
||||
// Flattening the map into a slice has the property that cmp.Equal is able to
|
||||
// use Comparers on K or the K.Equal method if it exists.
|
||||
//
|
||||
// The less function must be:
|
||||
// • Deterministic: less(x, y) == less(x, y)
|
||||
// • Irreflexive: !less(x, x)
|
||||
// • Transitive: if !less(x, y) and !less(y, z), then !less(x, z)
|
||||
// • Total: if x != y, then either less(x, y) or less(y, x)
|
||||
//
|
||||
// SortMaps can be used in conjuction with EquateEmpty.
|
||||
func SortMaps(less interface{}) cmp.Option {
|
||||
vf := reflect.ValueOf(less)
|
||||
if !isTTBoolFunc(vf.Type()) || vf.IsNil() {
|
||||
panic(fmt.Sprintf("invalid less function: %T", less))
|
||||
}
|
||||
ms := mapSorter{vf.Type().In(0), vf}
|
||||
return cmp.FilterValues(ms.filter, cmp.Transformer("Sort", ms.sort))
|
||||
}
|
||||
|
||||
type mapSorter struct {
|
||||
in reflect.Type // T
|
||||
fnc reflect.Value // func(T, T) bool
|
||||
}
|
||||
|
||||
func (ms mapSorter) filter(x, y interface{}) bool {
|
||||
vx, vy := reflect.ValueOf(x), reflect.ValueOf(y)
|
||||
return (x != nil && y != nil && vx.Type() == vy.Type()) &&
|
||||
(vx.Kind() == reflect.Map && vx.Type().Key().AssignableTo(ms.in)) &&
|
||||
(vx.Len() != 0 || vy.Len() != 0)
|
||||
}
|
||||
func (ms mapSorter) sort(x interface{}) interface{} {
|
||||
src := reflect.ValueOf(x)
|
||||
outType := mapEntryType(src.Type())
|
||||
dst := reflect.MakeSlice(reflect.SliceOf(outType), src.Len(), src.Len())
|
||||
for i, k := range src.MapKeys() {
|
||||
v := reflect.New(outType).Elem()
|
||||
v.Field(0).Set(k)
|
||||
v.Field(1).Set(src.MapIndex(k))
|
||||
dst.Index(i).Set(v)
|
||||
}
|
||||
sortSlice(dst.Interface(), func(i, j int) bool { return ms.less(dst, i, j) })
|
||||
ms.checkSort(dst)
|
||||
return dst.Interface()
|
||||
}
|
||||
func (ms mapSorter) checkSort(v reflect.Value) {
|
||||
for i := 1; i < v.Len(); i++ {
|
||||
if !ms.less(v, i-1, i) {
|
||||
panic(fmt.Sprintf("partial order detected: want %v < %v", v.Index(i-1), v.Index(i)))
|
||||
}
|
||||
}
|
||||
}
|
||||
func (ms mapSorter) less(v reflect.Value, i, j int) bool {
|
||||
vx, vy := v.Index(i).Field(0), v.Index(j).Field(0)
|
||||
if !hasReflectStructOf {
|
||||
vx, vy = vx.Elem(), vy.Elem()
|
||||
}
|
||||
return ms.fnc.Call([]reflect.Value{vx, vy})[0].Bool()
|
||||
}
|
||||
|
||||
var boolType = reflect.TypeOf(true)
|
||||
|
||||
// isTTBoolFunc reports whether f is of the form: func(T, T) bool.
|
||||
func isTTBoolFunc(t reflect.Type) bool {
|
||||
if t == nil || t.Kind() != reflect.Func || t.IsVariadic() {
|
||||
return false
|
||||
}
|
||||
return t.NumIn() == 2 && t.NumOut() == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// +build !go1.8
|
||||
|
||||
package cmpopts
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const hasReflectStructOf = false
|
||||
|
||||
func mapEntryType(reflect.Type) reflect.Type {
|
||||
return reflect.TypeOf(struct{ K, V interface{} }{})
|
||||
}
|
||||
|
||||
func sliceIsSorted(slice interface{}, less func(i, j int) bool) bool {
|
||||
return sort.IsSorted(reflectSliceSorter{reflect.ValueOf(slice), less})
|
||||
}
|
||||
func sortSlice(slice interface{}, less func(i, j int) bool) {
|
||||
sort.Sort(reflectSliceSorter{reflect.ValueOf(slice), less})
|
||||
}
|
||||
func sortSliceStable(slice interface{}, less func(i, j int) bool) {
|
||||
sort.Stable(reflectSliceSorter{reflect.ValueOf(slice), less})
|
||||
}
|
||||
|
||||
type reflectSliceSorter struct {
|
||||
slice reflect.Value
|
||||
less func(i, j int) bool
|
||||
}
|
||||
|
||||
func (ss reflectSliceSorter) Len() int {
|
||||
return ss.slice.Len()
|
||||
}
|
||||
func (ss reflectSliceSorter) Less(i, j int) bool {
|
||||
return ss.less(i, j)
|
||||
}
|
||||
func (ss reflectSliceSorter) Swap(i, j int) {
|
||||
vi := ss.slice.Index(i).Interface()
|
||||
vj := ss.slice.Index(j).Interface()
|
||||
ss.slice.Index(i).Set(reflect.ValueOf(vj))
|
||||
ss.slice.Index(j).Set(reflect.ValueOf(vi))
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
// +build go1.8
|
||||
|
||||
package cmpopts
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"sort"
|
||||
)
|
||||
|
||||
const hasReflectStructOf = true
|
||||
|
||||
func mapEntryType(t reflect.Type) reflect.Type {
|
||||
return reflect.StructOf([]reflect.StructField{
|
||||
{Name: "K", Type: t.Key()},
|
||||
{Name: "V", Type: t.Elem()},
|
||||
})
|
||||
}
|
||||
|
||||
func sliceIsSorted(slice interface{}, less func(i, j int) bool) bool {
|
||||
return sort.SliceIsSorted(slice, less)
|
||||
}
|
||||
func sortSlice(slice interface{}, less func(i, j int) bool) {
|
||||
sort.Slice(slice, less)
|
||||
}
|
||||
func sortSliceStable(slice interface{}, less func(i, j int) bool) {
|
||||
sort.SliceStable(slice, less)
|
||||
}
|
|
@ -0,0 +1,182 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
package cmpopts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
// filterField returns a new Option where opt is only evaluated on paths that
|
||||
// include a specific exported field on a single struct type.
|
||||
// The struct type is specified by passing in a value of that type.
|
||||
//
|
||||
// The name may be a dot-delimited string (e.g., "Foo.Bar") to select a
|
||||
// specific sub-field that is embedded or nested within the parent struct.
|
||||
func filterField(typ interface{}, name string, opt cmp.Option) cmp.Option {
|
||||
// TODO: This is currently unexported over concerns of how helper filters
|
||||
// can be composed together easily.
|
||||
// TODO: Add tests for FilterField.
|
||||
|
||||
sf := newStructFilter(typ, name)
|
||||
return cmp.FilterPath(sf.filter, opt)
|
||||
}
|
||||
|
||||
type structFilter struct {
|
||||
t reflect.Type // The root struct type to match on
|
||||
ft fieldTree // Tree of fields to match on
|
||||
}
|
||||
|
||||
func newStructFilter(typ interface{}, names ...string) structFilter {
|
||||
// TODO: Perhaps allow * as a special identifier to allow ignoring any
|
||||
// number of path steps until the next field match?
|
||||
// This could be useful when a concrete struct gets transformed into
|
||||
// an anonymous struct where it is not possible to specify that by type,
|
||||
// but the transformer happens to provide guarantees about the names of
|
||||
// the transformed fields.
|
||||
|
||||
t := reflect.TypeOf(typ)
|
||||
if t == nil || t.Kind() != reflect.Struct {
|
||||
panic(fmt.Sprintf("%T must be a struct", typ))
|
||||
}
|
||||
var ft fieldTree
|
||||
for _, name := range names {
|
||||
cname, err := canonicalName(t, name)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("%s: %v", strings.Join(cname, "."), err))
|
||||
}
|
||||
ft.insert(cname)
|
||||
}
|
||||
return structFilter{t, ft}
|
||||
}
|
||||
|
||||
func (sf structFilter) filter(p cmp.Path) bool {
|
||||
for i, ps := range p {
|
||||
if ps.Type().AssignableTo(sf.t) && sf.ft.matchPrefix(p[i+1:]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// fieldTree represents a set of dot-separated identifiers.
|
||||
//
|
||||
// For example, inserting the following selectors:
|
||||
// Foo
|
||||
// Foo.Bar.Baz
|
||||
// Foo.Buzz
|
||||
// Nuka.Cola.Quantum
|
||||
//
|
||||
// Results in a tree of the form:
|
||||
// {sub: {
|
||||
// "Foo": {ok: true, sub: {
|
||||
// "Bar": {sub: {
|
||||
// "Baz": {ok: true},
|
||||
// }},
|
||||
// "Buzz": {ok: true},
|
||||
// }},
|
||||
// "Nuka": {sub: {
|
||||
// "Cola": {sub: {
|
||||
// "Quantum": {ok: true},
|
||||
// }},
|
||||
// }},
|
||||
// }}
|
||||
type fieldTree struct {
|
||||
ok bool // Whether this is a specified node
|
||||
sub map[string]fieldTree // The sub-tree of fields under this node
|
||||
}
|
||||
|
||||
// insert inserts a sequence of field accesses into the tree.
|
||||
func (ft *fieldTree) insert(cname []string) {
|
||||
if ft.sub == nil {
|
||||
ft.sub = make(map[string]fieldTree)
|
||||
}
|
||||
if len(cname) == 0 {
|
||||
ft.ok = true
|
||||
return
|
||||
}
|
||||
sub := ft.sub[cname[0]]
|
||||
sub.insert(cname[1:])
|
||||
ft.sub[cname[0]] = sub
|
||||
}
|
||||
|
||||
// matchPrefix reports whether any selector in the fieldTree matches
|
||||
// the start of path p.
|
||||
func (ft fieldTree) matchPrefix(p cmp.Path) bool {
|
||||
for _, ps := range p {
|
||||
switch ps := ps.(type) {
|
||||
case cmp.StructField:
|
||||
ft = ft.sub[ps.Name()]
|
||||
if ft.ok {
|
||||
return true
|
||||
}
|
||||
if len(ft.sub) == 0 {
|
||||
return false
|
||||
}
|
||||
case cmp.Indirect:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// canonicalName returns a list of identifiers where any struct field access
|
||||
// through an embedded field is expanded to include the names of the embedded
|
||||
// types themselves.
|
||||
//
|
||||
// For example, suppose field "Foo" is not directly in the parent struct,
|
||||
// but actually from an embedded struct of type "Bar". Then, the canonical name
|
||||
// of "Foo" is actually "Bar.Foo".
|
||||
//
|
||||
// Suppose field "Foo" is not directly in the parent struct, but actually
|
||||
// a field in two different embedded structs of types "Bar" and "Baz".
|
||||
// Then the selector "Foo" causes a panic since it is ambiguous which one it
|
||||
// refers to. The user must specify either "Bar.Foo" or "Baz.Foo".
|
||||
func canonicalName(t reflect.Type, sel string) ([]string, error) {
|
||||
var name string
|
||||
sel = strings.TrimPrefix(sel, ".")
|
||||
if sel == "" {
|
||||
return nil, fmt.Errorf("name must not be empty")
|
||||
}
|
||||
if i := strings.IndexByte(sel, '.'); i < 0 {
|
||||
name, sel = sel, ""
|
||||
} else {
|
||||
name, sel = sel[:i], sel[i:]
|
||||
}
|
||||
|
||||
// Type must be a struct or pointer to struct.
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
if t.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf("%v must be a struct", t)
|
||||
}
|
||||
|
||||
// Find the canonical name for this current field name.
|
||||
// If the field exists in an embedded struct, then it will be expanded.
|
||||
if !isExported(name) {
|
||||
// Disallow unexported fields:
|
||||
// * To discourage people from actually touching unexported fields
|
||||
// * FieldByName is buggy (https://golang.org/issue/4876)
|
||||
return []string{name}, fmt.Errorf("name must be exported")
|
||||
}
|
||||
sf, ok := t.FieldByName(name)
|
||||
if !ok {
|
||||
return []string{name}, fmt.Errorf("does not exist")
|
||||
}
|
||||
var ss []string
|
||||
for i := range sf.Index {
|
||||
ss = append(ss, t.FieldByIndex(sf.Index[:i+1]).Name)
|
||||
}
|
||||
if sel == "" {
|
||||
return ss, nil
|
||||
}
|
||||
ssPost, err := canonicalName(sf.Type, sel)
|
||||
return append(ss, ssPost...), err
|
||||
}
|
|
@ -0,0 +1,996 @@
|
|||
// Copyright 2017, The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE.md file.
|
||||
|
||||
package cmpopts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"reflect"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type (
|
||||
MyInt int
|
||||
MyFloat float32
|
||||
MyTime struct{ time.Time }
|
||||
MyStruct struct {
|
||||
A, B []int
|
||||
C, D map[time.Time]string
|
||||
}
|
||||
|
||||
Foo1 struct{ Alpha, Bravo, Charlie int }
|
||||
Foo2 struct{ *Foo1 }
|
||||
Foo3 struct{ *Foo2 }
|
||||
Bar1 struct{ Foo3 }
|
||||
Bar2 struct {
|
||||
Bar1
|
||||
*Foo3
|
||||
Bravo float32
|
||||
}
|
||||
Bar3 struct {
|
||||
Bar1
|
||||
Bravo *Bar2
|
||||
Delta struct{ Echo Foo1 }
|
||||
*Foo3
|
||||
Alpha string
|
||||
}
|
||||
|
||||
privateStruct struct{ Public, private int }
|
||||
PublicStruct struct{ Public, private int }
|
||||
ParentStruct struct {
|
||||
*privateStruct
|
||||
*PublicStruct
|
||||
Public int
|
||||
private int
|
||||
}
|
||||
|
||||
Everything struct {
|
||||
MyInt
|
||||
MyFloat
|
||||
MyTime
|
||||
MyStruct
|
||||
Bar3
|
||||
ParentStruct
|
||||
}
|
||||
|
||||
EmptyInterface interface{}
|
||||
)
|
||||
|
||||
func TestOptions(t *testing.T) {
|
||||
createBar3X := func() *Bar3 {
|
||||
return &Bar3{
|
||||
Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 2}}}},
|
||||
Bravo: &Bar2{
|
||||
Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 7}}}},
|
||||
Foo3: &Foo3{&Foo2{&Foo1{Bravo: 5}}},
|
||||
Bravo: 4,
|
||||
},
|
||||
Delta: struct{ Echo Foo1 }{Foo1{Charlie: 3}},
|
||||
Foo3: &Foo3{&Foo2{&Foo1{Alpha: 1}}},
|
||||
Alpha: "alpha",
|
||||
}
|
||||
}
|
||||
createBar3Y := func() *Bar3 {
|
||||
return &Bar3{
|
||||
Bar1: Bar1{Foo3{&Foo2{&Foo1{Bravo: 3}}}},
|
||||
Bravo: &Bar2{
|
||||
Bar1: Bar1{Foo3{&Foo2{&Foo1{Charlie: 8}}}},
|
||||
Foo3: &Foo3{&Foo2{&Foo1{Bravo: 6}}},
|
||||
Bravo: 5,
|
||||
},
|
||||
Delta: struct{ Echo Foo1 }{Foo1{Charlie: 4}},
|
||||
Foo3: &Foo3{&Foo2{&Foo1{Alpha: 2}}},
|
||||
Alpha: "ALPHA",
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
label string // Test name
|
||||
x, y interface{} // Input values to compare
|
||||
opts []cmp.Option // Input options
|
||||
wantEqual bool // Whether the inputs are equal
|
||||
wantPanic bool // Whether Equal should panic
|
||||
reason string // The reason for the expected outcome
|
||||
}{{
|
||||
label: "EquateEmpty",
|
||||
x: []int{},
|
||||
y: []int(nil),
|
||||
wantEqual: false,
|
||||
reason: "not equal because empty non-nil and nil slice differ",
|
||||
}, {
|
||||
label: "EquateEmpty",
|
||||
x: []int{},
|
||||
y: []int(nil),
|
||||
opts: []cmp.Option{EquateEmpty()},
|
||||
wantEqual: true,
|
||||
reason: "equal because EquateEmpty equates empty slices",
|
||||
}, {
|
||||
label: "SortSlices",
|
||||
x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7},
|
||||
wantEqual: false,
|
||||
reason: "not equal because element order differs",
|
||||
}, {
|
||||
label: "SortSlices",
|
||||
x: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
y: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7},
|
||||
opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })},
|
||||
wantEqual: true,
|
||||
reason: "equal because SortSlices sorts the slices",
|
||||
}, {
|
||||
label: "SortSlices",
|
||||
x: []MyInt{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
y: []MyInt{1, 0, 5, 2, 8, 9, 4, 3, 6, 7},
|
||||
opts: []cmp.Option{SortSlices(func(x, y int) bool { return x < y })},
|
||||
wantEqual: false,
|
||||
reason: "not equal because MyInt is not the same type as int",
|
||||
}, {
|
||||
label: "SortSlices",
|
||||
x: []float64{0, 1, 1, 2, 2, 2},
|
||||
y: []float64{2, 0, 2, 1, 2, 1},
|
||||
opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })},
|
||||
wantEqual: true,
|
||||
reason: "equal even when sorted with duplicate elements",
|
||||
}, {
|
||||
label: "SortSlices",
|
||||
x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4},
|
||||
y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2},
|
||||
opts: []cmp.Option{SortSlices(func(x, y float64) bool { return x < y })},
|
||||
wantPanic: true,
|
||||
reason: "panics because SortSlices used with non-transitive less function",
|
||||
}, {
|
||||
label: "SortSlices",
|
||||
x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, 3, 4, 4, 4, 4},
|
||||
y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, 2},
|
||||
opts: []cmp.Option{SortSlices(func(x, y float64) bool {
|
||||
return (!math.IsNaN(x) && math.IsNaN(y)) || x < y
|
||||
})},
|
||||
wantEqual: false,
|
||||
reason: "no panics because SortSlices used with valid less function; not equal because NaN != NaN",
|
||||
}, {
|
||||
label: "SortSlices+EquateNaNs",
|
||||
x: []float64{0, 1, 1, 2, 2, 2, math.NaN(), 3, 3, 3, math.NaN(), 3, 4, 4, 4, 4},
|
||||
y: []float64{2, 0, 4, 4, 3, math.NaN(), 4, 1, 3, 2, 3, 3, 4, 1, math.NaN(), 2},
|
||||
opts: []cmp.Option{
|
||||
EquateNaNs(),
|
||||
SortSlices(func(x, y float64) bool {
|
||||
return (!math.IsNaN(x) && math.IsNaN(y)) || x < y
|
||||
}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "no panics because SortSlices used with valid less function; equal because EquateNaNs is used",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
x: map[time.Time]string{
|
||||
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday",
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday",
|
||||
time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday",
|
||||
},
|
||||
y: map[time.Time]string{
|
||||
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday",
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday",
|
||||
time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday",
|
||||
},
|
||||
wantEqual: false,
|
||||
reason: "not equal because timezones differ",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
x: map[time.Time]string{
|
||||
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday",
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday",
|
||||
time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC): "2nd birthday",
|
||||
},
|
||||
y: map[time.Time]string{
|
||||
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday",
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday",
|
||||
time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "2nd birthday",
|
||||
},
|
||||
opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })},
|
||||
wantEqual: true,
|
||||
reason: "equal because SortMaps flattens to a slice where Time.Equal can be used",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
x: map[MyTime]string{
|
||||
{time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)}: "0th birthday",
|
||||
{time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC)}: "1st birthday",
|
||||
{time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC)}: "2nd birthday",
|
||||
},
|
||||
y: map[MyTime]string{
|
||||
{time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "0th birthday",
|
||||
{time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "1st birthday",
|
||||
{time.Date(2011, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local)}: "2nd birthday",
|
||||
},
|
||||
opts: []cmp.Option{SortMaps(func(x, y time.Time) bool { return x.Before(y) })},
|
||||
wantEqual: false,
|
||||
reason: "not equal because MyTime is not assignable to time.Time",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""},
|
||||
// => {0, 1, 2, 3, -1, -2, -3},
|
||||
y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""},
|
||||
// => {0, 1, 2, 3, 100, 200, 300},
|
||||
opts: []cmp.Option{SortMaps(func(a, b int) bool {
|
||||
if -10 < a && a <= 0 {
|
||||
a *= -100
|
||||
}
|
||||
if -10 < b && b <= 0 {
|
||||
b *= -100
|
||||
}
|
||||
return a < b
|
||||
})},
|
||||
wantEqual: false,
|
||||
reason: "not equal because values differ even though SortMap provides valid ordering",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""},
|
||||
// => {0, 1, 2, 3, -1, -2, -3},
|
||||
y: map[int]string{300: "", 200: "", 100: "", 0: "", 1: "", 2: "", 3: ""},
|
||||
// => {0, 1, 2, 3, 100, 200, 300},
|
||||
opts: []cmp.Option{
|
||||
SortMaps(func(x, y int) bool {
|
||||
if -10 < x && x <= 0 {
|
||||
x *= -100
|
||||
}
|
||||
if -10 < y && y <= 0 {
|
||||
y *= -100
|
||||
}
|
||||
return x < y
|
||||
}),
|
||||
cmp.Comparer(func(x, y int) bool {
|
||||
if -10 < x && x <= 0 {
|
||||
x *= -100
|
||||
}
|
||||
if -10 < y && y <= 0 {
|
||||
y *= -100
|
||||
}
|
||||
return x == y
|
||||
}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because Comparer used to equate differences",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""},
|
||||
y: map[int]string{},
|
||||
opts: []cmp.Option{SortMaps(func(x, y int) bool {
|
||||
return x < y && x >= 0 && y >= 0
|
||||
})},
|
||||
wantPanic: true,
|
||||
reason: "panics because SortMaps used with non-transitive less function",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
x: map[int]string{-3: "", -2: "", -1: "", 0: "", 1: "", 2: "", 3: ""},
|
||||
y: map[int]string{},
|
||||
opts: []cmp.Option{SortMaps(func(x, y int) bool {
|
||||
return math.Abs(float64(x)) < math.Abs(float64(y))
|
||||
})},
|
||||
wantPanic: true,
|
||||
reason: "panics because SortMaps used with partial less function",
|
||||
}, {
|
||||
label: "EquateEmpty+SortSlices+SortMaps",
|
||||
x: MyStruct{
|
||||
A: []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9},
|
||||
C: map[time.Time]string{
|
||||
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC): "0th birthday",
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC): "1st birthday",
|
||||
},
|
||||
D: map[time.Time]string{},
|
||||
},
|
||||
y: MyStruct{
|
||||
A: []int{1, 0, 5, 2, 8, 9, 4, 3, 6, 7},
|
||||
B: []int{},
|
||||
C: map[time.Time]string{
|
||||
time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "0th birthday",
|
||||
time.Date(2010, time.November, 10, 23, 0, 0, 0, time.UTC).In(time.Local): "1st birthday",
|
||||
},
|
||||
},
|
||||
opts: []cmp.Option{
|
||||
EquateEmpty(),
|
||||
SortSlices(func(x, y int) bool { return x < y }),
|
||||
SortMaps(func(x, y time.Time) bool { return x.Before(y) }),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "no panics because EquateEmpty should compose with the sort options",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: 3.09,
|
||||
y: 3.10,
|
||||
wantEqual: false,
|
||||
reason: "not equal because floats do not exactly matches",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: 3.09,
|
||||
y: 3.10,
|
||||
opts: []cmp.Option{EquateApprox(0, 0)},
|
||||
wantEqual: false,
|
||||
reason: "not equal because EquateApprox(0 ,0) is equivalent to using ==",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: 3.09,
|
||||
y: 3.10,
|
||||
opts: []cmp.Option{EquateApprox(0.003, 0.009)},
|
||||
wantEqual: false,
|
||||
reason: "not equal because EquateApprox is too strict",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: 3.09,
|
||||
y: 3.10,
|
||||
opts: []cmp.Option{EquateApprox(0, 0.011)},
|
||||
wantEqual: true,
|
||||
reason: "equal because margin is loose enough to match",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: 3.09,
|
||||
y: 3.10,
|
||||
opts: []cmp.Option{EquateApprox(0.004, 0)},
|
||||
wantEqual: true,
|
||||
reason: "equal because fraction is loose enough to match",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: 3.09,
|
||||
y: 3.10,
|
||||
opts: []cmp.Option{EquateApprox(0.004, 0.011)},
|
||||
wantEqual: true,
|
||||
reason: "equal because both the margin and fraction are loose enough to match",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: float32(3.09),
|
||||
y: float64(3.10),
|
||||
opts: []cmp.Option{EquateApprox(0.004, 0)},
|
||||
wantEqual: false,
|
||||
reason: "not equal because the types differ",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: float32(3.09),
|
||||
y: float32(3.10),
|
||||
opts: []cmp.Option{EquateApprox(0.004, 0)},
|
||||
wantEqual: true,
|
||||
reason: "equal because EquateApprox also applies on float32s",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: []float64{math.Inf(+1), math.Inf(-1)},
|
||||
y: []float64{math.Inf(+1), math.Inf(-1)},
|
||||
opts: []cmp.Option{EquateApprox(0, 1)},
|
||||
wantEqual: true,
|
||||
reason: "equal because we fall back on == which matches Inf (EquateApprox does not apply on Inf) ",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: []float64{math.Inf(+1), -1e100},
|
||||
y: []float64{+1e100, math.Inf(-1)},
|
||||
opts: []cmp.Option{EquateApprox(0, 1)},
|
||||
wantEqual: false,
|
||||
reason: "not equal because we fall back on == where Inf != 1e100 (EquateApprox does not apply on Inf)",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: float64(+1e100),
|
||||
y: float64(-1e100),
|
||||
opts: []cmp.Option{EquateApprox(math.Inf(+1), 0)},
|
||||
wantEqual: true,
|
||||
reason: "equal because infinite fraction matches everything",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: float64(+1e100),
|
||||
y: float64(-1e100),
|
||||
opts: []cmp.Option{EquateApprox(0, math.Inf(+1))},
|
||||
wantEqual: true,
|
||||
reason: "equal because infinite margin matches everything",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: math.Pi,
|
||||
y: math.Pi,
|
||||
opts: []cmp.Option{EquateApprox(0, 0)},
|
||||
wantEqual: true,
|
||||
reason: "equal because EquateApprox(0, 0) is equivalent to ==",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
x: math.Pi,
|
||||
y: math.Nextafter(math.Pi, math.Inf(+1)),
|
||||
opts: []cmp.Option{EquateApprox(0, 0)},
|
||||
wantEqual: false,
|
||||
reason: "not equal because EquateApprox(0, 0) is equivalent to ==",
|
||||
}, {
|
||||
label: "EquateNaNs",
|
||||
x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)},
|
||||
y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)},
|
||||
wantEqual: false,
|
||||
reason: "not equal because NaN != NaN",
|
||||
}, {
|
||||
label: "EquateNaNs",
|
||||
x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)},
|
||||
y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1)},
|
||||
opts: []cmp.Option{EquateNaNs()},
|
||||
wantEqual: true,
|
||||
reason: "equal because EquateNaNs allows NaN == NaN",
|
||||
}, {
|
||||
label: "EquateNaNs",
|
||||
x: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0},
|
||||
y: []float32{1.0, float32(math.NaN()), math.E, -0.0, +0.0},
|
||||
opts: []cmp.Option{EquateNaNs()},
|
||||
wantEqual: true,
|
||||
reason: "equal because EquateNaNs operates on float32",
|
||||
}, {
|
||||
label: "EquateApprox+EquateNaNs",
|
||||
x: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.01, 5001},
|
||||
y: []float64{1.0, math.NaN(), math.E, -0.0, +0.0, math.Inf(+1), math.Inf(-1), 1.02, 5002},
|
||||
opts: []cmp.Option{
|
||||
EquateNaNs(),
|
||||
EquateApprox(0.01, 0),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because EquateNaNs and EquateApprox compose together",
|
||||
}, {
|
||||
label: "EquateApprox+EquateNaNs",
|
||||
x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001},
|
||||
y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002},
|
||||
opts: []cmp.Option{
|
||||
EquateNaNs(),
|
||||
EquateApprox(0.01, 0),
|
||||
},
|
||||
wantEqual: false,
|
||||
reason: "not equal because EquateApprox and EquateNaNs do not apply on a named type",
|
||||
}, {
|
||||
label: "EquateApprox+EquateNaNs+Transform",
|
||||
x: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.01, 5001},
|
||||
y: []MyFloat{1.0, MyFloat(math.NaN()), MyFloat(math.E), -0.0, +0.0, MyFloat(math.Inf(+1)), MyFloat(math.Inf(-1)), 1.02, 5002},
|
||||
opts: []cmp.Option{
|
||||
cmp.Transformer("", func(x MyFloat) float64 { return float64(x) }),
|
||||
EquateNaNs(),
|
||||
EquateApprox(0.01, 0),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because named type is transformed to float64",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
||||
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
||||
wantEqual: false,
|
||||
reason: "not equal because values do not match in deeply embedded field",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
||||
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
||||
opts: []cmp.Option{IgnoreFields(Bar1{}, "Alpha")},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreField ignores deeply embedded field: Alpha",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
||||
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
||||
opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo1.Alpha")},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreField ignores deeply embedded field: Foo1.Alpha",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
||||
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
||||
opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo2.Alpha")},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreField ignores deeply embedded field: Foo2.Alpha",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
||||
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
||||
opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Alpha")},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Alpha",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
|
||||
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
|
||||
opts: []cmp.Option{IgnoreFields(Bar1{}, "Foo3.Foo2.Alpha")},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreField ignores deeply embedded field: Foo3.Foo2.Alpha",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: createBar3X(),
|
||||
y: createBar3Y(),
|
||||
wantEqual: false,
|
||||
reason: "not equal because many deeply nested or embedded fields differ",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: createBar3X(),
|
||||
y: createBar3Y(),
|
||||
opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Foo3", "Alpha")},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreFields ignores fields at the highest levels",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: createBar3X(),
|
||||
y: createBar3Y(),
|
||||
opts: []cmp.Option{
|
||||
IgnoreFields(Bar3{},
|
||||
"Bar1.Foo3.Bravo",
|
||||
"Bravo.Bar1.Foo3.Foo2.Foo1.Charlie",
|
||||
"Bravo.Foo3.Foo2.Foo1.Bravo",
|
||||
"Bravo.Bravo",
|
||||
"Delta.Echo.Charlie",
|
||||
"Foo3.Foo2.Foo1.Alpha",
|
||||
"Alpha",
|
||||
),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreFields ignores fields using fully-qualified field",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: createBar3X(),
|
||||
y: createBar3Y(),
|
||||
opts: []cmp.Option{
|
||||
IgnoreFields(Bar3{},
|
||||
"Bar1.Foo3.Bravo",
|
||||
"Bravo.Foo3.Foo2.Foo1.Bravo",
|
||||
"Bravo.Bravo",
|
||||
"Delta.Echo.Charlie",
|
||||
"Foo3.Foo2.Foo1.Alpha",
|
||||
"Alpha",
|
||||
),
|
||||
},
|
||||
wantEqual: false,
|
||||
reason: "not equal because one fully-qualified field is not ignored: Bravo.Bar1.Foo3.Foo2.Foo1.Charlie",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
x: createBar3X(),
|
||||
y: createBar3Y(),
|
||||
opts: []cmp.Option{IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha")},
|
||||
wantEqual: false,
|
||||
reason: "not equal because highest-level field is not ignored: Foo3",
|
||||
}, {
|
||||
label: "IgnoreTypes",
|
||||
x: []interface{}{5, "same"},
|
||||
y: []interface{}{6, "same"},
|
||||
wantEqual: false,
|
||||
reason: "not equal because 5 != 6",
|
||||
}, {
|
||||
label: "IgnoreTypes",
|
||||
x: []interface{}{5, "same"},
|
||||
y: []interface{}{6, "same"},
|
||||
opts: []cmp.Option{IgnoreTypes(0)},
|
||||
wantEqual: true,
|
||||
reason: "equal because ints are ignored",
|
||||
}, {
|
||||
label: "IgnoreTypes+IgnoreInterfaces",
|
||||
x: []interface{}{5, "same", new(bytes.Buffer)},
|
||||
y: []interface{}{6, "same", new(bytes.Buffer)},
|
||||
opts: []cmp.Option{IgnoreTypes(0)},
|
||||
wantPanic: true,
|
||||
reason: "panics because bytes.Buffer has unexported fields",
|
||||
}, {
|
||||
label: "IgnoreTypes+IgnoreInterfaces",
|
||||
x: []interface{}{5, "same", new(bytes.Buffer)},
|
||||
y: []interface{}{6, "diff", new(bytes.Buffer)},
|
||||
opts: []cmp.Option{
|
||||
IgnoreTypes(0, ""),
|
||||
IgnoreInterfaces(struct{ io.Reader }{}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because bytes.Buffer is ignored by match on interface type",
|
||||
}, {
|
||||
label: "IgnoreTypes+IgnoreInterfaces",
|
||||
x: []interface{}{5, "same", new(bytes.Buffer)},
|
||||
y: []interface{}{6, "same", new(bytes.Buffer)},
|
||||
opts: []cmp.Option{
|
||||
IgnoreTypes(0, ""),
|
||||
IgnoreInterfaces(struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
fmt.Stringer
|
||||
}{}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because bytes.Buffer is ignored by match on multiple interface types",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
x: struct{ mu sync.Mutex }{},
|
||||
y: struct{ mu sync.Mutex }{},
|
||||
wantPanic: true,
|
||||
reason: "panics because sync.Mutex has unexported fields",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
x: struct{ mu sync.Mutex }{},
|
||||
y: struct{ mu sync.Mutex }{},
|
||||
opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreInterfaces applies on values (with pointer receiver)",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
x: struct{ mu *sync.Mutex }{},
|
||||
y: struct{ mu *sync.Mutex }{},
|
||||
opts: []cmp.Option{IgnoreInterfaces(struct{ sync.Locker }{})},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreInterfaces applies on pointers",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2},
|
||||
y: ParentStruct{Public: 1, private: -2},
|
||||
opts: []cmp.Option{cmp.AllowUnexported(ParentStruct{})},
|
||||
wantEqual: false,
|
||||
reason: "not equal because ParentStruct.private differs with AllowUnexported",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2},
|
||||
y: ParentStruct{Public: 1, private: -2},
|
||||
opts: []cmp.Option{IgnoreUnexported(ParentStruct{})},
|
||||
wantEqual: true,
|
||||
reason: "equal because IgnoreUnexported ignored ParentStruct.private",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}},
|
||||
y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: 4}},
|
||||
opts: []cmp.Option{
|
||||
cmp.AllowUnexported(PublicStruct{}),
|
||||
IgnoreUnexported(ParentStruct{}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because ParentStruct.private is ignored",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}},
|
||||
y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}},
|
||||
opts: []cmp.Option{
|
||||
cmp.AllowUnexported(PublicStruct{}),
|
||||
IgnoreUnexported(ParentStruct{}),
|
||||
},
|
||||
wantEqual: false,
|
||||
reason: "not equal because ParentStruct.PublicStruct.private differs and not ignored by IgnoreUnexported(ParentStruct{})",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4}},
|
||||
y: ParentStruct{Public: 1, private: -2, PublicStruct: &PublicStruct{Public: 3, private: -4}},
|
||||
opts: []cmp.Option{
|
||||
IgnoreUnexported(ParentStruct{}, PublicStruct{}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because both ParentStruct.PublicStruct and ParentStruct.PublicStruct.private are ignored",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}},
|
||||
y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}},
|
||||
opts: []cmp.Option{
|
||||
cmp.AllowUnexported(privateStruct{}, PublicStruct{}, ParentStruct{}),
|
||||
},
|
||||
wantEqual: false,
|
||||
reason: "not equal since ParentStruct.privateStruct differs",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}},
|
||||
y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}},
|
||||
opts: []cmp.Option{
|
||||
cmp.AllowUnexported(privateStruct{}, PublicStruct{}),
|
||||
IgnoreUnexported(ParentStruct{}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because ParentStruct.privateStruct ignored by IgnoreUnexported(ParentStruct{})",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}},
|
||||
y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: -4}},
|
||||
opts: []cmp.Option{
|
||||
cmp.AllowUnexported(PublicStruct{}, ParentStruct{}),
|
||||
IgnoreUnexported(privateStruct{}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because privateStruct.private ignored by IgnoreUnexported(privateStruct{})",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
x: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: 3, private: 4}},
|
||||
y: ParentStruct{Public: 1, private: 2, privateStruct: &privateStruct{Public: -3, private: -4}},
|
||||
opts: []cmp.Option{
|
||||
cmp.AllowUnexported(PublicStruct{}, ParentStruct{}),
|
||||
IgnoreUnexported(privateStruct{}),
|
||||
},
|
||||
wantEqual: false,
|
||||
reason: "not equal because privateStruct.Public differs and not ignored by IgnoreUnexported(privateStruct{})",
|
||||
}, {
|
||||
label: "IgnoreFields+IgnoreTypes+IgnoreUnexported",
|
||||
x: &Everything{
|
||||
MyInt: 5,
|
||||
MyFloat: 3.3,
|
||||
MyTime: MyTime{time.Now()},
|
||||
Bar3: *createBar3X(),
|
||||
ParentStruct: ParentStruct{
|
||||
Public: 1, private: 2, PublicStruct: &PublicStruct{Public: 3, private: 4},
|
||||
},
|
||||
},
|
||||
y: &Everything{
|
||||
MyInt: -5,
|
||||
MyFloat: 3.3,
|
||||
MyTime: MyTime{time.Now()},
|
||||
Bar3: *createBar3Y(),
|
||||
ParentStruct: ParentStruct{
|
||||
Public: 1, private: -2, PublicStruct: &PublicStruct{Public: -3, private: -4},
|
||||
},
|
||||
},
|
||||
opts: []cmp.Option{
|
||||
IgnoreFields(Everything{}, "MyTime", "Bar3.Foo3"),
|
||||
IgnoreFields(Bar3{}, "Bar1", "Bravo", "Delta", "Alpha"),
|
||||
IgnoreTypes(MyInt(0), PublicStruct{}),
|
||||
IgnoreUnexported(ParentStruct{}),
|
||||
},
|
||||
wantEqual: true,
|
||||
reason: "equal because all Ignore options can be composed together",
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
tRun(t, tt.label, func(t *testing.T) {
|
||||
var gotEqual bool
|
||||
var gotPanic string
|
||||
func() {
|
||||
defer func() {
|
||||
if ex := recover(); ex != nil {
|
||||
gotPanic = fmt.Sprint(ex)
|
||||
}
|
||||
}()
|
||||
gotEqual = cmp.Equal(tt.x, tt.y, tt.opts...)
|
||||
}()
|
||||
switch {
|
||||
case gotPanic == "" && tt.wantPanic:
|
||||
t.Errorf("expected Equal panic\nreason: %s", tt.reason)
|
||||
case gotPanic != "" && !tt.wantPanic:
|
||||
t.Errorf("unexpected Equal panic: got %v\nreason: %v", gotPanic, tt.reason)
|
||||
case gotEqual != tt.wantEqual:
|
||||
t.Errorf("Equal = %v, want %v\nreason: %v", gotEqual, tt.wantEqual, tt.reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPanic(t *testing.T) {
|
||||
args := func(x ...interface{}) []interface{} { return x }
|
||||
tests := []struct {
|
||||
label string // Test name
|
||||
fnc interface{} // Option function to call
|
||||
args []interface{} // Arguments to pass in
|
||||
wantPanic string // Expected panic message
|
||||
reason string // The reason for the expected outcome
|
||||
}{{
|
||||
label: "EquateApprox",
|
||||
fnc: EquateApprox,
|
||||
args: args(0.0, 0.0),
|
||||
reason: "zero margin and fraction is equivalent to exact equality",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
fnc: EquateApprox,
|
||||
args: args(-0.1, 0.0),
|
||||
wantPanic: "margin or fraction must be a non-negative number",
|
||||
reason: "negative inputs are invalid",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
fnc: EquateApprox,
|
||||
args: args(0.0, -0.1),
|
||||
wantPanic: "margin or fraction must be a non-negative number",
|
||||
reason: "negative inputs are invalid",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
fnc: EquateApprox,
|
||||
args: args(math.NaN(), 0.0),
|
||||
wantPanic: "margin or fraction must be a non-negative number",
|
||||
reason: "NaN inputs are invalid",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
fnc: EquateApprox,
|
||||
args: args(1.0, 0.0),
|
||||
reason: "fraction of 1.0 or greater is valid",
|
||||
}, {
|
||||
label: "EquateApprox",
|
||||
fnc: EquateApprox,
|
||||
args: args(0.0, math.Inf(+1)),
|
||||
reason: "margin of infinity is valid",
|
||||
}, {
|
||||
label: "SortSlices",
|
||||
fnc: SortSlices,
|
||||
args: args(strings.Compare),
|
||||
wantPanic: "invalid less function",
|
||||
reason: "func(x, y string) int is wrong signature for less",
|
||||
}, {
|
||||
label: "SortSlices",
|
||||
fnc: SortSlices,
|
||||
args: args((func(_, _ int) bool)(nil)),
|
||||
wantPanic: "invalid less function",
|
||||
reason: "nil value is not valid",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
fnc: SortMaps,
|
||||
args: args(strings.Compare),
|
||||
wantPanic: "invalid less function",
|
||||
reason: "func(x, y string) int is wrong signature for less",
|
||||
}, {
|
||||
label: "SortMaps",
|
||||
fnc: SortMaps,
|
||||
args: args((func(_, _ int) bool)(nil)),
|
||||
wantPanic: "invalid less function",
|
||||
reason: "nil value is not valid",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(Foo1{}, ""),
|
||||
wantPanic: "name must not be empty",
|
||||
reason: "empty selector is invalid",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(Foo1{}, "."),
|
||||
wantPanic: "name must not be empty",
|
||||
reason: "single dot selector is invalid",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(Foo1{}, ".Alpha"),
|
||||
reason: "dot-prefix is okay since Foo1.Alpha reads naturally",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(Foo1{}, "Alpha."),
|
||||
wantPanic: "name must not be empty",
|
||||
reason: "dot-suffix is invalid",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(Foo1{}, "Alpha "),
|
||||
wantPanic: "does not exist",
|
||||
reason: "identifiers must not have spaces",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(Foo1{}, "Zulu"),
|
||||
wantPanic: "does not exist",
|
||||
reason: "name of non-existent field is invalid",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(Foo1{}, "Alpha.NoExist"),
|
||||
wantPanic: "must be a struct",
|
||||
reason: "cannot select into a non-struct",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(&Foo1{}, "Alpha"),
|
||||
wantPanic: "must be a struct",
|
||||
reason: "the type must be a struct (not pointer to a struct)",
|
||||
}, {
|
||||
label: "IgnoreFields",
|
||||
fnc: IgnoreFields,
|
||||
args: args(Foo1{}, "unexported"),
|
||||
wantPanic: "name must be exported",
|
||||
reason: "unexported fields must not be specified",
|
||||
}, {
|
||||
label: "IgnoreTypes",
|
||||
fnc: IgnoreTypes,
|
||||
reason: "empty input is valid",
|
||||
}, {
|
||||
label: "IgnoreTypes",
|
||||
fnc: IgnoreTypes,
|
||||
args: args(nil),
|
||||
wantPanic: "cannot determine type",
|
||||
reason: "input must not be nil value",
|
||||
}, {
|
||||
label: "IgnoreTypes",
|
||||
fnc: IgnoreTypes,
|
||||
args: args(0, 0, 0),
|
||||
reason: "duplicate inputs of the same type is valid",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
fnc: IgnoreInterfaces,
|
||||
args: args(nil),
|
||||
wantPanic: "input must be an anonymous struct",
|
||||
reason: "input must not be nil value",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
fnc: IgnoreInterfaces,
|
||||
args: args(Foo1{}),
|
||||
wantPanic: "input must be an anonymous struct",
|
||||
reason: "input must not be a named struct type",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
fnc: IgnoreInterfaces,
|
||||
args: args(struct{ _ io.Reader }{}),
|
||||
wantPanic: "struct cannot have named fields",
|
||||
reason: "input must not have named fields",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
fnc: IgnoreInterfaces,
|
||||
args: args(struct{ Foo1 }{}),
|
||||
wantPanic: "embedded field must be an interface type",
|
||||
reason: "field types must be interfaces",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
fnc: IgnoreInterfaces,
|
||||
args: args(struct{ EmptyInterface }{}),
|
||||
wantPanic: "cannot ignore empty interface",
|
||||
reason: "field types must not be the empty interface",
|
||||
}, {
|
||||
label: "IgnoreInterfaces",
|
||||
fnc: IgnoreInterfaces,
|
||||
args: args(struct {
|
||||
io.Reader
|
||||
io.Writer
|
||||
io.Closer
|
||||
io.ReadWriteCloser
|
||||
}{}),
|
||||
reason: "multiple interfaces may be specified, even if they overlap",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
fnc: IgnoreUnexported,
|
||||
reason: "empty input is valid",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
fnc: IgnoreUnexported,
|
||||
args: args(nil),
|
||||
wantPanic: "invalid struct type",
|
||||
reason: "input must not be nil value",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
fnc: IgnoreUnexported,
|
||||
args: args(&Foo1{}),
|
||||
wantPanic: "invalid struct type",
|
||||
reason: "input must be a struct type (not a pointer to a struct)",
|
||||
}, {
|
||||
label: "IgnoreUnexported",
|
||||
fnc: IgnoreUnexported,
|
||||
args: args(Foo1{}, struct{ x, X int }{}),
|
||||
reason: "input may be named or unnamed structs",
|
||||
}}
|
||||
|
||||
for _, tt := range tests {
|
||||
tRun(t, tt.label, func(t *testing.T) {
|
||||
// Prepare function arguments.
|
||||
vf := reflect.ValueOf(tt.fnc)
|
||||
var vargs []reflect.Value
|
||||
for i, arg := range tt.args {
|
||||
if arg == nil {
|
||||
tf := vf.Type()
|
||||
if i == tf.NumIn()-1 && tf.IsVariadic() {
|
||||
vargs = append(vargs, reflect.Zero(tf.In(i).Elem()))
|
||||
} else {
|
||||
vargs = append(vargs, reflect.Zero(tf.In(i)))
|
||||
}
|
||||
} else {
|
||||
vargs = append(vargs, reflect.ValueOf(arg))
|
||||
}
|
||||
}
|
||||
|
||||
// Call the function and capture any panics.
|
||||
var gotPanic string
|
||||
func() {
|
||||
defer func() {
|
||||
if ex := recover(); ex != nil {
|
||||
if s, ok := ex.(string); ok {
|
||||
gotPanic = s
|
||||
} else {
|
||||
panic(ex)
|
||||
}
|
||||
}
|
||||
}()
|
||||
vf.Call(vargs)
|
||||
}()
|
||||
|
||||
switch {
|
||||
case tt.wantPanic == "" && gotPanic != "":
|
||||
t.Errorf("unexpected panic message: %s\nreason: %s", gotPanic, tt.reason)
|
||||
case tt.wantPanic != "" && !strings.Contains(gotPanic, tt.wantPanic):
|
||||
t.Errorf("panic message:\ngot: %s\nwant: %s\nreason: %s", gotPanic, tt.wantPanic, tt.reason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Delete this hack when we drop Go1.6 support.
|
||||
func tRun(t *testing.T, name string, f func(t *testing.T)) {
|
||||
type runner interface {
|
||||
Run(string, func(t *testing.T)) bool
|
||||
}
|
||||
var ti interface{} = t
|
||||
if r, ok := ti.(runner); ok {
|
||||
r.Run(name, f)
|
||||
} else {
|
||||
t.Logf("Test: %s", name)
|
||||
f(t)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue