From 213b76899fac883ac122728f7ab258166137be29 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Tue, 8 Feb 2022 18:44:55 +0100 Subject: [PATCH] feat: add timepb support library (#60) * feat: add timepb support library * use pointer for durpb, everywhere * use rapid for fuzzy testing * update IsZero * add docs * remove duplicated test * Update support/timepb/doc.go Co-authored-by: Aaron Craelius Co-authored-by: Tyler <48813565+technicallyty@users.noreply.github.com> Co-authored-by: Aaron Craelius --- support/README.md | 3 + support/timepb/README.md | 23 +++++ support/timepb/cmp.go | 92 ++++++++++++++++++++ support/timepb/cmp_example_test.go | 22 +++++ support/timepb/cmp_test.go | 134 +++++++++++++++++++++++++++++ support/timepb/doc.go | 5 ++ 6 files changed, 279 insertions(+) create mode 100644 support/README.md create mode 100644 support/timepb/README.md create mode 100644 support/timepb/cmp.go create mode 100644 support/timepb/cmp_example_test.go create mode 100644 support/timepb/cmp_test.go create mode 100644 support/timepb/doc.go diff --git a/support/README.md b/support/README.md new file mode 100644 index 0000000..a0c4c22 --- /dev/null +++ b/support/README.md @@ -0,0 +1,3 @@ +# Support Libraries + +This directory provides support libraries for known types. diff --git a/support/timepb/README.md b/support/timepb/README.md new file mode 100644 index 0000000..e3b2476 --- /dev/null +++ b/support/timepb/README.md @@ -0,0 +1,23 @@ +# timepb + +`timepb` is a Go package that provides functions to do time operations with +[protobuf timestamp](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#timestamp) +and [protobuf duration](https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#duration) +structures. + +### Example + +``` go +t1 := &tspb.Timestamp{Seconds: 10, Nanos: 1} +d := &durpb.Duration{Seconds: 1, Nanos: 1e9 - 1} +t2 := Add(t1, d) + +fmt.Println(Compare(&tspb.Timestamp{Seconds: 12, Nanos: 0}, t2) == 0) +fmt.Println(Compare(&tspb.Timestamp{Seconds: 10, Nanos: 1}, t1) == 0) +fmt.Println(Compare(t1, t2)) +// Output: +// true +// true +// -1 +``` + diff --git a/support/timepb/cmp.go b/support/timepb/cmp.go new file mode 100644 index 0000000..ddd259e --- /dev/null +++ b/support/timepb/cmp.go @@ -0,0 +1,92 @@ +package timepb + +import ( + "fmt" + "time" + + durpb "google.golang.org/protobuf/types/known/durationpb" + tspb "google.golang.org/protobuf/types/known/timestamppb" +) + +// IsZero returns true only when t is nil +func IsZero(t *tspb.Timestamp) bool { + return t == nil +} + +// Commpare t1 and t2 and returns -1 when t1 < t2, 0 when t1 == t2 and 1 otherwise. +// Returns false if t1 or t2 is nil +func Compare(t1, t2 *tspb.Timestamp) int { + if t1 == nil || t2 == nil { + panic(fmt.Sprint("Can't compare nil time, t1=", t1, "t2=", t2)) + } + if t1.Seconds == t2.Seconds && t1.Nanos == t2.Nanos { + return 0 + } + if t1.Seconds < t2.Seconds || t1.Seconds == t2.Seconds && t1.Nanos < t2.Nanos { + return -1 + } + return 1 +} + +// DurationIsNegative returns true if the duration is negative. It assumes that d is valid +// (d..CheckValid() is nil). +func DurationIsNegative(d *durpb.Duration) bool { + return d.Seconds < 0 || d.Seconds == 0 && d.Nanos < 0 +} + +// AddStd returns a new timestamp with value t + d, where d is stdlib Duration. +// If t is nil then nil is returned. +// Panics on overflow. +func AddStd(t *tspb.Timestamp, d time.Duration) *tspb.Timestamp { + if t == nil { + return nil + } + if d == 0 { + t2 := *t + return &t2 + } + t2 := tspb.New(t.AsTime().Add(d)) + overflowPanic(t, t2, d < 0) + return t2 +} + +func overflowPanic(t1, t2 *tspb.Timestamp, negative bool) { + cmp := Compare(t1, t2) + if negative { + if cmp < 0 { + panic("time overflow") + } + } else { + if cmp > 0 { + panic("time overflow") + } + } +} + +const second = int32(time.Second) + +// Add returns a new timestamp with value t + d, where d is protobuf Duration +// If t is nil then nil is returned. Panics on overflow. +// Note: d must be a valid PB Duration (d..CheckValid() is nil). +func Add(t *tspb.Timestamp, d *durpb.Duration) *tspb.Timestamp { + if t == nil { + return nil + } + if d.Seconds == 0 && d.Nanos == 0 { + t2 := *t + return &t2 + } + t2 := tspb.Timestamp{ + Seconds: t.Seconds + d.Seconds, + Nanos: t.Nanos + d.Nanos, + } + if t2.Nanos >= second { + t2.Nanos -= second + t2.Seconds++ + } else if t2.Nanos <= -second { + t2.Nanos += second + t2.Seconds-- + } + overflowPanic(t, &t2, DurationIsNegative(d)) + return &t2 +} diff --git a/support/timepb/cmp_example_test.go b/support/timepb/cmp_example_test.go new file mode 100644 index 0000000..0294391 --- /dev/null +++ b/support/timepb/cmp_example_test.go @@ -0,0 +1,22 @@ +package timepb + +import ( + "fmt" + + durpb "google.golang.org/protobuf/types/known/durationpb" + tspb "google.golang.org/protobuf/types/known/timestamppb" +) + +func ExampleAdd() { + t1 := &tspb.Timestamp{Seconds: 10, Nanos: 1} + d := &durpb.Duration{Seconds: 1, Nanos: 1e9 - 1} + t2 := Add(t1, d) + + fmt.Println(Compare(&tspb.Timestamp{Seconds: 12, Nanos: 0}, t2) == 0) + fmt.Println(Compare(&tspb.Timestamp{Seconds: 10, Nanos: 1}, t1) == 0) + fmt.Println(Compare(t1, t2)) + // Output: + // true + // true + // -1 +} diff --git a/support/timepb/cmp_test.go b/support/timepb/cmp_test.go new file mode 100644 index 0000000..fa159f8 --- /dev/null +++ b/support/timepb/cmp_test.go @@ -0,0 +1,134 @@ +package timepb + +import ( + "math" + "testing" + "time" + + "github.com/stretchr/testify/require" + durpb "google.golang.org/protobuf/types/known/durationpb" + tspb "google.golang.org/protobuf/types/known/timestamppb" + "pgregory.net/rapid" +) + +func new(s int64, n int32) *tspb.Timestamp { + return &tspb.Timestamp{Seconds: s, Nanos: n} +} + +func TestIsZero(t *testing.T) { + tcs := []struct { + t *tspb.Timestamp + expected bool + }{ + {nil, true}, + + {&tspb.Timestamp{}, false}, + {new(0, 0), false}, + {new(1, 0), false}, + {new(0, 1), false}, + {tspb.New(time.Time{}), false}, + } + + for i, tc := range tcs { + require.Equal(t, tc.expected, IsZero(tc.t), "test_id %d", i) + } +} + +func TestCompare(t *testing.T) { + tcs := []struct { + t1 *tspb.Timestamp + t2 *tspb.Timestamp + expected int + }{ + {&tspb.Timestamp{}, &tspb.Timestamp{}, 0}, + {new(1, 1), new(1, 1), 0}, + {new(-1, 1), new(-1, 1), 0}, + {new(231, -5), new(231, -5), 0}, + + {new(1, -1), new(1, 0), -1}, + {new(1, -1), new(12, -1), -1}, + {new(-11, -1), new(-1, -1), -1}, + + {new(1, -1), new(0, -1), 1}, + {new(1, -1), new(1, -2), 1}, + } + for i, tc := range tcs { + r := Compare(tc.t1, tc.t2) + require.Equal(t, tc.expected, r, "test %d", i) + } + + // test panics + tcs2 := []struct { + t1 *tspb.Timestamp + t2 *tspb.Timestamp + }{ + {nil, new(1, 1)}, + {new(1, 1), nil}, + {nil, nil}, + } + for i, tc := range tcs2 { + require.Panics(t, func() { + Compare(tc.t1, tc.t2) + }, "test-panics %d", i) + } +} + +func TestAddFuzzy(t *testing.T) { + check := func(t require.TestingT, s, n int64, d time.Duration) { + t_in := time.Unix(s, n) + t_expected := tspb.New(t_in.Add(d)) + tb := tspb.New(t_in) + tbPb := Add(tb, durpb.New(d)) + tbStd := AddStd(tb, d) + require.Equal(t, *t_expected, *tbStd, "checking pb add") + require.Equal(t, *t_expected, *tbPb, "checking stdlib add") + } + gen := rapid.Int64Range(0, 1<<62) + genNano := rapid.Int64Range(0, 1e9-1) + rInt := func(t *rapid.T, label string) int64 { return gen.Draw(t, label).(int64) } + + rapid.Check(t, func(t *rapid.T) { + s, n, d := rInt(t, "sec"), genNano.Draw(t, "nanos").(int64), time.Duration(rInt(t, "dur")) + check(t, s, n, d) + }) + + check(t, 0, 0, 0) + check(t, 1, 2, 0) + check(t, -1, -1, 1) + + require.Nil(t, Add(nil, &durpb.Duration{Seconds: 1}), "Pb works with nil values") + require.Nil(t, AddStd(nil, time.Second), "Std works with nil values") +} + +func TestAddOverflow(t *testing.T) { + require := require.New(t) + tb := tspb.Timestamp{ + Seconds: math.MaxInt64, + Nanos: 1000, + } + require.Panics(func() { + AddStd(&tb, time.Second) + }, "AddStd should panic on overflow") + + require.Panics(func() { + Add(&tb, &durpb.Duration{Nanos: second - 1}) + }, "Add should panic on overflow") + + // should panic on underflow + + tb = tspb.Timestamp{ + Seconds: -math.MaxInt64 - 1, + Nanos: -1000, + } + require.True(tb.Seconds < 0, "sanity check") + require.Panics(func() { + tt := AddStd(&tb, -time.Second) + t.Log(tt) + }, "AddStd should panic on underflow") + + require.Panics(func() { + tt := Add(&tb, &durpb.Duration{Nanos: -second + 1}) + t.Log(tt) + }, "Add should panic on underflow") + +} diff --git a/support/timepb/doc.go b/support/timepb/doc.go new file mode 100644 index 0000000..9342c49 --- /dev/null +++ b/support/timepb/doc.go @@ -0,0 +1,5 @@ +/* +Package timepb provides functions to do time operations with protobuf timestamp +and duration structures. +*/ +package timepb