duplicati/Duplicati/UnitTest/TimeSpanParserTests.cs
Kenneth Skovhede 8fca01f64f Updated NUnit
This closes #4939
2025-06-13 11:48:24 +02:00

291 lines
11 KiB
C#

// Copyright (C) 2025, The Duplicati Team
// https://duplicati.com, hello@duplicati.com
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Globalization;
using Duplicati.Library.Utility;
using NUnit.Framework;
using Assert = NUnit.Framework.Legacy.ClassicAssert;
namespace Duplicati.UnitTest;
public class TimeparserTests : BasicSetupHelper
{
[Category("Utility")]
[TestCase("0s", 0)]
[TestCase("10s", 10000)]
[TestCase("1m", 60000)]
[TestCase("2h", 7200000)]
[TestCase("1D", 86400000)]
[TestCase("1W", 604800000)]
[Category("Utility")]
public void TestParseTimeSpan_EnglishLocale(string input, long expectedMilliseconds)
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("en-US");
var ts = Timeparser.ParseTimeSpan(input);
Assert.AreEqual(expectedMilliseconds, (long)ts.TotalMilliseconds);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Category("Utility")]
[TestCase("fr-CA", "0s", 0)]
[TestCase("fr-CA", "10s", 10000)]
[TestCase("de-DE", "5m", 300000)]
[TestCase("en-GB", "2h", 7200000)]
[TestCase("sv-SE", "1D", 86400000)]
public void TestParseTimeSpan_LocaleVariations(string locale, string input, long expectedMilliseconds)
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo(locale);
var ts = Timeparser.ParseTimeSpan(input);
Assert.AreEqual(expectedMilliseconds, (long)ts.TotalMilliseconds);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Test]
[Category("Utility")]
public void TestParseTimeSpan_InvalidInput_Throws()
{
Assert.Throws<Exception>(() => Timeparser.ParseTimeSpan("xyz"));
}
[Test]
[Category("Utility")]
public void TestParseTimeSpan_EmptyString()
{
var ts = Timeparser.ParseTimeSpan("");
Assert.AreEqual(TimeSpan.Zero, ts);
}
[Test]
[Category("Utility")]
public void TestParseTimeSpan_Now()
{
var now = DateTime.Now;
var parsed = Timeparser.ParseTimeInterval("now", now);
Assert.That((parsed - now).TotalSeconds, Is.LessThan(1));
}
[Test]
[Category("Utility")]
public void TestParseTimeInterval_WithOffsetAndNegate()
{
var offset = new DateTime(2020, 1, 1);
var result = Timeparser.ParseTimeInterval("10s", offset, negate: true);
Assert.AreEqual(offset.AddSeconds(-10), result);
}
[Category("Utility")]
[TestCase("0s", 0)]
[TestCase("15m", 900000)]
[TestCase("1h30m", 5400000)]
public void TestParseTimeInterval_SpecialCases(string input, long expectedMilliseconds)
{
var baseTime = new DateTime(2023, 1, 1, 0, 0, 0);
var result = Timeparser.ParseTimeInterval(input, baseTime);
var diff = (long)(result - baseTime).TotalMilliseconds;
Assert.AreEqual(expectedMilliseconds, diff);
}
[Category("Utility")]
[TestCase("0s", "fr-CA")]
[TestCase("0s", "de-DE")]
public void TestParseTimeInterval_LocaleSensitiveAmbiguity(string input, string locale)
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo(locale);
var baseTime = new DateTime(2023, 1, 1);
var result = Timeparser.ParseTimeInterval(input, baseTime);
Assert.AreEqual(baseTime, result); // 0s should not change the time
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Category("Utility")]
[TestCase("en-US")]
[TestCase("fr-CA")]
[TestCase("de-DE")]
public void DSTAware_AddHour_SpringForward_ShouldSkipMissingHour(string culture)
{
var originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo(culture);
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var start = new DateTime(2024, 3, 10, 6, 30, 0, DateTimeKind.Utc); // UTC 1:30 AM EST
var result = Timeparser.DSTAwareParseTimeInterval("1h", start, tz);
var expected = new DateTime(2024, 3, 10, 7, 30, 0, DateTimeKind.Utc); // UTC 3:30 AM EDT
Assert.AreEqual(expected, result);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Category("Utility")]
[TestCase("en-US")]
[TestCase("fr-CA")]
[TestCase("de-DE")]
public void DSTAware_AddHour_FallBack_ShouldHandleAmbiguousTime(string culture)
{
var originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo(culture);
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var start = new DateTime(2024, 11, 3, 5, 30, 0, DateTimeKind.Utc); // 1:30 AM EDT
var result = Timeparser.DSTAwareParseTimeInterval("1h", start, tz);
var expected = new DateTime(2024, 11, 3, 6, 30, 0, DateTimeKind.Utc); // 1:30 AM EST
Assert.AreEqual(expected, result);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Category("Utility")]
[TestCase("en-US")]
[TestCase("fr-CA")]
[TestCase("de-DE")]
public void DSTAware_AddMultipleUnits_Combined(string culture)
{
var originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo(culture);
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var start = new DateTime(2024, 3, 10, 4, 0, 0, DateTimeKind.Utc); // March 9, 11 PM EST
var result = Timeparser.DSTAwareParseTimeInterval("1D2h", start, tz);
var expected = new DateTime(2024, 3, 11, 6, 0, 0, DateTimeKind.Utc); // March 11, 1 AM EDT
Assert.AreEqual(expected, result);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Category("Utility")]
[TestCase("en-US")]
[TestCase("fr-CA")]
[TestCase("de-DE")]
public void DSTAware_KeepTime_SpringForward_ShouldKeepClockTime(string culture)
{
var originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo(culture);
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var start = new DateTime(2024, 3, 9, 6, 30, 0, DateTimeKind.Utc); // March 9, 1:30 AM EST
var result = Timeparser.DSTAwareParseTimeInterval("1D", start, tz);
var expected = new DateTime(2024, 3, 10, 6, 30, 0, DateTimeKind.Utc); // March 10, 1:30 AM EDT
Assert.AreEqual(expected, result);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Category("Utility")]
[TestCase("en-US")]
[TestCase("fr-CA")]
[TestCase("de-DE")]
public void DSTAware_KeepTime_FallBack_ShouldKeepClockTime(string culture)
{
var originalCulture = CultureInfo.CurrentCulture;
CultureInfo.CurrentCulture = new CultureInfo(culture);
try
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var start = new DateTime(2024, 11, 2, 5, 30, 0, DateTimeKind.Utc); // Nov 2, 1:30 AM EDT
var result = Timeparser.DSTAwareParseTimeInterval("1D", start, tz);
var expected = new DateTime(2024, 11, 3, 5, 30, 0, DateTimeKind.Utc); // Nov 3, 1:30 AM EST
Assert.AreEqual(expected, result);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
[Test]
public void SpringForward_Add1Hour_ShouldNeverReturnNonexistentLocalTime()
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var rawStart = new DateTime(2024, 3, 10, 6, 30, 0, DateTimeKind.Utc); // 1:30 AM EST (UTC)
var resultUtc = Timeparser.DSTAwareParseTimeInterval("1h", rawStart, tz);
// Convert the result back to local time in the given time zone
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(resultUtc, tz);
// Verify the returned local time is not within the invalid DST range
Assert.IsFalse(tz.IsInvalidTime(resultLocal), $"Returned a non-existent local time: {resultLocal}");
}
[Test]
public void MultipleUnits_SpringForward_ShouldAvoidInvalidLocalTime()
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var rawStart = new DateTime(2024, 3, 10, 4, 30, 0, DateTimeKind.Utc); // 11:30 PM March 9 EST (UTC)
var resultUtc = Timeparser.DSTAwareParseTimeInterval("1D2h", rawStart, tz);
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(resultUtc, tz);
Assert.IsFalse(tz.IsInvalidTime(resultLocal), $"Returned a non-existent local time: {resultLocal}");
}
[Test]
public void AmbiguousTime_FallBack_ShouldReturnValidLocalTime()
{
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
var rawStart = new DateTime(2024, 11, 3, 5, 30, 0, DateTimeKind.Utc); // 1:30 AM EDT (UTC)
var resultUtc = Timeparser.DSTAwareParseTimeInterval("1h", rawStart, tz);
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(resultUtc, tz);
// This will be an ambiguous time but still valid
Assert.IsTrue(tz.IsAmbiguousTime(resultLocal) || !tz.IsInvalidTime(resultLocal),
$"Returned an invalid or ambiguous local time incorrectly: {resultLocal}");
}
}