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