blob: b1da93ab58757db38bade23937fe0bf948c460d8 [file] [log] [blame]
//
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
package dpagg
import (
"math"
"reflect"
"testing"
"github.com/google/differential-privacy/go/v2/noise"
"github.com/google/differential-privacy/go/v2/rand"
"github.com/google/go-cmp/cmp"
)
func TestNewBoundedVariance(t *testing.T) {
for _, tc := range []struct {
desc string
opt *BoundedVarianceOptions
want *BoundedVariance
}{
{"MaxPartitionsContributed is not set",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
Lower: -1,
Upper: 5,
Noise: noNoise{},
MaxContributionsPerPartition: 2,
},
&BoundedVariance{
lower: -1,
upper: 5,
state: defaultState,
midPoint: 2,
Count: Count{
epsilon: ln3 / 3,
delta: tenten / 3,
l0Sensitivity: 1,
lInfSensitivity: 2,
Noise: noNoise{},
noiseKind: noise.Unrecognised,
count: 0,
state: defaultState,
},
NormalizedSum: BoundedSumFloat64{
epsilon: ln3 / 3,
delta: tenten / 3,
l0Sensitivity: 1,
lInfSensitivity: 6,
lower: -3,
upper: 3,
Noise: noNoise{},
noiseKind: noise.Unrecognised,
sum: 0,
state: defaultState,
},
NormalizedSumOfSquares: BoundedSumFloat64{
epsilon: ln3 - ln3/3 - ln3/3,
delta: tenten - tenten/3 - tenten/3,
l0Sensitivity: 1,
lInfSensitivity: 18,
lower: 0,
upper: 9,
Noise: noNoise{},
noiseKind: noise.Unrecognised,
sum: 0,
state: defaultState,
},
}},
{"Noise is not set",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: 0,
Lower: -1,
Upper: 5,
MaxContributionsPerPartition: 2,
MaxPartitionsContributed: 1,
},
&BoundedVariance{
lower: -1,
upper: 5,
state: defaultState,
midPoint: 2,
Count: Count{
epsilon: ln3 / 3,
delta: 0,
l0Sensitivity: 1,
lInfSensitivity: 2,
noiseKind: noise.LaplaceNoise,
Noise: noise.Laplace(),
count: 0,
state: defaultState,
},
NormalizedSum: BoundedSumFloat64{
epsilon: ln3 / 3,
delta: 0,
l0Sensitivity: 1,
lInfSensitivity: 6,
lower: -3,
upper: 3,
noiseKind: noise.LaplaceNoise,
Noise: noise.Laplace(),
sum: 0,
state: defaultState,
},
NormalizedSumOfSquares: BoundedSumFloat64{
epsilon: ln3 - ln3/3 - ln3/3,
delta: 0,
l0Sensitivity: 1,
lInfSensitivity: 18,
lower: 0,
upper: 9,
noiseKind: noise.LaplaceNoise,
Noise: noise.Laplace(),
sum: 0,
state: defaultState,
},
}},
} {
bv, err := NewBoundedVariance(tc.opt)
if err != nil {
t.Fatalf("Couldn't initialize bv: %v", err)
}
if !reflect.DeepEqual(bv, tc.want) {
t.Errorf("NewBoundedVariance: when %s got %+v, want %+v", tc.desc, bv, tc.want)
}
}
}
func TestBVNoInput(t *testing.T) {
lower, upper := -1.0, 5.0
bv := getNoiselessBV(t, lower, upper)
got, err := bv.Result()
if err != nil {
t.Fatalf("Couldn't compute dp result: %v", err)
}
// count = 0 => variance should be 0.0
want := 0.0
if !ApproxEqual(got, want) {
t.Errorf("BoundedVariance: when there is no input data got=%f, want=%f", got, want)
}
}
func TestBVAdd(t *testing.T) {
lower, upper := -1.0, 5.0
bv := getNoiselessBV(t, lower, upper)
bv.Add(1.5)
bv.Add(2.5)
bv.Add(3.5)
bv.Add(4.5)
got, err := bv.Result()
if err != nil {
t.Fatalf("Couldn't compute dp result: %v", err)
}
want := 1.25
if !ApproxEqual(got, want) {
t.Errorf("Add: when dataset with elements inside boundaries got %f, want %f", got, want)
}
}
func TestBVAddIgnoresNaN(t *testing.T) {
lower, upper := -1.0, 5.0
bv := getNoiselessBV(t, lower, upper)
bv.Add(1)
bv.Add(math.NaN())
bv.Add(3)
got, err := bv.Result()
if err != nil {
t.Fatalf("Couldn't compute dp result: %v", err)
}
want := 1.0
if !ApproxEqual(got, want) {
t.Errorf("Add: when dataset contains NaN got %f, want %f", got, want)
}
}
func TestBVReturns0IfSingleEntryIsAdded(t *testing.T) {
lower, upper := -1.0, 5.0
bv := getNoiselessBV(t, lower, upper)
bv.Add(1.2345)
got, err := bv.Result()
if err != nil {
t.Fatalf("Couldn't compute dp result: %v", err)
}
want := 0.0 // single entry means 0 variance
if !ApproxEqual(got, want) {
t.Errorf("BoundedVariance: when dataset contains single entry got %f, want %f", got, want)
}
}
func TestBVClamp(t *testing.T) {
lower, upper := 2.0, 5.0
bv := getNoiselessBV(t, lower, upper)
bv.Add(1.0) // clamped to 2.0
bv.Add(3.5)
bv.Add(7.5) // clamped to 5.0
got, err := bv.Result()
if err != nil {
t.Fatalf("Couldn't compute dp result: %v", err)
}
want := 1.5
if !ApproxEqual(got, want) {
t.Errorf("Add: when dataset with elements outside boundaries got %f, want %f", got, want)
}
}
func TestBoundedVarianceResultSetsStateCorrectly(t *testing.T) {
lower, upper := -1.0, 5.0
bv := getNoiselessBV(t, lower, upper)
_, err := bv.Result()
if err != nil {
t.Fatalf("Couldn't compute dp result: %v", err)
}
if bv.state != resultReturned {
t.Errorf("BoundedVariance should have its state set to ResultReturned, got %v, want ResultReturned", bv.state)
}
}
func TestBVNoiseIsCorrectlyCalled(t *testing.T) {
bv := getMockBV(t)
bv.Add(1)
bv.Add(2)
got, _ := bv.Result() // will fail if parameters are wrong. See mockBVNoise implementation for details.
want := 0.25
if !ApproxEqual(got, want) {
t.Errorf("Add: when dataset = {1, 2} got %f, want %f", got, want)
}
}
type mockBVNoise struct {
t *testing.T
noise.Noise
}
// AddNoiseInt64 checks that the parameters passed are the ones we expect.
func (mn mockBVNoise) AddNoiseInt64(x, l0, lInf int64, eps, del float64) (int64, error) {
if x != 2 && x != 0 {
// AddNoiseInt64 is initially called with a placeholder value of 0, so we don't want to fail when that happens
mn.t.Errorf("AddNoiseInt64: for parameter x got %d, want %d", x, 2)
}
if l0 != 1 {
mn.t.Errorf("AddNoiseInt64: for parameter l0Sensitivity got %d, want %d", l0, 1)
}
if lInf != 1 {
mn.t.Errorf("AddNoiseInt64: for parameter lInfSensitivity got %d, want %d", lInf, 5)
}
if !ApproxEqual(eps, ln3/3) {
mn.t.Errorf("AddNoiseInt64: for parameter epsilon got %f, want %f", eps, ln3*0.5)
}
if !ApproxEqual(del, tenten/3) {
mn.t.Errorf("AddNoiseInt64: for parameter delta got %f, want %f", del, tenten*0.5)
}
return x, nil
}
// AddNoiseFloat64 checks that the parameters passed are the ones we expect.
func (mn mockBVNoise) AddNoiseFloat64(x float64, l0 int64, lInf, eps, del float64) (float64, error) {
if !ApproxEqual(x, 1.0) && !ApproxEqual(x, -1.0) && !ApproxEqual(x, 0.0) {
// AddNoiseFloat64 is initially called with a placeholder value of 0, so we don't want to fail when that happens
// Then, for normalizedSum it is called with a value of -1.0 (1.0-2.0 + 2.0-2.0 = -1.0)
// Finally, for normalizedSumOfSquares it is called with a value of 1.0 ((1.0-2.0)**2 + (2.0-2.0)**2 = 1.0)
mn.t.Errorf("AddNoiseFloat64: for parameter x got %f, want one of {%f, %f, %f}", x, 0.0, -1.0, 1.0)
}
if l0 != 1 {
mn.t.Errorf("AddNoiseFloat64: for parameter l0Sensitivity got %d, want %d", l0, 1)
}
if !ApproxEqual(lInf, 9.0) && !ApproxEqual(lInf, 3.0) && !ApproxEqual(lInf, 1.0) {
// AddNoiseFloat64 is initially called with an lInf of 1, so we don't want to fail when that happens
// Then, for normalizedSum it is called with an lInf of 3.0
// Finally, for normalizedSumOfSquares it is called with an lInf of 9.0
mn.t.Errorf("AddNoiseFloat64: for parameter lInfSensitivity got %f, want %f", lInf, 3.0)
}
if !ApproxEqual(eps, ln3/3) {
mn.t.Errorf("AddNoiseFloat64: for parameter epsilon got %f, want %f", eps, ln3*0.5)
}
if !ApproxEqual(del, tenten/3) {
mn.t.Errorf("AddNoiseFloat64: for parameter delta got %f, want %f", del, tenten*0.5)
}
return x, nil
}
func getMockBV(t *testing.T) *BoundedVariance {
t.Helper()
bv, err := NewBoundedVariance(&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
MaxContributionsPerPartition: 1,
Lower: -1,
Upper: 5,
Noise: mockBVNoise{t: t},
})
if err != nil {
t.Fatalf("Couldn't get mock BV: %v", err)
}
return bv
}
func getNoiselessBV(t *testing.T, lower, upper float64) *BoundedVariance {
t.Helper()
bv, err := NewBoundedVariance(&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
MaxContributionsPerPartition: 1,
Lower: lower,
Upper: upper,
Noise: noNoise{},
})
if err != nil {
t.Fatalf("Couldn't get noiseless BV: %v", err)
}
return bv
}
func TestBVReturnsResultInsidePossibleBoundaries(t *testing.T) {
lower := rand.Uniform() * 100
upper := lower + rand.Uniform()*100
bv, err := NewBoundedVariance(&BoundedVarianceOptions{
Epsilon: ln3,
MaxPartitionsContributed: 1,
MaxContributionsPerPartition: 1,
Lower: lower,
Upper: upper,
Noise: noise.Laplace(),
})
if err != nil {
t.Fatalf("Couldn't initialize bv: %v", err)
}
for i := 0; i <= 1000; i++ {
bv.Add(rand.Uniform() * 300 * rand.Sign())
}
res, err := bv.Result()
if err != nil {
t.Fatalf("Couldn't compute dp result: %v", err)
}
if res < 0 {
t.Errorf("BoundedVariance: variance can't be smaller than 0, got variance %f, want to be >= %f", res, 0.0)
}
if res > computeMaxVariance(lower, upper) {
t.Errorf("BoundedVariance: variance can't be larger than max variance, got %f, want to be <= %f", res, computeMaxVariance(lower, upper))
}
}
func TestMergeBoundedVariance(t *testing.T) {
lower, upper := -1.0, 5.0
bv1 := getNoiselessBV(t, lower, upper)
bv2 := getNoiselessBV(t, lower, upper)
bv1.Add(1)
bv1.Add(1)
bv2.Add(3)
bv2.Add(3)
err := bv1.Merge(bv2)
if err != nil {
t.Fatalf("Couldn't merge bv1 and bv2: %v", err)
}
got, err := bv1.Result()
if err != nil {
t.Fatalf("Couldn't compute dp result: %v", err)
}
want := 1.0 // Would be 0.0 if merge didn't work.
if !ApproxEqual(got, want) {
t.Errorf("Merge: when merging 2 instances of BoundedVariance got %f, want %f", got, want)
}
if bv2.state != merged {
t.Errorf("Merge: when merging 2 instances of BoundedVariance for bv2.state got %v, want Merged", bv2.state)
}
}
func TestCheckMergeBoundedVarianceCompatibility(t *testing.T) {
for _, tc := range []struct {
desc string
opt1 *BoundedVarianceOptions
opt2 *BoundedVarianceOptions
wantErr bool
}{
{"same options",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
false},
{"different epsilon",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
&BoundedVarianceOptions{
Epsilon: 2,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
true},
{"different delta",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenfive,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
true},
{"different MaxPartitionsContributed",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 2,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
true},
{"different lower bound",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: 0,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
true},
{"different upper bound",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 6,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
true},
{"different noise",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: 0,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Laplace(),
MaxContributionsPerPartition: 2,
},
true},
{"different maxContributionsPerPartition",
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 2,
},
&BoundedVarianceOptions{
Epsilon: ln3,
Delta: tenten,
MaxPartitionsContributed: 1,
Lower: -1,
Upper: 5,
Noise: noise.Gaussian(),
MaxContributionsPerPartition: 5,
},
true},
} {
bv1, err := NewBoundedVariance(tc.opt1)
if err != nil {
t.Fatalf("Couldn't initialize bv1: %v", err)
}
bv2, err := NewBoundedVariance(tc.opt2)
if err != nil {
t.Fatalf("Couldn't initialize bv2: %v", err)
}
if err := checkMergeBoundedVariance(bv1, bv2); (err != nil) != tc.wantErr {
t.Errorf("CheckMerge: when %s for err got %v, wantErr %t", tc.desc, err, tc.wantErr)
}
}
}
// Tests that checkMergeBoundedVariance() returns errors correctly with different BoundedVariance aggregation states.
func TestCheckMergeBoundedVarianceStateChecks(t *testing.T) {
for _, tc := range []struct {
state1 aggregationState
state2 aggregationState
wantErr bool
}{
{defaultState, defaultState, false},
{resultReturned, defaultState, true},
{defaultState, resultReturned, true},
{serialized, defaultState, true},
{defaultState, serialized, true},
{defaultState, merged, true},
{merged, defaultState, true},
} {
lower, upper := 0.0, 5.0
bv1 := getNoiselessBV(t, lower, upper)
bv2 := getNoiselessBV(t, lower, upper)
bv1.state = tc.state1
bv2.state = tc.state2
if err := checkMergeBoundedVariance(bv1, bv2); (err != nil) != tc.wantErr {
t.Errorf("CheckMerge: when states [%v, %v] for err got %v, wantErr %t", tc.state1, tc.state2, err, tc.wantErr)
}
}
}
func TestBVEquallyInitialized(t *testing.T) {
for _, tc := range []struct {
desc string
bv1 *BoundedVariance
bv2 *BoundedVariance
equal bool
}{
{
"equal parameters",
&BoundedVariance{
lower: 0,
upper: 2,
midPoint: 1,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
&BoundedVariance{
lower: 0,
upper: 2,
midPoint: 1,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
true,
},
{
"different lower",
&BoundedVariance{
lower: -1,
upper: 2,
midPoint: 0.5,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
&BoundedVariance{
lower: 0,
upper: 2,
midPoint: 1,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
false,
},
{
"different upper",
&BoundedVariance{
lower: 0,
upper: 2,
midPoint: 1,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
&BoundedVariance{
lower: 0,
upper: 3,
midPoint: 1.5,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
false,
},
{
"different count",
&BoundedVariance{
lower: 0,
upper: 3,
midPoint: 1.5,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{epsilon: ln3},
state: defaultState},
&BoundedVariance{
lower: 0,
upper: 3,
midPoint: 1.5,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{epsilon: 1},
state: defaultState},
false,
},
{
"different normalizedSum",
&BoundedVariance{
lower: 0,
upper: 3,
midPoint: 1.5,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{epsilon: ln3},
Count: Count{},
state: defaultState},
&BoundedVariance{
lower: 0,
upper: 3,
midPoint: 1.5,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{epsilon: 1},
Count: Count{},
state: defaultState},
false,
},
{
"different normalizedSumOfSquares",
&BoundedVariance{
lower: 0,
upper: 3,
midPoint: 1.5,
NormalizedSumOfSquares: BoundedSumFloat64{epsilon: ln3},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
&BoundedVariance{
lower: 0,
upper: 3,
midPoint: 1.5,
NormalizedSumOfSquares: BoundedSumFloat64{epsilon: 1},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
false,
},
{
"different state",
&BoundedVariance{
lower: 0,
upper: 2,
midPoint: 1,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: defaultState},
&BoundedVariance{
lower: 0,
upper: 2,
midPoint: 1,
NormalizedSumOfSquares: BoundedSumFloat64{},
NormalizedSum: BoundedSumFloat64{},
Count: Count{},
state: merged},
false,
},
} {
if bvEquallyInitialized(tc.bv1, tc.bv2) != tc.equal {
t.Errorf("bvEquallyInitialized: when %s got %t, want %t", tc.desc, !tc.equal, tc.equal)
}
}
}
func compareBoundedVariance(bv1, bv2 *BoundedVariance) bool {
return bv1.lower == bv2.lower &&
bv1.upper == bv2.upper &&
compareCount(&bv1.Count, &bv2.Count) &&
compareBoundedSumFloat64(&bv1.NormalizedSum, &bv2.NormalizedSum) &&
compareBoundedSumFloat64(&bv1.NormalizedSumOfSquares, &bv2.NormalizedSumOfSquares) &&
bv1.midPoint == bv2.midPoint &&
bv1.state == bv2.state
}
// Tests that serialization for BoundedVariance works as expected.
func TestBVSerialization(t *testing.T) {
for _, tc := range []struct {
desc string
opts *BoundedVarianceOptions
}{
{"default options", &BoundedVarianceOptions{
Epsilon: ln3,
Lower: 0,
Upper: 1,
Delta: 0,
MaxContributionsPerPartition: 1,
}},
{"non-default options", &BoundedVarianceOptions{
Lower: -100,
Upper: 555,
Epsilon: ln3,
Delta: 1e-5,
MaxPartitionsContributed: 5,
MaxContributionsPerPartition: 6,
Noise: noise.Gaussian(),
}},
} {
bv, err := NewBoundedVariance(tc.opts)
if err != nil {
t.Fatalf("Couldn't initialize bv: %v", err)
}
bvUnchanged, err := NewBoundedVariance(tc.opts)
if err != nil {
t.Fatalf("Couldn't initialize bvUnchanged: %v", err)
}
bytes, err := encode(bv)
if err != nil {
t.Fatalf("encode(BoundedVariance) error: %v", err)
}
bvUnmarshalled := new(BoundedVariance)
if err := decode(bvUnmarshalled, bytes); err != nil {
t.Fatalf("decode(BoundedVariance) error: %v", err)
}
// Check that encoding -> decoding is the identity function.
if !cmp.Equal(bvUnchanged, bvUnmarshalled, cmp.Comparer(compareBoundedVariance)) {
t.Errorf("decode(encode(_)): when %s got %+v, want %+v", tc.desc, bvUnmarshalled, bvUnchanged)
}
if bv.state != serialized {
t.Errorf("BoundedVariance should have its state set to Serialized, got %v , want Serialized", bv.state)
}
}
}
// Tests that GobEncode() returns errors correctly with different BoundedVariance aggregation states.
func TestBoundedVarianceSerializationStateChecks(t *testing.T) {
for _, tc := range []struct {
state aggregationState
wantErr bool
}{
{defaultState, false},
{merged, true},
{serialized, false},
{resultReturned, true},
} {
lower, upper := 0.0, 5.0
bv := getNoiselessBV(t, lower, upper)
bv.state = tc.state
if _, err := bv.GobEncode(); (err != nil) != tc.wantErr {
t.Errorf("GobEncode: when state %v for err got %v, wantErr %t", tc.state, err, tc.wantErr)
}
}
}