From f3165edd7478a79ca2af622781ac9cd9e863f8ed Mon Sep 17 00:00:00 2001 From: Sergey Date: Mon, 10 Jun 2024 03:39:51 +0300 Subject: [PATCH 1/4] chore: add HumanizeTimestamp; make ConvertToFloat exportable Signed-off-by: Sergey --- helpers/templates/time.go | 38 ++++++++++++- helpers/templates/time_test.go | 101 +++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+), 2 deletions(-) diff --git a/helpers/templates/time.go b/helpers/templates/time.go index 266c8c99..b7dc655f 100644 --- a/helpers/templates/time.go +++ b/helpers/templates/time.go @@ -14,13 +14,18 @@ package templates import ( + "errors" "fmt" "math" "strconv" "time" + + "github.com/prometheus/common/model" ) -func convertToFloat(i interface{}) (float64, error) { +var errNaNOrInf = errors.New("value is NaN or Inf") + +func ConvertToFloat(i interface{}) (float64, error) { switch v := i.(type) { case float64: return v, nil @@ -41,8 +46,20 @@ func convertToFloat(i interface{}) (float64, error) { } } +func FloatToTime(v float64) (*time.Time, error) { + if math.IsNaN(v) || math.IsInf(v, 0) { + return nil, errNaNOrInf + } + timestamp := v * 1e9 + if timestamp > math.MaxInt64 || timestamp < math.MinInt64 { + return nil, fmt.Errorf("%v cannot be represented as a nanoseconds timestamp since it overflows int64", v) + } + t := model.TimeFromUnixNano(int64(timestamp)).Time().UTC() + return &t, nil +} + func HumanizeDuration(i interface{}) (string, error) { - v, err := convertToFloat(i) + v, err := ConvertToFloat(i) if err != nil { return "", err } @@ -87,3 +104,20 @@ func HumanizeDuration(i interface{}) (string, error) { } return fmt.Sprintf("%.4g%ss", v, prefix), nil } + +func HumanizeTimestamp(i interface{}) (string, error) { + v, err := ConvertToFloat(i) + if err != nil { + return "", err + } + + tm, err := FloatToTime(v) + switch { + case errors.Is(err, errNaNOrInf): + return fmt.Sprintf("%.4g", v), nil + case err != nil: + return "", err + } + + return fmt.Sprint(tm), nil +} diff --git a/helpers/templates/time_test.go b/helpers/templates/time_test.go index 8c59b21b..524e15f6 100644 --- a/helpers/templates/time_test.go +++ b/helpers/templates/time_test.go @@ -14,6 +14,7 @@ package templates import ( + "math" "testing" "github.com/stretchr/testify/require" @@ -139,3 +140,103 @@ func TestHumanizeDurationSecondsInt(t *testing.T) { }) } } + +func TestHumanizeTimestampInt(t *testing.T) { + tc := []struct { + name string + input int + expected string + }{ + {name: "zero", input: 0, expected: "1970-01-01 00:00:00 +0000 UTC"}, + {name: "negative", input: -1, expected: "1969-12-31 23:59:59 +0000 UTC"}, + {name: "one", input: 1, expected: "1970-01-01 00:00:01 +0000 UTC"}, + {name: "past", input: 1234567, expected: "1970-01-15 06:56:07 +0000 UTC"}, + {name: "future", input: 9223372036, expected: "2262-04-11 23:47:16 +0000 UTC"}, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + result, err := HumanizeTimestamp(tt.input) + require.NoError(t, err) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestHumanizeTimestampUint(t *testing.T) { + tc := []struct { + name string + input uint64 + expected string + }{ + {name: "zero", input: 0, expected: "1970-01-01 00:00:00 +0000 UTC"}, + {name: "one", input: 1, expected: "1970-01-01 00:00:01 +0000 UTC"}, + {name: "past", input: 1234567, expected: "1970-01-15 06:56:07 +0000 UTC"}, + {name: "future", input: 9223372036, expected: "2262-04-11 23:47:16 +0000 UTC"}, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + result, err := HumanizeTimestamp(tt.input) + require.NoError(t, err) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestHumanizeTimestampError(t *testing.T) { + _, err := HumanizeTimestamp(math.MaxInt64) + require.Error(t, err) +} + +func TestHumanizeTimestampInfNanString(t *testing.T) { + tc := []struct { + name string + input string + expected string + }{ + {name: "infinity", input: "+Inf", expected: "+Inf"}, + {name: "minus infinity", input: "-Inf", expected: "-Inf"}, + {name: "NaN", input: "NaN", expected: "NaN"}, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + result, err := HumanizeTimestamp(tt.input) + require.NoError(t, err) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestHumanizeTimestampInfNanFloat(t *testing.T) { + tc := []struct { + name string + input float64 + expected string + }{ + {name: "infinity", input: math.Inf(1), expected: "+Inf"}, + {name: "minus infinity", input: math.Inf(-1), expected: "-Inf"}, + {name: "NaN", input: math.NaN(), expected: "NaN"}, + } + + for _, tt := range tc { + t.Run(tt.name, func(t *testing.T) { + result, err := HumanizeTimestamp(tt.input) + require.NoError(t, err) + require.Equal(t, tt.expected, result) + }) + } +} + +func TestHumanizeTimestampSampleFloat64(t *testing.T) { + result, err := HumanizeTimestamp(1435065584.128) + require.NoError(t, err) + require.Equal(t, "2015-06-23 13:19:44.128 +0000 UTC", result) +} + +func TestHumanizeTimestampSampleString(t *testing.T) { + result, err := HumanizeTimestamp(1435065584.128) + require.NoError(t, err) + require.Equal(t, "2015-06-23 13:19:44.128 +0000 UTC", result) +} From 75edce1c69aae48b3e6b2facec5d054c8d5925e0 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 21 Jun 2024 18:48:25 +0300 Subject: [PATCH 2/4] chore: refactor tests Signed-off-by: Sergey --- helpers/templates/time_test.go | 160 ++++++--------------------------- 1 file changed, 25 insertions(+), 135 deletions(-) diff --git a/helpers/templates/time_test.go b/helpers/templates/time_test.go index 524e15f6..7a2869d8 100644 --- a/helpers/templates/time_test.go +++ b/helpers/templates/time_test.go @@ -23,9 +23,10 @@ import ( func TestHumanizeDurationSecondsFloat64(t *testing.T) { tc := []struct { name string - input float64 + input interface{} expected string }{ + // Integers {name: "zero", input: 0, expected: "0s"}, {name: "one second", input: 1, expected: "1s"}, {name: "one minute", input: 60, expected: "1m 0s"}, @@ -33,24 +34,8 @@ func TestHumanizeDurationSecondsFloat64(t *testing.T) { {name: "one day", input: 86400, expected: "1d 0h 0m 0s"}, {name: "one day and one hour", input: 86400 + 3600, expected: "1d 1h 0m 0s"}, {name: "negative duration", input: -(86400*2 + 3600*3 + 60*4 + 5), expected: "-2d 3h 4m 5s"}, + // Float64 with fractions {name: "using a float", input: 899.99, expected: "14m 59s"}, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - result, err := HumanizeDuration(tt.input) - require.NoError(t, err) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestHumanizeDurationSubsecondAndFractionalSecondsFloat64(t *testing.T) { - tc := []struct { - name string - input float64 - expected string - }{ {name: "millseconds", input: .1, expected: "100ms"}, {name: "nanoseconds", input: .0001, expected: "100us"}, {name: "milliseconds + nanoseconds", input: .12345, expected: "123.5ms"}, @@ -58,50 +43,13 @@ func TestHumanizeDurationSubsecondAndFractionalSecondsFloat64(t *testing.T) { {name: "minute + milliseconds", input: 60.5, expected: "1m 0s"}, {name: "second + milliseconds", input: 1.2345, expected: "1.234s"}, {name: "second + milliseconds rounded", input: 12.345, expected: "12.35s"}, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - result, err := HumanizeDuration(tt.input) - require.NoError(t, err) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestHumanizeDurationErrorString(t *testing.T) { - _, err := HumanizeDuration("one") - require.Error(t, err) -} - -func TestHumanizeDurationSecondsString(t *testing.T) { - tc := []struct { - name string - input string - expected string - }{ + // String {name: "zero", input: "0", expected: "0s"}, {name: "second", input: "1", expected: "1s"}, {name: "minute", input: "60", expected: "1m 0s"}, {name: "hour", input: "3600", expected: "1h 0m 0s"}, {name: "day", input: "86400", expected: "1d 0h 0m 0s"}, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - result, err := HumanizeDuration(tt.input) - require.NoError(t, err) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestHumanizeDurationSubsecondAndFractionalSecondsString(t *testing.T) { - tc := []struct { - name string - input string - expected string - }{ + // String with fractions {name: "millseconds", input: ".1", expected: "100ms"}, {name: "nanoseconds", input: ".0001", expected: "100us"}, {name: "milliseconds + nanoseconds", input: ".12345", expected: "123.5ms"}, @@ -109,23 +57,7 @@ func TestHumanizeDurationSubsecondAndFractionalSecondsString(t *testing.T) { {name: "minute + milliseconds", input: "60.5", expected: "1m 0s"}, {name: "second + milliseconds", input: "1.2345", expected: "1.234s"}, {name: "second + milliseconds rounded", input: "12.345", expected: "12.35s"}, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - result, err := HumanizeDuration(tt.input) - require.NoError(t, err) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestHumanizeDurationSecondsInt(t *testing.T) { - tc := []struct { - name string - input int - expected string - }{ + // Int {name: "zero", input: 0, expected: "0s"}, {name: "negative", input: -1, expected: "-1s"}, {name: "second", input: 1, expected: "1s"}, @@ -141,80 +73,33 @@ func TestHumanizeDurationSecondsInt(t *testing.T) { } } -func TestHumanizeTimestampInt(t *testing.T) { - tc := []struct { - name string - input int - expected string - }{ - {name: "zero", input: 0, expected: "1970-01-01 00:00:00 +0000 UTC"}, - {name: "negative", input: -1, expected: "1969-12-31 23:59:59 +0000 UTC"}, - {name: "one", input: 1, expected: "1970-01-01 00:00:01 +0000 UTC"}, - {name: "past", input: 1234567, expected: "1970-01-15 06:56:07 +0000 UTC"}, - {name: "future", input: 9223372036, expected: "2262-04-11 23:47:16 +0000 UTC"}, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - result, err := HumanizeTimestamp(tt.input) - require.NoError(t, err) - require.Equal(t, tt.expected, result) - }) - } +func TestHumanizeDurationErrorString(t *testing.T) { + _, err := HumanizeDuration("one") + require.Error(t, err) } -func TestHumanizeTimestampUint(t *testing.T) { +func TestHumanizeTimestampInt(t *testing.T) { tc := []struct { name string - input uint64 + input interface{} expected string }{ + // Int {name: "zero", input: 0, expected: "1970-01-01 00:00:00 +0000 UTC"}, + {name: "negative", input: -1, expected: "1969-12-31 23:59:59 +0000 UTC"}, {name: "one", input: 1, expected: "1970-01-01 00:00:01 +0000 UTC"}, {name: "past", input: 1234567, expected: "1970-01-15 06:56:07 +0000 UTC"}, {name: "future", input: 9223372036, expected: "2262-04-11 23:47:16 +0000 UTC"}, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - result, err := HumanizeTimestamp(tt.input) - require.NoError(t, err) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestHumanizeTimestampError(t *testing.T) { - _, err := HumanizeTimestamp(math.MaxInt64) - require.Error(t, err) -} - -func TestHumanizeTimestampInfNanString(t *testing.T) { - tc := []struct { - name string - input string - expected string - }{ + // Uint + {name: "zero", input: uint64(0), expected: "1970-01-01 00:00:00 +0000 UTC"}, + {name: "one", input: uint64(1), expected: "1970-01-01 00:00:01 +0000 UTC"}, + {name: "past", input: uint64(1234567), expected: "1970-01-15 06:56:07 +0000 UTC"}, + {name: "future", input: uint64(9223372036), expected: "2262-04-11 23:47:16 +0000 UTC"}, + // NaN/Inf, strings {name: "infinity", input: "+Inf", expected: "+Inf"}, {name: "minus infinity", input: "-Inf", expected: "-Inf"}, {name: "NaN", input: "NaN", expected: "NaN"}, - } - - for _, tt := range tc { - t.Run(tt.name, func(t *testing.T) { - result, err := HumanizeTimestamp(tt.input) - require.NoError(t, err) - require.Equal(t, tt.expected, result) - }) - } -} - -func TestHumanizeTimestampInfNanFloat(t *testing.T) { - tc := []struct { - name string - input float64 - expected string - }{ + // Nan/Inf, float64 {name: "infinity", input: math.Inf(1), expected: "+Inf"}, {name: "minus infinity", input: math.Inf(-1), expected: "-Inf"}, {name: "NaN", input: math.NaN(), expected: "NaN"}, @@ -229,6 +114,11 @@ func TestHumanizeTimestampInfNanFloat(t *testing.T) { } } +func TestHumanizeTimestampError(t *testing.T) { + _, err := HumanizeTimestamp(math.MaxInt64) + require.Error(t, err) +} + func TestHumanizeTimestampSampleFloat64(t *testing.T) { result, err := HumanizeTimestamp(1435065584.128) require.NoError(t, err) From d6f28e10e6e26af9b7c2293926c7bfc944ba2419 Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 21 Jun 2024 18:50:44 +0300 Subject: [PATCH 3/4] chore: renamed tests Signed-off-by: Sergey --- helpers/templates/time_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helpers/templates/time_test.go b/helpers/templates/time_test.go index 7a2869d8..fb3e499f 100644 --- a/helpers/templates/time_test.go +++ b/helpers/templates/time_test.go @@ -20,7 +20,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestHumanizeDurationSecondsFloat64(t *testing.T) { +func TestHumanizeDuration(t *testing.T) { tc := []struct { name string input interface{} @@ -78,7 +78,7 @@ func TestHumanizeDurationErrorString(t *testing.T) { require.Error(t, err) } -func TestHumanizeTimestampInt(t *testing.T) { +func TestHumanizeTimestamp(t *testing.T) { tc := []struct { name string input interface{} From 5fb8640fdfbb100ce3c00009e24bcd0d9b5afcca Mon Sep 17 00:00:00 2001 From: Sergey Date: Fri, 21 Jun 2024 18:57:44 +0300 Subject: [PATCH 4/4] chore: refactored tests even more Signed-off-by: Sergey --- helpers/templates/time_test.go | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/helpers/templates/time_test.go b/helpers/templates/time_test.go index fb3e499f..911216ee 100644 --- a/helpers/templates/time_test.go +++ b/helpers/templates/time_test.go @@ -103,6 +103,9 @@ func TestHumanizeTimestamp(t *testing.T) { {name: "infinity", input: math.Inf(1), expected: "+Inf"}, {name: "minus infinity", input: math.Inf(-1), expected: "-Inf"}, {name: "NaN", input: math.NaN(), expected: "NaN"}, + // Sampled data + {name: "sample float64", input: 1435065584.128, expected: "2015-06-23 13:19:44.128 +0000 UTC"}, + {name: "sample string", input: "1435065584.128", expected: "2015-06-23 13:19:44.128 +0000 UTC"}, } for _, tt := range tc { @@ -118,15 +121,3 @@ func TestHumanizeTimestampError(t *testing.T) { _, err := HumanizeTimestamp(math.MaxInt64) require.Error(t, err) } - -func TestHumanizeTimestampSampleFloat64(t *testing.T) { - result, err := HumanizeTimestamp(1435065584.128) - require.NoError(t, err) - require.Equal(t, "2015-06-23 13:19:44.128 +0000 UTC", result) -} - -func TestHumanizeTimestampSampleString(t *testing.T) { - result, err := HumanizeTimestamp(1435065584.128) - require.NoError(t, err) - require.Equal(t, "2015-06-23 13:19:44.128 +0000 UTC", result) -}