Files
regorus/tests/azure_policy_builtins/cases/date_time_add.yaml
T
Anand Krishnamoorthi 5b60daabd9 feat: add Azure Policy builtins with YAML test suite (#630)
* feat: add Azure Policy builtins with YAML test suite

Implement ARM template functions for Azure Policy evaluation:

Builtins:
- String: indexOf, lastIndexOf, trim, format, split, startsWith, endsWith,
  padLeft, concat, replace, toLower, toUpper, substring, guid, uniqueString
- DateTime: dateTimeAdd, dateTimeFromEpoch, dateTimeToEpoch, addDays
- Collection: intersection, union, take, skip, first, last, min, max,
  range, items, tryGet, tryIndexFromEnd, empty, array, createObject
- Encoding: base64, base64ToString, base64ToJson, uri, uriComponent,
  uriComponentToString, dataUri, dataUriToString
- Numeric: int, float, intDiv, intMod
- Misc: json, join, bool, string, coalesce, if, getParameter, resolveField
- Logic: logicAll, logicAny

Key implementation details:
- Unicode case-insensitive search via ICU4X case folding with single-pass
  fold_with_char_map() for indexOf/lastIndexOf
- .NET composite formatting (System.String.Format) with alignment, standard
  and custom datetime format specifiers, numeric format specifiers
- DateTime round-trip preserves input shape (Z vs +00:00, T vs space,
  fractional seconds) when no explicit output format is supplied
- Zero-cost as_str() helper borrows directly from Value::String(Rc<str>)
- BTreeSet<&Value> in array union avoids redundant cloning

Test suite:
- 53 YAML test files exercising all builtins via direct BUILTINS registry
- Coverage for edge cases: empty inputs, Unicode, fractional seconds,
  invalid alignment, unknown format specifiers, RFC3339 offset shapes

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address PR review comments

- Fix percent_encode to only uppercase hex digits, not entire string
- Remove guid/uniqueString (unsupported); delete custom SHA-1 impl
- Replace unwrap_or(0) with proper error in format placeholder parsing
- Hoist CaseMapper into static CaseMapperBorrowed for zero per-call overhead
- Pre-allocate Vec in range() with_capacity
- Update bindings/ffi and bindings/ruby Cargo.lock
- Fix uri_component test expectations for correct case preservation

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address second round of PR review comments

- float(): return Undefined when as_f64() fails instead of leaking
  the original non-f64 representation
- createObject(): reject odd number of arguments with an error
  (ARM-template parity)
- format(): error on unknown numeric format specifiers instead of
  silently passing through (matches .NET FormatException behavior)
- format(): cap alignment width at 10,000 to prevent DoS from
  user-controlled format strings like {0,1000000000}
- Add YAML test cases for all new error behaviors

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address third round of PR review comments

- percent_decode: reject incomplete % escapes (e.g. "%", "%2") instead
  of treating them as literal characters
- parse_iso8601_duration: reject leftover digits without a unit designator
  at T boundary and end-of-input (e.g. "P1", "P1T2H")
- yaml_to_value: panic on unsupported YAML numeric representations instead
  of silently mapping to Null
- Revert unused src/languages/mod.rs changes (module is defined inline in
  lib.rs)

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: add missing edge-case tests and fix empty-delimiter panic

- fn_split: return input as single-element array for empty string
  delimiter instead of panicking (Rust's str::split("") panics)
- format: add test for F3 higher precision ({0:F3} + 1.23456 → 1.235)
- format: add test for N2 float with thousands separator
- format: add test for negative index error ({-1})
- split: add test for empty-string delimiter
- uri: add tests for query string and fragment in relative URI
- createObject: add test for non-string (numeric) keys

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

* fix: address fourth round of PR review comments

- Add MAX_VARIADIC_ARGS (64) constant for variadic builtin arity
  instead of registering with 0 (logic_all, logic_any, min, max,
  format, intersection, union, coalesce, createObject); set
  dateTimeAdd to exact arity 3

- Switch indexOf/lastIndexOf to UTF-16 code-unit indices to match
  .NET String.IndexOf semantics (track ch.len_utf16() in
  fold_with_char_map, use encode_utf16().count() for empty-needle
  lastIndexOf)

- Use DateTime::<Utc>::from_timestamp for explicit timezone type

- Remove stale docs/azure-policy/casing.md link from module doc

- Fix misleading comment in want_error test branch (code bails on
  Undefined, not accepts it)

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>

---------

Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
2026-03-25 17:32:39 -05:00

162 lines
4.8 KiB
YAML

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Tests for azure.policy.fn.date_time_add
builtin: azure.policy.fn.date_time_add
cases:
- note: add_one_day
args: ["2024-01-15T12:00:00Z", "P1D"]
want: "2024-01-16T12:00:00Z"
- note: add_one_hour
args: ["2024-01-15T12:00:00Z", "PT1H"]
want: "2024-01-15T13:00:00Z"
- note: add_thirty_minutes
args: ["2024-01-15T12:00:00Z", "PT30M"]
want: "2024-01-15T12:30:00Z"
- note: add_negative_one_day
args: ["2024-01-15T12:00:00Z", "-P1D"]
want: "2024-01-14T12:00:00Z"
- note: add_complex_duration
args: ["2024-01-15T12:00:00Z", "P1DT2H30M"]
want: "2024-01-16T14:30:00Z"
- note: add_one_year_approx
args: ["2024-01-01T00:00:00Z", "P1Y"]
want: "2024-12-31T00:00:00Z"
- note: add_zero_duration
args: ["2024-06-15T10:30:00Z", "PT0S"]
want: "2024-06-15T10:30:00Z"
- note: invalid_base_datetime
args: ["not-a-date", "P1D"]
want_undefined: true
- note: invalid_duration
args: ["2024-01-15T12:00:00Z", "invalid"]
want_undefined: true
- note: datetime_without_timezone
args: ["2024-01-15T12:00:00", "P1D"]
want: "2024-01-16T12:00:00"
- note: add_one_week
args: ["2024-01-15T00:00:00Z", "P1W"]
want: "2024-01-22T00:00:00Z"
- note: space_separated_datetime
args: ["2020-04-07 14:55:59", "P3D"]
want: "2020-04-10 14:55:59"
- note: space_separated_datetime_with_z
args: ["2020-04-07 14:55:59Z", "P3D"]
want: "2020-04-10 14:55:59Z"
- note: space_separated_datetime_with_offset
args: ["2020-04-07 14:55:59+05:30", "P1D"]
want: "2020-04-08 14:55:59+05:30"
- note: output_format_date_only
args: ["2020-04-07T14:55:59Z", "P3Y2M", "yyyy-MM-dd"]
want: "2023-06-06"
- note: output_format_custom
args: ["2024-01-15T12:00:00Z", "PT1H", "yyyy-MM-dd HH:mm:ss"]
want: "2024-01-15 13:00:00"
- note: output_format_with_k_utc
args: ["2024-01-15T12:00:00Z", "P0D", "yyyy-MM-ddTHH:mm:ssK"]
want: "2024-01-15T12:00:00Z"
# Standard .NET format specifiers
- note: output_standard_format_d
args: ["2024-01-15T12:30:00Z", "P1D", "d"]
want: "01/16/2024"
- note: output_standard_format_G
args: ["2024-01-15T12:30:00Z", "PT1H", "G"]
want: "01/15/2024 13:30:00"
- note: output_standard_format_o
args: ["2024-01-15T12:00:00Z", "P0D", "o"]
want: "2024-01-15T12:00:00.0000000Z"
- note: output_standard_format_o_with_nanos
args: ["2024-01-15T12:00:00.123456700Z", "P0D", "o"]
want: "2024-01-15T12:00:00.1234567Z"
- note: output_standard_format_u
args: ["2024-01-15T12:00:00Z", "P0D", "u"]
want: "2024-01-15 12:00:00Z"
- note: output_standard_format_u_with_offset
args: ["2024-01-15T12:00:00+05:30", "P0D", "u"]
want: "2024-01-15 06:30:00Z"
# Fractional seconds preservation (default round-trip, no explicit format)
- note: roundtrip_iso_no_tz_with_frac
args: ["2024-01-15T12:00:00.123", "P0D"]
want: "2024-01-15T12:00:00.123"
- note: roundtrip_iso_no_tz_with_frac_add
args: ["2024-01-15T12:00:00.500", "P1D"]
want: "2024-01-16T12:00:00.500"
- note: roundtrip_space_no_tz_with_frac
args: ["2024-01-15 12:00:00.456", "P0D"]
want: "2024-01-15 12:00:00.456"
- note: roundtrip_space_z_with_frac
args: ["2024-01-15 12:00:00.789Z", "P0D"]
want: "2024-01-15 12:00:00.789Z"
- note: roundtrip_space_offset_with_frac
args: ["2024-01-15 12:00:00.123+05:30", "P0D"]
want: "2024-01-15 12:00:00.123+05:30"
- note: roundtrip_no_frac_stays_clean
args: ["2024-01-15T12:00:00", "P0D"]
want: "2024-01-15T12:00:00"
- note: roundtrip_rfc3339_utc_with_frac
args: ["2024-01-15T12:00:00.123Z", "P0D"]
want: "2024-01-15T12:00:00.123Z"
- note: roundtrip_rfc3339_offset_with_frac
args: ["2024-01-15T12:00:00.456+05:30", "P0D"]
want: "2024-01-15T12:00:00.456+05:30"
- note: roundtrip_rfc3339_utc_no_frac_stays_clean
args: ["2024-01-15T12:00:00Z", "P0D"]
want: "2024-01-15T12:00:00Z"
- note: roundtrip_rfc3339_explicit_zero_offset
args: ["2024-01-15T12:00:00+00:00", "P0D"]
want: "2024-01-15T12:00:00+00:00"
- note: roundtrip_rfc3339_explicit_zero_offset_with_frac
args: ["2024-01-15T12:00:00.500+00:00", "P0D"]
want: "2024-01-15T12:00:00.500+00:00"
# Unknown single-char format specifier is a real error
- note: unknown_format_specifier_q
args: ["2024-01-15T12:00:00Z", "P0D", "q"]
want_error: "unrecognised standard format specifier 'q'"
- note: output_standard_format_U
args: ["2024-01-15T12:00:00+05:30", "P0D", "U"]
want: "Monday, 15 January 2024 06:30:00"
- note: output_standard_format_s
args: ["2024-01-15T12:00:00Z", "P0D", "s"]
want: "2024-01-15T12:00:00"
- note: output_standard_format_R
args: ["2024-01-15T12:00:00+05:30", "P0D", "R"]
want: "Mon, 15 Jan 2024 06:30:00 GMT"