diff --git a/Cargo.toml b/Cargo.toml index a9744f5..89f3cc3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,8 +20,27 @@ regex = ["dep:regex"] semver = ["dep:semver"] uuid = ["dep:uuid"] urlquery = ["dep:url"] +time = ["dep:chrono", "dep:chrono-tz"] yaml = ["serde_yaml"] -full-opa = ["base64", "base64url", "crypto", "deprecated", "glob", "graph", "hex", "jsonschema", "regex", "semver", "uuid", "urlquery", "yaml"] +full-opa = [ + "base64", + "base64url", + "crypto", + "deprecated", + "glob", + "graph", + "hex", + "jsonschema", + "regex", + "semver", + "uuid", + "urlquery", + "time", + "yaml" +] + +# This feature enables some testing utils for OPA tests. +opa-testutil = [] [dependencies] anyhow = {version = "1.0.66", features = ["backtrace"] } @@ -51,6 +70,8 @@ wax = { version = "0.6.0", features = [], default-features = false, optional = t url = { version = "2.5.0", optional = true } uuid = { version = "1.6.1", features = ["v4", "fast-rng"], optional = true } jsonschema = { version = "0.17.1", default-features = false, optional = true } +chrono = { version = "0.4.31", optional = true } +chrono-tz = { version = "0.8.5", optional = true } [dev-dependencies] diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 750470d..d8a03db 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -33,6 +33,9 @@ mod utils; #[cfg(feature = "uuid")] mod uuid; +#[cfg(feature = "opa-testutil")] +mod test; + use crate::ast::{Expr, Ref}; use crate::lexer::Span; use crate::value::Value; @@ -76,6 +79,7 @@ lazy_static! { encoding::register(&mut m); //token_signing::register(&mut m); //token_verification::register(&mut m); + #[cfg(feature = "time")] time::register(&mut m); #[cfg(feature = "crypto")] @@ -92,6 +96,10 @@ lazy_static! { debugging::register(&mut m); tracing::register(&mut m); units::register(&mut m); + + #[cfg(feature = "opa-testutil")] + test::register(&mut m); + m }; } @@ -100,6 +108,7 @@ pub fn must_cache(path: &str) -> Option<&'static str> { match path { "rand.intn" => Some("rand.intn"), "uuid.rfc4122" => Some("uuid.rfc4122"), + "time.now_ns" => Some("time.now_ns"), _ => None, } } diff --git a/src/builtins/test.rs b/src/builtins/test.rs new file mode 100644 index 0000000..9fafb53 --- /dev/null +++ b/src/builtins/test.rs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::ast::{Expr, Ref}; +use crate::builtins; +use crate::builtins::utils::{ensure_args_count, ensure_string}; +use crate::lexer::Span; +use crate::value::Value; + +use std::collections::HashMap; +use std::thread; +use std::time::Duration; + +use anyhow::{bail, Ok, Result}; + +pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { + m.insert("test.sleep", (sleep, 1)); +} + +fn sleep(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "test.sleep"; + ensure_args_count(span, name, params, args, 1)?; + + let val = ensure_string(name, ¶ms[0], &args[0])?; + + let duration = if let Some(millis) = val.strip_suffix("ms").and_then(|v| v.parse().ok()) { + Duration::from_millis(millis) + } else if let Some(secs) = val.strip_suffix("s").and_then(|v| v.parse().ok()) { + Duration::from_secs(secs) + } else { + bail!(params[0].span().error( + format!("`{name}` expects a simple duration ends with `ms` or `s`. Got {val} instead") + .as_str() + )) + }; + + thread::sleep(duration); + + Ok(Value::Null) +} diff --git a/src/builtins/time.rs b/src/builtins/time.rs index 0291160..fcbc0d9 100644 --- a/src/builtins/time.rs +++ b/src/builtins/time.rs @@ -3,28 +3,258 @@ use crate::ast::{Expr, Ref}; use crate::builtins; -use crate::builtins::utils::ensure_args_count; +use crate::builtins::utils::{ensure_args_count, ensure_numeric, ensure_string}; use crate::lexer::Span; use crate::value::Value; use std::collections::HashMap; -use std::time::SystemTime; -use anyhow::{bail, Result}; +use anyhow::{anyhow, bail, Result}; +use chrono::{ + DateTime, Datelike, Days, FixedOffset, Local, Months, NaiveDateTime, SecondsFormat, TimeZone, + Timelike, Utc, Weekday, +}; +use chrono_tz::Tz; + +mod diff; pub fn register(m: &mut HashMap<&'static str, builtins::BuiltinFcn>) { + m.insert("time.add_date", (add_date, 4)); + m.insert("time.clock", (clock, 1)); + m.insert("time.date", (date, 1)); + m.insert("time.diff", (diff, 2)); + m.insert("time.format", (format, 1)); m.insert("time.now_ns", (now_ns, 0)); + m.insert("time.parse_ns", (parse_ns, 2)); + m.insert("time.parse_rfc3339_ns", (parse_rfc3339_ns, 1)); + m.insert("time.weekday", (weekday, 1)); } -fn now_ns(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { +fn add_date(span: &Span, params: &[Ref], args: &[Value], strict: bool) -> Result { + let name = "time.add_date"; + ensure_args_count(span, name, params, args, 4)?; + + let (datetime, _) = parse_epoch(name, ¶ms[0], &args[0])?; + let years = ensure_i32(name, ¶ms[1], &args[1])?; + let months = ensure_i32(name, ¶ms[2], &args[2])?; + let days = ensure_i32(name, ¶ms[3], &args[3])?; + + let Some(new_year) = datetime.year().checked_add(years) else { + return Ok(Value::Undefined); + }; + + datetime + .with_year(new_year) + .and_then(|d| { + let rhs = Months::new(months.unsigned_abs()); + if months >= 0 { + d.checked_add_months(rhs) + } else { + d.checked_sub_months(rhs) + } + }) + .and_then(|d| { + let rhs = Days::new(days.unsigned_abs() as u64); + if days >= 0 { + d.checked_add_days(rhs) + } else { + d.checked_sub_days(rhs) + } + }) + .map_or(Ok(Value::Undefined), |d| { + safe_timestamp_nanos(span, strict, d.timestamp_nanos_opt()) + }) +} + +fn clock(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "time.clock"; + ensure_args_count(span, name, params, args, 1)?; + + let (datetime, _) = parse_epoch(name, ¶ms[0], &args[0])?; + + Ok(Vec::from([ + (datetime.hour() as u64).into(), + (datetime.minute() as u64).into(), + (datetime.second() as u64).into(), + ]) + .into()) +} + +fn date(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "time.date"; + ensure_args_count(span, name, params, args, 1)?; + + let (datetime, _) = parse_epoch(name, ¶ms[0], &args[0])?; + + Ok(Vec::from([ + (datetime.year() as u64).into(), + (datetime.month() as u64).into(), + (datetime.day() as u64).into(), + ]) + .into()) +} + +fn diff(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "time.diff"; + ensure_args_count(span, name, params, args, 2)?; + + let (datetime1, _) = parse_epoch(name, ¶ms[0], &args[0])?; + let (datetime2, _) = parse_epoch(name, ¶ms[1], &args[1])?; + + let (year, month, day, hour, min, sec) = diff::diff_between_datetimes(datetime1, datetime2)?; + + Ok(Vec::from([ + (year as i64).into(), + (month as i64).into(), + (day as i64).into(), + (hour as i64).into(), + (min as i64).into(), + (sec as i64).into(), + ]) + .into()) +} + +fn format(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "time.format"; + ensure_args_count(span, name, params, args, 1)?; + + let (datetime, format) = parse_epoch(name, ¶ms[0], &args[0])?; + + let result = match format { + Some(format) => datetime.format(&format).to_string(), + None => datetime.to_rfc3339_opts(SecondsFormat::AutoSi, true), + }; + + Ok(Value::String(result.into())) +} + +fn now_ns(span: &Span, params: &[Ref], args: &[Value], strict: bool) -> Result { let name = "time.now_ns"; ensure_args_count(span, name, params, args, 0)?; - let now = SystemTime::now(); - let elapsed = match now.duration_since(SystemTime::UNIX_EPOCH) { - Ok(e) => e, - Err(e) => bail!(span.error(format!("could not fetch elapsed time. {e}").as_str())), - }; - let nanos = elapsed.as_nanos(); - Ok(Value::from(nanos)) + safe_timestamp_nanos(span, strict, Utc::now().timestamp_nanos_opt()) +} + +fn parse_ns(span: &Span, params: &[Ref], args: &[Value], strict: bool) -> Result { + let name = "time.parse_ns"; + ensure_args_count(span, name, params, args, 2)?; + + let layout = ensure_string(name, ¶ms[0], &args[0])?; + let value = ensure_string(name, ¶ms[1], &args[1])?; + + let datetime = NaiveDateTime::parse_from_str(&value, &layout)?; + safe_timestamp_nanos(span, strict, datetime.timestamp_nanos_opt()) +} + +fn parse_rfc3339_ns( + span: &Span, + params: &[Ref], + args: &[Value], + strict: bool, +) -> Result { + let name = "time.parse_rfc3339_ns"; + ensure_args_count(span, name, params, args, 1)?; + + let value = ensure_string(name, ¶ms[0], &args[0])?; + + let datetime = DateTime::parse_from_rfc3339(&value)?; + safe_timestamp_nanos(span, strict, datetime.timestamp_nanos_opt()) +} + +fn weekday(span: &Span, params: &[Ref], args: &[Value], _strict: bool) -> Result { + let name = "time.weekday"; + ensure_args_count(span, name, params, args, 1)?; + + let (datetime, _) = parse_epoch(name, ¶ms[0], &args[0])?; + + let weekday = match datetime.weekday() { + Weekday::Mon => "Monday", + Weekday::Tue => "Tuesday", + Weekday::Wed => "Wednesday", + Weekday::Thu => "Thursday", + Weekday::Fri => "Friday", + Weekday::Sat => "Saturday", + Weekday::Sun => "Sunday", + }; + + Ok(Value::String(weekday.into())) +} + +fn ensure_i32(name: &str, arg: &Expr, v: &Value) -> Result { + ensure_numeric(name, arg, v)? + .as_i64() + .and_then(|n| n.try_into().ok()) + .ok_or_else(|| arg.span().error("could not convert to int32")) +} + +fn safe_timestamp_nanos(span: &Span, strict: bool, nanos: Option) -> Result { + match nanos { + Some(ns) => Ok(Value::Number(ns.into())), + None if strict => { + bail!(span.error("time outside of valid range")) + } + None => Ok(Value::Undefined), + } +} + +fn parse_epoch( + fcn: &str, + arg: &Expr, + val: &Value, +) -> Result<(DateTime, Option)> { + match val { + Value::Number(num) => { + let ns = num.as_i64().ok_or_else(|| { + arg.span() + .error("could not convert numeric value of `ns` to int64") + })?; + + return Ok((Utc.timestamp_nanos(ns).fixed_offset(), None)); + } + + Value::Array(arr) => match arr.as_slice() { + [Value::Number(num)] => { + let ns = num.as_i64().ok_or_else(|| { + arg.span() + .error("could not convert numeric value of `ns` to int64") + })?; + + return Ok((Utc.timestamp_nanos(ns).fixed_offset(), None)); + } + [Value::Number(num), Value::String(tz), rest @ ..] => { + let ns = num.as_i64().ok_or_else(|| { + arg.span() + .error("could not convert numeric value of `ns` to int64") + })?; + + let datetime = match tz.as_ref() { + "UTC" | "" => Utc.timestamp_nanos(ns).fixed_offset(), + "Local" => Local.timestamp_nanos(ns).fixed_offset(), + _ => { + let tz: Tz = tz.parse().map_err(|err: String| anyhow!(err))?; + tz.timestamp_nanos(ns).fixed_offset() + } + }; + + let format = match rest.first() { + Some(Value::String(format)) => Some(format.to_string()), + Some(other) => { + bail!(arg.span().error(&format!( + "`{fcn}` expects 3rd element of `ns` to be a `string`. Got `{other}` instead" + ))) + } + None => None, + }; + + return Ok((datetime, format)); + } + _ => {} + }, + + _ => {} + } + + bail!(arg.span().error(&format!( + "`{fcn}` expects `ns` to be a `number` or `array[number, string]`. Got `{val}` instead" + ))) } diff --git a/src/builtins/time/diff.rs b/src/builtins/time/diff.rs new file mode 100644 index 0000000..f75f155 --- /dev/null +++ b/src/builtins/time/diff.rs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT and Apache 2.0 License. + +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike, Utc}; + +// Adapted from the official Go implementation: +// https://github.com/open-policy-agent/opa/blob/eb17a716b97720a27c6569395ba7c4b7409aae87/topdown/time.go#L179-L243 +pub fn diff_between_datetimes( + datetime1: DateTime, + datetime2: DateTime, +) -> Result<(i32, i32, i32, i32, i32, i32)> { + // The following implementation of this function is taken + // from https://github.com/icza/gox licensed under Apache 2.0. + // The only modification made is to variable names. + // + // For details, see https://stackoverflow.com/a/36531443/1705598 + // + // Copyright 2021 icza + // BEGIN REDISTRIBUTION FROM APACHE 2.0 LICENSED PROJECT + + // Make sure both datetimes in the same timezone + let datetime2 = datetime2.with_timezone(&datetime1.timezone()); + + // Make sure `datetime1` is always the smallest one + let (datetime1, datetime2) = if datetime1 > datetime2 { + (datetime2, datetime1) + } else { + (datetime1, datetime2) + }; + + let mut year = datetime2.year() - datetime1.year(); + let mut month = datetime2.month() as i32 - datetime1.month() as i32; + let mut day = datetime2.day() as i32 - datetime1.day() as i32; + let mut hour = datetime2.hour() as i32 - datetime1.hour() as i32; + let mut min = datetime2.minute() as i32 - datetime1.minute() as i32; + let mut sec = datetime2.second() as i32 - datetime1.second() as i32; + + // Normalize negative values + if sec < 0 { + sec += 60; + min -= 1; + } + if min < 0 { + min += 60; + hour -= 1; + } + if hour < 0 { + hour += 24; + day -= 1; + } + if day < 0 { + // Days in month: + let t = Utc + .with_ymd_and_hms(datetime1.year(), datetime1.month(), 32, 0, 0, 0) + .single() + .ok_or(anyhow!("Could not convert `ns1` to datetime"))?; + day += 32 - t.day() as i32; + month -= 1; + } + if month < 0 { + month += 12; + year -= 1; + } + + // END REDISTRIBUTION FROM APACHE 2.0 LICENSED PROJECT + + Ok((year, month, day, hour, min, sec)) +} diff --git a/tests/interpreter/cases/builtins/time/add_date.yaml b/tests/interpreter/cases/builtins/time/add_date.yaml new file mode 100644 index 0000000..6fcd24c --- /dev/null +++ b/tests/interpreter/cases/builtins/time/add_date.yaml @@ -0,0 +1,253 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: adding-nothing + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, 0, 0) + query: data.test + want_result: + a: 1703444325734390000 + + - note: adding-years + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 1, 0, 0) + b := time.add_date(1703444325734390000, 60, 0, 0) + query: data.test + want_result: + a: 1735066725734390000 + b: 3596900325734390000 + + - note: subtracting-years + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, -1, 0, 0) + b := time.add_date(1703444325734390000, -60, 0, 0) + query: data.test + want_result: + a: 1671908325734390000 + b: -190011674265610000 + + - note: adding-overflowing-years + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 2147483647, 0, 0) + query: data.test + want_result: {} + + - note: subtracting-overflowing-years + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, -2147483647, 0, 0) + query: data.test + want_result: {} + + - note: adding-months + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, 1, 0) + b := time.add_date(1703444325734390000, 0, 12, 0) + query: data.test + want_result: + a: 1706122725734390000 + b: 1735066725734390000 + + - note: subtracting-months + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, -1, 0) + b := time.add_date(1703444325734390000, 0, -12, 0) + query: data.test + want_result: + a: 1700852325734390000 + b: 1671908325734390000 + + - note: adding-overflowing-months + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, 15, 0) + b := time.add_date(1703444325734390000, 0, 19, 0) + query: data.test + want_result: + a: 1742842725734390000 + b: 1753383525734390000 + + - note: subtracting-overflowing-months + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, -15, 0) + b := time.add_date(1703444325734390000, 0, -19, 0) + query: data.test + want_result: + a: 1664045925734390000 + b: 1653418725734390000 + + - note: adding-days + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, 0, 5) + b := time.add_date(1703444325734390000, 0, 0, 16) + query: data.test + want_result: + a: 1703876325734390000 + b: 1704826725734390000 + + - note: subtracting-days + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, 0, -5) + b := time.add_date(1703444325734390000, 0, 0, -16) + query: data.test + want_result: + a: 1703012325734390000 + b: 1702061925734390000 + + - note: adding-overflowing-days + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, 0, 31) + b := time.add_date(1703444325734390000, 0, 0, 37) + query: data.test + want_result: + a: 1706122725734390000 + b: 1706641125734390000 + + - note: subtracting-overflowing-days + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 0, 0, -31) + b := time.add_date(1703444325734390000, 0, 0, -37) + query: data.test + want_result: + a: 1700765925734390000 + b: 1700247525734390000 + + - note: adding-mixed + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, 213, 6, 5) + b := time.add_date(1703444325734390000, 5, 0, 6) + c := time.add_date(1703444325734390000, 0, 3, 16) + d := time.add_date(1703444325734390000, 7, 3, 0) + e := time.add_date(1703444325734390000, 10, 2, 16) + f := time.add_date(1703444325734390000, 10, 14, 16) + g := time.add_date(1703444325734390000, 10, 14, 75) + query: data.test + want_result: + a: 8441261925734390000 + b: 1861815525734390000 + c: 1712689125734390000 + d: 1932145125734390000 + e: 2025802725734390000 + f: 2057338725734390000 + g: 2062436325734390000 + + - note: subtracting-mixed + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, -213, -6, -5) + b := time.add_date(1703444325734390000, -5, 0, -6) + c := time.add_date(1703444325734390000, 0, -3, -16) + d := time.add_date(1703444325734390000, -7, -3, 0) + e := time.add_date(1703444325734390000, -10, -2, -16) + f := time.add_date(1703444325734390000, -10, -14, -16) + g := time.add_date(1703444325734390000, -10, -14, -75) + query: data.test + want_result: + a: -5034459674265610000 + b: 1545159525734390000 + c: 1694199525734390000 + d: 1474743525734390000 + e: 1381258725734390000 + f: 1349722725734390000 + g: 1344625125734390000 + + - note: mixed-operations + data: {} + modules: + - | + package test + + a := time.add_date(1703444325734390000, -213, 6, -5) + b := time.add_date(1703444325734390000, 5, 0, -6) + c := time.add_date(1703444325734390000, 0, -3, -16) + d := time.add_date(1703444325734390000, 8, -3, 0) + e := time.add_date(1703444325734390000, -10, 2, 16) + f := time.add_date(1703444325734390000, -10, 14, -16) + g := time.add_date(1703444325734390000, 10, 14, -75) + query: data.test + want_result: + a: -5002923674265610000 + b: 1860778725734390000 + c: 1694199525734390000 + d: 1948042725734390000 + e: 1394650725734390000 + f: 1423421925734390000 + g: 2049476325734390000 + + - note: missing-arguments + data: {} + modules: + - | + package test + + a := time.add_date() + query: data.test + error: '`time.add_date` expects 4 arguments' + + - note: invalid-type + data: {} + modules: + - | + package test + + a := time.add_date("1703444325734390000", 0, 0, 0) + query: data.test + error: '`time.add_date` expects `ns` to be a `number` or `array[number, string]`. Got `"1703444325734390000"` instead' diff --git a/tests/interpreter/cases/builtins/time/clock.yaml b/tests/interpreter/cases/builtins/time/clock.yaml new file mode 100644 index 0000000..71af635 --- /dev/null +++ b/tests/interpreter/cases/builtins/time/clock.yaml @@ -0,0 +1,71 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: without-timezone + data: {} + modules: + - | + package test + + a := time.clock(1703444325734390000) + query: data.test + want_result: + a: + - 18 + - 58 + - 45 + + - note: with-utc-timezone + data: {} + modules: + - | + package test + + a := time.clock([1703444325734390000, "UTC"]) + b := time.clock([1703444325734390000, ""]) + query: data.test + want_result: + a: + - 18 + - 58 + - 45 + b: + - 18 + - 58 + - 45 + + - note: with-cet-timezone + data: {} + modules: + - | + package test + + a := time.clock([1703444325734390000, "CET"]) + query: data.test + want_result: + a: + - 19 + - 58 + - 45 + + - note: with-local-timezone + data: {} + modules: + - | + package test + + a := time.clock([1703444325734390000, "Local"]) != null + query: data.test + want_result: + a: true + + - note: invalid-type + data: {} + modules: + - | + package test + + a := time.clock("1703444325734390000") + query: data.test + error: '`time.clock` expects `ns` to be a `number` or `array[number, string]`. Got `"1703444325734390000"` instead' diff --git a/tests/interpreter/cases/builtins/time/date.yaml b/tests/interpreter/cases/builtins/time/date.yaml new file mode 100644 index 0000000..b1e11f1 --- /dev/null +++ b/tests/interpreter/cases/builtins/time/date.yaml @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: without-timezone + data: {} + modules: + - | + package test + + a := time.date(1703444325734390000) + query: data.test + want_result: + a: + - 2023 + - 12 + - 24 + + - note: with-utc-timezone + data: {} + modules: + - | + package test + + a := time.date([1257894000000000000, "UTC"]) + b := time.date([1703896119423491000, ""]) + query: data.test + want_result: + a: + - 2009 + - 11 + - 10 + b: + - 2023 + - 12 + - 30 + + - note: invalid-type + data: {} + modules: + - | + package test + + a := time.date("1703444325734390000") + query: data.test + error: '`time.date` expects `ns` to be a `number` or `array[number, string]`. Got `"1703444325734390000"` instead' diff --git a/tests/interpreter/cases/builtins/time/diff.yaml b/tests/interpreter/cases/builtins/time/diff.yaml new file mode 100644 index 0000000..7251c8c --- /dev/null +++ b/tests/interpreter/cases/builtins/time/diff.yaml @@ -0,0 +1,64 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: without-timezone + data: {} + modules: + - | + package test + + a := time.diff(1703444325734390000, 1257894000000000000) + query: data.test + want_result: + a: + - 14 + - 1 + - 13 + - 19 + - 58 + - 45 + + - note: with-timezone + data: {} + modules: + - | + package test + + a := time.diff([1703444325734390000, "UTC"], [1257894000000000000, ""]) + query: data.test + want_result: + a: + - 14 + - 1 + - 13 + - 19 + - 58 + - 45 + + - note: tz1-greater-than-tz2 + data: {} + modules: + - | + package test + + a := time.diff(1257894000000000000, 1703444325734390000) + query: data.test + want_result: + a: + - 14 + - 1 + - 13 + - 19 + - 58 + - 45 + + - note: invalid-type + data: {} + modules: + - | + package test + + a := time.diff("1703444325734390000", 1257894000000000000) + query: data.test + error: '`time.diff` expects `ns` to be a `number` or `array[number, string]`. Got `"1703444325734390000"` instead' diff --git a/tests/interpreter/cases/builtins/time/format.yaml b/tests/interpreter/cases/builtins/time/format.yaml new file mode 100644 index 0000000..ecef6a3 --- /dev/null +++ b/tests/interpreter/cases/builtins/time/format.yaml @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: without-timezone-and-format + data: {} + modules: + - | + package test + + a := time.format(1703444325734390000) + b := time.format(1257894000000000000) + query: data.test + want_result: + a: "2023-12-24T18:58:45.734390Z" + b: "2009-11-10T23:00:00Z" + + - note: with-timezone-and-no-format + data: {} + modules: + - | + package test + + a := time.format([1703444325734390000, "UTC"]) + b := time.format([1257894000000000000, ""]) + query: data.test + want_result: + a: "2023-12-24T18:58:45.734390Z" + b: "2009-11-10T23:00:00Z" + + - note: with-timezone-and-format + data: {} + modules: + - | + package test + + a := time.format([1703444325734390000, "UTC", "%Y-%m-%dT%H:%M:%S"]) + b := time.format([1257894000000000000, "", "%d/%m/%Y %H:%M"]) + query: data.test + want_result: + a: "2023-12-24T18:58:45" + b: "10/11/2009 23:00" + + - note: invalid-type + data: {} + modules: + - | + package test + + a := time.format([1703444325734390000, "UTC", 42]) + query: data.test + error: '`time.format` expects 3rd element of `ns` to be a `string`. Got `42` instead' diff --git a/tests/interpreter/cases/builtins/time/now_ns.yaml b/tests/interpreter/cases/builtins/time/now_ns.yaml new file mode 100644 index 0000000..4b316e6 --- /dev/null +++ b/tests/interpreter/cases/builtins/time/now_ns.yaml @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: now-is-greater-than-2009 + data: {} + modules: + - | + package test + + a := time.now_ns() > 1257894000000000000 + query: data.test + want_result: + a: true + + - note: now-is-cached + data: {} + modules: + - | + package test + + a := res { + first := time.now_ns() + second := time.now_ns() + res := second == first + } + query: data.test + want_result: + a: true diff --git a/tests/interpreter/cases/builtins/time/parse_ns.yaml b/tests/interpreter/cases/builtins/time/parse_ns.yaml new file mode 100644 index 0000000..097178c --- /dev/null +++ b/tests/interpreter/cases/builtins/time/parse_ns.yaml @@ -0,0 +1,40 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: parse + data: {} + modules: + - | + package test + + a := time.parse_ns("%Y-%m-%dT%H:%M:%S", "2006-01-02T15:04:05") + b := time.parse_ns("%Y-%m-%d %H:%M:%S", "2015-09-05 23:56:04") + query: data.test + want_result: + a: 1136214245000000000 + b: 1441497364000000000 + + - note: format-and-parse-back + data: {} + modules: + - | + package test + + a := res { + date := time.format([1703444325734390000, "UTC", "%Y-%m-%dT%H:%M:%S%.f"]) + res := time.parse_ns("%Y-%m-%dT%H:%M:%S%.f", date) + } + query: data.test + want_result: + a: 1703444325734390000 + + - note: invalid-type + data: {} + modules: + - | + package test + + a := time.parse_ns("%Y-%m-%dT%H:%M:%S%.f", 1703444325734390000) + query: data.test + error: '`time.parse_ns` expects string argument. Got `1703444325734390000` instead' diff --git a/tests/interpreter/cases/builtins/time/parse_rfc3339_ns.yaml b/tests/interpreter/cases/builtins/time/parse_rfc3339_ns.yaml new file mode 100644 index 0000000..c96b999 --- /dev/null +++ b/tests/interpreter/cases/builtins/time/parse_rfc3339_ns.yaml @@ -0,0 +1,27 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: parse-rfc3339 + data: {} + modules: + - | + package test + + a := time.parse_rfc3339_ns("1985-04-12T23:20:50.52Z") + b := time.parse_rfc3339_ns("1996-12-19T16:39:57-08:00") + + query: data.test + want_result: + a: 482196050520000000 + b: 851042397000000000 + + - note: invalid-type + data: {} + modules: + - | + package test + + a := time.parse_rfc3339_ns(482196050520000000) + query: data.test + error: '`time.parse_rfc3339_ns` expects string argument. Got `482196050520000000` instead' diff --git a/tests/interpreter/cases/builtins/time/weekday.yaml b/tests/interpreter/cases/builtins/time/weekday.yaml new file mode 100644 index 0000000..ce5821f --- /dev/null +++ b/tests/interpreter/cases/builtins/time/weekday.yaml @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +cases: + - note: without-timezone + data: {} + modules: + - | + package test + + a := time.weekday(1703444325734390000) + b := time.weekday(1257894000000000000) + query: data.test + want_result: + a: "Sunday" + b: "Tuesday" + + - note: with-timezone + data: {} + modules: + - | + package test + + a := time.weekday([1703444325734390000, "UTC"]) + b := time.weekday([1257894000000000000, ""]) + query: data.test + want_result: + a: "Sunday" + b: "Tuesday" + + - note: invalid-type + data: {} + modules: + - | + package test + + a := time.weekday("1703444325734390000") + query: data.test + error: '`time.weekday` expects `ns` to be a `number` or `array[number, string]`. Got `"1703444325734390000"` instead'