Skip to content

Commit

Permalink
feat: add filter possibility for signals with parameters (#161)
Browse files Browse the repository at this point in the history
Refactor:
- Rename `Trigger` to `BeSignaled` and `NotTrigger` to `NotBeSignaled`
- Add an `IsSignaled(Times)` method on the recording interfaces
- Rename `CallbackRecorder` to `SignalCounter`
- Remove the static `Record` class
- Remove the `ISignalCounter` and `ISignalCounterResult` interfaces (and use the classes directly)
- Change the return type of `BeSignaled` and `NotBeSignaled` to use the result

### New feature:

You can now filter for signals with specific parameters by providing a `predicate`:
```csharp
SignalCounter<string> signal = new();

signal.Signal("foo");
signal.Signal("bar");
signal.Signal("foo");

await That(signal).Should().BeSignaled(2.Times()).With(p => p == "foo");
```
  • Loading branch information
vbreuss authored Dec 28, 2024
1 parent 6051de9 commit 1103b3a
Show file tree
Hide file tree
Showing 32 changed files with 1,334 additions and 1,029 deletions.
61 changes: 36 additions & 25 deletions Docs/pages/docs/expectations/callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ Describes the possible expectations for working with callbacks.

## Recording

First, you have to start a recording of callbacks. This method is available in the "aweXpect.Recording" namespace.
First, you have to start recording callback signals. This class is available in the "aweXpect.Recording" namespace.

```csharp
//Record callbacks without parameters
ICallbackRecording recording = Record.Callback();
ICallbackRecording<string> recording = Record.Callback<string>();
//Records callbacks with a string parameter
//Counts signals from callbacks without parameters
SignalCounter signal = new();
SignalCounter<string> signal = new();
//Counts signals from callbacks with a string parameter
```

Then you can trigger the callback on the recording.
Then, you can signal the callback on the recording.

```csharp
class MyClass
Expand All @@ -28,66 +28,77 @@ class MyClass
}
}

sut.Execute(v => recording.Trigger(v));
sut.Execute(v => signal.Signal(v));
```

At last you can wait for the callback to be triggered:
At last, you can wait for the callback to be signaled:

```csharp
await Expect.That(recording).Should().Trigger();
await Expect.That(signal).Should().BeSignaled();
```

You can also verify that the callback will not be triggered:
You can also verify that the callback will not be signaled:

```csharp
await Expect.That(recording).Should().NotTrigger();
await Expect.That(signal).Should().NotBeSignaled();
```

*NOTE: The last statement will result never return, unless a timeout or cancellation is specified. Therefore, when
nothing is specified, a default timeout of 30 seconds is applied!*
*NOTE: The last statement will result never return, unless a timeout or cancellation is specified.
Therefore, when nothing is specified, a default timeout of 30 seconds is applied!*

### Timeout

You can specify a timeout, how long you want to wait for the callback to be triggered:
You can specify a timeout, how long you want to wait for the callback to be signaled:

```csharp
await Expect.That(recording).Should().Trigger().Within(TimeSpan.FromSeconds(5))
await Expect.That(signal).Should().BeSignaled().Within(TimeSpan.FromSeconds(5))
.Because("it should take at most 5 seconds to complete");
```

Alternatively you can also use a `CancellationToken` for a timeout:

```csharp
CancellationToken cancellationToken = new CancellationTokenSource(5000).Token;
await Expect.That(recording).Should().Trigger().WithCancellation(cancellationToken)
await Expect.That(signal).Should().BeSignaled().WithCancellation(cancellationToken)
.Because("it should be completed, before the cancellationToken is cancelled");
```

### Amount

You can specify a number of times, that a callback must at least be executed:
You can specify a number of times, that a callback must at least be signaled:

```csharp
await Expect.That(recording).Should().Trigger(3.Times());
await Expect.That(signal).Should().BeSignaled(3.Times());
```

You can also verify, that the callback was not executed at least the given number of times:
You can also verify, that the callback was not signaled at least the given number of times:

```csharp
await Expect.That(recording).Should().NotTrigger(3.Times());
await Expect.That(signal).Should().NotBeSignaled(3.Times());
```

### Parameters

You can also use callbacks with a single parameter:
You can also include a parameter during signaling:

```csharp
ICallbackRecording<string> recording = Record.Callback<string>();
SignalCounter<string> signal = new();

recording.Trigger("foo");
recording.Trigger("bar");
signal.Signal("foo");
signal.Signal("bar");

await That(recording).Should().Trigger(2.Times());
await That(signal).Should().BeSignaled(2.Times());
```

You can filter for signals with specific parameters by providing a `predicate`:
```csharp
SignalCounter<string> signal = new();

signal.Signal("foo");
signal.Signal("bar");
signal.Signal("foo");

await That(signal).Should().BeSignaled(2.Times()).With(p => p == "foo");
```

*In case of a failed expectation, the recorded parameters will be displayed in the error message.*
81 changes: 81 additions & 0 deletions Source/aweXpect.Core/Options/SignalCounterOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using aweXpect.Recording;

namespace aweXpect.Options;

/// <summary>
/// Options for <see cref="SignalCounter" />
/// </summary>
public class SignalCounterOptions
{
/// <summary>
/// The timeout to use for the recording.
/// </summary>
public TimeSpan? Timeout { get; set; }

/// <inheritdoc cref="object.ToString()" />
public override string ToString()
{
if (Timeout == null)
{
return "";
}

return $" within {Formatter.Format(Timeout.Value)}";
}
}

/// <summary>
/// Options for <see cref="SignalCounter{TParameter}" />
/// </summary>
public class SignalCounterOptions<TParameter> : SignalCounterOptions
{
private StringBuilder? _builder;
private List<Func<TParameter, bool>>? _predicates;

/// <summary>
/// Add a predicate to the signal counter options.
/// </summary>
public void WithPredicate(Func<TParameter, bool> predicate, string predicateExpression)
{
_predicates ??= new List<Func<TParameter, bool>>();
_predicates.Add(predicate);
if (_builder is null)
{
_builder = new StringBuilder();
_builder.Append(" with ");
_builder.Append(predicateExpression);
}
else
{
_builder.Append(" and with ");
_builder.Append(predicateExpression);
}
}

/// <summary>
/// Checks if the <paramref name="parameter" /> matches all registered predicates.
/// </summary>
public bool Matches(TParameter parameter)
{
if (_predicates is null)
{
return true;
}

return _predicates.All(predicate => predicate(parameter));
}

/// <inheritdoc cref="object.ToString()" />
public override string ToString()
=> (_builder, Timeout) switch
{
(null, null) => "",
(null, _) => $" within {Formatter.Format(Timeout.Value)}",
(_, null) => _builder.ToString(),
(_, _) => _builder.Append($" within {Formatter.Format(Timeout.Value)}").ToString()
};
}
15 changes: 0 additions & 15 deletions Source/aweXpect.Core/Options/TriggerCallbackOptions.cs

This file was deleted.

Loading

0 comments on commit 1103b3a

Please sign in to comment.