mirror of https://github.com/dapr/dotnet-sdk.git
Actor reminder deserialization bugfix (#1483)
Signed-off-by: Whit Waldo <whit.waldo@innovian.net>
This commit is contained in:
parent
bb47132f98
commit
c14fcea0d4
|
@ -0,0 +1,124 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
internal static class DurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to parse the duration string accompanying an @every expression.
|
||||
/// </summary>
|
||||
private static readonly Regex durationRegex = new(@"(?<value>\d+(\.\d+)?)(?<unit>ns|us|µs|ms|s|m|h)", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
/// <summary>
|
||||
/// A regular expression used to evaluate whether a given prefix period embodies an @every statement.
|
||||
/// </summary>
|
||||
private static readonly Regex isEveryExpression = new(@"^@every (\d+(\.\d+)?(ns|us|µs|ms|s|m|h))+$");
|
||||
/// <summary>
|
||||
/// The various acceptable duration values for a period expression.
|
||||
/// </summary>
|
||||
private static readonly string[] acceptablePeriodValues =
|
||||
{
|
||||
"yearly", "monthly", "weekly", "daily", "midnight", "hourly"
|
||||
};
|
||||
|
||||
private const string YearlyPrefixPeriod = "@yearly";
|
||||
private const string MonthlyPrefixPeriod = "@monthly";
|
||||
private const string WeeklyPrefixPeriod = "@weekly";
|
||||
private const string DailyPrefixPeriod = "@daily";
|
||||
private const string MidnightPrefixPeriod = "@midnight";
|
||||
private const string HourlyPrefixPeriod = "@hourly";
|
||||
private const string EveryPrefixPeriod = "@every";
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the schedule represents a prefixed period expression.
|
||||
/// </summary>
|
||||
/// <param name="expression"></param>
|
||||
/// <returns></returns>
|
||||
public static bool IsDurationExpression(this string expression) => expression.StartsWith('@') &&
|
||||
(isEveryExpression.IsMatch(expression) ||
|
||||
expression.EndsWithAny(acceptablePeriodValues, StringComparison.InvariantCulture));
|
||||
|
||||
/// <summary>
|
||||
/// Creates a TimeSpan value from the prefixed period value.
|
||||
/// </summary>
|
||||
/// <param name="period">The prefixed period value to parse.</param>
|
||||
/// <returns>A TimeSpan value matching the provided period.</returns>
|
||||
public static TimeSpan FromPrefixedPeriod(this string period)
|
||||
{
|
||||
if (period.StartsWith(YearlyPrefixPeriod))
|
||||
{
|
||||
var dateTime = DateTime.UtcNow;
|
||||
return dateTime.AddYears(1) - dateTime;
|
||||
}
|
||||
|
||||
if (period.StartsWith(MonthlyPrefixPeriod))
|
||||
{
|
||||
var dateTime = DateTime.UtcNow;
|
||||
return dateTime.AddMonths(1) - dateTime;
|
||||
}
|
||||
|
||||
if (period.StartsWith(MidnightPrefixPeriod))
|
||||
{
|
||||
return new TimeSpan();
|
||||
}
|
||||
|
||||
if (period.StartsWith(WeeklyPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromDays(7);
|
||||
}
|
||||
|
||||
if (period.StartsWith(DailyPrefixPeriod) || period.StartsWith(MidnightPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromDays(1);
|
||||
}
|
||||
|
||||
if (period.StartsWith(HourlyPrefixPeriod))
|
||||
{
|
||||
return TimeSpan.FromHours(1);
|
||||
}
|
||||
|
||||
if (period.StartsWith(EveryPrefixPeriod))
|
||||
{
|
||||
//A sequence of decimal numbers each with an optional fraction and unit suffix
|
||||
//Valid time units are: 'ns', 'us'/'µs', 'ms', 's', 'm', and 'h'
|
||||
double totalMilliseconds = 0;
|
||||
var durationString = period.Split(' ').Last().Trim();
|
||||
|
||||
foreach (Match match in durationRegex.Matches(durationString))
|
||||
{
|
||||
var value = double.Parse(match.Groups["value"].Value, CultureInfo.InvariantCulture);
|
||||
var unit = match.Groups["unit"].Value.ToLower();
|
||||
|
||||
totalMilliseconds += unit switch
|
||||
{
|
||||
"ns" => value / 1_000_000,
|
||||
"us" or "µs" => value / 1_000,
|
||||
"ms" => value,
|
||||
"s" => value * 1_000,
|
||||
"m" => value * 1_000 * 60,
|
||||
"h" => value * 1_000 * 60 * 60,
|
||||
_ => throw new ArgumentException($"Unknown duration unit: {unit}")
|
||||
};
|
||||
}
|
||||
|
||||
return TimeSpan.FromMilliseconds(totalMilliseconds);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Unknown prefix period expression: {period}");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
internal static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension method that validates a string against a list of possible matches.
|
||||
/// </summary>
|
||||
/// <param name="value">The string value to evaluate.</param>
|
||||
/// <param name="possibleValues">The possible values to look for a match within.</param>
|
||||
/// <param name="comparisonType">The type of string comparison to perform.</param>
|
||||
/// <returns>True if the value ends with any of the possible values; otherwise false.</returns>
|
||||
public static bool EndsWithAny(this string value, IReadOnlyList<string> possibleValues, StringComparison comparisonType )
|
||||
=> possibleValues.Any(val => value.EndsWith(val, comparisonType));
|
||||
}
|
|
@ -11,138 +11,148 @@
|
|||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
namespace Dapr.Actors.Runtime
|
||||
using Dapr.Actors.Extensions;
|
||||
|
||||
namespace Dapr.Actors.Runtime;
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
internal static class ConverterUtils
|
||||
{
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
internal class ConverterUtils
|
||||
private static Regex regex = new("^(R(?<repetition>\\d+)/)?P((?<year>\\d+)Y)?((?<month>\\d+)M)?((?<week>\\d+)W)?((?<day>\\d+)D)?(T((?<hour>\\d+)H)?((?<minute>\\d+)M)?((?<second>\\d+)S)?)?$", RegexOptions.Compiled);
|
||||
public static TimeSpan ConvertTimeSpanFromDaprFormat(string valueString)
|
||||
{
|
||||
private static Regex regex = new Regex("^(R(?<repetition>\\d+)/)?P((?<year>\\d+)Y)?((?<month>\\d+)M)?((?<week>\\d+)W)?((?<day>\\d+)D)?(T((?<hour>\\d+)H)?((?<minute>\\d+)M)?((?<second>\\d+)S)?)?$", RegexOptions.Compiled);
|
||||
public static TimeSpan ConvertTimeSpanFromDaprFormat(string valueString)
|
||||
if (string.IsNullOrEmpty(valueString))
|
||||
{
|
||||
if (string.IsNullOrEmpty(valueString))
|
||||
{
|
||||
var never = TimeSpan.FromMilliseconds(-1);
|
||||
return never;
|
||||
}
|
||||
|
||||
// TimeSpan is a string. Format returned by Dapr is: 1h4m5s4ms4us4ns
|
||||
// acceptable values are: m, s, ms, us(micro), ns
|
||||
var spanOfValue = valueString.AsSpan();
|
||||
|
||||
// Change the value returned by Dapr runtime, so that it can be parsed with TimeSpan.
|
||||
// Format returned by Dapr runtime: 4h15m50s60ms. It doesnt have days.
|
||||
// Dapr runtime should handle timespans in ISO 8601 format.
|
||||
// Replace ms before m & s. Also append 0 days for parsing correctly with TimeSpan
|
||||
int hIndex = spanOfValue.IndexOf('h');
|
||||
int mIndex = spanOfValue.IndexOf('m');
|
||||
int sIndex = spanOfValue.IndexOf('s');
|
||||
int msIndex = spanOfValue.IndexOf("ms");
|
||||
|
||||
// handle days from hours.
|
||||
var hoursSpan = spanOfValue.Slice(0, hIndex);
|
||||
var hours = int.Parse(hoursSpan);
|
||||
var days = hours / 24;
|
||||
hours %= 24;
|
||||
|
||||
var minutesSpan = spanOfValue[(hIndex + 1)..mIndex];
|
||||
var minutes = int.Parse(minutesSpan);
|
||||
|
||||
var secondsSpan = spanOfValue[(mIndex + 1)..sIndex];
|
||||
var seconds = int.Parse(secondsSpan);
|
||||
|
||||
var millisecondsSpan = spanOfValue[(sIndex + 1)..msIndex];
|
||||
var milliseconds = int.Parse(millisecondsSpan);
|
||||
|
||||
return new TimeSpan(days, hours, minutes, seconds, milliseconds);
|
||||
var never = TimeSpan.FromMilliseconds(-1);
|
||||
return never;
|
||||
}
|
||||
|
||||
if (valueString.IsDurationExpression())
|
||||
{
|
||||
return valueString.FromPrefixedPeriod();
|
||||
}
|
||||
|
||||
public static string ConvertTimeSpanValueInDaprFormat(TimeSpan? value)
|
||||
{
|
||||
// write in format expected by Dapr, it only accepts h, m, s, ms, us(micro), ns
|
||||
var stringValue = string.Empty;
|
||||
if (value.Value >= TimeSpan.Zero)
|
||||
{
|
||||
var hours = (value.Value.Days * 24) + value.Value.Hours;
|
||||
stringValue = FormattableString.Invariant($"{hours}h{value.Value.Minutes}m{value.Value.Seconds}s{value.Value.Milliseconds}ms");
|
||||
}
|
||||
// TimeSpan is a string. Format returned by Dapr is: 1h4m5s4ms4us4ns
|
||||
// acceptable values are: m, s, ms, us(micro), ns
|
||||
var spanOfValue = valueString.AsSpan();
|
||||
|
||||
// Change the value returned by Dapr runtime, so that it can be parsed with TimeSpan.
|
||||
// Format returned by Dapr runtime: 4h15m50s60ms. It doesnt have days.
|
||||
// Dapr runtime should handle timespans in ISO 8601 format.
|
||||
// Replace ms before m & s. Also append 0 days for parsing correctly with TimeSpan
|
||||
int hIndex = spanOfValue.IndexOf('h');
|
||||
int mIndex = spanOfValue.IndexOf('m');
|
||||
int sIndex = spanOfValue.IndexOf('s');
|
||||
int msIndex = spanOfValue.IndexOf("ms");
|
||||
|
||||
// handle days from hours.
|
||||
var hoursSpan = spanOfValue.Slice(0, hIndex);
|
||||
var hours = int.Parse(hoursSpan);
|
||||
var days = hours / 24;
|
||||
hours %= 24;
|
||||
|
||||
var minutesSpan = spanOfValue[(hIndex + 1)..mIndex];
|
||||
var minutes = int.Parse(minutesSpan);
|
||||
|
||||
var secondsSpan = spanOfValue[(mIndex + 1)..sIndex];
|
||||
var seconds = int.Parse(secondsSpan);
|
||||
|
||||
var millisecondsSpan = spanOfValue[(sIndex + 1)..msIndex];
|
||||
var milliseconds = int.Parse(millisecondsSpan);
|
||||
|
||||
return new TimeSpan(days, hours, minutes, seconds, milliseconds);
|
||||
}
|
||||
|
||||
public static string ConvertTimeSpanValueInDaprFormat(TimeSpan? value)
|
||||
{
|
||||
// write in format expected by Dapr, it only accepts h, m, s, ms, us(micro), ns
|
||||
var stringValue = string.Empty;
|
||||
if (value is null)
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
public static string ConvertTimeSpanValueInISO8601Format(TimeSpan value, int? repetitions)
|
||||
|
||||
if (value.Value >= TimeSpan.Zero)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (repetitions == null)
|
||||
{
|
||||
return ConvertTimeSpanValueInDaprFormat(value);
|
||||
}
|
||||
|
||||
if (value.Milliseconds > 0)
|
||||
{
|
||||
throw new ArgumentException("The TimeSpan value, combined with repetition cannot be in milliseconds.", nameof(value));
|
||||
}
|
||||
|
||||
builder.AppendFormat("R{0}/P", repetitions);
|
||||
|
||||
if(value.Days > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}D", value.Days);
|
||||
}
|
||||
|
||||
builder.Append("T");
|
||||
|
||||
if(value.Hours > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}H", value.Hours);
|
||||
}
|
||||
|
||||
if(value.Minutes > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}M", value.Minutes);
|
||||
}
|
||||
|
||||
if(value.Seconds > 0)
|
||||
{
|
||||
builder.AppendFormat("{0}S", value.Seconds);
|
||||
}
|
||||
return builder.ToString();
|
||||
var hours = (value.Value.Days * 24) + value.Value.Hours;
|
||||
stringValue = FormattableString.Invariant($"{hours}h{value.Value.Minutes}m{value.Value.Seconds}s{value.Value.Milliseconds}ms");
|
||||
}
|
||||
|
||||
return stringValue;
|
||||
}
|
||||
|
||||
public static (TimeSpan, int?) ConvertTimeSpanValueFromISO8601Format(string valueString)
|
||||
public static string ConvertTimeSpanValueInISO8601Format(TimeSpan value, int? repetitions)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
if (repetitions == null)
|
||||
{
|
||||
// ISO 8601 format can be Rn/PaYbMcHTdHeMfS or PaYbMcHTdHeMfS so if it does
|
||||
// not start with R or P then assuming it to default Dapr format without repetition
|
||||
if (!(valueString.StartsWith('R') || valueString.StartsWith('P')))
|
||||
{
|
||||
return (ConvertTimeSpanFromDaprFormat(valueString), -1);
|
||||
}
|
||||
|
||||
var matches = regex.Match(valueString);
|
||||
|
||||
var repetition = matches.Groups["repetition"].Success ? int.Parse(matches.Groups["repetition"].Value) : (int?)null;
|
||||
|
||||
var days = 0;
|
||||
var year = matches.Groups["year"].Success ? int.Parse(matches.Groups["year"].Value) : 0;
|
||||
days = year * 365;
|
||||
|
||||
var month = matches.Groups["month"].Success ? int.Parse(matches.Groups["month"].Value) : 0;
|
||||
days += month * 30;
|
||||
|
||||
var week = matches.Groups["week"].Success ? int.Parse(matches.Groups["week"].Value) : 0;
|
||||
days += week * 7;
|
||||
|
||||
var day = matches.Groups["day"].Success ? int.Parse(matches.Groups["day"].Value) : 0;
|
||||
days += day;
|
||||
|
||||
var hour = matches.Groups["hour"].Success ? int.Parse(matches.Groups["hour"].Value) : 0;
|
||||
var minute = matches.Groups["minute"].Success ? int.Parse(matches.Groups["minute"].Value) : 0;
|
||||
var second = matches.Groups["second"].Success ? int.Parse(matches.Groups["second"].Value) : 0;
|
||||
|
||||
return (new TimeSpan(days, hour, minute, second), repetition);
|
||||
return ConvertTimeSpanValueInDaprFormat(value);
|
||||
}
|
||||
|
||||
if (value.Milliseconds > 0)
|
||||
{
|
||||
throw new ArgumentException("The TimeSpan value, combined with repetition cannot be in milliseconds.", nameof(value));
|
||||
}
|
||||
|
||||
builder.Append($"R{repetitions}/P");
|
||||
|
||||
if(value.Days > 0)
|
||||
{
|
||||
builder.Append($"{value.Days}D");
|
||||
}
|
||||
|
||||
builder.Append("T");
|
||||
|
||||
if(value.Hours > 0)
|
||||
{
|
||||
builder.Append($"{value.Hours}H");
|
||||
}
|
||||
|
||||
if(value.Minutes > 0)
|
||||
{
|
||||
builder.Append($"{value.Minutes}M");
|
||||
}
|
||||
|
||||
if(value.Seconds > 0)
|
||||
{
|
||||
builder.Append($"{value.Seconds}S");
|
||||
}
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
public static (TimeSpan, int?) ConvertTimeSpanValueFromISO8601Format(string valueString)
|
||||
{
|
||||
// ISO 8601 format can be Rn/PaYbMcHTdHeMfS or PaYbMcHTdHeMfS so if it does
|
||||
// not start with R or P then assuming it to default Dapr format without repetition
|
||||
if (!(valueString.StartsWith('R') || valueString.StartsWith('P')))
|
||||
{
|
||||
return (ConvertTimeSpanFromDaprFormat(valueString), -1);
|
||||
}
|
||||
|
||||
var matches = regex.Match(valueString);
|
||||
|
||||
var repetition = matches.Groups["repetition"].Success ? int.Parse(matches.Groups["repetition"].Value) : (int?)null;
|
||||
|
||||
|
||||
var days = 0;
|
||||
var year = matches.Groups["year"].Success ? int.Parse(matches.Groups["year"].Value) : 0;
|
||||
days = year * 365;
|
||||
|
||||
var month = matches.Groups["month"].Success ? int.Parse(matches.Groups["month"].Value) : 0;
|
||||
days += month * 30;
|
||||
|
||||
var week = matches.Groups["week"].Success ? int.Parse(matches.Groups["week"].Value) : 0;
|
||||
days += week * 7;
|
||||
|
||||
var day = matches.Groups["day"].Success ? int.Parse(matches.Groups["day"].Value) : 0;
|
||||
days += day;
|
||||
|
||||
var hour = matches.Groups["hour"].Success ? int.Parse(matches.Groups["hour"].Value) : 0;
|
||||
var minute = matches.Groups["minute"].Success ? int.Parse(matches.Groups["minute"].Value) : 0;
|
||||
var second = matches.Groups["second"].Success ? int.Parse(matches.Groups["second"].Value) : 0;
|
||||
|
||||
return (new TimeSpan(days, hour, minute, second), repetition);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
// ------------------------------------------------------------------------
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Dapr.Actors.Runtime;
|
||||
|
||||
using System;
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using Dapr.Actors.Runtime;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors;
|
||||
|
||||
public class ConverterUtilsTests
|
||||
{
|
||||
[Fact]
|
||||
public void Deserialize_Period_Duration1()
|
||||
{
|
||||
var result = ConverterUtils.ConvertTimeSpanValueFromISO8601Format("@every 15m");
|
||||
Assert.Equal(TimeSpan.FromMinutes(15), result.Item1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Deserialize_Period_Duration2()
|
||||
{
|
||||
var result = ConverterUtils.ConvertTimeSpanValueFromISO8601Format("@hourly");
|
||||
Assert.Equal(TimeSpan.FromHours(1), result.Item1);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
public sealed class DurationExtensionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("@yearly", 364, 0, 0, 0, 0)]
|
||||
[InlineData("@monthly", 28, 0, 0, 0, 0 )]
|
||||
[InlineData("@weekly", 7, 0, 0, 0, 0 )]
|
||||
[InlineData("@daily", 1, 0, 0, 0, 0)]
|
||||
[InlineData("@midnight", 0, 0, 0, 0, 0 )]
|
||||
[InlineData("@hourly", 0, 1, 0, 0, 0)]
|
||||
[InlineData("@every 1h", 0, 1, 0, 0, 0)]
|
||||
[InlineData("@every 30m", 0, 0, 30, 0, 0)]
|
||||
[InlineData("@every 45s", 0, 0, 0, 45, 0)]
|
||||
[InlineData("@every 1.5h", 0, 1, 30, 0, 0)]
|
||||
[InlineData("@every 1h30m", 0, 1, 30, 0, 0)]
|
||||
[InlineData("@every 1h30m45s", 0, 1, 30, 45, 0)]
|
||||
[InlineData("@every 1h30m45.3s", 0, 1, 30, 45, 300)]
|
||||
[InlineData("@every 100ms", 0, 0, 0, 0, 100)]
|
||||
[InlineData("@every 1s500ms", 0, 0, 0, 1, 500)]
|
||||
[InlineData("@every 1m1s", 0, 0, 1, 1, 0)]
|
||||
[InlineData("@every 1.1m", 0, 0, 1, 6, 0)]
|
||||
[InlineData("@every 1.5h30m45s100ms", 0, 2, 0, 45, 100)]
|
||||
public void ValidatePrefixedPeriodParsing(string input, int expectedDays, int expectedHours, int expectedMinutes, int expectedSeconds, int expectedMilliseconds)
|
||||
{
|
||||
var result = input.FromPrefixedPeriod();
|
||||
|
||||
if (input is "@yearly" or "@monthly")
|
||||
{
|
||||
Assert.True(result.Days >= expectedDays);
|
||||
return;
|
||||
}
|
||||
|
||||
Assert.Equal(expectedDays, result.Days);
|
||||
Assert.Equal(expectedHours, result.Hours);
|
||||
Assert.Equal(expectedMinutes, result.Minutes);
|
||||
Assert.Equal(expectedSeconds, result.Seconds);
|
||||
Assert.Equal(expectedMilliseconds, result.Milliseconds);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("@yearly", true)]
|
||||
[InlineData("@monthly", true)]
|
||||
[InlineData("@weekly", true)]
|
||||
[InlineData("@daily", true)]
|
||||
[InlineData("@midnight", true)]
|
||||
[InlineData("@hourly", true)]
|
||||
[InlineData("@every 1h", true)]
|
||||
[InlineData("@every 30m", true)]
|
||||
[InlineData("@every 45s", true)]
|
||||
[InlineData("@every 1.5h", true)]
|
||||
[InlineData("@every 1h30m", true)]
|
||||
[InlineData("@every 1h30m45s", true)]
|
||||
[InlineData("@every 1h30m45.3s", true)]
|
||||
[InlineData("@every 100ms", true)]
|
||||
[InlineData("@every 1s500ms", true)]
|
||||
[InlineData("@every 1m1s", true)]
|
||||
[InlineData("@every 1.1m", true)]
|
||||
[InlineData("@every 1.5h30m45s100ms", true)]
|
||||
public void TestIsDurationExpression(string input, bool expectedResult)
|
||||
{
|
||||
var actualResult = input.IsDurationExpression();
|
||||
Assert.Equal(expectedResult, actualResult);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateExceptionForUnknownExpression()
|
||||
{
|
||||
Assert.Throws<ArgumentException>(() =>
|
||||
{
|
||||
var result = "every 100s".FromPrefixedPeriod();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
// ------------------------------------------------------------------------
|
||||
// Copyright 2025 The Dapr Authors
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Xunit;
|
||||
|
||||
namespace Dapr.Actors.Extensions;
|
||||
|
||||
public sealed class StringExtensionsTests
|
||||
{
|
||||
[Fact]
|
||||
public void ValidateMatchesValue()
|
||||
{
|
||||
var matchingValues = new List<string> { "apples", "bananas", "cherries", };
|
||||
const string value = "I have four cherries";
|
||||
|
||||
var result = value.EndsWithAny(matchingValues, StringComparison.InvariantCulture);
|
||||
Assert.True(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ValidateDoesNotMatchValue()
|
||||
{
|
||||
var matchingValues = new List<string> { "apples", "bananas", "cherries", };
|
||||
const string value = "I have four grapes";
|
||||
|
||||
var result = value.EndsWithAny(matchingValues, StringComparison.InvariantCulture);
|
||||
Assert.False(result);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue