From 740c90016f2815c878bdd415a37cb45b3ce37dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Muraru=20=C8=98tefan?= Date: Mon, 3 Aug 2026 18:22:37 +0300 Subject: [PATCH] feat(testdata): Let destinations bound generated 64-bit integers arrow-go v18.7.0 fixed large-integer precision in JSON decoding (apache/arrow-go#816). The test data generator builds records via builder.UnmarshalJSON, so until now every int64/uint64 it produced was silently rounded to a float64-representable value. They are now exact, and destinations whose storage or wire format round-trips integers through float64 cannot reproduce them. MaxIntegerBits lets those destinations declare the width they can represent exactly. Unset keeps the full 64-bit range, so consumers that do not opt in are unaffected. TableDiff now points at the option when a differing column matches after a float64 round-trip. Timestamp_ns already handles the same root cause unconditionally; the opt-in keeps real 64-bit coverage for destinations that are correct. --- internal/memdb/memdb.go | 49 +++++++++- internal/memdb/memdb_test.go | 17 ++++ plugin/diff.go | 96 +++++++++++++++++++ plugin/diff_test.go | 47 +++++++++ plugin/testing_write_delete.go | 2 + plugin/testing_write_insert.go | 8 +- plugin/testing_write_migrate.go | 1 + plugin/testing_write_upsert.go | 8 +- schema/testdata.go | 29 +++++- schema/testdata_test.go | 164 +++++++++++++++++++++++++++++++- 10 files changed, 411 insertions(+), 10 deletions(-) create mode 100644 plugin/diff_test.go diff --git a/internal/memdb/memdb.go b/internal/memdb/memdb.go index ef2eb20231..278fd519b4 100644 --- a/internal/memdb/memdb.go +++ b/internal/memdb/memdb.go @@ -7,6 +7,7 @@ import ( "github.com/apache/arrow-go/v18/arrow" "github.com/apache/arrow-go/v18/arrow/array" + "github.com/apache/arrow-go/v18/arrow/memory" "github.com/cloudquery/plugin-sdk/v4/message" "github.com/cloudquery/plugin-sdk/v4/plugin" "github.com/cloudquery/plugin-sdk/v4/schema" @@ -21,6 +22,7 @@ type client struct { memoryDBLock sync.RWMutex errOnWrite bool blockingWrite bool + float64Ints bool } type Option func(*client) @@ -40,6 +42,47 @@ func WithBlockingWrite() Option { } } +// WithFloat64Ints makes writes round 64-bit integers through float64, mimicking destinations +// whose storage or wire format cannot represent them exactly. +func WithFloat64Ints() Option { + return func(c *client) { + c.float64Ints = true + } +} + +func roundIntsThroughFloat64(record arrow.RecordBatch) arrow.RecordBatch { + columns := make([]arrow.Array, record.NumCols()) + for i := range columns { + switch col := record.Column(i).(type) { + case *array.Int64: + bldr := array.NewInt64Builder(memory.DefaultAllocator) + for j := 0; j < col.Len(); j++ { + if col.IsNull(j) { + bldr.AppendNull() + continue + } + bldr.Append(int64(float64(col.Value(j)))) + } + columns[i] = bldr.NewArray() + bldr.Release() + case *array.Uint64: + bldr := array.NewUint64Builder(memory.DefaultAllocator) + for j := 0; j < col.Len(); j++ { + if col.IsNull(j) { + bldr.AppendNull() + continue + } + bldr.Append(uint64(float64(col.Value(j)))) + } + columns[i] = bldr.NewArray() + bldr.Release() + default: + columns[i] = record.Column(i) + } + } + return array.NewRecordBatch(record.Schema(), columns, record.NumRows()) +} + func GetNewClient(options ...Option) plugin.NewClientFunc { c := &client{ memoryDB: make(map[string][]arrow.RecordBatch), @@ -239,7 +282,11 @@ func (c *client) Write(ctx context.Context, msgs <-chan message.WriteMessage) er return errors.New("table name not found in schema metadata") } table := c.tables[tableName] - c.overwrite(table, msg.Record) + record := msg.Record + if c.float64Ints { + record = roundIntsThroughFloat64(record) + } + c.overwrite(table, record) } c.memoryDBLock.Unlock() diff --git a/internal/memdb/memdb_test.go b/internal/memdb/memdb_test.go index d17ecac73e..31c9da802a 100644 --- a/internal/memdb/memdb_test.go +++ b/internal/memdb/memdb_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/cloudquery/plugin-sdk/v4/plugin" + "github.com/cloudquery/plugin-sdk/v4/schema" ) func TestPlugin(t *testing.T) { @@ -22,6 +23,22 @@ func TestPlugin(t *testing.T) { ) } +func TestPluginFloat64Ints(t *testing.T) { + ctx := context.Background() + p := plugin.NewPlugin("test", "development", GetNewClient(WithFloat64Ints())) + if err := p.Init(ctx, nil, plugin.NewClientOptions{}); err != nil { + t.Fatal(err) + } + plugin.TestWriterSuiteRunner( + t, + p, + plugin.WriterTestSuiteTests{ + SafeMigrations: plugin.SafeMigrations{}, + }, + plugin.WithTestDataOptions(schema.TestSourceOptions{MaxIntegerBits: schema.Float64SafeIntegerBits}), + ) +} + func TestPluginOnNewError(t *testing.T) { ctx := context.Background() p := plugin.NewPlugin("test", "development", NewMemDBClientErrOnNew) diff --git a/plugin/diff.go b/plugin/diff.go index cbcad2443a..7be97f16c2 100644 --- a/plugin/diff.go +++ b/plugin/diff.go @@ -13,6 +13,99 @@ func RecordsDiff(sc *arrow.Schema, have, want []arrow.RecordBatch) string { return TableDiff(array.NewTableFromRecords(sc, have), array.NewTableFromRecords(sc, want)) } +func containsInt64(dt arrow.DataType) bool { + switch dt.ID() { + case arrow.INT64, arrow.UINT64: + return true + } + if nested, ok := dt.(arrow.NestedType); ok { + for _, field := range nested.Fields() { + if containsInt64(field.Type) { + return true + } + } + } + return false +} + +func equalAsFloat64(want, have arrow.Array) bool { + if want.Len() != have.Len() || !arrow.TypeEqual(want.DataType(), have.DataType()) { + return false + } + for i := 0; i < want.Len(); i++ { + if want.IsNull(i) != have.IsNull(i) { + return false + } + } + + switch wantCol := want.(type) { + case *array.Int64: + haveCol := have.(*array.Int64) + for i := 0; i < wantCol.Len(); i++ { + if !wantCol.IsNull(i) && float64(wantCol.Value(i)) != float64(haveCol.Value(i)) { + return false + } + } + return true + case *array.Uint64: + haveCol := have.(*array.Uint64) + for i := 0; i < wantCol.Len(); i++ { + if !wantCol.IsNull(i) && float64(wantCol.Value(i)) != float64(haveCol.Value(i)) { + return false + } + } + return true + case *array.Struct: + haveCol := have.(*array.Struct) + for i := 0; i < wantCol.NumField(); i++ { + if !equalAsFloat64(wantCol.Field(i), haveCol.Field(i)) { + return false + } + } + return true + case *array.List: + haveCol := have.(*array.List) + return sameOffsets(wantCol.Offsets(), haveCol.Offsets()) && equalAsFloat64(wantCol.ListValues(), haveCol.ListValues()) + case *array.LargeList: + haveCol := have.(*array.LargeList) + return sameLargeOffsets(wantCol.Offsets(), haveCol.Offsets()) && equalAsFloat64(wantCol.ListValues(), haveCol.ListValues()) + case *array.Map: + haveCol := have.(*array.Map) + return sameOffsets(wantCol.Offsets(), haveCol.Offsets()) && + equalAsFloat64(wantCol.Keys(), haveCol.Keys()) && + equalAsFloat64(wantCol.Items(), haveCol.Items()) + } + + if containsInt64(want.DataType()) { + return false + } + return array.Equal(want, have) +} + +func sameOffsets(want, have []int32) bool { + if len(want) != len(have) { + return false + } + for i := range want { + if want[i] != have[i] { + return false + } + } + return true +} + +func sameLargeOffsets(want, have []int64) bool { + if len(want) != len(have) { + return false + } + for i := range want { + if want[i] != have[i] { + return false + } + } + return true +} + func TableDiff(have, want arrow.Table) string { if array.TableApproxEqual(have, want, array.WithUnorderedMapKeys(true)) { return "" @@ -44,6 +137,9 @@ func TableDiff(have, want arrow.Table) string { sb.WriteString(have.Schema().Field(i).Name) sb.WriteString(": ") sb.WriteString(diff) + if equalAsFloat64(wantCol, haveCol) { + sb.WriteString("values are equal after a float64 round-trip. If this destination cannot store 64-bit integers exactly, set schema.TestSourceOptions.MaxIntegerBits to schema.Float64SafeIntegerBits\n") + } sb.WriteString("\n") } } diff --git a/plugin/diff_test.go b/plugin/diff_test.go new file mode 100644 index 0000000000..4f6b1d7817 --- /dev/null +++ b/plugin/diff_test.go @@ -0,0 +1,47 @@ +package plugin + +import ( + "strings" + "testing" + + "github.com/apache/arrow-go/v18/arrow" + "github.com/apache/arrow-go/v18/arrow/array" + "github.com/apache/arrow-go/v18/arrow/memory" +) + +func int64Record(t *testing.T, values []int64) arrow.RecordBatch { + t.Helper() + sc := arrow.NewSchema([]arrow.Field{{Name: "int64", Type: arrow.PrimitiveTypes.Int64}}, nil) + bldr := array.NewRecordBuilder(memory.DefaultAllocator, sc) + defer bldr.Release() + bldr.Field(0).(*array.Int64Builder).AppendValues(values, nil) + return bldr.NewRecordBatch() +} + +func TestRecordsDiffHintsAtMaxIntegerBits(t *testing.T) { + sc := arrow.NewSchema([]arrow.Field{{Name: "int64", Type: arrow.PrimitiveTypes.Int64}}, nil) + exact := int64Record(t, []int64{-8717895732742165505}) + rounded := int64Record(t, []int64{-8717895732742165504}) + + diff := RecordsDiff(sc, []arrow.RecordBatch{rounded}, []arrow.RecordBatch{exact}) + if diff == "" { + t.Fatal("expected a diff between an exact and a float64-rounded int64") + } + if !strings.Contains(diff, "MaxIntegerBits") { + t.Errorf("expected the diff to point at MaxIntegerBits, got: %s", diff) + } +} + +func TestRecordsDiffOmitsHintForUnrelatedDifference(t *testing.T) { + sc := arrow.NewSchema([]arrow.Field{{Name: "int64", Type: arrow.PrimitiveTypes.Int64}}, nil) + want := int64Record(t, []int64{1}) + have := int64Record(t, []int64{2}) + + diff := RecordsDiff(sc, []arrow.RecordBatch{have}, []arrow.RecordBatch{want}) + if diff == "" { + t.Fatal("expected a diff between 1 and 2") + } + if strings.Contains(diff, "MaxIntegerBits") { + t.Errorf("did not expect a MaxIntegerBits hint for a difference float64 cannot explain, got: %s", diff) + } +} diff --git a/plugin/testing_write_delete.go b/plugin/testing_write_delete.go index e3a62d0e7e..a4d466ec99 100644 --- a/plugin/testing_write_delete.go +++ b/plugin/testing_write_delete.go @@ -96,6 +96,7 @@ func (s *WriterTestSuite) testDeleteStaleAll(ctx context.Context, t *testing.T) normalRecord := tg.Generate(table, schema.GenTestDataOptions{ MaxRows: rowsPerRecord, TimePrecision: s.genDatOptions.TimePrecision, + MaxIntegerBits: s.genDatOptions.MaxIntegerBits, SourceName: "test", SyncTime: syncTime, // Generate call may truncate the value further based on the options UseHomogeneousType: s.useHomogeneousTypes, @@ -122,6 +123,7 @@ func (s *WriterTestSuite) testDeleteStaleAll(ctx context.Context, t *testing.T) nullRecord := tg.Generate(table, schema.GenTestDataOptions{ MaxRows: rowsPerRecord, TimePrecision: s.genDatOptions.TimePrecision, + MaxIntegerBits: s.genDatOptions.MaxIntegerBits, NullRows: true, SourceName: "test", SyncTime: syncTime, // Generate call may truncate the value further based on the options diff --git a/plugin/testing_write_insert.go b/plugin/testing_write_insert.go index b078b9b40e..a7de6720cb 100644 --- a/plugin/testing_write_insert.go +++ b/plugin/testing_write_insert.go @@ -93,6 +93,7 @@ func (s *WriterTestSuite) testInsertAll(ctx context.Context) error { normalRecord := tg.Generate(table, schema.GenTestDataOptions{ MaxRows: rowsPerRecord, TimePrecision: s.genDatOptions.TimePrecision, + MaxIntegerBits: s.genDatOptions.MaxIntegerBits, UseHomogeneousType: s.useHomogeneousTypes, }) if err := s.plugin.writeOne(ctx, &message.WriteInsert{ @@ -113,9 +114,10 @@ func (s *WriterTestSuite) testInsertAll(ctx context.Context) error { } nullRecord := tg.Generate(table, schema.GenTestDataOptions{ - MaxRows: rowsPerRecord, - TimePrecision: s.genDatOptions.TimePrecision, - NullRows: true, + MaxRows: rowsPerRecord, + TimePrecision: s.genDatOptions.TimePrecision, + MaxIntegerBits: s.genDatOptions.MaxIntegerBits, + NullRows: true, }) if err := s.plugin.writeOne(ctx, &message.WriteInsert{ Record: nullRecord, diff --git a/plugin/testing_write_migrate.go b/plugin/testing_write_migrate.go index 9939ab62dc..6f5e4afadc 100644 --- a/plugin/testing_write_migrate.go +++ b/plugin/testing_write_migrate.go @@ -42,6 +42,7 @@ func (s *WriterTestSuite) migrate(ctx context.Context, target *schema.Table, sou SyncTime: syncTime, MaxRows: rowsPerRecord, TimePrecision: s.genDatOptions.TimePrecision, + MaxIntegerBits: s.genDatOptions.MaxIntegerBits, UseHomogeneousType: s.useHomogeneousTypes, } // Test Generator should be initialized with the current number of items in the destination diff --git a/plugin/testing_write_upsert.go b/plugin/testing_write_upsert.go index f5013aca82..22c3bb87b6 100644 --- a/plugin/testing_write_upsert.go +++ b/plugin/testing_write_upsert.go @@ -82,6 +82,7 @@ func (s *WriterTestSuite) testUpsertAll(ctx context.Context) error { normalRecord := tg.Generate(table, schema.GenTestDataOptions{ MaxRows: rowsPerRecord, TimePrecision: s.genDatOptions.TimePrecision, + MaxIntegerBits: s.genDatOptions.MaxIntegerBits, UseHomogeneousType: s.useHomogeneousTypes, }) if err := s.plugin.writeOne(ctx, &message.WriteInsert{ @@ -107,7 +108,12 @@ func (s *WriterTestSuite) testUpsertAll(ctx context.Context) error { } tg.Reset() - nullRecord := tg.Generate(table, schema.GenTestDataOptions{MaxRows: rowsPerRecord, TimePrecision: s.genDatOptions.TimePrecision, NullRows: true}) + nullRecord := tg.Generate(table, schema.GenTestDataOptions{ + MaxRows: rowsPerRecord, + TimePrecision: s.genDatOptions.TimePrecision, + MaxIntegerBits: s.genDatOptions.MaxIntegerBits, + NullRows: true, + }) if err := s.plugin.writeOne(ctx, &message.WriteInsert{ Record: nullRecord, }); err != nil { diff --git a/schema/testdata.go b/schema/testdata.go index a1375fc7dd..e887fa1f82 100644 --- a/schema/testdata.go +++ b/schema/testdata.go @@ -4,6 +4,7 @@ import ( "crypto/sha256" "encoding/base64" "fmt" + "math" "math/rand" "strconv" "strings" @@ -16,6 +17,9 @@ import ( "github.com/google/uuid" ) +// Float64SafeIntegerBits is the MaxIntegerBits value for destinations that round-trip integers through float64. +const Float64SafeIntegerBits = 53 + // TestSourceOptions controls which types are included by TestSourceColumns. type TestSourceOptions struct { SkipDates bool @@ -29,6 +33,7 @@ type TestSourceOptions struct { SkipTimestamps bool // timestamp types. Microsecond timestamp is always be included, regardless of this setting. TimePrecision time.Duration SkipDecimals bool + MaxIntegerBits int // bounds the magnitude of generated int64/uint64 values to this many bits. Zero means the full 64-bit range. See Float64SafeIntegerBits. } // listOfColumns returns a list of columns that are lists of the given columns. @@ -198,6 +203,8 @@ type GenTestDataOptions struct { NullRows bool // UseHomogeneousType indicates whether to use a single type for JSON arrays. UseHomogeneousType bool + // MaxIntegerBits bounds the magnitude of generated int64/uint64 values to this many bits. Zero means the full 64-bit range. See Float64SafeIntegerBits. + MaxIntegerBits int } type TestDataGenerator struct { @@ -265,6 +272,20 @@ func (tg *TestDataGenerator) Generate(table *Table, opts GenTestDataOptions) arr return array.NewRecordBatch(sc, columns, -1) } +func signedInt64Bound(maxIntegerBits int) int64 { + if maxIntegerBits <= 0 || maxIntegerBits >= 63 { + return math.MaxInt64 + } + return int64(1) << uint(maxIntegerBits) +} + +func boundUint64(v uint64, maxIntegerBits int) uint64 { + if maxIntegerBits <= 0 || maxIntegerBits >= 64 { + return v + } + return v & (uint64(1)<>1))) case arrow.PrimitiveTypes.Int64: - return fmt.Sprintf("-%d", rnd.Int63n(int64(^uint64(0)>>1))) + return fmt.Sprintf("-%d", rnd.Int63n(signedInt64Bound(opts.MaxIntegerBits))) } } @@ -353,7 +374,7 @@ func (tg TestDataGenerator) getExampleJSON(colName string, dataType arrow.DataTy case arrow.PrimitiveTypes.Uint32: return fmt.Sprintf("%d", rnd.Uint64()%(uint64(^uint32(0)))) case arrow.PrimitiveTypes.Uint64: - return fmt.Sprintf("%d", rnd.Uint64()) + return fmt.Sprintf("%d", boundUint64(rnd.Uint64(), opts.MaxIntegerBits)) } } @@ -447,8 +468,8 @@ func (tg TestDataGenerator) getExampleJSON(colName string, dataType arrow.DataTy // For now, we begrudgingly accept loss of precision in these cases. // See https://github.com/cloudquery/plugin-sdk/issues/830 t = t.Truncate(time.Microsecond) - // Use string timestamp string format here because JSON integers are - // unmarshalled as float64, losing precision for nanosecond timestamps. + // Use the string timestamp format here so the value stays exact + // regardless of how the JSON decoder handles large integers. return t.Format(`"2006-01-02 15:04:05.999999999"`) case arrow.FixedWidthTypes.Time32s: h, m, s := t.Clock() diff --git a/schema/testdata_test.go b/schema/testdata_test.go index b246974ed3..994d72b3f6 100644 --- a/schema/testdata_test.go +++ b/schema/testdata_test.go @@ -1,6 +1,12 @@ package schema -import "testing" +import ( + "math" + "testing" + + "github.com/apache/arrow-go/v18/arrow" + "github.com/apache/arrow-go/v18/arrow/array" +) func TestTestSourceColumns_Default(t *testing.T) { // basic sanity check for tested columns @@ -52,3 +58,159 @@ func TestGenTestData(*testing.T) { tg := NewTestDataGenerator(0) _ = tg.Generate(table, GenTestDataOptions{}) } + +func countInt64Leaves(arr arrow.Array) (total, inexact int) { + switch col := arr.(type) { + case *array.Int64: + for i := 0; i < col.Len(); i++ { + if col.IsNull(i) { + continue + } + total++ + if v := col.Value(i); int64(float64(v)) != v { + inexact++ + } + } + case *array.Uint64: + for i := 0; i < col.Len(); i++ { + if col.IsNull(i) { + continue + } + total++ + if v := col.Value(i); uint64(float64(v)) != v { + inexact++ + } + } + case *array.Struct: + for i := 0; i < col.NumField(); i++ { + leaves, bad := countInt64Leaves(col.Field(i)) + total, inexact = total+leaves, inexact+bad + } + case *array.List: + total, inexact = countInt64Leaves(col.ListValues()) + case *array.LargeList: + total, inexact = countInt64Leaves(col.ListValues()) + case *array.Map: + kt, kx := countInt64Leaves(col.Keys()) + it, ix := countInt64Leaves(col.Items()) + total, inexact = kt+it, kx+ix + } + return total, inexact +} + +const minInt64Leaves = 100 + +func TestGenTestDataMaxIntegerBits(t *testing.T) { + table := TestTable("test", TestSourceOptions{}) + + for _, tc := range []struct { + name string + maxIntegerBits int + wantExact bool + }{ + {name: "unbounded", maxIntegerBits: 0, wantExact: false}, + {name: "float64 safe", maxIntegerBits: Float64SafeIntegerBits, wantExact: true}, + {name: "one below signed width", maxIntegerBits: 62, wantExact: false}, + {name: "signed width", maxIntegerBits: 63, wantExact: false}, + {name: "full width", maxIntegerBits: 64, wantExact: false}, + } { + t.Run(tc.name, func(t *testing.T) { + tg := NewTestDataGenerator(0) + record := tg.Generate(table, GenTestDataOptions{MaxRows: 50, MaxIntegerBits: tc.maxIntegerBits}) + + total, inexact := 0, 0 + for i := range record.Schema().Fields() { + leaves, bad := countInt64Leaves(record.Column(i)) + total, inexact = total+leaves, inexact+bad + } + + if total < minInt64Leaves { + t.Fatalf("inspected only %d 64-bit integer values, expected at least %d; the walk is not reaching nested columns", total, minInt64Leaves) + } + if tc.wantExact && inexact > 0 { + t.Errorf("MaxIntegerBits=%d generated %d integers that do not survive a float64 round-trip", tc.maxIntegerBits, inexact) + } + if !tc.wantExact && inexact == 0 { + t.Errorf("MaxIntegerBits=%d generated no integers beyond float64 precision", tc.maxIntegerBits) + } + }) + } +} + +func TestTestTableFixtureHasNestedLargeIntegers(t *testing.T) { + table := TestTable("test", TestSourceOptions{}) + record := NewTestDataGenerator(0).Generate(table, GenTestDataOptions{MaxRows: 50}) + + nested := 0 + for i := range record.Schema().Fields() { + switch record.Column(i).(type) { + case *array.Int64, *array.Uint64: + default: + _, inexact := countInt64Leaves(record.Column(i)) + nested += inexact + } + } + + if nested == 0 { + t.Fatal("expected nested columns to carry integers beyond float64 precision, so the bounded case proves propagation") + } +} + +func TestGenTestDataMaxIntegerBitsUnsetPreservesFullRange(t *testing.T) { + if got, want := signedInt64Bound(0), int64(^uint64(0)>>1); got != want { + t.Errorf("signedInt64Bound(0) = %d, want the full signed range %d", got, want) + } + for _, v := range []uint64{0, 1, math.MaxInt64, math.MaxUint64} { + if got := boundUint64(v, 0); got != v { + t.Errorf("boundUint64(%d, 0) = %d, want it unchanged", v, got) + } + } + + wantInt64 := []int64{-8717895732742165505, -7144924247938981575, -1395437218309923052, -4345851588384648695, -7242748068272024738} + wantUint64 := []uint64{8717895732742165505, 16368296284793757383, 1395437218309923052, 13569223625239424503, 7242748068272024738} + + table := TestTable("test", TestSourceOptions{}) + record := NewTestDataGenerator(0).Generate(table, GenTestDataOptions{MaxRows: len(wantInt64)}) + + for i, field := range record.Schema().Fields() { + switch field.Name { + case "int64": + col := record.Column(i).(*array.Int64) + for j, want := range wantInt64 { + if got := col.Value(j); got != want { + t.Errorf("int64 row %d = %d, want %d", j, got, want) + } + } + case "uint64": + col := record.Column(i).(*array.Uint64) + for j, want := range wantUint64 { + if got := col.Value(j); got != want { + t.Errorf("uint64 row %d = %d, want %d", j, got, want) + } + } + } + } +} + +func TestSignedInt64BoundIsUsableWithInt63n(t *testing.T) { + for _, bits := range []int{-1, 0, 1, 52, 53, 62, 63, 64, 65} { + if got := signedInt64Bound(bits); got <= 0 { + t.Errorf("signedInt64Bound(%d) = %d, which panics rand.Int63n", bits, got) + } + } +} + +func TestBoundUint64Width(t *testing.T) { + for _, tc := range []struct { + bits int + want uint64 + }{ + {bits: 1, want: 1}, + {bits: 53, want: 1<<53 - 1}, + {bits: 63, want: 1<<63 - 1}, + } { + if got := boundUint64(math.MaxUint64, tc.bits); got != tc.want { + t.Errorf("boundUint64(MaxUint64, %d) = %d, want %d", tc.bits, got, tc.want) + } + } +}