From 3c5f4b26700fe50c43314177dae620001fcf3713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 13 Mar 2026 15:28:08 +0100 Subject: [PATCH 1/2] feat: add support for `PeriodicTimer` in abstractions --- Feature.Flags.props | 1 + .../ITimeSystem.cs | 7 ++++ .../TimeSystem/IPeriodicTimer.cs | 19 +++++++++ .../TimeSystem/IPeriodicTimerFactory.cs | 20 ++++++++++ .../Testably.Abstractions/RealTimeSystem.cs | 10 +++++ .../TimeSystem/PeriodicTimerFactory.cs | 29 ++++++++++++++ .../TimeSystem/PeriodicTimerWrapper.cs | 40 +++++++++++++++++++ .../Testably.Abstractions_net10.0.txt | 1 + .../Expected/Testably.Abstractions_net8.0.txt | 1 + .../Expected/Testably.Abstractions_net9.0.txt | 1 + ...estably.Abstractions.Interface_net10.0.txt | 11 +++++ ...Testably.Abstractions.Interface_net8.0.txt | 11 +++++ ...Testably.Abstractions.Interface_net9.0.txt | 11 +++++ .../ParityTests.cs | 35 ++++++++++------ .../TestHelpers/Parity.cs | 10 +++++ 15 files changed, 194 insertions(+), 13 deletions(-) create mode 100644 Source/Testably.Abstractions.Interface/TimeSystem/IPeriodicTimer.cs create mode 100644 Source/Testably.Abstractions.Interface/TimeSystem/IPeriodicTimerFactory.cs create mode 100644 Source/Testably.Abstractions/TimeSystem/PeriodicTimerFactory.cs create mode 100644 Source/Testably.Abstractions/TimeSystem/PeriodicTimerWrapper.cs diff --git a/Feature.Flags.props b/Feature.Flags.props index 5abb8a49..36b5ccda 100644 --- a/Feature.Flags.props +++ b/Feature.Flags.props @@ -34,6 +34,7 @@ $(DefineConstants);FEATURE_RANDOM_ITEMS $(DefineConstants);FEATURE_COMPRESSION_STREAM $(DefineConstants);FEATURE_STOPWATCH_GETELAPSEDTIME + $(DefineConstants);FEATURE_PERIODIC_TIMER $(DefineConstants);FEATURE_PATH_SPAN $(DefineConstants);FEATURE_FILE_SPAN $(DefineConstants);FEATURE_GUID_V7 diff --git a/Source/Testably.Abstractions.Interface/ITimeSystem.cs b/Source/Testably.Abstractions.Interface/ITimeSystem.cs index e2de801e..9b3ed5fc 100644 --- a/Source/Testably.Abstractions.Interface/ITimeSystem.cs +++ b/Source/Testably.Abstractions.Interface/ITimeSystem.cs @@ -12,6 +12,13 @@ public interface ITimeSystem /// IDateTime DateTime { get; } +#if FEATURE_PERIODIC_TIMER + /// + /// Abstractions for . + /// + IPeriodicTimerFactory PeriodicTimer { get; } +#endif + /// /// Abstractions for . /// diff --git a/Source/Testably.Abstractions.Interface/TimeSystem/IPeriodicTimer.cs b/Source/Testably.Abstractions.Interface/TimeSystem/IPeriodicTimer.cs new file mode 100644 index 00000000..2f260798 --- /dev/null +++ b/Source/Testably.Abstractions.Interface/TimeSystem/IPeriodicTimer.cs @@ -0,0 +1,19 @@ +#if FEATURE_PERIODIC_TIMER +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Testably.Abstractions.TimeSystem; + +/// +/// Abstractions for . +/// +public interface IPeriodicTimer : ITimeSystemEntity, IDisposable +{ + /// + TimeSpan Period { get; set; } + + /// + ValueTask WaitForNextTickAsync(CancellationToken cancellationToken = default); +} +#endif diff --git a/Source/Testably.Abstractions.Interface/TimeSystem/IPeriodicTimerFactory.cs b/Source/Testably.Abstractions.Interface/TimeSystem/IPeriodicTimerFactory.cs new file mode 100644 index 00000000..2c5c03b8 --- /dev/null +++ b/Source/Testably.Abstractions.Interface/TimeSystem/IPeriodicTimerFactory.cs @@ -0,0 +1,20 @@ +#if FEATURE_PERIODIC_TIMER +using System; +using System.Threading; + +namespace Testably.Abstractions.TimeSystem; + +/// +/// Factory for abstracting creation of . +/// +public interface IPeriodicTimerFactory : ITimeSystemEntity +{ + /// + IPeriodicTimer New(TimeSpan period); + + /// + /// Wraps the to the testable . + /// + IPeriodicTimer Wrap(PeriodicTimer timer); +} +#endif diff --git a/Source/Testably.Abstractions/RealTimeSystem.cs b/Source/Testably.Abstractions/RealTimeSystem.cs index 348e3291..396831ea 100644 --- a/Source/Testably.Abstractions/RealTimeSystem.cs +++ b/Source/Testably.Abstractions/RealTimeSystem.cs @@ -16,6 +16,9 @@ public sealed class RealTimeSystem : ITimeSystem public RealTimeSystem() { DateTime = new DateTimeWrapper(this); +#if FEATURE_PERIODIC_TIMER + PeriodicTimer = new PeriodicTimerFactory(this); +#endif Stopwatch = new StopwatchFactory(this); Task = new TaskWrapper(this); Thread = new ThreadWrapper(this); @@ -27,6 +30,13 @@ public RealTimeSystem() /// public IDateTime DateTime { get; } +#if FEATURE_PERIODIC_TIMER + /// + /// Abstractions for . + /// + public IPeriodicTimerFactory PeriodicTimer { get; } +#endif + /// public IStopwatchFactory Stopwatch { get; } diff --git a/Source/Testably.Abstractions/TimeSystem/PeriodicTimerFactory.cs b/Source/Testably.Abstractions/TimeSystem/PeriodicTimerFactory.cs new file mode 100644 index 00000000..cb68a707 --- /dev/null +++ b/Source/Testably.Abstractions/TimeSystem/PeriodicTimerFactory.cs @@ -0,0 +1,29 @@ +#if FEATURE_PERIODIC_TIMER +using System; +using System.Threading; + +namespace Testably.Abstractions.TimeSystem; + +internal sealed class PeriodicTimerFactory : IPeriodicTimerFactory +{ + internal PeriodicTimerFactory(RealTimeSystem timeSystem) + { + TimeSystem = timeSystem; + } + + #region IPeriodicTimerFactory Members + + /// + public ITimeSystem TimeSystem { get; } + + /// + public IPeriodicTimer New(TimeSpan period) + => Wrap(new PeriodicTimer(period)); + + /// + public IPeriodicTimer Wrap(PeriodicTimer timer) + => new PeriodicTimerWrapper(TimeSystem, timer); + + #endregion +} +#endif diff --git a/Source/Testably.Abstractions/TimeSystem/PeriodicTimerWrapper.cs b/Source/Testably.Abstractions/TimeSystem/PeriodicTimerWrapper.cs new file mode 100644 index 00000000..c7a718e9 --- /dev/null +++ b/Source/Testably.Abstractions/TimeSystem/PeriodicTimerWrapper.cs @@ -0,0 +1,40 @@ +#if FEATURE_PERIODIC_TIMER +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Testably.Abstractions.TimeSystem; + +internal sealed class PeriodicTimerWrapper : IPeriodicTimer +{ + private readonly PeriodicTimer _periodicTimer; + + internal PeriodicTimerWrapper(ITimeSystem timeSystem, PeriodicTimer periodicTimer) + { + TimeSystem = timeSystem; + _periodicTimer = periodicTimer; + } + + #region IPeriodicTimer Members + + /// + public TimeSpan Period + { + get => _periodicTimer.Period; + set => _periodicTimer.Period = value; + } + + /// + public ITimeSystem TimeSystem { get; } + + /// + public void Dispose() + => _periodicTimer.Dispose(); + + /// + public ValueTask WaitForNextTickAsync(CancellationToken cancellationToken = default) + => _periodicTimer.WaitForNextTickAsync(cancellationToken); + + #endregion +} +#endif diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net10.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net10.0.txt index 61b9410e..62263d28 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net10.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net10.0.txt @@ -27,6 +27,7 @@ namespace Testably.Abstractions { public RealTimeSystem() { } public Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + public Testably.Abstractions.TimeSystem.IPeriodicTimerFactory PeriodicTimer { get; } public Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } public Testably.Abstractions.TimeSystem.ITask Task { get; } public Testably.Abstractions.TimeSystem.IThread Thread { get; } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net8.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net8.0.txt index 525e9347..3773771e 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net8.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net8.0.txt @@ -27,6 +27,7 @@ namespace Testably.Abstractions { public RealTimeSystem() { } public Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + public Testably.Abstractions.TimeSystem.IPeriodicTimerFactory PeriodicTimer { get; } public Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } public Testably.Abstractions.TimeSystem.ITask Task { get; } public Testably.Abstractions.TimeSystem.IThread Thread { get; } diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net9.0.txt b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net9.0.txt index 3b58557c..8bd2fd1b 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net9.0.txt +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Expected/Testably.Abstractions_net9.0.txt @@ -27,6 +27,7 @@ namespace Testably.Abstractions { public RealTimeSystem() { } public Testably.Abstractions.TimeSystem.IDateTime DateTime { get; } + public Testably.Abstractions.TimeSystem.IPeriodicTimerFactory PeriodicTimer { get; } public Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } public Testably.Abstractions.TimeSystem.ITask Task { get; } public Testably.Abstractions.TimeSystem.IThread Thread { get; } 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 be88a6a3..97f9f720 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.IPeriodicTimerFactory PeriodicTimer { get; } Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } @@ -141,6 +142,16 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IPeriodicTimer : System.IDisposable, Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Period { get; set; } + System.Threading.Tasks.ValueTask WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken = default); + } + public interface IPeriodicTimerFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + Testably.Abstractions.TimeSystem.IPeriodicTimer New(System.TimeSpan period); + Testably.Abstractions.TimeSystem.IPeriodicTimer Wrap(System.Threading.PeriodicTimer timer); + } public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.TimeSpan Elapsed { get; } 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 25decbac..d4bec9fe 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.IPeriodicTimerFactory PeriodicTimer { get; } Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } @@ -123,6 +124,16 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IPeriodicTimer : System.IDisposable, Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Period { get; set; } + System.Threading.Tasks.ValueTask WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken = default); + } + public interface IPeriodicTimerFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + Testably.Abstractions.TimeSystem.IPeriodicTimer New(System.TimeSpan period); + Testably.Abstractions.TimeSystem.IPeriodicTimer Wrap(System.Threading.PeriodicTimer timer); + } public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.TimeSpan Elapsed { get; } 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 f9a857b7..ed0cf769 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.IPeriodicTimerFactory PeriodicTimer { get; } Testably.Abstractions.TimeSystem.IStopwatchFactory Stopwatch { get; } Testably.Abstractions.TimeSystem.ITask Task { get; } Testably.Abstractions.TimeSystem.IThread Thread { get; } @@ -127,6 +128,16 @@ namespace Testably.Abstractions.TimeSystem System.DateTime UnixEpoch { get; } System.DateTime UtcNow { get; } } + public interface IPeriodicTimer : System.IDisposable, Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + System.TimeSpan Period { get; set; } + System.Threading.Tasks.ValueTask WaitForNextTickAsync(System.Threading.CancellationToken cancellationToken = default); + } + public interface IPeriodicTimerFactory : Testably.Abstractions.TimeSystem.ITimeSystemEntity + { + Testably.Abstractions.TimeSystem.IPeriodicTimer New(System.TimeSpan period); + Testably.Abstractions.TimeSystem.IPeriodicTimer Wrap(System.Threading.PeriodicTimer timer); + } public interface IStopwatch : Testably.Abstractions.TimeSystem.ITimeSystemEntity { System.TimeSpan Elapsed { get; } diff --git a/Tests/Testably.Abstractions.Parity.Tests/ParityTests.cs b/Tests/Testably.Abstractions.Parity.Tests/ParityTests.cs index c486e168..4ebe8b3d 100644 --- a/Tests/Testably.Abstractions.Parity.Tests/ParityTests.cs +++ b/Tests/Testably.Abstractions.Parity.Tests/ParityTests.cs @@ -13,12 +13,8 @@ namespace Testably.Abstractions.Parity.Tests; public abstract class ParityTests( TestHelpers.Parity parity) { - #region Test Setup - public TestHelpers.Parity Parity { get; } = parity; - #endregion - [Test] public async Task IDirectory_EnsureParityWith_Directory() { @@ -121,23 +117,25 @@ public async Task IPath_EnsureParityWith_Path() await That(parityErrors).IsEmpty(); } +#if FEATURE_PERIODIC_TIMER [Test] - public async Task IRandomAndIRandomFactory_EnsureParityWith_Random() + public async Task + IPeriodicTimerAndIPeriodicTimerFactory_EnsureParityWith_PeriodicTimer() { - List parityErrors = Parity.Random - .GetErrorsToInstanceType( - typeof(Random)); + List parityErrors = Parity.PeriodicTimer + .GetErrorsToInstanceType( + typeof(PeriodicTimer)); await That(parityErrors).IsEmpty(); } +#endif [Test] - public async Task - ITimerAndITimerFactory_EnsureParityWith_Timer() + public async Task IRandomAndIRandomFactory_EnsureParityWith_Random() { - List parityErrors = Parity.Timer - .GetErrorsToInstanceType( - typeof(Timer)); + List parityErrors = Parity.Random + .GetErrorsToInstanceType( + typeof(Random)); await That(parityErrors).IsEmpty(); } @@ -156,6 +154,17 @@ public async Task await That(parityErrors).IsEmpty(); } + [Test] + public async Task + ITimerAndITimerFactory_EnsureParityWith_Timer() + { + List parityErrors = Parity.Timer + .GetErrorsToInstanceType( + typeof(Timer)); + + await That(parityErrors).IsEmpty(); + } + [Test] 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 f6b6edeb..0a120b49 100644 --- a/Tests/Testably.Abstractions.Parity.Tests/TestHelpers/Parity.cs +++ b/Tests/Testably.Abstractions.Parity.Tests/TestHelpers/Parity.cs @@ -73,6 +73,16 @@ public class Parity #pragma warning restore CS0618 }); +#if FEATURE_PERIODIC_TIMER + public ParityCheck PeriodicTimer { get; } = new(excludeConstructors: + [ + typeof(PeriodicTimer).GetConstructor([ + typeof(TimeSpan), + typeof(TimeProvider), + ]), + ]); +#endif + public ParityCheck Random { get; } = new(); public ParityCheck Stopwatch { get; } = new(excludeMethods: From f3a08c8f6c30a2d00a7367e09c7db945180368ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Fri, 13 Mar 2026 17:02:50 +0100 Subject: [PATCH 2/2] refactor: update BuildScope to CoreOnly and improve PeriodicTimer documentation --- Pipeline/Build.cs | 2 +- Source/Testably.Abstractions/RealTimeSystem.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Pipeline/Build.cs b/Pipeline/Build.cs index 65bdc27a..f5452a74 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/Testably.Abstractions/RealTimeSystem.cs b/Source/Testably.Abstractions/RealTimeSystem.cs index 396831ea..1be790d5 100644 --- a/Source/Testably.Abstractions/RealTimeSystem.cs +++ b/Source/Testably.Abstractions/RealTimeSystem.cs @@ -31,9 +31,7 @@ public RealTimeSystem() public IDateTime DateTime { get; } #if FEATURE_PERIODIC_TIMER - /// - /// Abstractions for . - /// + /// public IPeriodicTimerFactory PeriodicTimer { get; } #endif