diff --git a/Feature.Flags.props b/Feature.Flags.props index 40304cde5..5abb8a491 100644 --- a/Feature.Flags.props +++ b/Feature.Flags.props @@ -33,6 +33,7 @@ $(DefineConstants);FEATURE_GUID_FORMATPROVIDER $(DefineConstants);FEATURE_RANDOM_ITEMS $(DefineConstants);FEATURE_COMPRESSION_STREAM + $(DefineConstants);FEATURE_STOPWATCH_GETELAPSEDTIME $(DefineConstants);FEATURE_PATH_SPAN $(DefineConstants);FEATURE_FILE_SPAN $(DefineConstants);FEATURE_GUID_V7 diff --git a/Pipeline/Build.Compile.cs b/Pipeline/Build.Compile.cs index 7842f0866..09d8a163e 100644 --- a/Pipeline/Build.Compile.cs +++ b/Pipeline/Build.Compile.cs @@ -120,35 +120,41 @@ partial class Build ReportSummary(s => s .WhenNotNull(SemVer, (summary, semVer) => summary .AddPair("Version", semVer))); - - UpdateReadme(MainVersion!.FileVersion, false); - foreach (var mainProject in MainProjects) + + if (BuildScope != BuildScope.CoreOnly) { - ClearNugetPackages(mainProject.Directory / "bin"); - DotNetBuild(s => s - .SetProjectFile(mainProject) - .SetConfiguration(Configuration) - .EnableNoLogo() - .EnableNoRestore() - .SetProcessAdditionalArguments($"/p:SolutionDir={RootDirectory}/") - .SetVersion(MainVersion.FileVersion + CoreVersion!.PreRelease) - .SetAssemblyVersion(MainVersion.FileVersion) - .SetFileVersion(MainVersion.FileVersion)); + UpdateReadme(MainVersion!.FileVersion, false); + foreach (var mainProject in MainProjects) + { + ClearNugetPackages(mainProject.Directory / "bin"); + DotNetBuild(s => s + .SetProjectFile(mainProject) + .SetConfiguration(Configuration) + .EnableNoLogo() + .EnableNoRestore() + .SetProcessAdditionalArguments($"/p:SolutionDir={RootDirectory}/") + .SetVersion(MainVersion.FileVersion + CoreVersion!.PreRelease) + .SetAssemblyVersion(MainVersion.FileVersion) + .SetFileVersion(MainVersion.FileVersion)); + } } - - UpdateReadme(CoreVersion!.FileVersion, true); - foreach (var coreProject in CoreProjects) + + if (BuildScope != BuildScope.MainOnly) { - ClearNugetPackages(coreProject.Directory / "bin"); - DotNetBuild(s => s - .SetProjectFile(coreProject) - .SetConfiguration(Configuration) - .EnableNoLogo() - .EnableNoRestore() - .SetProcessAdditionalArguments($"/p:SolutionDir={RootDirectory}/") - .SetVersion(CoreVersion.FileVersion + CoreVersion.PreRelease) - .SetAssemblyVersion(CoreVersion.FileVersion) - .SetFileVersion(CoreVersion.FileVersion)); + UpdateReadme(CoreVersion!.FileVersion, true); + foreach (var coreProject in CoreProjects) + { + ClearNugetPackages(coreProject.Directory / "bin"); + DotNetBuild(s => s + .SetProjectFile(coreProject) + .SetConfiguration(Configuration) + .EnableNoLogo() + .EnableNoRestore() + .SetProcessAdditionalArguments($"/p:SolutionDir={RootDirectory}/") + .SetVersion(CoreVersion.FileVersion + CoreVersion.PreRelease) + .SetAssemblyVersion(CoreVersion.FileVersion) + .SetFileVersion(CoreVersion.FileVersion)); + } } }); diff --git a/Pipeline/Build.cs b/Pipeline/Build.cs index 65bdc27a7..f5452a744 100644 --- a/Pipeline/Build.cs +++ b/Pipeline/Build.cs @@ -20,7 +20,7 @@ partial class Build : NukeBuild /// /// Afterward, you can update the package reference in `Directory.Packages.props` and reset this flag. /// - readonly BuildScope BuildScope = BuildScope.Default; + readonly BuildScope BuildScope = BuildScope.CoreOnly; [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index 37222e051..24f2c4c85 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -54,4 +54,10 @@ + + + 255.255.255.255 + + diff --git a/Source/Testably.Abstractions.AccessControl/Testably.Abstractions.AccessControl.csproj b/Source/Testably.Abstractions.AccessControl/Testably.Abstractions.AccessControl.csproj index 3b82ecf6f..8e63c0dfc 100644 --- a/Source/Testably.Abstractions.AccessControl/Testably.Abstractions.AccessControl.csproj +++ b/Source/Testably.Abstractions.AccessControl/Testably.Abstractions.AccessControl.csproj @@ -16,10 +16,14 @@ - + + + + + <_Parameter1>true diff --git a/Source/Testably.Abstractions.Compression/Testably.Abstractions.Compression.csproj b/Source/Testably.Abstractions.Compression/Testably.Abstractions.Compression.csproj index a5c676a9f..d39f6951b 100644 --- a/Source/Testably.Abstractions.Compression/Testably.Abstractions.Compression.csproj +++ b/Source/Testably.Abstractions.Compression/Testably.Abstractions.Compression.csproj @@ -12,10 +12,14 @@ Link="Docs\Compression.md" /> - + + + + + diff --git a/Source/Testably.Abstractions.Interface/ITimeSystem.cs b/Source/Testably.Abstractions.Interface/ITimeSystem.cs index 8f41f8449..e2de801ec 100644 --- a/Source/Testably.Abstractions.Interface/ITimeSystem.cs +++ b/Source/Testably.Abstractions.Interface/ITimeSystem.cs @@ -12,6 +12,11 @@ public interface ITimeSystem /// IDateTime DateTime { get; } + /// + /// Abstractions for . + /// + IStopwatchFactory Stopwatch { get; } + /// /// Abstractions for . /// diff --git a/Source/Testably.Abstractions.Interface/TimeSystem/IStopwatch.cs b/Source/Testably.Abstractions.Interface/TimeSystem/IStopwatch.cs new file mode 100644 index 000000000..67a7ea58d --- /dev/null +++ b/Source/Testably.Abstractions.Interface/TimeSystem/IStopwatch.cs @@ -0,0 +1,34 @@ +using System; +using System.Diagnostics; + +namespace Testably.Abstractions.TimeSystem; + +/// +/// Abstractions for . +/// +public interface IStopwatch : ITimeSystemEntity +{ + /// + TimeSpan Elapsed { get; } + + /// + long ElapsedMilliseconds { get; } + + /// + long ElapsedTicks { get; } + + /// + bool IsRunning { get; } + + /// + void Reset(); + + /// + void Restart(); + + /// + void Start(); + + /// + void Stop(); +} diff --git a/Source/Testably.Abstractions.Interface/TimeSystem/IStopwatchFactory.cs b/Source/Testably.Abstractions.Interface/TimeSystem/IStopwatchFactory.cs new file mode 100644 index 000000000..320f8df7e --- /dev/null +++ b/Source/Testably.Abstractions.Interface/TimeSystem/IStopwatchFactory.cs @@ -0,0 +1,40 @@ +using System.Diagnostics; +#if FEATURE_STOPWATCH_GETELAPSEDTIME +using System; +#endif + +namespace Testably.Abstractions.TimeSystem; + +/// +/// Factory for abstracting creation of . +/// +public interface IStopwatchFactory : ITimeSystemEntity +{ + /// + long Frequency { get; } + + /// + bool IsHighResolution { get; } + + /// + long GetTimestamp(); + + /// + IStopwatch New(); + + /// + IStopwatch StartNew(); + + /// + /// Wraps the to the testable . + /// + IStopwatch Wrap(Stopwatch stopwatch); + +#if FEATURE_STOPWATCH_GETELAPSEDTIME + /// + TimeSpan GetElapsedTime(long startingTimestamp); + + /// + TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp); +#endif +} diff --git a/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs b/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs index e39ca8d08..7ffcccefa 100644 --- a/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs +++ b/Source/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs @@ -153,6 +153,9 @@ internal static NotSupportedException NotSupportedSafeFileHandle() => new( "You cannot mock a safe file handle in the mocked file system without registering a strategy explicitly. Use `MockFileSystem.WithSafeFileHandleStrategy`!"); + internal static NotSupportedException NotSupportedStopwatchWrapping() + => new("You cannot wrap an existing Stopwatch in the MockTimeSystem instance!"); + internal static NotSupportedException NotSupportedTimerWrapping() => new("You cannot wrap an existing Timer in the MockTimeSystem instance!"); diff --git a/Source/Testably.Abstractions.Testing/MockTimeSystem.cs b/Source/Testably.Abstractions.Testing/MockTimeSystem.cs index 5eee0bc3d..85715efa3 100644 --- a/Source/Testably.Abstractions.Testing/MockTimeSystem.cs +++ b/Source/Testably.Abstractions.Testing/MockTimeSystem.cs @@ -25,13 +25,14 @@ public INotificationHandler On /// /// The handler for mocked timers. /// - public ITimerHandler TimerHandler => _timerMock; + public ITimerHandler TimerHandler => _timerFactoryMock; private readonly NotificationHandler _callbackHandler; private readonly DateTimeMock _dateTimeMock; + private readonly StopwatchFactoryMock _stopwatchFactoryMock; private readonly TaskMock _taskMock; private readonly ThreadMock _threadMock; - private readonly TimerFactoryMock _timerMock; + private readonly TimerFactoryMock _timerFactoryMock; /// /// Initializes the with a random time. @@ -55,9 +56,10 @@ public MockTimeSystem(ITimeProvider timeProvider) TimeProvider = timeProvider; _callbackHandler = new NotificationHandler(); _dateTimeMock = new DateTimeMock(this, _callbackHandler); + _stopwatchFactoryMock = new StopwatchFactoryMock(this); _threadMock = new ThreadMock(this, _callbackHandler); _taskMock = new TaskMock(this, _callbackHandler); - _timerMock = new TimerFactoryMock(this); + _timerFactoryMock = new TimerFactoryMock(this); } #region ITimeSystem Members @@ -66,6 +68,10 @@ public MockTimeSystem(ITimeProvider timeProvider) public IDateTime DateTime => _dateTimeMock; + /// + public IStopwatchFactory Stopwatch + => _stopwatchFactoryMock; + /// public ITask Task => _taskMock; @@ -76,7 +82,7 @@ public IThread Thread /// public ITimerFactory Timer - => _timerMock; + => _timerFactoryMock; #endregion @@ -90,7 +96,7 @@ public override string ToString() /// The timer strategy. public MockTimeSystem WithTimerStrategy(ITimerStrategy timerStrategy) { - _timerMock.SetTimerStrategy(timerStrategy); + _timerFactoryMock.SetTimerStrategy(timerStrategy); return this; } } diff --git a/Source/Testably.Abstractions.Testing/Testably.Abstractions.Testing.csproj b/Source/Testably.Abstractions.Testing/Testably.Abstractions.Testing.csproj index 1c3d8c96b..78ab9aca9 100644 --- a/Source/Testably.Abstractions.Testing/Testably.Abstractions.Testing.csproj +++ b/Source/Testably.Abstractions.Testing/Testably.Abstractions.Testing.csproj @@ -9,10 +9,14 @@ - + + + + + <_Parameter1>true diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchFactoryMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchFactoryMock.cs new file mode 100644 index 000000000..78155c810 --- /dev/null +++ b/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchFactoryMock.cs @@ -0,0 +1,66 @@ +using System; +using System.Diagnostics; +using Testably.Abstractions.Testing.Helpers; +using Testably.Abstractions.TimeSystem; +using IStopwatch = Testably.Abstractions.TimeSystem.IStopwatch; + +namespace Testably.Abstractions.Testing.TimeSystem; + +internal sealed class StopwatchFactoryMock : IStopwatchFactory +{ + private readonly MockTimeSystem _mockTimeSystem; + + internal StopwatchFactoryMock(MockTimeSystem timeSystem) + { + _mockTimeSystem = timeSystem; + } + + #region IStopwatchFactory Members + + /// + public long Frequency => TimeSpan.TicksPerSecond; + + /// + public bool IsHighResolution => true; + + /// + public ITimeSystem TimeSystem + => _mockTimeSystem; + +#if FEATURE_STOPWATCH_GETELAPSEDTIME + /// + public TimeSpan GetElapsedTime(long startingTimestamp) + => GetElapsedTime(startingTimestamp, GetTimestamp()); +#endif + +#if FEATURE_STOPWATCH_GETELAPSEDTIME + /// + public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) + => TimeSpan.FromTicks(endingTimestamp - startingTimestamp); +#endif + + /// + public long GetTimestamp() + => _mockTimeSystem.TimeProvider.Read().Ticks; + + /// + public IStopwatch New() + { + StopwatchMock stopwatchMock = new(_mockTimeSystem); + return stopwatchMock; + } + + /// + public IStopwatch StartNew() + { + IStopwatch stopwatch = New(); + stopwatch.Start(); + return stopwatch; + } + + /// + public IStopwatch Wrap(Stopwatch stopwatch) + => throw ExceptionFactory.NotSupportedStopwatchWrapping(); + + #endregion +} diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchMock.cs new file mode 100644 index 000000000..f043d4b73 --- /dev/null +++ b/Source/Testably.Abstractions.Testing/TimeSystem/StopwatchMock.cs @@ -0,0 +1,85 @@ +using System; +using Testably.Abstractions.TimeSystem; + +namespace Testably.Abstractions.Testing.TimeSystem; + +internal sealed class StopwatchMock : IStopwatch +{ + private long _elapsedTicks; + private readonly MockTimeSystem _mockTimeSystem; + private DateTime? _start; + + internal StopwatchMock(MockTimeSystem timeSystem) + { + _mockTimeSystem = timeSystem; + } + + #region IStopwatch Members + + /// + public TimeSpan Elapsed + => new(ElapsedTicks); + + /// + public long ElapsedMilliseconds + => ElapsedTicks / TimeSpan.TicksPerMillisecond; + + /// + public long ElapsedTicks + { + get + { + long timeElapsed = _elapsedTicks; + + // If the Stopwatch is running, add elapsed time since the Stopwatch is started last time. + if (_start is not null) + { + timeElapsed += (_mockTimeSystem.TimeProvider.Read() - _start.Value).Ticks; + } + + return timeElapsed; + } + } + + /// + public bool IsRunning => _start is not null; + + /// + public ITimeSystem TimeSystem + => _mockTimeSystem; + + /// + public void Reset() + { + Stop(); + _elapsedTicks = 0; + } + + /// + public void Restart() + { + Reset(); + Start(); + } + + /// + public void Start() + { + if (_start is null) + { + _start = _mockTimeSystem.TimeProvider.Read(); + } + } + + /// + public void Stop() + { + if (_start.HasValue) + { + _elapsedTicks += (_mockTimeSystem.TimeProvider.Read() - _start.Value).Ticks; + _start = null; + } + } + + #endregion +} diff --git a/Source/Testably.Abstractions.Testing/TimeSystem/TimerFactoryMock.cs b/Source/Testably.Abstractions.Testing/TimeSystem/TimerFactoryMock.cs index 3d24b5301..d85f1284c 100644 --- a/Source/Testably.Abstractions.Testing/TimeSystem/TimerFactoryMock.cs +++ b/Source/Testably.Abstractions.Testing/TimeSystem/TimerFactoryMock.cs @@ -66,7 +66,7 @@ public ITimer New(TimerCallback callback, object? state, TimeSpan dueTime, TimeS return RegisterTimerMock(timerMock); } - /// + /// public ITimer Wrap(Timer timer) => throw ExceptionFactory.NotSupportedTimerWrapping(); diff --git a/Source/Testably.Abstractions/RealTimeSystem.cs b/Source/Testably.Abstractions/RealTimeSystem.cs index 93fcb5d6f..348e32913 100644 --- a/Source/Testably.Abstractions/RealTimeSystem.cs +++ b/Source/Testably.Abstractions/RealTimeSystem.cs @@ -16,6 +16,7 @@ public sealed class RealTimeSystem : ITimeSystem public RealTimeSystem() { DateTime = new DateTimeWrapper(this); + Stopwatch = new StopwatchFactory(this); Task = new TaskWrapper(this); Thread = new ThreadWrapper(this); Timer = new TimerFactory(this); @@ -26,6 +27,9 @@ public RealTimeSystem() /// public IDateTime DateTime { get; } + /// + public IStopwatchFactory Stopwatch { get; } + /// public ITask Task { get; } diff --git a/Source/Testably.Abstractions/Testably.Abstractions.csproj b/Source/Testably.Abstractions/Testably.Abstractions.csproj index 89d14fb68..28e8a3b28 100644 --- a/Source/Testably.Abstractions/Testably.Abstractions.csproj +++ b/Source/Testably.Abstractions/Testably.Abstractions.csproj @@ -20,7 +20,7 @@ - + diff --git a/Source/Testably.Abstractions/TimeSystem/StopwatchFactory.cs b/Source/Testably.Abstractions/TimeSystem/StopwatchFactory.cs new file mode 100644 index 000000000..52808fd55 --- /dev/null +++ b/Source/Testably.Abstractions/TimeSystem/StopwatchFactory.cs @@ -0,0 +1,57 @@ +using System.Diagnostics; +#if FEATURE_STOPWATCH_GETELAPSEDTIME +using System; +#endif + +namespace Testably.Abstractions.TimeSystem; + +internal sealed class StopwatchFactory : IStopwatchFactory +{ + internal StopwatchFactory(RealTimeSystem timeSystem) + { + TimeSystem = timeSystem; + } + + #region IStopwatchFactory Members + + /// + public long Frequency + => Stopwatch.Frequency; + + /// + public bool IsHighResolution + => Stopwatch.IsHighResolution; + + /// + public ITimeSystem TimeSystem { get; } + +#if FEATURE_STOPWATCH_GETELAPSEDTIME + /// + public TimeSpan GetElapsedTime(long startingTimestamp) + => Stopwatch.GetElapsedTime(startingTimestamp); +#endif + +#if FEATURE_STOPWATCH_GETELAPSEDTIME + /// + public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp) + => Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp); +#endif + + /// + public long GetTimestamp() + => Stopwatch.GetTimestamp(); + + /// + public IStopwatch New() + => Wrap(new Stopwatch()); + + /// + public IStopwatch StartNew() + => Wrap(Stopwatch.StartNew()); + + /// + public IStopwatch Wrap(Stopwatch stopwatch) + => new StopwatchWrapper(TimeSystem, stopwatch); + + #endregion +} diff --git a/Source/Testably.Abstractions/TimeSystem/StopwatchWrapper.cs b/Source/Testably.Abstractions/TimeSystem/StopwatchWrapper.cs new file mode 100644 index 000000000..c08b2ff76 --- /dev/null +++ b/Source/Testably.Abstractions/TimeSystem/StopwatchWrapper.cs @@ -0,0 +1,54 @@ +using System; +using System.Diagnostics; + +namespace Testably.Abstractions.TimeSystem; + +internal sealed class StopwatchWrapper : IStopwatch +{ + private readonly Stopwatch _stopwatch; + + internal StopwatchWrapper(ITimeSystem timeSystem, Stopwatch stopwatch) + { + TimeSystem = timeSystem; + _stopwatch = stopwatch; + } + + #region IStopwatch Members + + /// + public TimeSpan Elapsed + => _stopwatch.Elapsed; + + /// + public long ElapsedMilliseconds + => _stopwatch.ElapsedMilliseconds; + + /// + public long ElapsedTicks + => _stopwatch.ElapsedTicks; + + /// + public bool IsRunning + => _stopwatch.IsRunning; + + /// + public ITimeSystem TimeSystem { get; } + + /// + public void Reset() + => _stopwatch.Reset(); + + /// + public void Restart() + => _stopwatch.Restart(); + + /// + public void Start() + => _stopwatch.Start(); + + /// + public void Stop() + => _stopwatch.Stop(); + + #endregion +} diff --git a/Source/Testably.Abstractions/TimeSystem/TimerFactory.cs b/Source/Testably.Abstractions/TimeSystem/TimerFactory.cs index 9f2708fa6..6106fc586 100644 --- a/Source/Testably.Abstractions/TimeSystem/TimerFactory.cs +++ b/Source/Testably.Abstractions/TimeSystem/TimerFactory.cs @@ -35,7 +35,7 @@ public ITimer New(TimerCallback callback, object? state, long dueTime, long peri public ITimer New(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) => Wrap(new Timer(callback, state, dueTime, period)); - /// + /// public ITimer Wrap(Timer timer) => new TimerWrapper(TimeSystem, timer); } diff --git a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net10.0.txt b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net10.0.txt index 1d47ccc57..be88a6a3e 100644 --- a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net10.0.txt +++ b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net10.0.txt @@ -67,6 +67,7 @@ namespace Testably.Abstractions public interface ITimeSystem { Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } Testably.Abstractions.TimeSystem.ITimerFactory Timer { get; } @@ -140,6 +141,28 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Elapsed { get; } + long ElapsedMilliseconds { get; } + long ElapsedTicks { get; } + bool IsRunning { get; } + void Reset(); + void Restart(); + void Start(); + void Stop(); + } + public interface IStopwatchFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + long Frequency { get; } + bool IsHighResolution { get; } + System.TimeSpan GetElapsedTime(long startingTimestamp); + System.TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp); + long GetTimestamp(); + Testably.Abstractions.TimeSystem.IStopwatch New(); + Testably.Abstractions.TimeSystem.IStopwatch StartNew(); + Testably.Abstractions.TimeSystem.IStopwatch Wrap(System.Diagnostics.Stopwatch stopwatch); + } public interface ITask : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.Threading.Tasks.Task Delay(System.TimeSpan delay); diff --git a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net6.0.txt b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net6.0.txt index 708bb8a04..d51411606 100644 --- a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net6.0.txt +++ b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net6.0.txt @@ -49,6 +49,7 @@ namespace Testably.Abstractions public interface ITimeSystem { Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } Testably.Abstractions.TimeSystem.ITimerFactory Timer { get; } @@ -104,6 +105,26 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Elapsed { get; } + long ElapsedMilliseconds { get; } + long ElapsedTicks { get; } + bool IsRunning { get; } + void Reset(); + void Restart(); + void Start(); + void Stop(); + } + public interface IStopwatchFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + long Frequency { get; } + bool IsHighResolution { get; } + long GetTimestamp(); + Testably.Abstractions.TimeSystem.IStopwatch New(); + Testably.Abstractions.TimeSystem.IStopwatch StartNew(); + Testably.Abstractions.TimeSystem.IStopwatch Wrap(System.Diagnostics.Stopwatch stopwatch); + } public interface ITask : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.Threading.Tasks.Task Delay(System.TimeSpan delay); diff --git a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net8.0.txt b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net8.0.txt index 944543004..25decbaca 100644 --- a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net8.0.txt +++ b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net8.0.txt @@ -58,6 +58,7 @@ namespace Testably.Abstractions public interface ITimeSystem { Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } Testably.Abstractions.TimeSystem.ITimerFactory Timer { get; } @@ -122,6 +123,28 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Elapsed { get; } + long ElapsedMilliseconds { get; } + long ElapsedTicks { get; } + bool IsRunning { get; } + void Reset(); + void Restart(); + void Start(); + void Stop(); + } + public interface IStopwatchFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + long Frequency { get; } + bool IsHighResolution { get; } + System.TimeSpan GetElapsedTime(long startingTimestamp); + System.TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp); + long GetTimestamp(); + Testably.Abstractions.TimeSystem.IStopwatch New(); + Testably.Abstractions.TimeSystem.IStopwatch StartNew(); + Testably.Abstractions.TimeSystem.IStopwatch Wrap(System.Diagnostics.Stopwatch stopwatch); + } public interface ITask : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.Threading.Tasks.Task Delay(System.TimeSpan delay); diff --git a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net9.0.txt b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net9.0.txt index 830147a3a..f9a857b7a 100644 --- a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net9.0.txt +++ b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_net9.0.txt @@ -60,6 +60,7 @@ namespace Testably.Abstractions public interface ITimeSystem { Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } Testably.Abstractions.TimeSystem.ITimerFactory Timer { get; } @@ -126,6 +127,28 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Elapsed { get; } + long ElapsedMilliseconds { get; } + long ElapsedTicks { get; } + bool IsRunning { get; } + void Reset(); + void Restart(); + void Start(); + void Stop(); + } + public interface IStopwatchFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + long Frequency { get; } + bool IsHighResolution { get; } + System.TimeSpan GetElapsedTime(long startingTimestamp); + System.TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp); + long GetTimestamp(); + Testably.Abstractions.TimeSystem.IStopwatch New(); + Testably.Abstractions.TimeSystem.IStopwatch StartNew(); + Testably.Abstractions.TimeSystem.IStopwatch Wrap(System.Diagnostics.Stopwatch stopwatch); + } public interface ITask : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.Threading.Tasks.Task Delay(System.TimeSpan delay); diff --git a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_netstandard2.0.txt b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_netstandard2.0.txt index 41ed63b15..9bd955d47 100644 --- a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_netstandard2.0.txt +++ b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_netstandard2.0.txt @@ -36,6 +36,7 @@ namespace Testably.Abstractions public interface ITimeSystem { Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } Testably.Abstractions.TimeSystem.ITimerFactory Timer { get; } @@ -78,6 +79,26 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Elapsed { get; } + long ElapsedMilliseconds { get; } + long ElapsedTicks { get; } + bool IsRunning { get; } + void Reset(); + void Restart(); + void Start(); + void Stop(); + } + public interface IStopwatchFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + long Frequency { get; } + bool IsHighResolution { get; } + long GetTimestamp(); + Testably.Abstractions.TimeSystem.IStopwatch New(); + Testably.Abstractions.TimeSystem.IStopwatch StartNew(); + Testably.Abstractions.TimeSystem.IStopwatch Wrap(System.Diagnostics.Stopwatch stopwatch); + } public interface ITask : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.Threading.Tasks.Task Delay(System.TimeSpan delay); diff --git a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_netstandard2.1.txt b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_netstandard2.1.txt index 6ecb287a4..7c05321e4 100644 --- a/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_netstandard2.1.txt +++ b/Tests/Api/Testably.Abstractions.Core.Api.Tests/Expected/Testably.Abstractions.Interface_netstandard2.1.txt @@ -45,6 +45,7 @@ namespace Testably.Abstractions public interface ITimeSystem { Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } Testably.Abstractions.TimeSystem.ITimerFactory Timer { get; } @@ -96,6 +97,26 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Elapsed { get; } + long ElapsedMilliseconds { get; } + long ElapsedTicks { get; } + bool IsRunning { get; } + void Reset(); + void Restart(); + void Start(); + void Stop(); + } + public interface IStopwatchFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + long Frequency { get; } + bool IsHighResolution { get; } + long GetTimestamp(); + Testably.Abstractions.TimeSystem.IStopwatch New(); + Testably.Abstractions.TimeSystem.IStopwatch StartNew(); + Testably.Abstractions.TimeSystem.IStopwatch Wrap(System.Diagnostics.Stopwatch stopwatch); + } public interface ITask : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.Threading.Tasks.Task Delay(System.TimeSpan delay); diff --git a/Tests/Helpers/Testably.Abstractions.TestHelpers/Testably.Abstractions.TestHelpers.csproj b/Tests/Helpers/Testably.Abstractions.TestHelpers/Testably.Abstractions.TestHelpers.csproj index d4cfbcbd3..1aaeba546 100644 --- a/Tests/Helpers/Testably.Abstractions.TestHelpers/Testably.Abstractions.TestHelpers.csproj +++ b/Tests/Helpers/Testably.Abstractions.TestHelpers/Testably.Abstractions.TestHelpers.csproj @@ -7,10 +7,17 @@ - + + + + + + + + diff --git a/Tests/Testably.Abstractions.Parity.Tests/ParityTests.cs b/Tests/Testably.Abstractions.Parity.Tests/ParityTests.cs index be73eb6d1..869678fb2 100644 --- a/Tests/Testably.Abstractions.Parity.Tests/ParityTests.cs +++ b/Tests/Testably.Abstractions.Parity.Tests/ParityTests.cs @@ -155,6 +155,22 @@ public async Task await That(parityErrors).IsEmpty(); } + [Fact] + public async Task + IStopwatchAndIStopwatchFactory_EnsureParityWith_Stopwatch() + { + List parityErrors = Parity.Stopwatch + .GetErrorsToInstanceType( + typeof(Stopwatch), + testOutputHelper); + parityErrors.AddRange(Parity.Stopwatch + .GetErrorsToStaticType( + typeof(Stopwatch), + testOutputHelper)); + + await That(parityErrors).IsEmpty(); + } + [Fact] public async Task IZipArchive_EnsureParityWith_ZipArchive() { diff --git a/Tests/Testably.Abstractions.Parity.Tests/TestHelpers/Parity.cs b/Tests/Testably.Abstractions.Parity.Tests/TestHelpers/Parity.cs index bfc197af4..f6b6edeb9 100644 --- a/Tests/Testably.Abstractions.Parity.Tests/TestHelpers/Parity.cs +++ b/Tests/Testably.Abstractions.Parity.Tests/TestHelpers/Parity.cs @@ -75,6 +75,11 @@ public class Parity public ParityCheck Random { get; } = new(); + public ParityCheck Stopwatch { get; } = new(excludeMethods: + [ + typeof(Stopwatch).GetMethod(nameof(ToString)), + ]); + public ParityCheck Timer { get; } = new(excludeMethods: [ typeof(Timer).GetMethod(nameof(System.Threading.Timer.Change), [ diff --git a/Tests/Testably.Abstractions.Parity.Tests/Testably.Abstractions.Parity.Tests.csproj b/Tests/Testably.Abstractions.Parity.Tests/Testably.Abstractions.Parity.Tests.csproj index e49edcab8..a38cf0b0d 100644 --- a/Tests/Testably.Abstractions.Parity.Tests/Testably.Abstractions.Parity.Tests.csproj +++ b/Tests/Testably.Abstractions.Parity.Tests/Testably.Abstractions.Parity.Tests.csproj @@ -1,12 +1,15 @@  - - + + + + + diff --git a/Tests/Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj b/Tests/Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj index 44fe15096..d0780630e 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj +++ b/Tests/Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj @@ -13,12 +13,19 @@ - + + + + + + + + True diff --git a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/StopwatchFactoryMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/StopwatchFactoryMockTests.cs new file mode 100644 index 000000000..c7f73a04c --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/StopwatchFactoryMockTests.cs @@ -0,0 +1,18 @@ +using System.Diagnostics; + +namespace Testably.Abstractions.Testing.Tests.TimeSystem; + +public class StopwatchFactoryMockTests +{ + [Fact] + public async Task Wrap_ShouldThrowNotSupportedException() + { + MockTimeSystem timeSystem = new(); + + void Act() + => timeSystem.Stopwatch.Wrap(new Stopwatch()); + + await That(Act).ThrowsExactly() + .WithMessage("You cannot wrap an existing Stopwatch in the MockTimeSystem instance!"); + } +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/StopwatchMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/StopwatchMockTests.cs new file mode 100644 index 000000000..63c28b341 --- /dev/null +++ b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/StopwatchMockTests.cs @@ -0,0 +1,23 @@ +using Testably.Abstractions.TimeSystem; + +namespace Testably.Abstractions.Testing.Tests.TimeSystem; + +public class StopwatchMockTests +{ + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(15)] + [InlineData(123456789000)] + public async Task ShouldSupportTicksPrecision(long delayTicks) + { + MockTimeSystem timeSystem = new(); + IStopwatch stopwatch = timeSystem.Stopwatch.StartNew(); + await timeSystem.Task.Delay(TimeSpan.FromTicks(delayTicks), + TestContext.Current.CancellationToken); + + long elapsedTicks = stopwatch.ElapsedTicks; + + await That(elapsedTicks).IsEqualTo(delayTicks); + } +} diff --git a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerFactoryMockTests.cs b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerFactoryMockTests.cs index a5c6645bb..caae83d4b 100644 --- a/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerFactoryMockTests.cs +++ b/Tests/Testably.Abstractions.Testing.Tests/TimeSystem/TimerFactoryMockTests.cs @@ -33,11 +33,10 @@ public async Task Wrap_ShouldThrowNotSupportedException() { MockTimeSystem timeSystem = new(); - Exception? exception = Record.Exception(() => - { - timeSystem.Timer.Wrap(new Timer(_ => { })); - }); + void Act() + => timeSystem.Timer.Wrap(new Timer(_ => { })); - await That(exception).IsExactly(); + await That(Act).ThrowsExactly() + .WithMessage("You cannot wrap an existing Timer in the MockTimeSystem instance!"); } } diff --git a/Tests/Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj b/Tests/Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj index 88ddce542..f5c5913fd 100644 --- a/Tests/Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj +++ b/Tests/Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj @@ -1,13 +1,20 @@  - + + + + + + + + true Generated diff --git a/Tests/Testably.Abstractions.Tests/TimeSystem/StopwatchFactoryTests.cs b/Tests/Testably.Abstractions.Tests/TimeSystem/StopwatchFactoryTests.cs new file mode 100644 index 000000000..bb462ebe6 --- /dev/null +++ b/Tests/Testably.Abstractions.Tests/TimeSystem/StopwatchFactoryTests.cs @@ -0,0 +1,93 @@ +using System.Diagnostics; + +namespace Testably.Abstractions.Tests.TimeSystem; + +[TimeSystemTests] +public class StopwatchFactoryTests +{ + [Fact] + public async Task Frequency_ShouldReturnValueOfAtLeastTicksPerSecond() + { + var expectedMinimum = TimeSystem is RealTimeSystem + ? Stopwatch.Frequency + : TimeSpan.TicksPerSecond; + long frequency = TimeSystem.Stopwatch.Frequency; + + await That(frequency).IsGreaterThanOrEqualTo(expectedMinimum); + } + +#if FEATURE_STOPWATCH_GETELAPSEDTIME + [Theory] + [InlineData(50L, 80L, 30L)] + [InlineData(80L, 50L, -30L)] + [InlineData(40L, 40L, 0L)] + public async Task GetElapsedTime_WithStartingAndEndingTimestamp_ShouldReturnValue( + long startingTimestamp, long endingTimestamp, long expectedTicks) + { + TimeSpan timestamp = + TimeSystem.Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp); + + await That(timestamp.Ticks).IsEqualTo(expectedTicks); + } +#endif + +#if FEATURE_STOPWATCH_GETELAPSEDTIME + [Theory] + [InlineData(10L)] + [InlineData(1000L)] + [InlineData(100000L)] + public async Task GetElapsedTime_WithStartingTimestamp_ShouldReturnValue(long expectedTicks) + { + long endingTimestamp = TimeSystem.Stopwatch.GetTimestamp(); + long startingTimestamp = endingTimestamp - expectedTicks; + TimeSpan timestamp = + TimeSystem.Stopwatch.GetElapsedTime(startingTimestamp); + + await That(timestamp.Ticks).IsGreaterThanOrEqualTo(expectedTicks); + } +#endif + + [Fact] + public async Task GetTimestamp_AfterDelay_ShouldBeDifferent() + { + long timestamp1 = TimeSystem.Stopwatch.GetTimestamp(); + + await TimeSystem.Task.Delay(10, TestContext.Current.CancellationToken); + + long timestamp2 = TimeSystem.Stopwatch.GetTimestamp(); + await That(timestamp2).IsGreaterThan(timestamp1); + } + + [Fact] + public async Task GetTimestamp_ShouldReturnValue() + { + long timestamp = TimeSystem.Stopwatch.GetTimestamp(); + + await That(timestamp).IsGreaterThan(0); + } + + [Fact] + public async Task IsHighResolution_ShouldReturnTrue() + { + bool expected = TimeSystem is MockTimeSystem || Stopwatch.IsHighResolution; + bool isHighResolution = TimeSystem.Stopwatch.IsHighResolution; + + await That(isHighResolution).IsEqualTo(expected); + } + + [Fact] + public async Task New_ShouldCreateNotRunningStopwatch() + { + IStopwatch stopwatch = TimeSystem.Stopwatch.New(); + + await That(stopwatch.IsRunning).IsFalse(); + } + + [Fact] + public async Task StartNew_ShouldCreateRunningStopwatch() + { + IStopwatch stopwatch = TimeSystem.Stopwatch.StartNew(); + + await That(stopwatch.IsRunning).IsTrue(); + } +} diff --git a/Tests/Testably.Abstractions.Tests/TimeSystem/StopwatchTests.cs b/Tests/Testably.Abstractions.Tests/TimeSystem/StopwatchTests.cs new file mode 100644 index 000000000..2a457ee1c --- /dev/null +++ b/Tests/Testably.Abstractions.Tests/TimeSystem/StopwatchTests.cs @@ -0,0 +1,155 @@ +using Testably.Abstractions.TimeSystem; + +namespace Testably.Abstractions.Tests.TimeSystem; + +[TimeSystemTests] +public partial class StopwatchTests +{ + [Fact] + public async Task Elapsed_ShouldInitializeToZero() + { + IStopwatch stopwatch = TimeSystem.Stopwatch.New(); + TimeSpan elapsed = stopwatch.Elapsed; + + await That(elapsed).IsEqualTo(TimeSpan.Zero); + } + + [Fact] + public async Task Elapsed_WhenRunning_ShouldIncreaseValue() + { + TimeSpan delay = TimeSpan.FromMilliseconds(100); + IStopwatch stopwatch = TimeSystem.Stopwatch.New(); + stopwatch.Start(); + + TimeSpan elapsedBefore = stopwatch.Elapsed; + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + TimeSpan elapsedAfter = stopwatch.Elapsed; + + await That(elapsedAfter).IsGreaterThanOrEqualTo(elapsedBefore + delay); + } + + [Fact] + public async Task Elapsed_WhenStopped_ShouldRemainUnchanged() + { + TimeSpan delay = TimeSpan.FromMilliseconds(100); + IStopwatch stopwatch = TimeSystem.Stopwatch.StartNew(); + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + stopwatch.Stop(); + + TimeSpan elapsedBefore = stopwatch.Elapsed; + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + TimeSpan elapsedAfter = stopwatch.Elapsed; + + await That(elapsedAfter).IsEqualTo(elapsedBefore); + } + + [Fact] + public async Task ElapsedMilliseconds_ShouldBeEqualToElapsed() + { + TimeSpan delay = TimeSpan.FromMilliseconds(100); + IStopwatch stopwatch = TimeSystem.Stopwatch.StartNew(); + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + + stopwatch.Stop(); + TimeSpan elapsed = stopwatch.Elapsed; + long elapsedMilliseconds = stopwatch.ElapsedMilliseconds; + + await That(elapsedMilliseconds).IsEqualTo(elapsed.TotalMilliseconds); + } + + [Fact] + public async Task ElapsedTicks_ShouldBeEqualToElapsed() + { + TimeSpan delay = TimeSpan.FromMilliseconds(100); + IStopwatch stopwatch = TimeSystem.Stopwatch.StartNew(); + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + + stopwatch.Stop(); + TimeSpan elapsed = stopwatch.Elapsed; + long elapsedTicks = stopwatch.ElapsedTicks; + + await That(elapsedTicks).IsEqualTo(elapsed.Ticks); + } + + [Fact] + public async Task Reset_ShouldResetElapsedAndStop() + { + TimeSpan delay = TimeSpan.FromMilliseconds(100); + IStopwatch stopwatch = TimeSystem.Stopwatch.StartNew(); + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + + stopwatch.Reset(); + TimeSpan elapsed = stopwatch.Elapsed; + + await That(elapsed.TotalMilliseconds).IsEqualTo(0); + await That(stopwatch.IsRunning).IsFalse(); + } + + [Fact] + public async Task Restart_ShouldResetElapsedAndSetIsRunningToTrue() + { + TimeSpan delay = TimeSpan.FromMilliseconds(100); + IStopwatch stopwatch = TimeSystem.Stopwatch.StartNew(); + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + stopwatch.Stop(); + + stopwatch.Restart(); + TimeSpan elapsed = stopwatch.Elapsed; + + await That(elapsed.TotalMilliseconds).IsLessThan(100); + await That(stopwatch.IsRunning).IsTrue(); + } + + [Fact] + public async Task Start_ShouldSetIsRunningToTrue() + { + IStopwatch stopwatch = TimeSystem.Stopwatch.New(); + + await That(stopwatch.IsRunning).IsFalse(); + + stopwatch.Start(); + + await That(stopwatch.IsRunning).IsTrue(); + } + + [Fact] + public async Task Start_WhenStarted_ShouldDoNothing() + { + IStopwatch stopwatch = TimeSystem.Stopwatch.New(); + stopwatch.Start(); + + stopwatch.Start(); + + await That(stopwatch.IsRunning).IsTrue(); + } + + [Fact] + public async Task Stop_ShouldSetIsRunningToFalse() + { + TimeSpan delay = TimeSpan.FromMilliseconds(100); + IStopwatch stopwatch = TimeSystem.Stopwatch.StartNew(); + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + + stopwatch.Stop(); + TimeSpan elapsed = stopwatch.Elapsed; + + await That(elapsed.TotalMilliseconds).IsGreaterThanOrEqualTo(100); + await That(stopwatch.IsRunning).IsFalse(); + } + + [Fact] + public async Task Stop_WhenStopped_ShouldDoNothing() + { + TimeSpan delay = TimeSpan.FromMilliseconds(100); + IStopwatch stopwatch = TimeSystem.Stopwatch.StartNew(); + await TimeSystem.Task.Delay(delay, TestContext.Current.CancellationToken); + + stopwatch.Stop(); + TimeSpan elapsed1 = stopwatch.Elapsed; + stopwatch.Stop(); + TimeSpan elapsed2 = stopwatch.Elapsed; + + await That(elapsed2).IsEqualTo(elapsed1); + await That(stopwatch.IsRunning).IsFalse(); + } +}