From c04cadcad09cac2858e9b1f58328b8fc63ba3bf7 Mon Sep 17 00:00:00 2001 From: Tristan Swadell Date: Wed, 6 Jul 2022 13:29:26 -0700 Subject: [PATCH] Provide an option to ensure time operations are in UTC by default (#560) * Provide an option to ensure time operations are in UTC by default * Make it possible to opt-in or opt-out using a feature flag * Minor refinement to library.go setup for stdLib compile options * Fixed issue with substring that caused wrong time offsets in minutes --- cel/BUILD.bazel | 1 + cel/cel_test.go | 87 +++++++++++- cel/env.go | 8 ++ cel/library.go | 243 +++++++++++++++++++++++++++++++++ cel/options.go | 11 ++ checker/env.go | 27 +++- checker/env_test.go | 6 +- common/types/timestamp.go | 2 +- common/types/timestamp_test.go | 103 ++++++++++++-- 9 files changed, 466 insertions(+), 22 deletions(-) diff --git a/cel/BUILD.bazel b/cel/BUILD.bazel index 57cc4fbe..e973abfc 100644 --- a/cel/BUILD.bazel +++ b/cel/BUILD.bazel @@ -23,6 +23,7 @@ go_library( "//checker/decls:go_default_library", "//common:go_default_library", "//common/containers:go_default_library", + "//common/overloads:go_default_library", "//common/types:go_default_library", "//common/types/pb:go_default_library", "//common/types/ref:go_default_library", diff --git a/cel/cel_test.go b/cel/cel_test.go index 19750a2b..d3eca6ba 100644 --- a/cel/cel_test.go +++ b/cel/cel_test.go @@ -144,7 +144,7 @@ func TestAbbrevsParsed(t *testing.T) { } } -func TestAbbrevs_Disambiguation(t *testing.T) { +func TestAbbrevsDisambiguation(t *testing.T) { env, err := NewEnv( Abbrevs("external.Expr"), Container("google.api.expr.v1alpha1"), @@ -1085,7 +1085,7 @@ func TestResidualAstComplex(t *testing.T) { } } -func Benchmark_EvalOptions(b *testing.B) { +func BenchmarkEvalOptions(b *testing.B) { e, _ := NewEnv( Variable("ai", IntType), Variable("ar", MapType(StringType, StringType)), @@ -1650,6 +1650,89 @@ func TestRegexOptimizer(t *testing.T) { } } +func TestDefaultUTCTimeZone(t *testing.T) { + env, err := NewEnv(Variable("x", TimestampType), DefaultUTCTimeZone(true)) + if err != nil { + t.Fatalf("NewEnv() failed: %v", err) + } + ast, iss := env.Compile(` + x.getFullYear() == 1970 + && x.getMonth() == 0 + && x.getDayOfYear() == 0 + && x.getDayOfMonth() == 0 + && x.getDate() == 1 + && x.getDayOfWeek() == 4 + && x.getHours() == 2 + && x.getMinutes() == 5 + && x.getSeconds() == 6 + && x.getMilliseconds() == 1 + && x.getFullYear('-07:30') == 1969 + && x.getDayOfYear('-07:30') == 364 + && x.getMonth('-07:30') == 11 + && x.getDayOfMonth('-07:30') == 30 + && x.getDate('-07:30') == 31 + && x.getDayOfWeek('-07:30') == 3 + && x.getHours('-07:30') == 18 + && x.getMinutes('-07:30') == 35 + && x.getSeconds('-07:30') == 6 + && x.getMilliseconds('-07:30') == 1 + && x.getFullYear('23:15') == 1970 + && x.getDayOfYear('23:15') == 1 + && x.getMonth('23:15') == 0 + && x.getDayOfMonth('23:15') == 1 + && x.getDate('23:15') == 2 + && x.getDayOfWeek('23:15') == 5 + && x.getHours('23:15') == 1 + && x.getMinutes('23:15') == 20 + && x.getSeconds('23:15') == 6 + && x.getMilliseconds('23:15') == 1 + `) + if iss.Err() != nil { + t.Fatalf("env.Compile() failed: %v", iss.Err()) + } + prg, err := env.Program(ast) + if err != nil { + t.Fatalf("env.Program() failed: %v", err) + } + out, _, err := prg.Eval(map[string]interface{}{"x": time.Unix(7506, 1000000).Local()}) + if err != nil { + t.Fatalf("prg.Eval() failed: %v", err) + } + if out != types.True { + t.Errorf("Eval() got %v, wanted true", out) + } +} + +func TestDefaultUTCTimeZoneError(t *testing.T) { + env, err := NewEnv(Variable("x", TimestampType), DefaultUTCTimeZone(true)) + if err != nil { + t.Fatalf("NewEnv() failed: %v", err) + } + ast, iss := env.Compile(` + x.getFullYear(':xx') == 1969 + || x.getDayOfYear('xx:') == 364 + || x.getMonth('Am/Ph') == 11 + || x.getDayOfMonth('Am/Ph') == 30 + || x.getDate('Am/Ph') == 31 + || x.getDayOfWeek('Am/Ph') == 3 + || x.getHours('Am/Ph') == 19 + || x.getMinutes('Am/Ph') == 5 + || x.getSeconds('Am/Ph') == 6 + || x.getMilliseconds('Am/Ph') == 1 + `) + if iss.Err() != nil { + t.Fatalf("env.Compile() failed: %v", iss.Err()) + } + prg, err := env.Program(ast) + if err != nil { + t.Fatalf("env.Program() failed: %v", err) + } + out, _, err := prg.Eval(map[string]interface{}{"x": time.Unix(7506, 1000000).Local()}) + if err == nil { + t.Fatalf("prg.Eval() got %v wanted error", out) + } +} + func interpret(t *testing.T, env *Env, expr string, vars interface{}) (ref.Val, error) { t.Helper() ast, iss := env.Compile(expr) diff --git a/cel/env.go b/cel/env.go index ecc5bdd8..f037405a 100644 --- a/cel/env.go +++ b/cel/env.go @@ -452,6 +452,14 @@ func (e *Env) configure(opts []EnvOption) (*Env, error) { } } + // If the default UTC timezone fix has been enabled, make sure the library is configured + if e.HasFeature(featureDefaultUTCTimeZone) { + e, err = Lib(timeUTCLibrary{})(e) + if err != nil { + return nil, err + } + } + // Initialize all of the functions configured within the environment. for _, fn := range e.functions { err = fn.init() diff --git a/cel/library.go b/cel/library.go index 4b8c9f1d..2b403599 100644 --- a/cel/library.go +++ b/cel/library.go @@ -15,7 +15,14 @@ package cel import ( + "strconv" + "strings" + "time" + "github.com/google/cel-go/checker" + "github.com/google/cel-go/common/overloads" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/interpreter/functions" ) @@ -74,3 +81,239 @@ func (stdLibrary) ProgramOptions() []ProgramOption { Functions(functions.StandardOverloads()...), } } + +type timeUTCLibrary struct{} + +func (timeUTCLibrary) CompileOptions() []EnvOption { + return timestampOverloadDeclarations +} + +func (timeUTCLibrary) ProgramOptions() []ProgramOption { + return []ProgramOption{} +} + +// Declarations and functions which enable using UTC on time.Time inputs when the timezone is unspecified +// in the CEL expression. +var ( + utcTZ = types.String("UTC") + + timestampOverloadDeclarations = []EnvOption{ + Function(overloads.TimeGetFullYear, + MemberOverload(overloads.TimestampToYear, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetFullYear(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToYearWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetFullYear), + ), + ), + Function(overloads.TimeGetMonth, + MemberOverload(overloads.TimestampToMonth, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetMonth(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToMonthWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetMonth), + ), + ), + Function(overloads.TimeGetDayOfYear, + MemberOverload(overloads.TimestampToDayOfYear, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetDayOfYear(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToDayOfYearWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(func(ts, tz ref.Val) ref.Val { + return timestampGetDayOfYear(ts, tz) + }), + ), + ), + Function(overloads.TimeGetDayOfMonth, + MemberOverload(overloads.TimestampToDayOfMonthZeroBased, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetDayOfMonthZeroBased(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToDayOfMonthZeroBasedWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetDayOfMonthZeroBased), + ), + ), + Function(overloads.TimeGetDate, + MemberOverload(overloads.TimestampToDayOfMonthOneBased, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetDayOfMonthOneBased(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToDayOfMonthOneBasedWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetDayOfMonthOneBased), + ), + ), + Function(overloads.TimeGetDayOfWeek, + MemberOverload(overloads.TimestampToDayOfWeek, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetDayOfWeek(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToDayOfWeekWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetDayOfWeek), + ), + ), + Function(overloads.TimeGetHours, + MemberOverload(overloads.TimestampToHours, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetHours(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToHoursWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetHours), + ), + ), + Function(overloads.TimeGetMinutes, + MemberOverload(overloads.TimestampToMinutes, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetMinutes(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToMinutesWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetMinutes), + ), + ), + Function(overloads.TimeGetSeconds, + MemberOverload(overloads.TimestampToSeconds, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetSeconds(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToSecondsWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetSeconds), + ), + ), + Function(overloads.TimeGetMilliseconds, + MemberOverload(overloads.TimestampToMilliseconds, []*Type{TimestampType}, IntType, + UnaryBinding(func(ts ref.Val) ref.Val { + return timestampGetMilliseconds(ts, utcTZ) + }), + ), + MemberOverload(overloads.TimestampToMillisecondsWithTz, []*Type{TimestampType, StringType}, IntType, + BinaryBinding(timestampGetMilliseconds), + ), + ), + } +) + +func timestampGetFullYear(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.Year()) +} + +func timestampGetMonth(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + // CEL spec indicates that the month should be 0-based, but the Time value + // for Month() is 1-based. + return types.Int(t.Month() - 1) +} + +func timestampGetDayOfYear(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.YearDay() - 1) +} + +func timestampGetDayOfMonthZeroBased(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.Day() - 1) +} + +func timestampGetDayOfMonthOneBased(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.Day()) +} + +func timestampGetDayOfWeek(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.Weekday()) +} + +func timestampGetHours(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.Hour()) +} + +func timestampGetMinutes(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.Minute()) +} + +func timestampGetSeconds(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.Second()) +} + +func timestampGetMilliseconds(ts, tz ref.Val) ref.Val { + t, err := inTimeZone(ts, tz) + if err != nil { + return types.NewErr(err.Error()) + } + return types.Int(t.Nanosecond() / 1000000) +} + +func inTimeZone(ts, tz ref.Val) (time.Time, error) { + t := ts.(types.Timestamp) + val := string(tz.(types.String)) + ind := strings.Index(val, ":") + if ind == -1 { + loc, err := time.LoadLocation(val) + if err != nil { + return time.Time{}, err + } + return t.In(loc), nil + } + + // If the input is not the name of a timezone (for example, 'US/Central'), it should be a numerical offset from UTC + // in the format ^(+|-)(0[0-9]|1[0-4]):[0-5][0-9]$. The numerical input is parsed in terms of hours and minutes. + hr, err := strconv.Atoi(string(val[0:ind])) + if err != nil { + return time.Time{}, err + } + min, err := strconv.Atoi(string(val[ind+1:])) + if err != nil { + return time.Time{}, err + } + var offset int + if string(val[0]) == "-" { + offset = hr*60 - min + } else { + offset = hr*60 + min + } + secondsEastOfUTC := int((time.Duration(offset) * time.Minute).Seconds()) + timezone := time.FixedZone("", secondsEastOfUTC) + return t.In(timezone), nil +} diff --git a/cel/options.go b/cel/options.go index dc32cb63..21c75701 100644 --- a/cel/options.go +++ b/cel/options.go @@ -56,6 +56,11 @@ const ( // Enable eager validation of declarations to ensure that Env values created // with `Extend` inherit a validated list of declarations from the parent Env. featureEagerlyValidateDeclarations + + // Enable the use of the default UTC timezone when a timezone is not specified + // on a CEL timestamp operation. This fixes the scenario where the input time + // is not already in UTC. + featureDefaultUTCTimeZone ) // EnvOption is a functional interface for configuring the environment. @@ -523,6 +528,12 @@ func CrossTypeNumericComparisons(enabled bool) EnvOption { return features(featureCrossTypeNumericComparisons, enabled) } +// DefaultUTCTimeZone ensures that time-based operations use the UTC timezone rather than the +// input time's local timezone. +func DefaultUTCTimeZone(enabled bool) EnvOption { + return features(featureDefaultUTCTimeZone, enabled) +} + // features sets the given feature flags. See list of Feature constants above. func features(flag int, enabled bool) EnvOption { return func(e *Env) (*Env, error) { diff --git a/checker/env.go b/checker/env.go index c9d0614e..c7eeb04e 100644 --- a/checker/env.go +++ b/checker/env.go @@ -181,8 +181,7 @@ func (e *Env) addOverload(f *exprpb.Decl, overload *exprpb.Decl_FunctionDecl_Ove overload.GetParams()...) overloadErased := substitute(emptyMappings, overloadFunction, true) for _, existing := range function.GetOverloads() { - existingFunction := decls.NewFunctionType(existing.GetResultType(), - existing.GetParams()...) + existingFunction := decls.NewFunctionType(existing.GetResultType(), existing.GetParams()...) existingErased := substitute(emptyMappings, existingFunction, true) overlap := isAssignable(emptyMappings, overloadErased, existingErased) != nil || isAssignable(emptyMappings, existingErased, overloadErased) != nil @@ -213,18 +212,33 @@ func (e *Env) addOverload(f *exprpb.Decl, overload *exprpb.Decl_FunctionDecl_Ove // Adds a function decl if one doesn't already exist, then adds all overloads from the Decl. // If overload overlaps with an existing overload, adds to the errors in the Env instead. func (e *Env) setFunction(decl *exprpb.Decl) []errorMsg { + errorMsgs := make([]errorMsg, 0) + overloads := decl.GetFunction().GetOverloads() current := e.declarations.FindFunction(decl.Name) if current == nil { //Add the function declaration without overloads and check the overloads below. current = decls.NewFunction(decl.Name) } else { + existingOverloads := map[string]*exprpb.Decl_FunctionDecl_Overload{} + for _, overload := range current.GetFunction().GetOverloads() { + existingOverloads[overload.GetOverloadId()] = overload + } + newOverloads := []*exprpb.Decl_FunctionDecl_Overload{} + for _, overload := range overloads { + existing, found := existingOverloads[overload.GetOverloadId()] + if !found || !proto.Equal(existing, overload) { + newOverloads = append(newOverloads, overload) + } + } + overloads = newOverloads + if len(newOverloads) == 0 { + return errorMsgs + } // Copy on write since we don't know where this original definition came from. current = proto.Clone(current).(*exprpb.Decl) } e.declarations.SetFunction(current) - - errorMsgs := make([]errorMsg, 0) - for _, overload := range decl.GetFunction().GetOverloads() { + for _, overload := range overloads { errorMsgs = append(errorMsgs, e.addOverload(current, overload)...) } return errorMsgs @@ -235,6 +249,9 @@ func (e *Env) setFunction(decl *exprpb.Decl) []errorMsg { func (e *Env) addIdent(decl *exprpb.Decl) errorMsg { current := e.declarations.FindIdentInScope(decl.Name) if current != nil { + if proto.Equal(current, decl) { + return "" + } return overlappingIdentifierError(decl.Name) } e.declarations.AddIdent(decl) diff --git a/checker/env_test.go b/checker/env_test.go index b5e5b6d4..13be53d1 100644 --- a/checker/env_test.go +++ b/checker/env_test.go @@ -54,12 +54,8 @@ func TestOverlappingMacro(t *testing.T) { func TestOverlappingOverload(t *testing.T) { env := newStdEnv(t) - paramA := decls.NewTypeParamType("A") - typeParamAList := []string{"A"} err := env.Add(decls.NewFunction(overloads.TypeConvertDyn, - decls.NewParameterizedOverload(overloads.ToDyn, - []*exprpb.Type{paramA}, decls.Dyn, - typeParamAList))) + decls.NewOverload(overloads.ToDyn, []*exprpb.Type{decls.String}, decls.Dyn))) if err == nil { t.Error("Got nil, wanted error") } else if !strings.Contains(err.Error(), "overlapping overload") { diff --git a/common/types/timestamp.go b/common/types/timestamp.go index cb323744..7513a1b2 100644 --- a/common/types/timestamp.go +++ b/common/types/timestamp.go @@ -299,7 +299,7 @@ func timeZone(tz ref.Val, visitor timestampVisitor) timestampVisitor { if err != nil { return wrapErr(err) } - min, err := strconv.Atoi(string(val[ind+1])) + min, err := strconv.Atoi(string(val[ind+1:])) if err != nil { return wrapErr(err) } diff --git a/common/types/timestamp_test.go b/common/types/timestamp_test.go index 88264c87..0749c014 100644 --- a/common/types/timestamp_test.go +++ b/common/types/timestamp_test.go @@ -23,6 +23,7 @@ import ( "github.com/google/cel-go/common/overloads" "github.com/google/cel-go/common/types/ref" + "google.golang.org/protobuf/proto" anypb "google.golang.org/protobuf/types/known/anypb" @@ -312,9 +313,48 @@ func TestTimestampConvertToNative(t *testing.T) { } } +func TestTimestampGetDayOfMonth(t *testing.T) { + // 1970-01-01T02:05:06Z + ts := timestampOf(time.Unix(7506, 0).UTC()) + mon := ts.Receive(overloads.TimeGetDayOfMonth, overloads.TimestampToDayOfMonthZeroBased, []ref.Val{}) + if !mon.Equal(Int(0)).(Bool) { + t.Errorf("ts.getDayOfMonth() got %v, wanted 0", mon) + } + // 1969-12-31T19:05:06Z + monTz := ts.Receive(overloads.TimeGetDayOfMonth, overloads.TimestampToDayOfMonthZeroBasedWithTz, + []ref.Val{String("America/Phoenix")}) + if !monTz.Equal(Int(30)).(Bool) { + t.Errorf("ts.getDayOfMonth() got %v, wanted 30", mon) + } + // 1969-12-31T19:05:06Z + monTz = ts.Receive(overloads.TimeGetDayOfMonth, overloads.TimestampToDayOfMonthZeroBasedWithTz, + []ref.Val{String("-07:00")}) + if !monTz.Equal(Int(30)).(Bool) { + t.Errorf("ts.getDayOfMonth() got %v, wanted 30", mon) + } + + // 1970-01-01T02:05:06Z + mon = ts.Receive(overloads.TimeGetDate, overloads.TimestampToDayOfMonthOneBased, []ref.Val{}) + if !mon.Equal(Int(1)).(Bool) { + t.Errorf("ts.getDate() got %v, wanted 1", mon) + } + // 1969-12-31T19:05:06Z + monTz = ts.Receive(overloads.TimeGetDate, overloads.TimestampToDayOfMonthOneBasedWithTz, + []ref.Val{String("America/Phoenix")}) + if !monTz.Equal(Int(31)).(Bool) { + t.Errorf("ts.getDate() got %v, wanted 31", mon) + } + // 1970-01-02T01:05:06Z + monTz = ts.Receive(overloads.TimeGetDate, overloads.TimestampToDayOfMonthOneBasedWithTz, + []ref.Val{String("+23:00")}) + if !monTz.Equal(Int(2)).(Bool) { + t.Errorf("ts.getDate() got %v, wanted 2", mon) + } +} + func TestTimestampGetDayOfYear(t *testing.T) { // 1970-01-01T02:05:06Z - ts := Timestamp{Time: time.Unix(7506, 0).UTC()} + ts := timestampOf(time.Unix(7506, 0).UTC()) hr := ts.Receive(overloads.TimeGetDayOfYear, overloads.TimestampToDayOfYear, []ref.Val{}) if !hr.Equal(Int(0)).(Bool) { t.Error("Expected 0, got", hr) @@ -332,18 +372,48 @@ func TestTimestampGetDayOfYear(t *testing.T) { } } +func TestTimestampGetFullYear(t *testing.T) { + // 1970-01-01T02:05:06Z + ts := Timestamp{Time: time.Unix(7506, 0).UTC()} + year := ts.Receive(overloads.TimeGetFullYear, overloads.TimestampToYear, []ref.Val{}) + if !year.Equal(Int(1970)).(Bool) { + t.Errorf("ts.getFullYear() got %v, wanted 1970", year) + } + // 1969-12-31T19:05:06Z + yearTz := ts.Receive(overloads.TimeGetFullYear, overloads.TimestampToYearWithTz, + []ref.Val{String("America/Phoenix")}) + if !yearTz.Equal(Int(1969)).(Bool) { + t.Errorf("ts.getFullYear('America/Phoenix') got %v, wanted 1969", yearTz) + } +} + func TestTimestampGetMonth(t *testing.T) { // 1970-01-01T02:05:06Z ts := Timestamp{Time: time.Unix(7506, 0).UTC()} hr := ts.Receive(overloads.TimeGetMonth, overloads.TimestampToMonth, []ref.Val{}) if !hr.Equal(Int(0)).(Bool) { - t.Error("Expected 0, got", hr) + t.Errorf("ts.getMonth() got %v, wanted 0", hr) } // 1969-12-31T19:05:06Z hrTz := ts.Receive(overloads.TimeGetMonth, overloads.TimestampToMonthWithTz, []ref.Val{String("America/Phoenix")}) if !hrTz.Equal(Int(11)).(Bool) { - t.Error("Expected 11, got", hrTz) + t.Errorf("ts.getMonth('America/Phoenix') got %v, wanted 11", hrTz) + } +} + +func TestTimestampGetDayOfWeek(t *testing.T) { + // 1970-01-01T02:05:06Z + ts := Timestamp{Time: time.Unix(7506, 0).UTC()} + day := ts.Receive(overloads.TimeGetDayOfWeek, overloads.TimestampToDayOfWeek, []ref.Val{}) + if !day.Equal(Int(4)).(Bool) { + t.Errorf("ts.getDayOfWeek() got %v, wanted 4", day) + } + // 1969-12-31T19:05:06Z + dayTz := ts.Receive(overloads.TimeGetDayOfWeek, overloads.TimestampToDayOfWeekWithTz, + []ref.Val{String("America/Phoenix")}) + if !dayTz.Equal(Int(3)).(Bool) { + t.Errorf("ts.getDayOfWeek('America/Phoenix') got %v, wanted 3", dayTz) } } @@ -352,13 +422,13 @@ func TestTimestampGetHours(t *testing.T) { ts := Timestamp{Time: time.Unix(7506, 0).UTC()} hr := ts.Receive(overloads.TimeGetHours, overloads.TimestampToHours, []ref.Val{}) if !hr.Equal(Int(2)).(Bool) { - t.Error("Expected 2 hours, got", hr) + t.Errorf("ts.getHours() got %v, wanted 2", hr) } // 1969-12-31T19:05:06Z hrTz := ts.Receive(overloads.TimeGetHours, overloads.TimestampToHoursWithTz, []ref.Val{String("America/Phoenix")}) if !hrTz.Equal(Int(19)).(Bool) { - t.Error("Expected 19 hours, got", hrTz) + t.Errorf("ts.getHours('America/Phoenix') got %v, wanted 19 hours", hrTz) } } @@ -367,13 +437,13 @@ func TestTimestampGetMinutes(t *testing.T) { ts := Timestamp{Time: time.Unix(7506, 0).UTC()} min := ts.Receive(overloads.TimeGetMinutes, overloads.TimestampToMinutes, []ref.Val{}) if !min.Equal(Int(5)).(Bool) { - t.Error("Expected 5 minutes, got", min) + t.Errorf("ts.getMinutes() got %v, wanted 5 minutes", min) } // 1969-12-31T19:05:06Z minTz := ts.Receive(overloads.TimeGetMinutes, overloads.TimestampToMinutesWithTz, []ref.Val{String("America/Phoenix")}) if !minTz.Equal(Int(5)).(Bool) { - t.Error("Expected 5 minutes, got", minTz) + t.Errorf("ts.getMinutes('America/Phoenix') got %v, wanted 5 minutes", min) } } @@ -382,12 +452,27 @@ func TestTimestampGetSeconds(t *testing.T) { ts := Timestamp{Time: time.Unix(7506, 0).UTC()} sec := ts.Receive(overloads.TimeGetSeconds, overloads.TimestampToSeconds, []ref.Val{}) if !sec.Equal(Int(6)).(Bool) { - t.Error("Expected 6 seconds, got", sec) + t.Errorf("ts.getSeconds() got %v, wanted 6 seconds", sec) } // 1969-12-31T19:05:06Z secTz := ts.Receive(overloads.TimeGetSeconds, overloads.TimestampToSecondsWithTz, []ref.Val{String("America/Phoenix")}) if !secTz.Equal(Int(6)).(Bool) { - t.Error("Expected 6 seconds, got", secTz) + t.Errorf("ts.getSeconds('America/Phoenix') got %v, wanted 6 seconds", secTz) + } +} + +func TestTimestampGetMilliseconds(t *testing.T) { + // 1970-01-01T02:05:06Z + ts := Timestamp{Time: time.Unix(7506, 1000000).UTC()} + ms := ts.Receive(overloads.TimeGetMilliseconds, overloads.TimestampToMilliseconds, []ref.Val{}) + if !ms.Equal(Int(1)).(Bool) { + t.Errorf("ts.getMilliseconds() got %v, wanted 1 ms", ms) + } + // 1969-12-31T19:05:06Z + msTz := ts.Receive(overloads.TimeGetMilliseconds, overloads.TimestampToMillisecondsWithTz, + []ref.Val{String("America/Phoenix")}) + if !msTz.Equal(Int(1)).(Bool) { + t.Errorf("ts.getMilliseconds('America/Phoenix') got %v, wanted 1 ms", msTz) } }