From 2a2ad59403b3b2cc9d5c9d19abf0b0023f7e90ad Mon Sep 17 00:00:00 2001 From: Rishesh Agarwal Date: Wed, 21 Oct 2026 10:09:16 -0700 Subject: [PATCH] Support getHours, getMinutes, getSeconds and getMilliseconds in the C runtime. PiperOrigin-RevId: 1004031013 --- cel-c/internal/runtime/instr.h | 4 + cel-c/internal/runtime/interpretable.cc | 127 ++++++++ cel-c/internal/runtime/interpreter.cc | 29 ++ cel-c/internal/runtime_test.cc | 408 ++++++++++++++++++++++++ 4 files changed, 568 insertions(+) diff --git a/cel-c/internal/runtime/instr.h b/cel-c/internal/runtime/instr.h index 22f0fd1..ae9692b 100644 --- a/cel-c/internal/runtime/instr.h +++ b/cel-c/internal/runtime/instr.h @@ -78,6 +78,10 @@ typedef enum CEL_ATTRIBUTE_CLOSED_ENUM { _cel_InstrKind_kCallTimestamp, _cel_InstrKind_kCallDuration, _cel_InstrKind_kCallSize, + _cel_InstrKind_kCallGetHours, + _cel_InstrKind_kCallGetMinutes, + _cel_InstrKind_kCallGetSeconds, + _cel_InstrKind_kCallGetMilliseconds, _cel_InstrKind_kCallContainsString, _cel_InstrKind_kCallStartsWithString, _cel_InstrKind_kCallEndsWithString, diff --git a/cel-c/internal/runtime/interpretable.cc b/cel-c/internal/runtime/interpretable.cc index 56e5e2b..af95cdc 100644 --- a/cel-c/internal/runtime/interpretable.cc +++ b/cel-c/internal/runtime/interpretable.cc @@ -2765,6 +2765,93 @@ static inline void _cel_Interpretable_CallSize( } } +typedef enum { + _cel_TimeAccessor_kHours, + _cel_TimeAccessor_kMinutes, + _cel_TimeAccessor_kSeconds, + _cel_TimeAccessor_kMilliseconds, +} _cel_TimeAccessor; + +// Returns the requested component of a duration, matching cel-cpp. Hours, +// minutes and seconds are the total number of whole units in the duration, +// truncated towards zero. Milliseconds is the sub-second component, in the +// range [-999, 999]. +CEL_ATTRIBUTE_NODISCARD +static inline int64_t _cel_Interpretable_DurationComponent( + cel_Duration d, _cel_TimeAccessor accessor) { + int64_t sec; + int32_t nsec; + cel_Duration_ToUnix(d, &sec, &nsec); + // The seconds and nanoseconds of a duration always share the same sign, so + // truncating the seconds is equivalent to truncating the total duration. + switch (accessor) { + case _cel_TimeAccessor_kHours: + return sec / INT64_C(3600); + case _cel_TimeAccessor_kMinutes: + return sec / INT64_C(60); + case _cel_TimeAccessor_kSeconds: + return sec; + case _cel_TimeAccessor_kMilliseconds: + return nsec / INT32_C(1000000); + } + CEL_UNREACHABLE(); +} + +CEL_ATTRIBUTE_NODISCARD +static inline int64_t _cel_Interpretable_CivilTimeComponent( + const _cel_CivilTime* cel_nonnull civil_time, _cel_TimeAccessor accessor) { + switch (accessor) { + case _cel_TimeAccessor_kHours: + return civil_time->hour; + case _cel_TimeAccessor_kMinutes: + return civil_time->minute; + case _cel_TimeAccessor_kSeconds: + return civil_time->second; + case _cel_TimeAccessor_kMilliseconds: + return civil_time->millisecond; + } + CEL_UNREACHABLE(); +} + +// Implements `getHours`, `getMinutes`, `getSeconds` and `getMilliseconds`: +// +// (google.protobuf.Duration) -> int +// (google.protobuf.Timestamp) -> int +// +// Timestamps are broken down in UTC. +static inline void _cel_Interpretable_CallTimeAccessor( + _cel_Interpretable* cel_nonnull interp, + const _cel_InstrData* cel_nonnull instr, _cel_TimeAccessor accessor) { + CEL_ASSERT_NOT_NULL(interp); + CEL_ASSERT_NOT_NULL(instr); + CEL_ASSERT(instr->call.args == 1); + + cel_Value* const top = _cel_Interpretable_Top(interp, instr); + switch (cel_Value_Kind(top)) { + case cel_ValueKind_kError: + break; + case cel_ValueKind_kDuration: + cel_Value_SetInt(top, _cel_Interpretable_DurationComponent( + cel_Value_GetDuration(top), accessor)); + break; + case cel_ValueKind_kTimestamp: { + _cel_CivilTime civil_time; + if (CEL_UNLIKELY(!_cel_Timestamp_ToCivilTime(cel_Value_GetTimestamp(top), + cel_StringView_From(""), + &civil_time))) { + // An empty time zone means UTC, which is always valid. + CEL_UNREACHABLE(); + } + cel_Value_SetInt( + top, _cel_Interpretable_CivilTimeComponent(&civil_time, accessor)); + } break; + default: + cel_Value_SetError(top, + _cel_Interpretable_NewNoSuchOverloadError(interp)); + break; + } +} + static inline void _cel_Interpretable_Index( _cel_Interpretable* cel_nonnull interp, const _cel_InstrData* cel_nonnull instr) { @@ -3113,6 +3200,10 @@ static inline void _cel_Interpretable_ComputedGoto( &&INSTR_CALL_TIMESTAMP, &&INSTR_CALL_DURATION, &&INSTR_CALL_SIZE, + &&INSTR_CALL_GET_HOURS, + &&INSTR_CALL_GET_MINUTES, + &&INSTR_CALL_GET_SECONDS, + &&INSTR_CALL_GET_MILLISECONDS, &&INSTR_CALL_CONTAINS_STRING, &&INSTR_CALL_STARTS_WITH_STRING, &&INSTR_CALL_ENDS_WITH_STRING, @@ -3283,6 +3374,22 @@ static inline void _cel_Interpretable_ComputedGoto( INSTR_CALL_SIZE: _cel_Interpretable_CallSize(interp, &instr->data); DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_HOURS: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kHours); + DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_MINUTES: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kMinutes); + DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_SECONDS: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kSeconds); + DISPATCH_NEXT_INSTR(); +INSTR_CALL_GET_MILLISECONDS: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kMilliseconds); + DISPATCH_NEXT_INSTR(); INSTR_CALL_CONTAINS_STRING: _cel_Interpretable_CallContainsString(interp, &instr->data); DISPATCH_NEXT_INSTR(); @@ -3521,6 +3628,26 @@ static inline void _cel_Interpretable_Switch( _cel_Interpretable_CallSize(interp, &instr->data); ++instr; break; + case _cel_InstrKind_kCallGetHours: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kHours); + ++instr; + break; + case _cel_InstrKind_kCallGetMinutes: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kMinutes); + ++instr; + break; + case _cel_InstrKind_kCallGetSeconds: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kSeconds); + ++instr; + break; + case _cel_InstrKind_kCallGetMilliseconds: + _cel_Interpretable_CallTimeAccessor(interp, &instr->data, + _cel_TimeAccessor_kMilliseconds); + ++instr; + break; case _cel_InstrKind_kCallContainsString: _cel_Interpretable_CallContainsString(interp, &instr->data); ++instr; diff --git a/cel-c/internal/runtime/interpreter.cc b/cel-c/internal/runtime/interpreter.cc index 7cb61b8..49d23f8 100644 --- a/cel-c/internal/runtime/interpreter.cc +++ b/cel-c/internal/runtime/interpreter.cc @@ -1563,6 +1563,27 @@ static void _cel_Interpreter_PreVisitCallArgExpr( } } +// Returns the instruction kind for the duration/timestamp accessor `fn_name`, +// or `_cel_InstrKind_kUnreachable` if `fn_name` is not an accessor. Accessors +// accept (google.protobuf.Duration) or (google.protobuf.Timestamp). +CEL_ATTRIBUTE_NODISCARD +static _cel_InstrKind _cel_Interpreter_TimeAccessorKind( + cel_StringView fn_name) { + if (cel_StringView_Equals(fn_name, cel_StringView_From("getHours"))) { + return _cel_InstrKind_kCallGetHours; + } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getMinutes"))) { + return _cel_InstrKind_kCallGetMinutes; + } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getSeconds"))) { + return _cel_InstrKind_kCallGetSeconds; + } + if (cel_StringView_Equals(fn_name, cel_StringView_From("getMilliseconds"))) { + return _cel_InstrKind_kCallGetMilliseconds; + } + return _cel_InstrKind_kUnreachable; +} + static void _cel_Interpreter_PostVisitCallExpr( cel_AstVisitor* cel_nonnull visitor, const cel_CallExpr* cel_nonnull expr) { _cel_Interpreter* interp = _cel_Interpreter_FromAstVisitor(visitor); @@ -1624,6 +1645,14 @@ static void _cel_Interpreter_PostVisitCallExpr( } } + if (!found && instr->data.call.args == 1) { + _cel_InstrKind kind = _cel_Interpreter_TimeAccessorKind(fn_name); + if (kind != _cel_InstrKind_kUnreachable) { + instr->kind = kind; + found = true; + } + } + if (!found) { cel_InvalidArgumentStatusF(interp->status, "cel: function with name " CEL_STRINGVIEW_FMT diff --git a/cel-c/internal/runtime_test.cc b/cel-c/internal/runtime_test.cc index f7f13be..cfeed4c 100644 --- a/cel-c/internal/runtime_test.cc +++ b/cel-c/internal/runtime_test.cc @@ -805,6 +805,414 @@ TEST_F(RuntimeTest, CallDuration) { ASSERT_TRUE(cel_Value_IsError(&value)); } +TEST_F(RuntimeTest, CallGetHoursDuration) { + _cel_ProgramPtr prog = Parse(R"cel(duration("10h20m30.456s").getHours())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 10); + + prog = Parse(R"cel(getHours(duration("10h20m30.456s")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 10); + + prog = Parse(R"cel(duration("59m59.999s").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + prog = Parse(R"cel(duration("0s").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Negative durations truncate towards zero. + prog = Parse(R"cel(duration("-10h20m30.456s").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), -10); + + prog = Parse(R"cel(duration("-59m59.999s").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Duration bounds. + prog = Parse(R"cel(duration("315576000000s").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 87660000); + + prog = Parse(R"cel(duration("-315576000000s").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), -87660000); +} + +TEST_F(RuntimeTest, CallGetMinutesDuration) { + _cel_ProgramPtr prog = + Parse(R"cel(duration("10h20m30.456s").getMinutes())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 620); + + prog = Parse(R"cel(duration("59.999s").getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Negative durations truncate towards zero. + prog = Parse(R"cel(duration("-10h20m30.456s").getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), -620); + + // Duration bounds. + prog = Parse(R"cel(duration("315576000000s").getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 5259600000); + + prog = Parse(R"cel(duration("-315576000000s").getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), -5259600000); +} + +TEST_F(RuntimeTest, CallGetSecondsDuration) { + _cel_ProgramPtr prog = + Parse(R"cel(duration("10h20m30.456s").getSeconds())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 37230); + + prog = Parse(R"cel(duration("0.999s").getSeconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Negative durations truncate towards zero. + prog = Parse(R"cel(duration("-10h20m30.456s").getSeconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), -37230); + + // Duration bounds. + prog = Parse(R"cel(duration("315576000000s").getSeconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 315576000000); + + prog = Parse(R"cel(duration("-315576000000s").getSeconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), -315576000000); +} + +TEST_F(RuntimeTest, CallGetMillisecondsDuration) { + _cel_ProgramPtr prog = + Parse(R"cel(duration("10h20m30.456s").getMilliseconds())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 456); + + prog = Parse(R"cel(duration("1.5s").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 500); + + prog = Parse(R"cel(duration("0.0005s").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + prog = Parse(R"cel(duration("0s").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Negative durations truncate towards zero. + prog = Parse(R"cel(duration("-10h20m30.456s").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), -456); + + prog = Parse(R"cel(duration("-1.5s").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), -500); + + // Duration bounds. + prog = Parse(R"cel(duration("-315576000000s").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); +} + +TEST_F(RuntimeTest, CallGetHoursTimestamp) { + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getHours())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 10); + + prog = Parse(R"cel(getHours(timestamp("2025-12-01T10:20:30.456Z")))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 10); + + prog = + Parse(R"cel(timestamp("2025-12-01T23:59:59.999999999Z").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 23); + + prog = Parse(R"cel(timestamp("1970-01-01T00:00:00Z").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Non-UTC offsets are normalized to UTC. + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30+05:30").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 4); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T22:58:57Z").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 22); + + prog = Parse(R"cel(timestamp("0001-01-01T01:02:03Z").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 1); + + prog = + Parse(R"cel(timestamp("9999-12-31T23:59:59.999999999Z").getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 23); +} + +TEST_F(RuntimeTest, CallGetMinutesTimestamp) { + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getMinutes())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 20); + + prog = Parse( + R"cel(timestamp("2025-12-01T23:59:59.999999999Z").getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 59); + + // Non-UTC offsets are normalized to UTC. + prog = Parse(R"cel(timestamp("2025-12-01T10:20:30+05:30").getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 50); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T22:58:57Z").getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 58); + + prog = Parse(R"cel(timestamp("0001-01-01T01:02:03Z").getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 2); +} + +TEST_F(RuntimeTest, CallGetSecondsTimestamp) { + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getSeconds())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 30); + + prog = Parse( + R"cel(timestamp("2025-12-01T23:59:59.999999999Z").getSeconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 59); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T22:58:57Z").getSeconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 57); + + prog = Parse(R"cel(timestamp("0001-01-01T01:02:03Z").getSeconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 3); +} + +TEST_F(RuntimeTest, CallGetMillisecondsTimestamp) { + _cel_ProgramPtr prog = + Parse(R"cel(timestamp("2025-12-01T10:20:30.456Z").getMilliseconds())cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 456); + + prog = Parse( + R"cel(timestamp("2025-12-01T23:59:59.999999999Z").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 999); + + prog = Parse(R"cel(timestamp("1970-01-01T00:00:00Z").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + // Timestamps before the Unix epoch. + prog = Parse(R"cel(timestamp("1969-12-31T22:58:57Z").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); + + prog = Parse(R"cel(timestamp("0001-01-01T01:02:03Z").getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsInt(&value)); + EXPECT_EQ(cel_Value_GetInt(&value), 0); +} + +TEST_F(RuntimeTest, CallTimeAccessorsNoSuchOverload) { + _cel_ProgramPtr prog = Parse(R"cel(getHours(1))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(getMinutes("foo"))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(getSeconds(1.0))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(getMilliseconds(true))cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel(1.getHours())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); + + prog = Parse(R"cel("foo".getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: no such overload")); +} + +TEST_F(RuntimeTest, CallTimeAccessorsPropagatesError) { + _cel_ProgramPtr prog = Parse(R"cel(getHours(1 / 0))cel"); + _cel_TestingVariableResolver var_resolver; + _cel_ActivationPtr act(Activate(prog.get(), &var_resolver)); + cel_Value value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel((1 / 0).getMinutes())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel((1 / 0).getSeconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); + + prog = Parse(R"cel((1 / 0).getMilliseconds())cel"); + act = Activate(prog.get(), &var_resolver); + value = Execute(act.get()); + ASSERT_TRUE(cel_Value_IsError(&value)); + EXPECT_EQ(cel_Error_Message(cel_Value_GetError(&value)), + cel_StringView_From("cel: divide by zero")); +} + // TODO: Add addition tests for duration/timestamp TEST_F(RuntimeTest, IntSub) {