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);
+ }
+ }
+}