diff --git a/DotNetThoughts.sln b/DotNetThoughts.sln index 87dcb5e..eea63eb 100644 --- a/DotNetThoughts.sln +++ b/DotNetThoughts.sln @@ -11,10 +11,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetThoughts.TimeKeeping. EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TimeKeeping", "TimeKeeping", "{695E363E-055E-4ED5-9613-587E02A67578}" ProjectSection(SolutionItems) = preProject - ReadMe.md = ReadMe.md + TimeKeeping\ReadMe.md = TimeKeeping\ReadMe.md EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetThoughts.LocalTimeKit", "TimeKeeping\DotNetThoughts.LocalTimeKit\DotNetThoughts.LocalTimeKit.csproj", "{03DA1023-FDF4-4EF5-8951-19806434F8CA}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetThoughts.LocalTimeKit", "TimeKeeping\DotNetThoughts.LocalTimeKit\DotNetThoughts.LocalTimeKit.csproj", "{03DA1023-FDF4-4EF5-8951-19806434F8CA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DotNetThoughts.Results", "Results\DotNetThoughts.Results\DotNetThoughts.Results.csproj", "{7032EE8C-FC18-4610-B7D6-613E552C278D}" EndProject @@ -52,7 +52,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ReadMe.md = ReadMe.md EndProjectSection EndProject - Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -71,6 +70,10 @@ Global {DF62C221-71AD-49A7-A9D8-48E31F06E27B}.Debug|Any CPU.Build.0 = Debug|Any CPU {DF62C221-71AD-49A7-A9D8-48E31F06E27B}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF62C221-71AD-49A7-A9D8-48E31F06E27B}.Release|Any CPU.Build.0 = Release|Any CPU + {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Release|Any CPU.Build.0 = Release|Any CPU {7032EE8C-FC18-4610-B7D6-613E552C278D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7032EE8C-FC18-4610-B7D6-613E552C278D}.Debug|Any CPU.Build.0 = Debug|Any CPU {7032EE8C-FC18-4610-B7D6-613E552C278D}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -103,10 +106,6 @@ Global {E0E86A0B-F4CE-4E88-99CF-C54AFC1C0C58}.Debug|Any CPU.Build.0 = Debug|Any CPU {E0E86A0B-F4CE-4E88-99CF-C54AFC1C0C58}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0E86A0B-F4CE-4E88-99CF-C54AFC1C0C58}.Release|Any CPU.Build.0 = Release|Any CPU - {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU - {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU - {03DA1023-FDF4-4EF5-8951-19806434F8CA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -115,6 +114,7 @@ Global {6B9CCBB9-73D3-4062-8317-5D54CF6B85D4} = {695E363E-055E-4ED5-9613-587E02A67578} {917A0D7B-F7FA-4D39-B892-B9C1BA9BE284} = {695E363E-055E-4ED5-9613-587E02A67578} {DF62C221-71AD-49A7-A9D8-48E31F06E27B} = {695E363E-055E-4ED5-9613-587E02A67578} + {03DA1023-FDF4-4EF5-8951-19806434F8CA} = {695E363E-055E-4ED5-9613-587E02A67578} {7032EE8C-FC18-4610-B7D6-613E552C278D} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} {8CA73623-9009-4FEF-885E-84F9BBD61536} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} {02C498D0-532A-4F1D-9004-7E2E24121DF3} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} @@ -123,7 +123,6 @@ Global {66DCA5D2-4A06-4F1A-8DBB-36118CC01A34} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} {3D9F5F06-E09D-46B4-8058-1458467E0252} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} {E0E86A0B-F4CE-4E88-99CF-C54AFC1C0C58} = {BECD3795-EDD5-459E-BFCC-017082E1C34D} - {03DA1023-FDF4-4EF5-8951-19806434F8CA} = {695E363E-055E-4ED5-9613-587E02A67578} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {CAA5B6BA-B6C8-4A90-B9E6-D7634295532E} diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping.App/Pages/Timer.razor b/TimeKeeping/DotNetThoughts.TimeKeeping.App/Pages/Timer.razor index 29b481b..d38e1a9 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping.App/Pages/Timer.razor +++ b/TimeKeeping/DotNetThoughts.TimeKeeping.App/Pages/Timer.razor @@ -52,7 +52,7 @@ private void UpdateUI() { - systemTime = clock.UtcNow(); + systemTime = clock.Now(); realTime = DateTimeOffset.UtcNow; localSystemTime = new LocalDateTime(systemTime.DateTime, TimeZoneInfo.Utc).ToTimeZone(TimeZoneInfo.Local).ToDateTimeOffsetLocal(); nairobiLocalSystemTime = new LocalDateTime(systemTime.DateTime, TimeZoneInfo.Utc).ToTimeZone(TimeZoneInfo.FindSystemTimeZoneById(selectedTimeZone)).ToDateTimeOffsetLocal(); @@ -139,7 +139,7 @@ private void SetBaseLine() { - clock.SetBaseline(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero)); + clock.SetNow(new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero)); UpdateUI(); } diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/TimeTravelersClockTests.cs b/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/TimeTravelersClockTests.cs index c1da058..df2eb0f 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/TimeTravelersClockTests.cs +++ b/TimeKeeping/DotNetThoughts.TimeKeeping.Tests/TimeTravelersClockTests.cs @@ -11,7 +11,7 @@ public void FreezeTime() var sut = new TimeTravelersClock(); sut.Freeze(frozenTime); sut.IsFrozen().Should().BeTrue(); - sut.UtcNow().Should().Be(frozenTime); + sut.Now().Should().Be(frozenTime); } [Fact] public void FreezeAgainOverridesCurrentFreeze() @@ -22,7 +22,7 @@ public void FreezeAgainOverridesCurrentFreeze() sut.Freeze(frozenTime); sut.Freeze(newFrozenTime); sut.IsFrozen().Should().BeTrue(); - sut.UtcNow().Should().Be(newFrozenTime); + sut.Now().Should().Be(newFrozenTime); } [Fact] @@ -31,8 +31,8 @@ public void FreezeCurrentTime() var sut = new TimeTravelersClock(); sut.Freeze(); sut.IsFrozen().Should().BeTrue(); - sut.UtcNow().Should().BeCloseTo(DateTimeOffset.Now, _allowedDeviation); - sut.UtcNow().Should().NotBeAfter(DateTimeOffset.Now); + sut.Now().Should().BeCloseTo(DateTimeOffset.Now, _allowedDeviation); + sut.Now().Should().NotBeAfter(DateTimeOffset.Now); } [Fact] @@ -44,12 +44,12 @@ public void ResetFrozenTimeShouldRevertEverythingToNormal() sut.Freeze(frozenTime); // check arrange sut.IsFrozen().Should().BeTrue(); - sut.UtcNow().Should().Be(frozenTime); + sut.Now().Should().Be(frozenTime); // Act sut.Reset(); // Assert sut.IsFrozen().Should().BeFalse(); - sut.UtcNow().Should().BeCloseTo(DateTimeOffset.Now, _allowedDeviation); + sut.Now().Should().BeCloseTo(DateTimeOffset.Now, _allowedDeviation); } [Fact] @@ -58,10 +58,10 @@ public void BaselineMocksNow() var sut = new TimeTravelersClock(); var now = DateTimeOffset.Now; var baseLine = now.AddDays(-1); - sut.SetBaseline(baseLine); - sut.UtcNow().Should().BeCloseTo(baseLine, _allowedDeviation); + sut.SetNow(baseLine); + sut.Now().Should().BeCloseTo(baseLine, _allowedDeviation); var slept = Sleep(1000); - sut.UtcNow().Should().BeCloseTo(baseLine.Add(slept), _allowedDeviation); + sut.Now().Should().BeCloseTo(baseLine.Add(slept), _allowedDeviation); } [Fact] @@ -71,7 +71,7 @@ public void AdvanceFrozenTime() var frozenTime = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); sut.Freeze(frozenTime); sut.Advance(TimeSpan.FromDays(1)); - sut.UtcNow().Should().Be(new DateTimeOffset(2021, 1, 2, 0, 0, 0, TimeSpan.Zero)); + sut.Now().Should().Be(new DateTimeOffset(2021, 1, 2, 0, 0, 0, TimeSpan.Zero)); } [Fact] @@ -79,7 +79,7 @@ public void AdvanceLiveTime() { var sut = new TimeTravelersClock(); sut.Advance(TimeSpan.FromDays(1)); - sut.UtcNow().Should().BeCloseTo(DateTimeOffset.Now.AddDays(1), _allowedDeviation); + sut.Now().Should().BeCloseTo(DateTimeOffset.Now.AddDays(1), _allowedDeviation); } [Fact] @@ -91,7 +91,7 @@ public void AdvanceFrozenTimeAndThenThaw() sut.Advance(TimeSpan.FromDays(1)); sut.Thaw(); var slept = Sleep(1000); - sut.UtcNow().Should().BeCloseTo(frozenTime.AddDays(1).Add(slept), _allowedDeviation); + sut.Now().Should().BeCloseTo(frozenTime.AddDays(1).Add(slept), _allowedDeviation); } [Fact] @@ -100,16 +100,16 @@ public void AComplicatedTest() var sut = new TimeTravelersClock(); var baseLine = new DateTimeOffset(2021, 1, 1, 0, 0, 0, TimeSpan.Zero); var timer = new Stopwatch(); timer.Start(); - sut.SetBaseline(baseLine); - sut.UtcNow().Should().BeCloseTo(baseLine.Add(timer.Elapsed), _allowedDeviation); + sut.SetNow(baseLine); + sut.Now().Should().BeCloseTo(baseLine.Add(timer.Elapsed), _allowedDeviation); Sleep(20); - sut.UtcNow().Should().BeCloseTo(baseLine.Add(timer.Elapsed), _allowedDeviation); + sut.Now().Should().BeCloseTo(baseLine.Add(timer.Elapsed), _allowedDeviation); var frozen = sut.Freeze(); - sut.UtcNow().Should().Be(frozen); + sut.Now().Should().Be(frozen); var sleptWhileFrozen = Sleep(2000); sut.Thaw(); Sleep(1000); - sut.UtcNow().Should().BeCloseTo(baseLine.Add(timer.Elapsed).Add(-sleptWhileFrozen), _allowedDeviation); + sut.Now().Should().BeCloseTo(baseLine.Add(timer.Elapsed).Add(-sleptWhileFrozen), _allowedDeviation); } private static TimeSpan Sleep(int milliseconds) diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping/DotNetThoughts.TimeKeeping.csproj b/TimeKeeping/DotNetThoughts.TimeKeeping/DotNetThoughts.TimeKeeping.csproj index 18987c9..b8b6657 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping/DotNetThoughts.TimeKeeping.csproj +++ b/TimeKeeping/DotNetThoughts.TimeKeeping/DotNetThoughts.TimeKeeping.csproj @@ -1,6 +1,6 @@  - 1.2.0 + 1.3.0 True MIT diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping/SystemTime.cs b/TimeKeeping/DotNetThoughts.TimeKeeping/SystemTime.cs index b26e989..7bc5a3a 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping/SystemTime.cs +++ b/TimeKeeping/DotNetThoughts.TimeKeeping/SystemTime.cs @@ -7,6 +7,9 @@ public class SystemTime { private static AsyncLocal _asyncLocalState = new(); + /// + /// Returns the underlying instance. + /// public static TimeTravelersClock Clock { get @@ -17,60 +20,114 @@ public static TimeTravelersClock Clock } } - public static DateTimeOffset SetBaseline(DateTimeOffset baseLine) + /// + /// See + /// + /// + /// + public static DateTimeOffset SetNow(DateTimeOffset baseLine) { - return Clock.SetBaseline(baseLine); + return Clock.SetNow(baseLine); } + /// + /// See + /// + /// + /// public static DateTimeOffset SetOffset(TimeSpan offset) { return Clock.SetOffset(offset); } + + /// + /// See + /// + /// + /// public static DateTimeOffset Advance(TimeSpan timeSpan) { return Clock.Advance(timeSpan); } + /// + /// See + /// + /// + /// public static DateTimeOffset AdvanceDays(double days) { return Clock.AdvanceDays(days); } - public static DateTimeOffset UtcNow() + /// + /// See + /// + /// + public static DateTimeOffset Now() { - return Clock.UtcNow(); + return Clock.Now(); } - public static DateTimeOffset Freeze(DateTimeOffset baseLine) + /// + /// See + /// + /// + /// + public static DateTimeOffset Freeze(DateTimeOffset now) { - return Clock.Freeze(baseLine); + return Clock.Freeze(now); } + /// + /// See + /// + /// public static DateTimeOffset Freeze() { return Clock.Freeze(); } + /// + /// See + /// + /// public static bool IsFrozen() { return Clock.IsFrozen(); } + /// + /// See + /// + /// public static DateTimeOffset Reset() { return Clock.Reset(); } + /// + /// See + /// + /// public static DateTimeOffset Thaw() { return Clock.Thaw(); } - public static void From(TimeTravelersClock xSystemTime) + /// + /// See + /// + /// + public static void SyncWith(TimeTravelersClock otherTimeTravelersClock) { - Clock.From(xSystemTime); + Clock.SyncWith(otherTimeTravelersClock); } + /// + /// See + /// + /// public static bool IsManipulated() { return Clock.IsManipulated(); diff --git a/TimeKeeping/DotNetThoughts.TimeKeeping/TimeTravelersClock.cs b/TimeKeeping/DotNetThoughts.TimeKeeping/TimeTravelersClock.cs index 952146c..2469e67 100644 --- a/TimeKeeping/DotNetThoughts.TimeKeeping/TimeTravelersClock.cs +++ b/TimeKeeping/DotNetThoughts.TimeKeeping/TimeTravelersClock.cs @@ -1,26 +1,54 @@ namespace DotNetThoughts.TimeKeeping; +/// +/// A clock that can be manipulated to simulate time travel and time freezing. +/// public class TimeTravelersClock { + /// + /// Don't use this. It is internal stuff. Only read or write as part of serialization. + /// public DateTimeOffset? FreezedAt { get; set; } + + /// + /// Don't use this. It is internal stuff. Only read or write as part of serialization. + /// public DateTimeOffset? Frozen { get; set; } + + /// + /// Don't use this. It is internal stuff. Only read or write as part of serialization. + /// public TimeSpan Offset { get; set; } = TimeSpan.Zero; + /// + /// Returns true if the clock is currently frozen and time is standing still, otherwise false. + /// When time is frozen, a series of calls to Now() will return the same value. + /// public bool IsFrozen() { using var context = new OperationContext(); return FreezedAt != null; } + /// + /// Resets the clock to the current time and removes any time manipulation. + /// This means the clock is not frozen and time is not offset. + /// + /// Now() public DateTimeOffset Reset() { using var context = new OperationContext(); Frozen = null; FreezedAt = null; Offset = TimeSpan.Zero; - return UtcNow(); + return Now(); } + /// + /// Thaws the clock if it is currently frozen. + /// + /// Now() + /// If you're trying to that a clock that is not frozen. public DateTimeOffset Thaw() { using var context = new OperationContext(); @@ -31,39 +59,66 @@ public DateTimeOffset Thaw() Offset = Offset.Add(-RealTimeFrozen()); FreezedAt = null; Frozen = null; - return UtcNow(); + return Now(); } + /// + /// Stops time from moving forward. + /// A series of calls to Now() will return the same value. + /// + /// Now() public DateTimeOffset Freeze() { using var context = new OperationContext(); - FreezedAt = context.UtcNow(); - Frozen = UtcNow(); + FreezedAt = OperationContext.Now(); + Frozen = Now(); return Frozen.Value; } - public DateTimeOffset Freeze(DateTimeOffset baseLine) + /// + /// Stops time from moving forward. + /// A series of calls to Now() will return the passed time `now`. + /// + /// The time the clock supposedly was when frozen. + /// Now() + public DateTimeOffset Freeze(DateTimeOffset now) { using var context = new OperationContext(); Reset(); - SetBaseline(baseLine); - FreezedAt = context.UtcNow(); - Frozen = UtcNow(); + SetNow(now); + FreezedAt = OperationContext.Now(); + Frozen = Now(); return Frozen.Value; } - public DateTimeOffset UtcNow() + /// + /// Returns the current time, manipulated or not. + /// + /// The time of the time travelers clock + public DateTimeOffset Now() { using var context = new OperationContext(); - return Frozen ?? context.UtcNow().Add(Offset); + return Frozen ?? OperationContext.Now().Add(Offset); } + /// + /// Shortcut for advancing the clock by a number of days. + /// Works on both frozen and unfrozen clocks. + /// + /// + /// Now() public DateTimeOffset AdvanceDays(double days) { using var context = new OperationContext(); return Advance(TimeSpan.FromDays(days)); } + /// + /// Advances the clock by a given time span. + /// Works on both frozen and unfrozen clocks. + /// + /// + /// Now() public DateTimeOffset Advance(TimeSpan timeSpan) { using var context = new OperationContext(); @@ -72,50 +127,70 @@ public DateTimeOffset Advance(TimeSpan timeSpan) { Frozen = Frozen!.Value.Add(timeSpan); } - return UtcNow(); + return Now(); } - public DateTimeOffset SetBaseline(DateTimeOffset baseLine) + /// + /// Instead of advancing the clock, this method sets the clock to a specific time. + /// An unfrozen clock will still be unfrozen, and a frozen clock will still be frozen, but frozen at the given time. + /// + /// + /// Now() + /// + public DateTimeOffset SetNow(DateTimeOffset now) { if (IsFrozen()) throw new InvalidOperationException(); using var context = new OperationContext(); - Offset = baseLine - context.UtcNow(); - return UtcNow(); + Offset = now - OperationContext.Now(); + return Now(); } + /// + /// Sets the clocks offset (offset to real world clock) to a specific time span. + /// + /// + /// + /// Invalid operation on a frozen clock! (I forgot why) public DateTimeOffset SetOffset(TimeSpan offset) { if (IsFrozen()) throw new InvalidOperationException(); using var context = new OperationContext(); Offset = offset; - return UtcNow(); + return Now(); } private TimeSpan RealTimeFrozen() { using var context = new OperationContext(); - return FreezedAt != null ? context.UtcNow() - FreezedAt.Value : TimeSpan.Zero; + return FreezedAt != null ? OperationContext.Now() - FreezedAt.Value : TimeSpan.Zero; } - internal void From(TimeTravelersClock xSystemTime) + /// + /// Syncs this clock with another time travelers clock. + /// + public void SyncWith(TimeTravelersClock otherTimeTravelersClock) { using var context = new OperationContext(); - Offset = xSystemTime.Offset; - FreezedAt = xSystemTime.FreezedAt; - Frozen = xSystemTime.Frozen; + Offset = otherTimeTravelersClock.Offset; + FreezedAt = otherTimeTravelersClock.FreezedAt; + Frozen = otherTimeTravelersClock.Frozen; } - internal bool IsManipulated() + /// + /// Returns true if the clock is currently manipulated in any way. (frozen or offset) + /// + /// + public bool IsManipulated() { return Offset != TimeSpan.Zero || FreezedAt != null; } internal class OperationContext : IDisposable { - private static AsyncLocal _operationNow = new AsyncLocal(); + private static readonly AsyncLocal _operationNow = new AsyncLocal(); - private bool _shouldReset = false; + private readonly bool _shouldReset = false; public OperationContext() { @@ -133,6 +208,6 @@ public void Dispose() } } - public DateTimeOffset UtcNow() => _operationNow.Value!.Value; + public static DateTimeOffset Now() => _operationNow.Value!.Value; } } \ No newline at end of file diff --git a/TimeKeeping/ReadMe.md b/TimeKeeping/ReadMe.md index e69de29..4fcbd1c 100644 --- a/TimeKeeping/ReadMe.md +++ b/TimeKeeping/ReadMe.md @@ -0,0 +1,33 @@ +# Time Keeping + +## SystemTime + +`SystemTime.Now()` is a drop-in replacement for `DateTimeOffset.UtcNow` with a couple of extra features. + +To begin with, you can manipulate time using SystemTime. +This is useful for testing, where you want to test how your code behave at different times, or where you want the code to be deterministic. +`SystemTime` works through `AsyncLocal`, so you do not have to inject a `ITimeProvider` wherever you need to be able to mock time. + +Some example usages: + +```csharp +// Set time to midnight 2024-01-01 (utc I guess) and stop time from moving. +// This means SystemTime.Now() will always return 2024-01-01 +SystemTime.Freeze(DateTimeOffset.Parse("2024-01-01")); + +// Move time one day forward. Time is still frozen. +// This means SystemTime.Now() will always return 2024-01-02 +SystemTime.Advance(TimeSpan.FromDays(1)); + +// Unfreezes time. Time is now 2024-01-02 but time started moving again. +// This means SystemTime.Now() will work as normal, but at an offset to the real-world time. +SystemTime.Thaw(); + +// Reset time to real world time. +SystemTime.Reset(); +``` + + +## TimeTravelersClock + +TimeTravelersClock is SystemTime but without the AsyncLocal magic. You can instantiate a new TimeTravelsersClock and manipulate it just like SystemTime. \ No newline at end of file