diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems index 8c9d4d651f37d..830a0c676278e 100644 --- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems +++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems @@ -258,12 +258,16 @@ + + + + @@ -1621,6 +1625,7 @@ + @@ -1823,6 +1828,7 @@ + diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.Unix.cs index 2fd3e7368e5f4..e661425450ab7 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Unix.cs @@ -7,14 +7,6 @@ public readonly partial struct DateTime { internal const bool s_systemSupportsLeapSeconds = false; - public static DateTime UtcNow - { - get - { - return new DateTime(((ulong)(Interop.Sys.GetSystemTimeAsTicks() + UnixEpochTicks)) | KindUtc); - } - } - private static DateTime FromFileTimeLeapSecondsAware(ulong fileTime) => default; private static ulong ToFileTimeLeapSecondsAware(long ticks) => default; diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs index 7814a9f5fdbcf..e4ea2dc8c5ab1 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.Windows.cs @@ -1,9 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; namespace System { @@ -11,39 +9,6 @@ public readonly partial struct DateTime { internal static readonly bool s_systemSupportsLeapSeconds = SystemSupportsLeapSeconds(); - public static unsafe DateTime UtcNow - { - get - { - ulong fileTime; - s_pfnGetSystemTimeAsFileTime(&fileTime); - - if (s_systemSupportsLeapSeconds) - { - Interop.Kernel32.SYSTEMTIME time; - ulong hundredNanoSecond; - - if (Interop.Kernel32.FileTimeToSystemTime(&fileTime, &time) != Interop.BOOL.FALSE) - { - // to keep the time precision - ulong tmp = fileTime; // temp. variable avoids double read from memory - hundredNanoSecond = tmp % TicksPerMillisecond; - } - else - { - Interop.Kernel32.GetSystemTime(&time); - hundredNanoSecond = 0; - } - - return CreateDateTimeFromSystemTime(in time, hundredNanoSecond); - } - else - { - return new DateTime(fileTime + FileTimeOffset | KindUtc); - } - } - } - internal static unsafe bool IsValidTimeWithLeapSeconds(int year, int month, int day, int hour, int minute, DateTimeKind kind) { Interop.Kernel32.SYSTEMTIME time; @@ -80,7 +45,9 @@ private static unsafe DateTime FromFileTimeLeapSecondsAware(ulong fileTime) { throw new ArgumentOutOfRangeException(nameof(fileTime), SR.ArgumentOutOfRange_DateTimeBadTicks); } - return CreateDateTimeFromSystemTime(in time, fileTime % TicksPerMillisecond); + + ulong ticks = GetTicksFromSystemTime(in time, fileTime % TicksPerMillisecond); + return new DateTime(ticks | KindUtc); } private static unsafe ulong ToFileTimeLeapSecondsAware(long ticks) @@ -110,7 +77,7 @@ private static unsafe ulong ToFileTimeLeapSecondsAware(long ticks) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static DateTime CreateDateTimeFromSystemTime(in Interop.Kernel32.SYSTEMTIME time, ulong hundredNanoSecond) + internal static ulong GetTicksFromSystemTime(in Interop.Kernel32.SYSTEMTIME time, ulong hundredNanoSecond) { uint year = time.Year; uint[] days = IsLeapYear((int)year) ? s_daysToMonth366 : s_daysToMonth365; @@ -124,52 +91,12 @@ private static DateTime CreateDateTimeFromSystemTime(in Interop.Kernel32.SYSTEMT if (second <= 59) { ulong tmp = second * (uint)TicksPerSecond + time.Milliseconds * (uint)TicksPerMillisecond + hundredNanoSecond; - return new DateTime(ticks + tmp | KindUtc); + return ticks + tmp; } // we have a leap second, force it to last second in the minute as DateTime doesn't account for leap seconds in its calculation. // we use the maxvalue from the milliseconds and the 100-nano seconds to avoid reporting two out of order 59 seconds - ticks += TicksPerMinute - 1 | KindUtc; - return new DateTime(ticks); - } - - private static unsafe readonly delegate* unmanaged[SuppressGCTransition] s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr(); - - private static unsafe delegate* unmanaged[SuppressGCTransition] GetGetSystemTimeAsFileTimeFnPtr() - { - IntPtr kernel32Lib = Interop.Kernel32.LoadLibraryEx(Interop.Libraries.Kernel32, IntPtr.Zero, Interop.Kernel32.LOAD_LIBRARY_SEARCH_SYSTEM32); - Debug.Assert(kernel32Lib != IntPtr.Zero); - - IntPtr pfnGetSystemTime = NativeLibrary.GetExport(kernel32Lib, "GetSystemTimeAsFileTime"); - - if (NativeLibrary.TryGetExport(kernel32Lib, "GetSystemTimePreciseAsFileTime", out IntPtr pfnGetSystemTimePrecise)) - { - // GetSystemTimePreciseAsFileTime exists and we'd like to use it. However, on - // misconfigured systems, it's possible for the "precise" time to be inaccurate: - // https://github.com/dotnet/runtime/issues/9014 - // If it's inaccurate, though, we expect it to be wildly inaccurate, so as a - // workaround/heuristic, we get both the "normal" and "precise" times, and as - // long as they're close, we use the precise one. This workaround can be removed - // when we better understand what's causing the drift and the issue is no longer - // a problem or can be better worked around on all targeted OSes. - - // Retry this check several times to reduce chance of false negatives due to thread being rescheduled - // at wrong time. - for (int i = 0; i < 10; i++) - { - long systemTimeResult, preciseSystemTimeResult; - ((delegate* unmanaged[SuppressGCTransition])pfnGetSystemTime)(&systemTimeResult); - ((delegate* unmanaged[SuppressGCTransition])pfnGetSystemTimePrecise)(&preciseSystemTimeResult); - - if (Math.Abs(preciseSystemTimeResult - systemTimeResult) <= 100 * TicksPerMillisecond) - { - pfnGetSystemTime = pfnGetSystemTimePrecise; // use the precise version - break; - } - } - } - - return (delegate* unmanaged[SuppressGCTransition])pfnGetSystemTime; + return ticks + TicksPerMinute - 1; } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTime.cs b/src/libraries/System.Private.CoreLib/src/System/DateTime.cs index 3afc0e8d7b23c..4d772f3bc0783 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTime.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTime.cs @@ -81,7 +81,7 @@ namespace System private const long MaxMillis = (long)DaysTo10000 * MillisPerDay; internal const long UnixEpochTicks = DaysTo1970 * TicksPerDay; - private const long FileTimeOffset = DaysTo1601 * TicksPerDay; + internal const long FileTimeOffset = DaysTo1601 * TicksPerDay; private const long DoubleDateOffset = DaysTo1899 * TicksPerDay; // The minimum OA date is 0100/01/01 (Note it's year 100). // The maximum OA date is 9999/12/31 @@ -112,7 +112,7 @@ namespace System private const ulong FlagsMask = 0xC000000000000000; private const long TicksCeiling = 0x4000000000000000; private const ulong KindUnspecified = 0x0000000000000000; - private const ulong KindUtc = 0x4000000000000000; + internal const ulong KindUtc = 0x4000000000000000; private const ulong KindLocal = 0x8000000000000000; private const ulong KindLocalAmbiguousDst = 0xC000000000000000; private const int KindShift = 62; @@ -140,7 +140,7 @@ public DateTime(long ticks) _dateData = (ulong)ticks; } - private DateTime(ulong dateData) + internal DateTime(ulong dateData) { this._dateData = dateData; } @@ -743,7 +743,7 @@ public static DateTime FromFileTimeUtc(long fileTime) throw new ArgumentOutOfRangeException(nameof(fileTime), SR.ArgumentOutOfRange_FileTimeInvalid); } -#pragma warning disable 162 // Unrechable code on Unix +#pragma warning disable 162 // Unreachable code on Unix if (s_systemSupportsLeapSeconds) { return FromFileTimeLeapSecondsAware((ulong)fileTime); @@ -1057,6 +1057,8 @@ public static DateTime Now // public static DateTime Today => Now.Date; + public static DateTime UtcNow => TimeContext.Current.Clock.GetCurrentUtcDateTime(); + // Returns the year part of this DateTime. The returned value is an // integer between 1 and 9999. // @@ -1203,7 +1205,7 @@ public long ToFileTimeUtc() // Treats the input as universal if it is not specified long ticks = ((_dateData & KindLocal) != 0) ? ToUniversalTime().Ticks : Ticks; -#pragma warning disable 162 // Unrechable code on Unix +#pragma warning disable 162 // Unreachable code on Unix if (s_systemSupportsLeapSeconds) { return (long)ToFileTimeLeapSecondsAware(ticks); diff --git a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs index 5b5fbd044919a..4273c7f20264d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateTimeOffset.cs @@ -54,7 +54,7 @@ namespace System // Constructors - private DateTimeOffset(short validOffsetMinutes, DateTime validDateTime) + internal DateTimeOffset(short validOffsetMinutes, DateTime validDateTime) { _dateTime = validDateTime; _offsetMinutes = validOffsetMinutes; @@ -176,18 +176,7 @@ public DateTimeOffset(int year, int month, int day, int hour, int minute, int se // resolution of the returned value depends on the system timer. public static DateTimeOffset Now => ToLocalTime(DateTime.UtcNow, true); - public static DateTimeOffset UtcNow - { - get - { - DateTime utcNow = DateTime.UtcNow; - var result = new DateTimeOffset(0, utcNow); - - Debug.Assert(new DateTimeOffset(utcNow) == result); // ensure lack of verification does not break anything - - return result; - } - } + public static DateTimeOffset UtcNow => TimeContext.Current.Clock.GetCurrentUtcDateTimeOffset(); public DateTime DateTime => ClockDateTime; diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.Unix.cs new file mode 100644 index 0000000000000..a5b4886e26eec --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.Unix.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics +{ + public partial class ActualSystemClock + { + private static unsafe ulong GetTicks() => (ulong)(Interop.Sys.GetSystemTimeAsTicks() + DateTime.UnixEpochTicks); + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.Windows.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.Windows.cs new file mode 100644 index 0000000000000..f3b98626cfd4c --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.Windows.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.InteropServices; + +namespace System.Diagnostics +{ + public partial class ActualSystemClock + { + private static unsafe ulong GetTicks() + { + ulong fileTime; + s_pfnGetSystemTimeAsFileTime(&fileTime); + + if (!DateTime.s_systemSupportsLeapSeconds) + { + return fileTime + DateTime.FileTimeOffset; + } + + Interop.Kernel32.SYSTEMTIME time; + ulong hundredNanoSecond; + + if (Interop.Kernel32.FileTimeToSystemTime(&fileTime, &time) != Interop.BOOL.FALSE) + { + // to keep the time precision + ulong tmp = fileTime; // temp. variable avoids double read from memory + hundredNanoSecond = tmp % TimeSpan.TicksPerMillisecond; + } + else + { + Interop.Kernel32.GetSystemTime(&time); + hundredNanoSecond = 0; + } + + return DateTime.GetTicksFromSystemTime(in time, hundredNanoSecond); + } + + private static readonly unsafe delegate* unmanaged[SuppressGCTransition] s_pfnGetSystemTimeAsFileTime = GetGetSystemTimeAsFileTimeFnPtr(); + + private static unsafe delegate* unmanaged[SuppressGCTransition] GetGetSystemTimeAsFileTimeFnPtr() + { + IntPtr kernel32Lib = Interop.Kernel32.LoadLibraryEx(Interop.Libraries.Kernel32, IntPtr.Zero, Interop.Kernel32.LOAD_LIBRARY_SEARCH_SYSTEM32); + Debug.Assert(kernel32Lib != IntPtr.Zero); + + IntPtr pfnGetSystemTime = NativeLibrary.GetExport(kernel32Lib, "GetSystemTimeAsFileTime"); + + if (NativeLibrary.TryGetExport(kernel32Lib, "GetSystemTimePreciseAsFileTime", out IntPtr pfnGetSystemTimePrecise)) + { + // GetSystemTimePreciseAsFileTime exists and we'd like to use it. However, on + // misconfigured systems, it's possible for the "precise" time to be inaccurate: + // https://github.com/dotnet/runtime/issues/9014 + // If it's inaccurate, though, we expect it to be wildly inaccurate, so as a + // workaround/heuristic, we get both the "normal" and "precise" times, and as + // long as they're close, we use the precise one. This workaround can be removed + // when we better understand what's causing the drift and the issue is no longer + // a problem or can be better worked around on all targeted OSes. + + // Retry this check several times to reduce chance of false negatives due to thread being rescheduled + // at wrong time. + for (int i = 0; i < 10; i++) + { + long systemTimeResult, preciseSystemTimeResult; + ((delegate* unmanaged[SuppressGCTransition])pfnGetSystemTime)(&systemTimeResult); + ((delegate* unmanaged[SuppressGCTransition])pfnGetSystemTimePrecise)(&preciseSystemTimeResult); + + if (Math.Abs(preciseSystemTimeResult - systemTimeResult) <= 100 * TimeSpan.TicksPerMillisecond) + { + pfnGetSystemTime = pfnGetSystemTimePrecise; // use the precise version + break; + } + } + } + + return (delegate* unmanaged[SuppressGCTransition])pfnGetSystemTime; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.cs new file mode 100644 index 0000000000000..c97030b9c20d8 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/ActualSystemClock.cs @@ -0,0 +1,33 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics +{ + /// + /// Implements a that retrieves the current time from + /// the actual system clock, as provided by the underlying operating system. + /// + public sealed partial class ActualSystemClock : TimeClock + { + private ActualSystemClock() + { + } + + /// + /// Gets a singleton instance of the . + /// + public static ActualSystemClock Instance { get; } = new(); + + protected override DateTimeOffset GetCurrentUtcDateTimeOffsetImpl() + { + ulong ticks = GetTicks(); + return new DateTimeOffset(0, new DateTime(ticks)); + } + + internal override DateTime GetCurrentUtcDateTimeImpl() + { + ulong ticks = GetTicks(); + return new DateTime(ticks | DateTime.KindUtc); + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/TimeClock.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/TimeClock.cs new file mode 100644 index 0000000000000..cfa34770bcd9f --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/TimeClock.cs @@ -0,0 +1,55 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics +{ + /// + /// Provides an abstraction for a clock which is used to retrieve the current time. + /// + public abstract class TimeClock + { + /// + /// Gets the current time from the clock instance, as a , + /// in terms of Coordinated Universal Time (UTC). + /// + /// + /// A value representing the current UTC time. + /// The value will always have a zero offset. + /// + public DateTimeOffset GetCurrentUtcDateTimeOffset() + { + DateTimeOffset value = GetCurrentUtcDateTimeOffsetImpl(); + return value.Offset == TimeSpan.Zero ? value : value.ToUniversalTime(); + } + + /// + /// Gets the current time from the clock instance, as a , + /// in terms of Coordinated Universal Time (UTC). + /// + /// + /// A value representing the current UTC time. + /// The value will always have a kind of . + /// + public DateTime GetCurrentUtcDateTime() => GetCurrentUtcDateTimeImpl(); + + /// + /// Provides the implementation logic for the clock instance to return the current time. + /// + /// + /// A value representing the current time from the clock instance. + /// + protected abstract DateTimeOffset GetCurrentUtcDateTimeOffsetImpl(); + + /// + /// Provides the implementation logic for the clock instance to return the current time. + /// + /// + /// A value representing the current time from the clock instance. + /// + /// + /// This method is overriden internally by the only, for performance benefits. + /// All other clocks will implement only the method. + /// + internal virtual DateTime GetCurrentUtcDateTimeImpl() => GetCurrentUtcDateTimeOffsetImpl().UtcDateTime; + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/TimeContext.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/TimeContext.cs new file mode 100644 index 0000000000000..0eb554f7b7dff --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/TimeContext.cs @@ -0,0 +1,272 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using System.Threading.Tasks; + +namespace System.Diagnostics +{ + /// + /// Represents an ambient context that can be used to run an operation using a specific , + /// a specific , or both. + /// + /// + /// + /// Providing a to any of the Run or RunAsync static methods, for the + /// scope of the provided operation, will use that clock to control the values of properties that give the + /// current time, including , , + /// , , and . + /// + /// + /// + /// + /// Providing a to any of the Run or RunAsync static methods, for the + /// scope of the provided operation, will use that time zone to control the values of properties that give or use + /// the local time zone, including , , + /// , and , and becomes the local time zone used by all + /// platform features that convert to or from local time, such as + /// , + /// and many others. + /// + /// + /// + /// + public sealed class TimeContext + { + private readonly TimeClock _clock; + private readonly Func _localTimeZoneAccessor; + + // The default time context will use the actual system clock and local system time zone. + private static readonly TimeContext s_defaultTimeContext = new TimeContext(ActualSystemClock.Instance, TimeZoneInfo.GetActualSystemLocal); + + // This is the ambient storage for the time context. + private static readonly AsyncLocal s_context = new(); + + // Private constructor only. No public construction allowed. + private TimeContext(TimeClock clock, Func localTimeZoneAccessor) + { + _clock = clock; + _localTimeZoneAccessor = localTimeZoneAccessor; + } + + /// + /// Gets the currently active ambient time context. + /// + public static TimeContext Current + { + get => s_context.Value ?? s_defaultTimeContext; + private set => s_context.Value = value; + } + + /// + /// Gets the actual system clock, regardless of whether it is the current clock or not. + /// + public static ActualSystemClock ActualSystemClock => ActualSystemClock.Instance; + + /// + /// Gets a value that indicates whether the current clock is the actual system clock. + /// + public static bool ActualSystemClockIsActive => Current._clock is ActualSystemClock; + + /// + /// Gets the actual system time zone, regardless of whether it is the current local time zone or not. + /// + public static TimeZoneInfo ActualSystemLocalTimeZone => TimeZoneInfo.GetActualSystemLocal(); + + /// + /// Gets a value that indicates whether the current local time zone is the actual system local time zone. + /// + public static bool ActualSystemLocalTimeZoneIsActive => Current._localTimeZoneAccessor == TimeZoneInfo.GetActualSystemLocal; + + /// + /// Gets the clock used by this time context. + /// + public TimeClock Clock => _clock; + + /// + /// Gets the local time zone used by this time context. + /// + public TimeZoneInfo LocalTimeZone => _localTimeZoneAccessor(); + + /// + /// Runs a synchronous action under a with the specified clock. + /// + /// The clock that will be in effect during the action. + /// The action to run. + public static void Run(TimeClock clock, Action action) + { + TimeContext original = Current; + Current = new TimeContext(clock, original._localTimeZoneAccessor); + action.Invoke(); + Current = original; + } + + /// + /// Runs a synchronous action under a with the specified local time zone. + /// + /// The local time zone that will be in effect during the action. + /// The action to run. + public static void Run(TimeZoneInfo localTimeZone, Action action) + { + TimeContext original = Current; + Current = new TimeContext(original._clock, () => localTimeZone); + action.Invoke(); + Current = original; + } + + /// + /// Runs a synchronous action under a with the specified clock and local time zone. + /// + /// The clock that will be in effect during the action. + /// The local time zone that will be in effect during the action. + /// The action to run. + public static void Run(TimeClock clock, TimeZoneInfo localTimeZone, Action action) + { + TimeContext original = Current; + Current = new TimeContext(clock, () => localTimeZone); + action.Invoke(); + Current = original; + } + + /// + /// Runs a synchronous function under a with the specified clock. + /// + /// The type of the result of the function. + /// The clock that will be in effect during the function. + /// The function to run. + /// The result from running the function. + public static TResult Run(TimeClock clock, Func function) + { + TimeContext original = Current; + Current = new TimeContext(clock, original._localTimeZoneAccessor); + TResult result = function.Invoke(); + Current = original; + return result; + } + + /// + /// Runs a synchronous function under a with the specified local time zone. + /// + /// The type of the result of the function. + /// The local time zone that will be in effect during the function. + /// The function to run. + /// The result from running the function. + public static TResult Run(TimeZoneInfo localTimeZone, Func function) + { + TimeContext original = Current; + Current = new TimeContext(original._clock, () => localTimeZone); + TResult result = function.Invoke(); + Current = original; + return result; + } + + /// + /// Runs a synchronous function under a with the specified clock and local time zone. + /// + /// The type of the result of the function. + /// The clock that will be in effect during the function. + /// The local time zone that will be in effect during the function. + /// The function to run. + /// The result from running the function. + public static TResult Run(TimeClock clock, TimeZoneInfo localTimeZone, Func function) + { + TimeContext original = Current; + Current = new TimeContext(clock, () => localTimeZone); + TResult result = function.Invoke(); + Current = original; + return result; + } + + /// + /// Runs an asynchronous function under a with the specified clock. + /// + /// The clock that will be in effect during the function. + /// The function to run. + /// A from the asynchronous function call. + public static async Task RunAsync(TimeClock clock, Func function) + { + TimeContext original = Current; + Current = new TimeContext(clock, original._localTimeZoneAccessor); + await function.Invoke().ConfigureAwait(false); + Current = original; + } + + /// + /// Runs an asynchronous function under a with the specified local time zone. + /// + /// The local time zone that will be in effect during the function. + /// The function to run. + /// A from the asynchronous function call. + public static async Task RunAsync(TimeZoneInfo localTimeZone, Func function) + { + TimeContext original = Current; + Current = new TimeContext(original._clock, () => localTimeZone); + await function.Invoke().ConfigureAwait(false); + Current = original; + } + + /// + /// Runs an asynchronous function under a with the specified clock and local time zone. + /// + /// The clock that will be in effect during the function. + /// The local time zone that will be in effect during the function. + /// The function to run. + /// A from the asynchronous function call. + public static async Task RunAsync(TimeClock clock, TimeZoneInfo localTimeZone, Func function) + { + TimeContext original = Current; + Current = new TimeContext(clock, () => localTimeZone); + await function.Invoke().ConfigureAwait(false); + Current = original; + } + + /// + /// Runs an asynchronous function under a with the specified clock. + /// + /// The type of the result of the function. + /// The clock that will be in effect during the function. + /// The function to run. + /// A from the asynchronous function call. + public static async Task RunAsync(TimeClock clock, Func> function) + { + TimeContext original = Current; + Current = new TimeContext(clock, original._localTimeZoneAccessor); + TResult result = await function.Invoke().ConfigureAwait(false); + Current = original; + return result; + } + + /// + /// Runs an asynchronous function under a with the specified local time zone. + /// + /// The type of the result of the function. + /// The local time zone that will be in effect during the function. + /// The function to run. + /// A from the asynchronous function call. + public static async Task RunAsync(TimeZoneInfo localTimeZone, Func> function) + { + TimeContext original = Current; + Current = new TimeContext(original._clock, () => localTimeZone); + TResult result = await function.Invoke().ConfigureAwait(false); + Current = original; + return result; + } + + /// + /// Runs an asynchronous function under a with the specified clock and local time zone. + /// + /// The type of the result of the function. + /// The clock that will be in effect during the function. + /// The local time zone that will be in effect during the function. + /// The function to run. + /// A from the asynchronous function call. + public static async Task RunAsync(TimeClock clock, TimeZoneInfo localTimeZone, Func> function) + { + TimeContext original = Current; + Current = new TimeContext(clock, () => localTimeZone); + TResult result = await function.Invoke().ConfigureAwait(false); + Current = original; + return result; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/Diagnostics/VirtualClock.cs b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/VirtualClock.cs new file mode 100644 index 0000000000000..1a8f853645890 --- /dev/null +++ b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/VirtualClock.cs @@ -0,0 +1,135 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Diagnostics +{ + /// + /// Implements a that tracks time in its internal state. + /// This is primarily intended for use in unit tests, so that an artificial time can be provided + /// to the code being tested rather than the actual system time. + /// + public sealed class VirtualClock : TimeClock + { + // This is the internal time value of the virtual clock. + private DateTimeOffset? _value; + + // This is only used when constructing the virtual clock using a DateTime instead of a DateTimeOffset. + // It is later converted to a DateTimeOffset when used in the GetCurrentUtcDateTimeOffsetImpl method. + private readonly DateTime _initialDateTimeValue; + + // When set, this function controls how the internal time value advances after each value is retrieved. + // Otherwise, the internal time value is fixed and does not advance. + private readonly Func? _advancementFunction; + + /// + /// Constructs a virtual clock that always provides a fixed time value. + /// + /// The time that the clock is set to. + /// + /// The parameter will internally be converted to UTC using its . + /// + public VirtualClock(DateTimeOffset timeValue) + { + _value = timeValue.ToUniversalTime(); + } + + /// + /// Constructs a virtual clock that always provides a fixed time value. + /// + /// The time that the clock is set to. + /// + /// The parameter will internally be converted to UTC using its . + /// Values with or will be treated as local time, + /// using the local time zone as given by or the current . + /// + public VirtualClock(DateTime timeValue) + { + _initialDateTimeValue = timeValue; + } + + /// + /// Constructs a virtual clock that is initialized to a given value, and advances after each retrieval by a given amount. + /// + /// The time that the clock is initially set to. + /// The amount of time that the clock will advance after the current time is retrieved. + /// + /// The parameter will internally be converted to UTC using its . + /// + public VirtualClock(DateTimeOffset initialTimeValue, TimeSpan advancementAmount) + { + _value = initialTimeValue.ToUniversalTime(); + _advancementFunction = value => value.Add(advancementAmount); + } + + /// + /// Constructs a virtual clock that is initialized to a given value, and advances after each retrieval by a given amount. + /// + /// The time that the clock is initially set to. + /// The amount of time that the clock will advance after the current time is retrieved. + /// + /// The parameter will internally be converted to UTC using its . + /// Values with or will be treated as local time, + /// using the local time zone as given by or the current . + /// + public VirtualClock(DateTime initialTimeValue, TimeSpan advancementAmount) + { + _initialDateTimeValue = initialTimeValue; + _advancementFunction = value => value.Add(advancementAmount); + } + + /// + /// Constructs a virtual clock that is initialized to a given value, and advances after each retrieval according + /// to a given function. + /// + /// The time that the clock is initially set to. + /// + /// A function that advances the clock after the current time is retrieved. + /// The function's input parameter is the clock's current value, and the function should return the clock's new value. + /// + /// + /// The parameter will internally be converted to UTC using its . + /// + public VirtualClock(DateTimeOffset initialTimeValue, Func advancementFunction) + { + _value = initialTimeValue.ToUniversalTime(); + _advancementFunction = advancementFunction; + } + + /// + /// Constructs a virtual clock that is initialized to a given value, and advances after each retrieval according + /// to a given function. + /// + /// The time that the clock is initially set to. + /// + /// A function that advances the clock after the current time is retrieved. + /// The function's input parameter is the clock's current value, and the function should return the clock's new value. + /// + /// + /// The parameter will internally be converted to UTC using its . + /// Values with or will be treated as local time, + /// using the local time zone as given by or the current . + /// + public VirtualClock(DateTime initialTimeValue, Func advancementFunction) + { + _initialDateTimeValue = initialTimeValue; + _advancementFunction = advancementFunction; + } + + protected override DateTimeOffset GetCurrentUtcDateTimeOffsetImpl() + { + // Note: When the clock is initialized using a DateTime, the conversion to DateTimeOffset is done here + // rather than in the constructor. This allows for the local time zone to be changed (if desired) + // when using this clock within an ambient TimeContext, thus affecting the conversion behavior + // in the following line of code when the DateTime value has a Kind of Local or Unspecified. + DateTimeOffset value = _value ?? new DateTimeOffset(_initialDateTimeValue); + + if (_advancementFunction != null) + { + DateTimeOffset newValue = _advancementFunction.Invoke(value); + _value = newValue.ToUniversalTime(); + } + + return value; + } + } +} diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs index 93195932f052f..f692616a08962 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Win32.cs @@ -364,6 +364,13 @@ public static TimeZoneInfo FindSystemTimeZoneById(string id) // DateTime.Now fast path that avoids allocating an historically accurate TimeZoneInfo.Local and just creates a 1-year (current year) accurate time zone internal static TimeSpan GetDateTimeNowUtcOffsetFromUtc(DateTime time, out bool isAmbiguousLocalDst) { + if (!TimeContext.ActualSystemLocalTimeZoneIsActive) + { + // Use the standard code path when we're in a time context that has changed the system local time zone. + bool isDaylightSavings; + return GetUtcOffsetFromUtc(time, Local, out isDaylightSavings, out isAmbiguousLocalDst); + } + isAmbiguousLocalDst = false; int timeYear = time.Year; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs index 3c8ddb932f348..ca3706197230f 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.cs @@ -56,10 +56,8 @@ private enum TimeZoneInfoResult private static CachedData s_cachedData = new CachedData(); // - // All cached data are encapsulated in a helper class to allow consistent view even when the data are refreshed using ClearCachedData() - // - // For example, TimeZoneInfo.Local can be cleared by another thread calling TimeZoneInfo.ClearCachedData. Without the consistent snapshot, - // there is a chance that the internal ConvertTime calls will throw since 'source' won't be reference equal to the new TimeZoneInfo.Local. + // All cached data come from the underlying operating system and are encapsulated in a helper class to allow + // consistent view even when refreshed using ClearCachedData() // private sealed partial class CachedData { @@ -94,35 +92,6 @@ private TimeZoneInfo CreateLocal() public TimeZoneInfo Local => _localTimeZone ?? CreateLocal(); - /// - /// Helper function that returns the corresponding DateTimeKind for this TimeZoneInfo. - /// - public DateTimeKind GetCorrespondingKind(TimeZoneInfo? timeZone) - { - // We check reference equality to see if 'this' is the same as - // TimeZoneInfo.Local or TimeZoneInfo.Utc. This check is needed to - // support setting the DateTime Kind property to 'Local' or - // 'Utc' on the ConverTime(...) return value. - // - // Using reference equality instead of value equality was a - // performance based design compromise. The reference equality - // has much greater performance, but it reduces the number of - // returned DateTime's that can be properly set as 'Local' or 'Utc'. - // - // For example, the user could be converting to the TimeZoneInfo returned - // by FindSystemTimeZoneById("Pacific Standard Time") and their local - // machine may be in Pacific time. If we used value equality to determine - // the corresponding Kind then this conversion would be tagged as 'Local'; - // where as we are currently tagging the returned DateTime as 'Unspecified' - // in this example. Only when the user passes in TimeZoneInfo.Local or - // TimeZoneInfo.Utc to the ConvertTime(...) methods will this check succeed. - // - return - ReferenceEquals(timeZone, s_utcTimeZone) ? DateTimeKind.Utc : - ReferenceEquals(timeZone, _localTimeZone) ? DateTimeKind.Local : - DateTimeKind.Unspecified; - } - public Dictionary? _systemTimeZones; public ReadOnlyCollection? _readOnlySystemTimeZones; public bool _allSystemTimeZonesRead; @@ -201,19 +170,17 @@ public TimeSpan[] GetAmbiguousTimeOffsets(DateTime dateTime) } DateTime adjustedTime; - if (dateTime.Kind == DateTimeKind.Local) + DateTimeKind sourceKind = dateTime.Kind; + if (sourceKind == DateTimeKind.Unspecified) { - CachedData cachedData = s_cachedData; - adjustedTime = ConvertTime(dateTime, cachedData.Local, this, TimeZoneInfoOptions.None, cachedData); - } - else if (dateTime.Kind == DateTimeKind.Utc) - { - CachedData cachedData = s_cachedData; - adjustedTime = ConvertTime(dateTime, s_utcTimeZone, this, TimeZoneInfoOptions.None, cachedData); + adjustedTime = dateTime; } else { - adjustedTime = dateTime; + TimeZoneInfo local = Local; + TimeZoneInfo sourceZone = sourceKind == DateTimeKind.Local ? local : s_utcTimeZone; + DateTimeKind targetKind = GetCorrespondingKind(this, local); + adjustedTime = ConvertTime(dateTime, sourceZone, this, sourceKind, targetKind, TimeZoneInfoOptions.None); } bool isAmbiguous = false; @@ -303,31 +270,29 @@ public TimeSpan GetUtcOffset(DateTimeOffset dateTimeOffset) => /// Returns the Universal Coordinated Time (UTC) Offset for the current TimeZoneInfo instance. /// public TimeSpan GetUtcOffset(DateTime dateTime) => - GetUtcOffset(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime, s_cachedData); + GetUtcOffset(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime); // Shortcut for TimeZoneInfo.Local.GetUtcOffset internal static TimeSpan GetLocalUtcOffset(DateTime dateTime, TimeZoneInfoOptions flags) { - CachedData cachedData = s_cachedData; - return cachedData.Local.GetUtcOffset(dateTime, flags, cachedData); + return Local.GetUtcOffset(dateTime, flags); } /// /// Returns the Universal Coordinated Time (UTC) Offset for the current TimeZoneInfo instance. /// - internal TimeSpan GetUtcOffset(DateTime dateTime, TimeZoneInfoOptions flags) => - GetUtcOffset(dateTime, flags, s_cachedData); - - private TimeSpan GetUtcOffset(DateTime dateTime, TimeZoneInfoOptions flags, CachedData cachedData) + internal TimeSpan GetUtcOffset(DateTime dateTime, TimeZoneInfoOptions flags) { if (dateTime.Kind == DateTimeKind.Local) { - if (cachedData.GetCorrespondingKind(this) != DateTimeKind.Local) + TimeZoneInfo local = Local; + if (ReferenceEquals(this, local)) { // // normal case of converting from Local to Utc and then getting the offset from the UTC DateTime // - DateTime adjustedTime = ConvertTime(dateTime, cachedData.Local, s_utcTimeZone, flags); + DateTime adjustedTime = ConvertTime(dateTime, local, s_utcTimeZone, + DateTimeKind.Local, DateTimeKind.Utc, flags); return GetUtcOffsetFromUtc(adjustedTime, this); } @@ -348,7 +313,7 @@ private TimeSpan GetUtcOffset(DateTime dateTime, TimeZoneInfoOptions flags, Cach } else if (dateTime.Kind == DateTimeKind.Utc) { - if (cachedData.GetCorrespondingKind(this) == DateTimeKind.Utc) + if (ReferenceEquals(this, s_utcTimeZone)) { return _baseUtcOffset; } @@ -398,11 +363,19 @@ internal bool IsAmbiguousTime(DateTime dateTime, TimeZoneInfoOptions flags) return false; } - CachedData cachedData = s_cachedData; - DateTime adjustedTime = - dateTime.Kind == DateTimeKind.Local ? ConvertTime(dateTime, cachedData.Local, this, flags, cachedData) : - dateTime.Kind == DateTimeKind.Utc ? ConvertTime(dateTime, s_utcTimeZone, this, flags, cachedData) : - dateTime; + DateTime adjustedTime; + DateTimeKind sourceKind = dateTime.Kind; + if (sourceKind == DateTimeKind.Unspecified) + { + adjustedTime = dateTime; + } + else + { + TimeZoneInfo local = Local; + TimeZoneInfo sourceZone = sourceKind == DateTimeKind.Local ? local : s_utcTimeZone; + DateTimeKind targetKind = GetCorrespondingKind(this, local); + adjustedTime = ConvertTime(dateTime, sourceZone, this, sourceKind, targetKind, flags); + } AdjustmentRule? rule = GetAdjustmentRuleForTime(adjustedTime, out int? ruleIndex); if (rule != null && rule.HasDaylightSaving) @@ -426,15 +399,12 @@ public bool IsDaylightSavingTime(DateTimeOffset dateTimeOffset) /// Returns true if the time is during Daylight Saving time for the current TimeZoneInfo instance. /// public bool IsDaylightSavingTime(DateTime dateTime) => - IsDaylightSavingTime(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime, s_cachedData); + IsDaylightSavingTime(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime); /// /// Returns true if the time is during Daylight Saving time for the current TimeZoneInfo instance. /// - internal bool IsDaylightSavingTime(DateTime dateTime, TimeZoneInfoOptions flags) => - IsDaylightSavingTime(dateTime, flags, s_cachedData); - - private bool IsDaylightSavingTime(DateTime dateTime, TimeZoneInfoOptions flags, CachedData cachedData) + internal bool IsDaylightSavingTime(DateTime dateTime, TimeZoneInfoOptions flags) { // // dateTime.Kind is UTC, then time will be converted from UTC @@ -460,11 +430,13 @@ private bool IsDaylightSavingTime(DateTime dateTime, TimeZoneInfoOptions flags, // if (dateTime.Kind == DateTimeKind.Local) { - adjustedTime = ConvertTime(dateTime, cachedData.Local, this, flags, cachedData); + TimeZoneInfo local = Local; + DateTimeKind targetKind = GetCorrespondingKind(this, local); + adjustedTime = ConvertTime(dateTime, local, this, DateTimeKind.Local, targetKind, flags); } else if (dateTime.Kind == DateTimeKind.Utc) { - if (cachedData.GetCorrespondingKind(this) == DateTimeKind.Utc) + if (ReferenceEquals(this, s_utcTimeZone)) { // simple always false case: TimeZoneInfo.Utc.IsDaylightSavingTime(dateTime, flags); return false; @@ -505,9 +477,10 @@ private bool IsDaylightSavingTime(DateTime dateTime, TimeZoneInfoOptions flags, public bool IsInvalidTime(DateTime dateTime) { bool isInvalid = false; + TimeZoneInfo local = Local; if ((dateTime.Kind == DateTimeKind.Unspecified) || - (dateTime.Kind == DateTimeKind.Local && s_cachedData.GetCorrespondingKind(this) == DateTimeKind.Local)) + (dateTime.Kind == DateTimeKind.Local && ReferenceEquals(this, local))) { // only check Unspecified and (Local when this TimeZoneInfo instance is Local) AdjustmentRule? rule = GetAdjustmentRuleForTime(dateTime, out int? ruleIndex); @@ -552,24 +525,22 @@ public static DateTime ConvertTimeBySystemTimeZoneId(DateTime dateTime, string d /// public static DateTime ConvertTimeBySystemTimeZoneId(DateTime dateTime, string sourceTimeZoneId, string destinationTimeZoneId) { - if (dateTime.Kind == DateTimeKind.Local && string.Equals(sourceTimeZoneId, Local.Id, StringComparison.OrdinalIgnoreCase)) - { - // TimeZoneInfo.Local can be cleared by another thread calling TimeZoneInfo.ClearCachedData. - // Take snapshot of cached data to guarantee this method will not be impacted by the ClearCachedData call. - // Without the snapshot, there is a chance that ConvertTime will throw since 'source' won't - // be reference equal to the new TimeZoneInfo.Local - // - CachedData cachedData = s_cachedData; - return ConvertTime(dateTime, cachedData.Local, FindSystemTimeZoneById(destinationTimeZoneId), TimeZoneInfoOptions.None, cachedData); - } - else if (dateTime.Kind == DateTimeKind.Utc && string.Equals(sourceTimeZoneId, Utc.Id, StringComparison.OrdinalIgnoreCase)) - { - return ConvertTime(dateTime, s_utcTimeZone, FindSystemTimeZoneById(destinationTimeZoneId), TimeZoneInfoOptions.None, s_cachedData); - } - else + TimeZoneInfo local = Local; + + TimeZoneInfo destinationTimeZone = FindSystemTimeZoneById(destinationTimeZoneId); + DateTimeKind targetKind = GetCorrespondingKind(destinationTimeZone, local); + + DateTimeKind sourceKind = dateTime.Kind; + return sourceKind switch { - return ConvertTime(dateTime, FindSystemTimeZoneById(sourceTimeZoneId), FindSystemTimeZoneById(destinationTimeZoneId)); - } + DateTimeKind.Local when string.Equals(sourceTimeZoneId, local.Id, StringComparison.OrdinalIgnoreCase) => + ConvertTime(dateTime, local, destinationTimeZone, sourceKind, targetKind, TimeZoneInfoOptions.None), + + DateTimeKind.Utc when string.Equals(sourceTimeZoneId, UtcId, StringComparison.OrdinalIgnoreCase) => + ConvertTime(dateTime, s_utcTimeZone, destinationTimeZone, sourceKind, targetKind, TimeZoneInfoOptions.None), + + _ => ConvertTime(dateTime, FindSystemTimeZoneById(sourceTimeZoneId), destinationTimeZone, sourceKind, targetKind, TimeZoneInfoOptions.None) + }; } /// @@ -605,29 +576,34 @@ public static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo destinationTi throw new ArgumentNullException(nameof(destinationTimeZone)); } - // Special case to give a way clearing the cache without exposing ClearCachedData() + // Special case added for .NET Core/Standard 1.x to have a way for clearing the cache without + // exposing ClearCachedData(), which was made public in .NET Core/Standard 2.0, and was always + // public in .NET Framework. Retaining this behavior for backwards compatibility purposes only. if (dateTime.Ticks == 0) { ClearCachedData(); } - CachedData cachedData = s_cachedData; - TimeZoneInfo sourceTimeZone = dateTime.Kind == DateTimeKind.Utc ? s_utcTimeZone : cachedData.Local; - return ConvertTime(dateTime, sourceTimeZone, destinationTimeZone, TimeZoneInfoOptions.None, cachedData); + + TimeZoneInfo local = Local; + DateTimeKind sourceKind = dateTime.Kind; + DateTimeKind targetKind = GetCorrespondingKind(destinationTimeZone, local); + TimeZoneInfo sourceTimeZone = sourceKind == DateTimeKind.Utc ? s_utcTimeZone : local; + return ConvertTime(dateTime, sourceTimeZone, destinationTimeZone, sourceKind, targetKind, TimeZoneInfoOptions.None); } /// /// Converts the value of the dateTime object from sourceTimeZone to destinationTimeZone /// - public static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZone, TimeZoneInfo destinationTimeZone) => - ConvertTime(dateTime, sourceTimeZone, destinationTimeZone, TimeZoneInfoOptions.None, s_cachedData); + public static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZone, TimeZoneInfo destinationTimeZone) + { + TimeZoneInfo local = Local; + DateTimeKind sourceKind = GetCorrespondingKind(sourceTimeZone, local); + DateTimeKind targetKind = GetCorrespondingKind(destinationTimeZone, local); - /// - /// Converts the value of the dateTime object from sourceTimeZone to destinationTimeZone - /// - internal static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZone, TimeZoneInfo destinationTimeZone, TimeZoneInfoOptions flags) => - ConvertTime(dateTime, sourceTimeZone, destinationTimeZone, flags, s_cachedData); + return ConvertTime(dateTime, sourceTimeZone, destinationTimeZone, sourceKind, targetKind, TimeZoneInfoOptions.None); + } - private static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZone, TimeZoneInfo destinationTimeZone, TimeZoneInfoOptions flags, CachedData cachedData) + private static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZone, TimeZoneInfo destinationTimeZone, DateTimeKind sourceKind, DateTimeKind targetKind, TimeZoneInfoOptions flags) { if (sourceTimeZone == null) { @@ -639,7 +615,6 @@ private static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZo throw new ArgumentNullException(nameof(destinationTimeZone)); } - DateTimeKind sourceKind = cachedData.GetCorrespondingKind(sourceTimeZone); if (((flags & TimeZoneInfoOptions.NoThrowOnInvalidTime) == 0) && (dateTime.Kind != DateTimeKind.Unspecified) && (dateTime.Kind != sourceKind)) { throw new ArgumentException(SR.Argument_ConvertMismatch, nameof(sourceTimeZone)); @@ -677,8 +652,6 @@ private static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZo } } - DateTimeKind targetKind = cachedData.GetCorrespondingKind(destinationTimeZone); - // handle the special case of Loss-less Local->Local and UTC->UTC) if (dateTime.Kind != DateTimeKind.Unspecified && sourceKind != DateTimeKind.Unspecified && sourceKind == targetKind) { @@ -705,8 +678,16 @@ private static DateTime ConvertTime(DateTime dateTime, TimeZoneInfo sourceTimeZo /// /// Converts the value of a DateTime object from Coordinated Universal Time (UTC) to the destinationTimeZone. /// - public static DateTime ConvertTimeFromUtc(DateTime dateTime, TimeZoneInfo destinationTimeZone) => - ConvertTime(dateTime, s_utcTimeZone, destinationTimeZone, TimeZoneInfoOptions.None, s_cachedData); + public static DateTime ConvertTimeFromUtc(DateTime dateTime, TimeZoneInfo destinationTimeZone) + { + if (dateTime.Kind == DateTimeKind.Utc && ReferenceEquals(destinationTimeZone, s_utcTimeZone)) + { + return dateTime; + } + + DateTimeKind targetKind = GetCorrespondingKind(destinationTimeZone, Local); + return ConvertTime(dateTime, s_utcTimeZone, destinationTimeZone, DateTimeKind.Utc, targetKind, TimeZoneInfoOptions.None); + } /// /// Converts the value of a DateTime object to Coordinated Universal Time (UTC). @@ -717,8 +698,8 @@ public static DateTime ConvertTimeToUtc(DateTime dateTime) { return dateTime; } - CachedData cachedData = s_cachedData; - return ConvertTime(dateTime, cachedData.Local, s_utcTimeZone, TimeZoneInfoOptions.None, cachedData); + + return ConvertTime(dateTime, Local, s_utcTimeZone, DateTimeKind.Local, DateTimeKind.Utc, TimeZoneInfoOptions.None); } /// @@ -730,15 +711,18 @@ internal static DateTime ConvertTimeToUtc(DateTime dateTime, TimeZoneInfoOptions { return dateTime; } - CachedData cachedData = s_cachedData; - return ConvertTime(dateTime, cachedData.Local, s_utcTimeZone, flags, cachedData); + + return ConvertTime(dateTime, Local, s_utcTimeZone, DateTimeKind.Local, DateTimeKind.Utc, flags); } /// /// Converts the value of a DateTime object to Coordinated Universal Time (UTC). /// - public static DateTime ConvertTimeToUtc(DateTime dateTime, TimeZoneInfo sourceTimeZone) => - ConvertTime(dateTime, sourceTimeZone, s_utcTimeZone, TimeZoneInfoOptions.None, s_cachedData); + public static DateTime ConvertTimeToUtc(DateTime dateTime, TimeZoneInfo sourceTimeZone) + { + DateTimeKind sourceKind = GetCorrespondingKind(sourceTimeZone, Local); + return ConvertTime(dateTime, sourceTimeZone, s_utcTimeZone, sourceKind, DateTimeKind.Utc, TimeZoneInfoOptions.None); + } /// /// Returns value equality. Equals does not compare any localizable @@ -862,11 +846,16 @@ public bool HasSameRules(TimeZoneInfo other) } /// - /// Returns a TimeZoneInfo instance that represents the local time on the machine. - /// Accessing this property may throw InvalidTimeZoneException or COMException + /// Returns a TimeZoneInfo instance that represents the local time zone currently in effect. + /// + public static TimeZoneInfo Local => TimeContext.Current.LocalTimeZone; + + /// + /// Returns a TimeZoneInfo instance that represents the actual local time zone currently on the machine. + /// Calling this method may throw InvalidTimeZoneException or COMException /// if the machine is in an unstable or corrupt state. /// - public static TimeZoneInfo Local => s_cachedData.Local; + internal static TimeZoneInfo GetActualSystemLocal() => s_cachedData.Local; // // ToSerializedString - @@ -1983,5 +1972,33 @@ private static bool IsValidAdjustmentRuleOffest(TimeSpan baseUtcOffset, Adjustme TimeSpan utcOffset = GetUtcOffset(baseUtcOffset, adjustmentRule); return !UtcOffsetOutOfRange(utcOffset); } + + /// + /// Helper function that returns the corresponding DateTimeKind for a TimeZoneInfo. + /// + /// + /// We check reference equality to see if the time zone is the same as + /// the given local time zone, or as TimeZoneInfo.Utc. This check is needed to + /// support setting the DateTime Kind property to 'Local' or + /// 'Utc' on the ConvertTime(...) return value. + /// + /// Using reference equality instead of value equality was a + /// performance based design compromise. The reference equality + /// has much greater performance, but it reduces the number of + /// returned DateTime's that can be properly set as 'Local' or 'Utc'. + /// + /// For example, the user could be converting to the TimeZoneInfo returned + /// by FindSystemTimeZoneById("Pacific Standard Time") and their local + /// machine may be in Pacific time. If we used value equality to determine + /// the corresponding Kind then this conversion would be tagged as 'Local'; + /// where as we are currently tagging the returned DateTime as 'Unspecified' + /// in this example. Only when the user passes in TimeZoneInfo.Local or + /// TimeZoneInfo.Utc to the ConvertTime(...) methods will this check succeed. + /// + /// + private static DateTimeKind GetCorrespondingKind(TimeZoneInfo? timeZone, TimeZoneInfo? localTimeZone) => + ReferenceEquals(timeZone, s_utcTimeZone) ? DateTimeKind.Utc : + ReferenceEquals(timeZone, localTimeZone) ? DateTimeKind.Local : + DateTimeKind.Unspecified; } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 66ba7517ce8c1..fa5e2e2037c4b 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -5691,6 +5691,12 @@ public enum AssemblyVersionCompatibility } namespace System.Diagnostics { + public sealed partial class ActualSystemClock : System.Diagnostics.TimeClock + { + internal ActualSystemClock() { } + public static System.Diagnostics.ActualSystemClock Instance { get { throw null; } } + protected override System.DateTimeOffset GetCurrentUtcDateTimeOffsetImpl() { throw null; } + } [System.AttributeUsageAttribute(System.AttributeTargets.Class | System.AttributeTargets.Method, AllowMultiple=true)] public sealed partial class ConditionalAttribute : System.Attribute { @@ -5879,6 +5885,46 @@ public void Start() { } public static System.Diagnostics.Stopwatch StartNew() { throw null; } public void Stop() { } } + public abstract partial class TimeClock + { + protected TimeClock() { } + public System.DateTime GetCurrentUtcDateTime() { throw null; } + public System.DateTimeOffset GetCurrentUtcDateTimeOffset() { throw null; } + protected abstract System.DateTimeOffset GetCurrentUtcDateTimeOffsetImpl(); + } + public sealed partial class TimeContext + { + internal TimeContext() { } + public static System.TimeZoneInfo ActualSystemLocalTimeZone { get { throw null; } } + public static bool ActualSystemLocalTimeZoneIsActive { get { throw null; } } + public System.Diagnostics.TimeClock Clock { get { throw null; } } + public static System.Diagnostics.TimeContext Current { get { throw null; } } + public System.TimeZoneInfo LocalTimeZone { get { throw null; } } + public static System.Diagnostics.ActualSystemClock ActualSystemClock { get { throw null; } } + public static bool ActualSystemClockIsActive { get { throw null; } } + public static void Run(System.Diagnostics.TimeClock clock, System.Action action) { } + public static void Run(System.Diagnostics.TimeClock clock, System.TimeZoneInfo localTimeZone, System.Action action) { } + public static void Run(System.TimeZoneInfo localTimeZone, System.Action action) { } + public static System.Threading.Tasks.Task RunAsync(System.Diagnostics.TimeClock clock, System.Func function) { throw null; } + public static System.Threading.Tasks.Task RunAsync(System.Diagnostics.TimeClock clock, System.TimeZoneInfo localTimeZone, System.Func function) { throw null; } + public static System.Threading.Tasks.Task RunAsync(System.TimeZoneInfo localTimeZone, System.Func function) { throw null; } + public static System.Threading.Tasks.Task RunAsync(System.Diagnostics.TimeClock clock, System.Func> function) { throw null; } + public static System.Threading.Tasks.Task RunAsync(System.Diagnostics.TimeClock clock, System.TimeZoneInfo localTimeZone, System.Func> function) { throw null; } + public static System.Threading.Tasks.Task RunAsync(System.TimeZoneInfo localTimeZone, System.Func> function) { throw null; } + public static TResult Run(System.Diagnostics.TimeClock clock, System.Func function) { throw null; } + public static TResult Run(System.Diagnostics.TimeClock clock, System.TimeZoneInfo localTimeZone, System.Func function) { throw null; } + public static TResult Run(System.TimeZoneInfo localTimeZone, System.Func function) { throw null; } + } + public sealed partial class VirtualClock : System.Diagnostics.TimeClock + { + public VirtualClock(System.DateTime timeValue) { } + public VirtualClock(System.DateTime initialTimeValue, System.Func advancementFunction) { } + public VirtualClock(System.DateTime initialTimeValue, System.TimeSpan advancementAmount) { } + public VirtualClock(System.DateTimeOffset timeValue) { } + public VirtualClock(System.DateTimeOffset initialTimeValue, System.Func advancementFunction) { } + public VirtualClock(System.DateTimeOffset initialTimeValue, System.TimeSpan advancementAmount) { } + protected override System.DateTimeOffset GetCurrentUtcDateTimeOffsetImpl() { throw null; } + } } namespace System.Diagnostics.CodeAnalysis { diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index e4b0b3580f585..dd8bfe3658443 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -64,7 +64,6 @@ - @@ -157,7 +156,12 @@ + + + + + diff --git a/src/libraries/System.Runtime/tests/System/Diagnostics/ActualSystemClockTests.cs b/src/libraries/System.Runtime/tests/System/Diagnostics/ActualSystemClockTests.cs new file mode 100644 index 0000000000000..c0f6051ed60be --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/Diagnostics/ActualSystemClockTests.cs @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Threading; +using Xunit; + +using TestClock = System.Tests.TimeClockTests.TestClock; + +namespace System.Tests +{ + public class ActualSystemClockTests + { + [Fact] + public void CurrentUtcDateTimeOffset_Advances() + { + DateTimeOffset start = ActualSystemClock.Instance.GetCurrentUtcDateTimeOffset(); + Assert.True( + SpinWait.SpinUntil(() => ActualSystemClock.Instance.GetCurrentUtcDateTimeOffset() > start, TimeSpan.FromSeconds(30)), + "Expected GetCurrentUtcDateTimeOffset result to change"); + } + + [Fact] + public void CurrentUtcDateTime_Advances() + { + DateTime start = ActualSystemClock.Instance.GetCurrentUtcDateTime(); + Assert.True( + SpinWait.SpinUntil(() => ActualSystemClock.Instance.GetCurrentUtcDateTime() > start, TimeSpan.FromSeconds(30)), + "Expected GetCurrentUtcDateTime result to change"); + } + + [Fact] + public void CurrentUtcDateTimeOffset_AdvancesEvenWhenAnotherFixedClockIsActive() + { + // The test clock has a fixed value that never changes. + var testClock = new TestClock(); + + TimeContext.Run(testClock, () => + { + // Make sure the test clock is active, not the actual system clock. + Assert.Same(testClock, TimeContext.Current.Clock); + Assert.False(TimeContext.ActualSystemClockIsActive); + + // Despite the test clock being active, the actual system clock should still advance. + DateTimeOffset start = ActualSystemClock.Instance.GetCurrentUtcDateTimeOffset(); + Assert.True( + SpinWait.SpinUntil(() => ActualSystemClock.Instance.GetCurrentUtcDateTimeOffset() > start, TimeSpan.FromSeconds(30)), + "Expected GetCurrentUtcDateTimeOffset result to change"); + }); + } + + [Fact] + public void CurrentUtcDateTime_AdvancesEvenWhenAnotherFixedClockIsActive() + { + // The test clock has a fixed value that never changes. + var testClock = new TestClock(); + + TimeContext.Run(testClock, () => + { + // Make sure the test clock is active, not the actual system clock. + Assert.Same(testClock, TimeContext.Current.Clock); + Assert.False(TimeContext.ActualSystemClockIsActive); + + // Despite the test clock being active, the actual system clock should still advance. + DateTime start = ActualSystemClock.Instance.GetCurrentUtcDateTime(); + Assert.True( + SpinWait.SpinUntil(() => ActualSystemClock.Instance.GetCurrentUtcDateTime() > start, TimeSpan.FromSeconds(30)), + "Expected GetCurrentUtcDateTime result to change"); + }); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System/Diagnostics/TimeClockTests.cs b/src/libraries/System.Runtime/tests/System/Diagnostics/TimeClockTests.cs new file mode 100644 index 0000000000000..08d0fa8c5e902 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/Diagnostics/TimeClockTests.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Xunit; + +namespace System.Tests +{ + public class TimeClockTests + { + public class TestClock : TimeClock + { + public static readonly DateTimeOffset Value = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.FromHours(-8)); + protected override DateTimeOffset GetCurrentUtcDateTimeOffsetImpl() => Value; + } + + [Fact] + public void CurrentDateTimeOffset_AdjustsToUniversalTime() + { + TimeClock clock = new TestClock(); + DateTimeOffset actual = clock.GetCurrentUtcDateTimeOffset(); + + DateTimeOffset expected = TestClock.Value.ToUniversalTime(); + Assert.Equal(expected.DateTime, actual.DateTime); + Assert.Equal(expected.Offset, actual.Offset); + } + + [Fact] + public void CurrentDateTime_AdjustsToUniversalTime() + { + TimeClock clock = new TestClock(); + DateTime actual = clock.GetCurrentUtcDateTime(); + + DateTime expected = TestClock.Value.UtcDateTime; + Assert.Equal(expected, actual); + Assert.Equal(expected.Kind, actual.Kind); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System/Diagnostics/TimeContextTests.cs b/src/libraries/System.Runtime/tests/System/Diagnostics/TimeContextTests.cs new file mode 100644 index 0000000000000..6e5c76a0e54bc --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/Diagnostics/TimeContextTests.cs @@ -0,0 +1,1029 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; + +using TestClock = System.Tests.TimeClockTests.TestClock; + +namespace System.Tests +{ + public class TimeContextTests + { + private readonly ITestOutputHelper _output; + private readonly TimeClock _testClock = new TestClock(); + private static readonly TimeZoneInfo s_testTimeZone = GetTestTimeZone(); + + public TimeContextTests(ITestOutputHelper output) + { + _output = output; + } + + #region Examples + + [Fact] + public void LeapYearBusinessLogicTest1() + { + // This test demonstrates testing code that is sensitive to leap years. + // Testing actual application code like this might expose leap day bugs before they become an issue. + + DateTime GetOneYearFromToday_Incorrect() + { + // This shows an incorrect way to get a date that is one year from today. + // It will throw an exception when "today" is a leap day (Februrary 29th). + return new DateTime(DateTime.Today.Year + 1, DateTime.Today.Month, DateTime.Today.Day); + } + + DateTime GetOneYearFromToday_Correct() + { + // This shows the correct way to get a date that is one year from today. + // When run on a leap day (Februrary 29th), it will return February 28th of the following year. + return DateTime.Today.AddYears(1); + } + + // When the current year is not a leap year, either method will pass the test on the last day of February. + var clock1 = new VirtualClock(new DateTime(2021, 2, DateTime.DaysInMonth(2021, 2))); + TimeContext.Run(clock1, () => + { + var result1a = GetOneYearFromToday_Correct(); + Assert.Equal(new DateTime(2022, 2, 28), result1a); + + var result1b = GetOneYearFromToday_Incorrect(); + Assert.Equal(new DateTime(2022, 2, 28), result1b); + }); + + // When the current year is a leap year, only the correct method will pass the test on the last day of February. + var clock2 = new VirtualClock(new DateTime(2024, 2, DateTime.DaysInMonth(2024, 2))); + TimeContext.Run(clock2, () => + { + var result2a = GetOneYearFromToday_Correct(); + Assert.Equal(new DateTime(2025, 2, 28), result2a); + + // In an application's unit testing, we'd expect the bad code to fail the test. + // However here we're expecting it to throw, highlighting the value of testing such code. + Assert.Throws(() => + { + var result2b = GetOneYearFromToday_Incorrect(); + }); + }); + } + + [Fact] + public void LeapYearBusinessLogicTest2() + { + // This test demonstrates testing code that is sensitive to leap years. + // Testing actual application code like this might expose leap day bugs before they become an issue. + + DateTime GetBirthdayThisYear_Incorrect(DateTime dateOfBirth) + { + // This shows an incorrect way to get a birthday for the current year. + // It will throw an exception for February 29th birthdays when the current year is not a leap year. + return new DateTime(DateTime.Now.Year, dateOfBirth.Month, dateOfBirth.Day); + } + + DateTime GetBirthdayThisYear_Correct(DateTime dateOfBirth) + { + // This is the correct way to get a birthday for the current year. + // It will treat February 29th birthdays as February 28th when the current year is not a leap year. + return dateOfBirth.AddYears(DateTime.Now.Year - dateOfBirth.Year); + } + + var dateOfBirth = new DateTime(2000, 2, 29); + + // When the current year is a leap year, either method will pass the test. + var clock1 = new VirtualClock(new DateTime(2020, 1, 1)); + TimeContext.Run(clock1, () => + { + var result1a = GetBirthdayThisYear_Correct(dateOfBirth); + Assert.Equal(new DateTime(2020, 2, 29), result1a); + + var result1b = GetBirthdayThisYear_Incorrect(dateOfBirth); + Assert.Equal(new DateTime(2020, 2, 29), result1b); + }); + + // When the current year is not a leap year, only the correct method will pass the test. + var clock2 = new VirtualClock(new DateTime(2021, 1, 1)); + TimeContext.Run(clock2, () => + { + var result2a = GetBirthdayThisYear_Correct(dateOfBirth); + Assert.Equal(new DateTime(2021, 2, 28), result2a); + + // In an application's unit testing, we'd expect the bad code to fail the test. + // However here we're expecting it to throw, highlighting the value of testing such code. + Assert.Throws(() => + { + var result2b = GetBirthdayThisYear_Incorrect(dateOfBirth); + }); + }); + } + + // The following two test classes are used in the LeapYearBusinessLogicTest3 below it. + + private class TestClassWithIncorrectInternalArray + { + // This shows an incorrect way to define an array with values for each day of the current year. + private int[] _values = new int[365]; + + public void AssignTodaysValue(int value) + { + // This will work correctly on most days, but it will throw an exeception when used + // on the last day of a leap year (Day 366). + _values[DateTime.Today.DayOfYear - 1] = value; + } + } + + private class TestClassWithCorrectInternalArray + { + // This shows one way to correctly define an array with values for each day of the current year. + private int[] _values = new int[DateTime.IsLeapYear(DateTime.Today.Year) ? 366 : 365]; + + public void AssignTodaysValue(int value) + { + // This will work correctly for every day, including Day 366. + _values[DateTime.Today.DayOfYear - 1] = value; + } + } + + [Fact] + public void LeapYearBusinessLogicTest3() + { + // This test demonstrates testing code that is sensitive to leap years. + // Testing actual application code like this might expose leap day bugs before they become an issue. + + // When the current year is not a leap year, either method will pass the test. + var clock1 = new VirtualClock(new DateTime(2021, 12, 31)); + TimeContext.Run(clock1, () => + { + var test1 = new TestClassWithCorrectInternalArray(); + test1.AssignTodaysValue(12345); + + var test2 = new TestClassWithIncorrectInternalArray(); + test2.AssignTodaysValue(12345); + }); + + // When the current year is a leap year, only the correct method will pass the test. + var clock2 = new VirtualClock(new DateTime(2024, 12, 31)); + TimeContext.Run(clock2, () => + { + var test1 = new TestClassWithCorrectInternalArray(); + test1.AssignTodaysValue(12345); + + // In an application's unit testing, we'd expect the bad code to fail the test. + // However here we're expecting it to throw, highlighting the value of testing such code. + Assert.Throws(() => + { + var test2 = new TestClassWithIncorrectInternalArray(); + test2.AssignTodaysValue(12345); + }); + }); + } + + [Fact] + public void DaylightSavingTime_SpringForwardTest() + { + // This test demonstrates advancing the clock by whole hours on the day of a DST "spring-forward" transition. + // The time context ensures that the values returned from DateTime.Now are controled by the provided VirtualClock. + // Testing actual application code like this might expose any bugs related to the start of daylight saving time. + + var dt = new DateTime(2020, 3, 8, 0, 0, 0); + var advancement = TimeSpan.FromHours(1); + var clock = new VirtualClock(dt, advancement); + + var stdOffset = s_testTimeZone.GetUtcOffset(new DateTime(2020, 1, 1)); + var dstOffset = s_testTimeZone.GetUtcOffset(new DateTime(2020, 7, 1)); + + TimeContext.Run(clock, s_testTimeZone, () => + { + var firstValue = DateTimeOffset.Now; + var secondValue = DateTimeOffset.Now; + var thirdValue = DateTimeOffset.Now; + + // In the test time zone, DST started on 2020-03-08, when the local time advanced from 1:59:59 to 3:00:00. + // Thus, the 2:00 hour is skipped. Since we are advancing by whole hours, we should not see it in + // consecutive calls to DateTime.Now. + Assert.Equal(new DateTimeOffset(2020, 3, 8, 0, 0, 0, stdOffset), firstValue); + Assert.Equal(new DateTimeOffset(2020, 3, 8, 1, 0, 0, stdOffset), secondValue); + Assert.Equal(new DateTimeOffset(2020, 3, 8, 3, 0, 0, dstOffset), thirdValue); + + // Test the offsets separately also. + Assert.Equal(stdOffset, firstValue.Offset); + Assert.Equal(stdOffset, secondValue.Offset); + Assert.Equal(dstOffset, thirdValue.Offset); + }); + } + + [Fact] + public void DaylightSavingTime_FallBackTest() + { + // This test demonstrates advancing the clock by whole hours on the day of a DST "fall-back" transition. + // The time context ensures that the values returned from DateTime.Now are controled by the provided VirtualClock. + // Testing actual application code like this might expose any bugs related to the end of daylight saving time. + + var dt = new DateTime(2020, 11, 1, 0, 0, 0); + var advancement = TimeSpan.FromHours(1); + var clock = new VirtualClock(dt, advancement); + + var stdOffset = s_testTimeZone.GetUtcOffset(new DateTime(2020, 1, 1)); + var dstOffset = s_testTimeZone.GetUtcOffset(new DateTime(2020, 7, 1)); + + TimeContext.Run(clock, s_testTimeZone, () => + { + var firstValue = DateTimeOffset.Now; + var secondValue = DateTimeOffset.Now; + var thirdValue = DateTimeOffset.Now; + var fourthValue = DateTimeOffset.Now; + + // In the test time zone, DST ended on 2020-11-01, when the local time advanced from 1:59:59 to 1:00:00. + // The 1:00 hour is repeated. Since we are advancing by whole hours, we should see it twice in + // consecutive calls to DateTime.Now. + Assert.Equal(new DateTimeOffset(2020, 11, 1, 0, 0, 0, dstOffset), firstValue); + Assert.Equal(new DateTimeOffset(2020, 11, 1, 1, 0, 0, dstOffset), secondValue); + Assert.Equal(new DateTimeOffset(2020, 11, 1, 1, 0, 0, stdOffset), thirdValue); + Assert.Equal(new DateTimeOffset(2020, 11, 1, 2, 0, 0, stdOffset), fourthValue); + + // Test the offsets separately also. + Assert.Equal(dstOffset, firstValue.Offset); + Assert.Equal(dstOffset, secondValue.Offset); + Assert.Equal(stdOffset, thirdValue.Offset); + Assert.Equal(stdOffset, fourthValue.Offset); + }); + } + + #endregion + + #region Default Values Tests + + [Fact] + public void CurrentClock_IsActualSystemClockByDefault() + { + Assert.True(TimeContext.ActualSystemClockIsActive); + Assert.IsType(TimeContext.Current.Clock); + } + + [Fact] + public void CurrentLocalTimeZone_IsActualSystemLocalTimeZoneByDefault() + { + // Unlike the clock test, we cannot tell if the value from ActualSystemLocalTimeZone + // is indeed the actual system local time zone by testing the value or its type. + // We'll just emit it to the test output for easy inspection if needed. + TimeZoneInfo tz = TimeContext.ActualSystemLocalTimeZone; + _output.WriteLine($"The local system time zone is: [{ tz.Id }] { tz.DisplayName }."); + + // However, we can test if the ActualSystemLocalTimeZoneIsActive property is working. + // Internally this property compares the accessor function rather than the time zone value itself. + Assert.True(TimeContext.ActualSystemLocalTimeZoneIsActive); + } + + #endregion + + #region Multi-threading/task Tests + + [Fact] + public void CanUseTwoDifferentClockInTwoDifferentThreads() + { + var dt1 = new DateTime(2000, 1, 1); + var dt2 = new DateTime(2020, 12, 31); + + var clock1 = new VirtualClock(dt1); + var clock2 = new VirtualClock(dt2); + + var t1 = new Thread(() => + { + TimeContext.Run(clock1, () => + { + var now = DateTime.Now; + Assert.Equal(dt1, now); + }); + }); + + var t2 = new Thread(() => + { + TimeContext.Run(clock2, () => + { + var now = DateTime.Now; + Assert.Equal(dt2, now); + }); + }); + + t1.Start(); + t2.Start(); + + t1.Join(); + t2.Join(); + } + + [Fact] + public async Task CanUseTwoDifferentClockInTwoDifferentTasks() + { + var dt1 = new DateTime(2000, 1, 1); + var dt2 = new DateTime(2020, 12, 31); + + var clock1 = new VirtualClock(dt1); + var clock2 = new VirtualClock(dt2); + + var t1 = new Task(async () => + { + await TimeContext.RunAsync(clock1, async () => + { + await Task.Yield(); + + var now = DateTime.Now; + Assert.Equal(dt1, now); + }); + }); + + var t2 = new Task(async () => + { + await TimeContext.RunAsync(clock2, async () => + { + await Task.Yield(); + + var now = DateTime.Now; + Assert.Equal(dt2, now); + }); + }); + + t1.Start(); + t2.Start(); + + await Task.WhenAll(t1, t2); + } + #endregion + + #region Clock Changing Tests + + [Fact] + public void CurrentClock_CanBeChangedForAnOperation() + { + TimeContext.Run(_testClock, () => + { + Assert.IsType(TimeContext.Current.Clock); + Assert.False(TimeContext.ActualSystemClockIsActive); + }); + } + + [Fact] + public void CurrentClock_IsRestoredAfterAnOperation() + { + TimeContext.Run(_testClock, () => { }); + + Assert.IsType(TimeContext.Current.Clock); + Assert.True(TimeContext.ActualSystemClockIsActive); + } + + [Fact] + public void CurrentClock_CanBeChangedForAnOperationWithResult() + { + int result = TimeContext.Run(_testClock, () => + { + Assert.IsType(TimeContext.Current.Clock); + Assert.False(TimeContext.ActualSystemClockIsActive); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public void CurrentClock_IsRestoredAfterAnOperationWithResult() + { + int result = TimeContext.Run(_testClock, () => 1); + + Assert.IsType(TimeContext.Current.Clock); + Assert.True(TimeContext.ActualSystemClockIsActive); + Assert.Equal(1, result); + } + + [Fact] + public async Task CurrentClock_CanBeChangedForAnAsyncOperation() + { + await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + Assert.IsType(TimeContext.Current.Clock); + Assert.False(TimeContext.ActualSystemClockIsActive); + }); + } + + [Fact] + public async Task CurrentClock_IsRestoredAfterAnAsyncOperation() + { + await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + }); + + Assert.IsType(TimeContext.Current.Clock); + Assert.True(TimeContext.ActualSystemClockIsActive); + } + + [Fact] + public async Task CurrentClock_CanBeChangedForAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + Assert.IsType(TimeContext.Current.Clock); + Assert.False(TimeContext.ActualSystemClockIsActive); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task CurrentClock_IsRestoredAfterAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + return 1; + }); + + Assert.IsType(TimeContext.Current.Clock); + Assert.True(TimeContext.ActualSystemClockIsActive); + Assert.Equal(1, result); + } + + [Fact] + public void DateTimeOffset_UtcNow_UsesTestClockDuringAnOperation() + { + TimeContext.Run(_testClock, () => + { + DateTimeOffset actual = DateTimeOffset.UtcNow; + DateTimeOffset expected = TestClock.Value; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public void DateTimeOffset_Now_UsesTestClockDuringAnOperation() + { + TimeContext.Run(_testClock, () => + { + DateTimeOffset actual = DateTimeOffset.Now; + DateTimeOffset expected = TestClock.Value; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public void DateTime_UtcNow_UsesTestClockDuringAnOperation() + { + TimeContext.Run(_testClock, () => + { + DateTime actual = DateTime.UtcNow; + DateTime expected = TestClock.Value.UtcDateTime; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public void DateTime_Now_UsesTestClockDuringAnOperation() + { + TimeContext.Run(_testClock, () => + { + DateTime actual = DateTime.Now; + DateTime expected = TestClock.Value.LocalDateTime; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public void DateTime_Today_UsesTestClockDuringAnOperation() + { + TimeContext.Run(_testClock, () => + { + DateTime actual = DateTime.Today; + DateTime expected = TestClock.Value.LocalDateTime.Date; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public void DateTimeOffset_UtcNow_UsesTestClockDuringAnOperationWithResult() + { + int result = TimeContext.Run(_testClock, () => + { + DateTimeOffset actual = DateTimeOffset.UtcNow; + DateTimeOffset expected = TestClock.Value; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public void DateTimeOffset_Now_UsesTestClockDuringAnOperationWithResult() + { + int result = TimeContext.Run(_testClock, () => + { + DateTimeOffset actual = DateTimeOffset.Now; + DateTimeOffset expected = TestClock.Value; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public void DateTime_UtcNow_UsesTestClockDuringAnOperationWithResult() + { + int result = TimeContext.Run(_testClock, () => + { + DateTime actual = DateTime.UtcNow; + DateTime expected = TestClock.Value.UtcDateTime; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public void DateTime_Now_UsesTestClockDuringAnOperationWithResult() + { + int result = TimeContext.Run(_testClock, () => + { + DateTime actual = DateTime.Now; + DateTime expected = TestClock.Value.LocalDateTime; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public void DateTime_Today_UsesTestClockDuringAnOperationWithResult() + { + int result = TimeContext.Run(_testClock, () => + { + DateTime actual = DateTime.Today; + DateTime expected = TestClock.Value.LocalDateTime.Date; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task DateTimeOffset_UtcNow_UsesTestClockDuringAnAsyncOperation() + { + await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTimeOffset actual = DateTimeOffset.UtcNow; + DateTimeOffset expected = TestClock.Value; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public async Task DateTimeOffset_Now_UsesTestClockDuringAnAsyncOperation() + { + await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTimeOffset actual = DateTimeOffset.Now; + DateTimeOffset expected = TestClock.Value; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public async Task DateTime_UtcNow_UsesTestClockDuringAnAsyncOperation() + { + await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.UtcNow; + DateTime expected = TestClock.Value.UtcDateTime; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public async Task DateTime_Now_UsesTestClockDuringAnAsyncOperation() + { + await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.Now; + DateTime expected = TestClock.Value.LocalDateTime; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public async Task DateTime_Today_UsesTestClockDuringAnAsyncOperation() + { + await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.Today; + DateTime expected = TestClock.Value.LocalDateTime.Date; + Assert.Equal(expected, actual); + }); + } + + [Fact] + public async Task DateTimeOffset_UtcNow_UsesTestClockDuringAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTimeOffset actual = DateTimeOffset.UtcNow; + DateTimeOffset expected = TestClock.Value; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task DateTimeOffset_Now_UsesTestClockDuringAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTimeOffset actual = DateTimeOffset.Now; + DateTimeOffset expected = TestClock.Value; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task DateTime_UtcNow_UsesTestClockDuringAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.UtcNow; + DateTime expected = TestClock.Value.UtcDateTime; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task DateTime_Now_UsesTestClockDuringAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.Now; + DateTime expected = TestClock.Value.LocalDateTime; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task DateTime_Today_UsesTestClockDuringAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(_testClock, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.Today; + DateTime expected = TestClock.Value.LocalDateTime.Date; + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + #endregion + + #region Local Time Zone Changing Tests + + [Fact] + public void CurrentLocalTimeZone_CanBeChangedForAnOperation() + { + var originalTimeZone = TimeContext.Current.LocalTimeZone; + + TimeContext.Run(s_testTimeZone, () => + { + var localTimeZone = TimeContext.Current.LocalTimeZone; + Assert.Equal(s_testTimeZone, localTimeZone); + Assert.NotEqual(originalTimeZone, localTimeZone); + }); + } + + [Fact] + public void CurrentLocalTimeZone_IsRestoredAfterAnOperation() + { + var originalTimeZone = TimeContext.Current.LocalTimeZone; + + TimeContext.Run(s_testTimeZone, () => { }); + + var localTimeZone = TimeContext.Current.LocalTimeZone; + Assert.Equal(originalTimeZone, localTimeZone); + Assert.NotEqual(s_testTimeZone, localTimeZone); + } + + [Fact] + public void CurrentLocalTimeZone_CanBeChangedForAnOperationWithResult() + { + var originalTimeZone = TimeContext.Current.LocalTimeZone; + + int result = TimeContext.Run(s_testTimeZone, () => + { + var localTimeZone = TimeContext.Current.LocalTimeZone; + Assert.Equal(s_testTimeZone, localTimeZone); + Assert.NotEqual(originalTimeZone, localTimeZone); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public void CurrentLocalTimeZone_IsRestoredAfterAnOperationWithResult() + { + var originalTimeZone = TimeContext.Current.LocalTimeZone; + + int result = TimeContext.Run(s_testTimeZone, () => 1); + + var localTimeZone = TimeContext.Current.LocalTimeZone; + Assert.Equal(originalTimeZone, localTimeZone); + Assert.NotEqual(s_testTimeZone, localTimeZone); + Assert.Equal(1, result); + } + + [Fact] + public async Task CurrentLocalTimeZone_CanBeChangedForAnAsyncOperation() + { + var originalTimeZone = TimeContext.Current.LocalTimeZone; + + await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + var localTimeZone = TimeContext.Current.LocalTimeZone; + Assert.Equal(s_testTimeZone, localTimeZone); + Assert.NotEqual(originalTimeZone, localTimeZone); + }); + } + + [Fact] + public async Task CurrentLocalTimeZone_IsRestoredAfterAnAsyncOperation() + { + var originalTimeZone = TimeContext.Current.LocalTimeZone; + + await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + }); + + var localTimeZone = TimeContext.Current.LocalTimeZone; + Assert.Equal(originalTimeZone, localTimeZone); + Assert.NotEqual(s_testTimeZone, localTimeZone); + } + + [Fact] + public async Task CurrentLocalTimeZone_CanBeChangedForAnAsyncOperationWithResult() + { + var originalTimeZone = TimeContext.Current.LocalTimeZone; + + int result = await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + var localTimeZone = TimeContext.Current.LocalTimeZone; + Assert.Equal(s_testTimeZone, localTimeZone); + Assert.NotEqual(originalTimeZone, localTimeZone); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task CurrentLocalTimeZone_IsRestoredAfterAnAsyncOperationWithResult() + { + var originalTimeZone = TimeContext.Current.LocalTimeZone; + + int result = await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + return 1; + }); + + var localTimeZone = TimeContext.Current.LocalTimeZone; + Assert.Equal(originalTimeZone, localTimeZone); + Assert.NotEqual(s_testTimeZone, localTimeZone); + Assert.Equal(1, result); + } + + [Fact] + public void DateTimeOffset_Now_UsesTestTimeZoneDuringAnOperation() + { + TimeContext.Run(s_testTimeZone, () => + { + DateTimeOffset actual = DateTimeOffset.Now; + DateTimeOffset expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected.Offset, actual.Offset); + }); + } + + [Fact] + public void DateTime_Now_UsesTestTimeZoneDuringAnOperation() + { + TimeContext.Run(s_testTimeZone, () => + { + DateTime actual = DateTime.Now; + DateTime expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected, actual); + }); + } + + [Fact] + public void DateTime_Today_UsesTestTimeZoneDuringAnOperation() + { + TimeContext.Run(s_testTimeZone, () => + { + DateTime actual = DateTime.Today; + DateTime expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected, actual); + }); + } + + [Fact] + public void DateTimeOffset_Now_UsesTestTimeZoneDuringAnOperationWithResult() + { + int result = TimeContext.Run(s_testTimeZone, () => + { + DateTimeOffset actual = DateTimeOffset.Now; + DateTimeOffset expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected.Offset, actual.Offset); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public void DateTime_Now_UsesTestTimeZoneDuringAnOperationWithResult() + { + int result = TimeContext.Run(s_testTimeZone, () => + { + DateTime actual = DateTime.Now; + DateTime expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public void DateTime_Today_UsesTestTimeZoneDuringAnOperationWithResult() + { + int result = TimeContext.Run(s_testTimeZone, () => + { + DateTime actual = DateTime.Today; + DateTime expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task DateTimeOffset_Now_UsesTestTimeZoneDuringAnAsyncOperation() + { + await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + DateTimeOffset actual = DateTimeOffset.Now; + DateTimeOffset expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected.Offset, actual.Offset); + }); + } + + [Fact] + public async Task DateTime_Now_UsesTestTimeZoneDuringAnAsyncOperation() + { + await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.Now; + DateTime expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected, actual); + }); + } + + [Fact] + public async Task DateTime_Today_UsesTestTimeZoneDuringAnAsyncOperation() + { + await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.Today; + DateTime expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected, actual); + }); + } + + [Fact] + public async Task DateTimeOffset_Now_UsesTestTimeZoneDuringAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + DateTimeOffset actual = DateTimeOffset.Now; + DateTimeOffset expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected.Offset, actual.Offset); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task DateTime_Now_UsesTestTimeZoneDuringAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.Now; + DateTime expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + [Fact] + public async Task DateTime_Today_UsesTestTimeZoneDuringAnAsyncOperationWithResult() + { + int result = await TimeContext.RunAsync(s_testTimeZone, async () => + { + await Task.Yield(); + + DateTime actual = DateTime.Today; + DateTime expected = TimeZoneInfo.ConvertTime(actual, s_testTimeZone); + Assert.Equal(expected, actual); + return 1; + }); + + Assert.Equal(1, result); + } + + #endregion + + #region Helper functions + + private static TimeZoneInfo GetTestTimeZone() + { + // Choose a test time zone that is not the actual system time zone. + // We'll use USA time zones that have the same DST rules so we can test some transitions. + bool windows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); + string id1 = windows ? "Eastern Standard Time" : "America/New_York"; + string id2 = windows ? "Central Standard Time" : "America/Chicago"; + string tzid = TimeContext.ActualSystemLocalTimeZone.Id.Equals(id1, StringComparison.OrdinalIgnoreCase) ? id2 : id1; + return TimeZoneInfo.FindSystemTimeZoneById(tzid); + } + + #endregion + } +} diff --git a/src/libraries/System.Runtime/tests/System/Diagnostics/VirtualClockTests.cs b/src/libraries/System.Runtime/tests/System/Diagnostics/VirtualClockTests.cs new file mode 100644 index 0000000000000..2101c72a544c6 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/Diagnostics/VirtualClockTests.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Xunit; + +namespace System.Tests +{ + public class VirtualClockTests + { + [Fact] + public void CanUseFixedDateTimeOffsetValue() + { + var value = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); + TimeClock clock = new VirtualClock(value); + + DateTimeOffset firstResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(value, firstResult); + + DateTimeOffset secondResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(value, secondResult); + + DateTimeOffset thirdResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(value, thirdResult); + } + + [Fact] + public void CanUseFixedDateTimeValue() + { + var value = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + TimeClock clock = new VirtualClock(value); + + DateTime firstResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(value, firstResult); + + DateTime secondResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(value, secondResult); + + DateTime thirdResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(value, thirdResult); + } + + [Fact] + public void CanUseInitialDateTimeOffsetValueAndTimespanIncrement() + { + var initialValue = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); + var increment = TimeSpan.FromHours(1); + TimeClock clock = new VirtualClock(initialValue, increment); + + DateTimeOffset firstResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(initialValue, firstResult); + + DateTimeOffset secondResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(initialValue.Add(increment), secondResult); + + DateTimeOffset thirdResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(initialValue.Add(increment * 2), thirdResult); + } + + [Fact] + public void CanUseInitialDateTimeValueAndTimespanIncrement() + { + var initialValue = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var increment = TimeSpan.FromHours(1); + TimeClock clock = new VirtualClock(initialValue, increment); + + DateTime firstResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(initialValue, firstResult); + + DateTime secondResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(initialValue.Add(increment), secondResult); + + DateTime thirdResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(initialValue.Add(increment * 2), thirdResult); + } + + [Fact] + public void CanUseInitialDateTimeOffsetValueAndFunctionIncrement() + { + var initialValue = new DateTimeOffset(2000, 1, 1, 0, 0, 0, TimeSpan.Zero); + var increment = TimeSpan.FromHours(1); + TimeClock clock = new VirtualClock(initialValue, x => x.Add(increment)); + + DateTimeOffset firstResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(initialValue, firstResult); + + DateTimeOffset secondResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(initialValue.Add(increment), secondResult); + + DateTimeOffset thirdResult = clock.GetCurrentUtcDateTimeOffset(); + Assert.Equal(initialValue.Add(increment * 2), thirdResult); + } + + [Fact] + public void CanUseInitialDateTimeValueAndFunctionIncrement() + { + var initialValue = new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var increment = TimeSpan.FromHours(1); + TimeClock clock = new VirtualClock(initialValue, x => x.Add(increment)); + + DateTime firstResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(initialValue, firstResult); + + DateTime secondResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(initialValue.Add(increment), secondResult); + + DateTime thirdResult = clock.GetCurrentUtcDateTime(); + Assert.Equal(initialValue.Add(increment * 2), thirdResult); + } + } +}