diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6d6f7b1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,57 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +`Reactive.Boolean` is a NuGet library of Rx.NET extension methods for `IObservable`: logical operators (`And`/`Or`/`Xor` plus `Nand`/`Nor`/`Xnor`/`Not`), time-based "scheduling" operators (`TrueForAtLeast`, `PersistTrueFor`, `WhenTrueFor`, `WhenStableFor`, `LimitTrueDuration`, `PulseTrueFor`, `BlinkWhileTrue` and their `False` mirrors), and `SubscribeTrue`/`SubscribeFalse`/`SubscribeTrueFalse` helpers. Everything public lives on the single `partial static class BooleanObservableExtensions` in namespace `Reactive.Boolean`, split across `BooleanObservableExtensions.*.cs` files. Only dependency: `System.Reactive`. + +## Commands + +Targets `net8.0;net9.0;net10.0` for both the library and the tests, so every `dotnet test` runs the suite three times unless you pass `-f`. + +```powershell +dotnet build --configuration Release -p:TreatWarningsAsErrors=true # what CI runs; warnings fail the build +dotnet test # all TFMs +dotnet test -f net10.0 # one TFM (much faster while iterating) +dotnet test -f net10.0 --filter "FullyQualifiedName~PersistTrueFor" # one test class (class names match file names, minus dots) +dotnet test -f net10.0 --filter "Name~TrueForAtLeast_TrueThenFalse_SameForEverySourceShape" # one test method (all DataRows; use ~ not =, data-driven names include the arguments) +``` + +`GenerateDocumentationFile` is on and CI treats warnings as errors, so every new public member needs an XML doc comment. + +## Architecture + +### Scheduling operators are three state machines behind one base class + +All time-based operators are built on `TimedBooleanOperator` (`src/Reactive.Boolean/TimedBooleanOperator.cs`). It subscribes to the source exactly once, owns a single restartable timer on the caller's `IScheduler`, and serialises source values, timer expiry, completion and disposal under one lock. Timer callbacks carry a generation number so a stale callback that was already dequeued when the timer was restarted is ignored. Subclasses only implement `OnSourceValue` / `OnTimerElapsed` and use `Emit`, `StartTimer`, `StopTimer`, `LastSourceValue`, `LastEmittedValue`, `TimerRunning`. + +Three concrete machines cover all public operators; the public methods in `BooleanObservableExtensions.Scheduling.cs` are thin `Observable.Create` wrappers that pick a machine and its flags: + +| Machine | Flags | Public operators | +|---|---|---| +| `TimedWindowOperator` | `withholdFalseDuringWindow`, `forceFalseAtEnd` | `TrueForAtLeast` (withhold), `LimitTrueDuration` (force), `PulseTrueFor` (both) | +| `DelayedTransitionOperator` | `delayTrue`, `delayFalse`, `assumedInitialValue` | `WhenTrueFor` (delay true, assume initial false), `PersistTrueFor` (delay false), `WhenStableFor` (delay both) | +| `BlinkWhileTrueOperator` | `onDuration`, `offDuration` | `BlinkWhileTrue` | + +Every `False` variant is literally `source.Not().TrueVariant(...).Not()`; do not implement them separately. + +Cross-cutting options shared by every scheduling method: `distinctUntilChanged` (handled centrally in `Emit`), `resetTimerOnConsecutive*` (whether a repeated value restarts a running timer), and `CompletionBehavior` (`CompleteImmediately` vs `CompleteAfterTimer`; the latter relies on each machine's `HasPendingValue` override to decide whether there is anything worth waiting for). + +### Logical operators + +`And`/`Or` are `CombineLatest` with `OperatorDistinctness` deciding where `DistinctUntilChanged` is applied (output, inputs, or nowhere). Each has many overloads (two observables, `IEnumerable`, `params`, 3- and 4-arity) plus `AndOp`/`OrOp` aliases that exist only to dodge name clashes with Rx join patterns. When adding an overload, add it to the alias and the inverted (`Nand`/`Nor`) families too. + +### Tests + +MSTest with `Microsoft.Reactive.Testing.TestScheduler`; time is advanced with `scheduler.AdvanceBy(ticks)` and tests typically use `TimeSpan.FromTicks(2)` so each tick is observable. Tests run in parallel at method level (`MSTestSettings.cs`). + +`tests/Reactive.Boolean.Tests/SourceShapes.cs` defines `SourceShape` (hot `Subject`, `SelectMany` burst, cold array, cold `Concat`, `Defer`+`Prepend`). The `*_SameForEverySourceShape` tests assert that an operator behaves identically whether values arrive one at a time or re-entrantly in a burst, and whether or not the source completes. Any change to a scheduling operator should keep those passing, and new scheduling behaviour should get its own shape-parameterised test. + +Test file naming mirrors the source: `BooleanObservableExtensions.Scheduling..Tests.cs` holds class `BooleanObservableExtensionsSchedulingTests`. + +## Conventions + +- README.md is packed into the NuGet package and is the user-facing documentation. When adding or changing operator behaviour, update the matching README section (and its marble diagram in `img/` if one exists) in the same change. +- Sources use CRLF line endings; files under `src/` also start with a UTF-8 BOM. +- PRs to `main` must carry at least one `pr:` label (`pr: bugfix`, `pr: new-feature`, `pr: enhancement`, `pr: breaking change`, `pr: documentation`, `pr: dependency-update`). Release-drafter builds the release notes from PR titles grouped by these labels, and publishing a GitHub release triggers the NuGet push with the tag as the package version. diff --git a/README.md b/README.md index fc2b9c8..48210d7 100644 --- a/README.md +++ b/README.md @@ -1,193 +1,272 @@ -# Reactive.Boolean - -[![GitHub license](https://img.shields.io/github/license/DevJasperNL/Reactive.Boolean?label=License)](https://github.com/DevJasperNL/Reactive.Boolean?tab=MIT-1-ov-file) -[![GitHub release](https://img.shields.io/github/v/release/DevJasperNL/Reactive.Boolean?label=Release)](https://github.com/DevJasperNL/Reactive.Boolean/releases/latest) -[![Build Status](https://github.com/DevJasperNL/Reactive.Boolean/actions/workflows/ci-build-and-test.yml/badge.svg)](https://github.com/DevJasperNL/Reactive.Boolean/actions/workflows/ci-build-and-test.yml) - -Reactive Extensions meant specifically for implementations of `IObservable` - -This documentation uses marble diagrams to explain the transformations of `IObservable`. More on marble diagrams can be found in the documentation of [ReactiveX](https://reactivex.io/documentation/observable.html). - -Article containing examples in relation to home automation: [Article with examples](https://dev.to/devjaspernl/supercharging-home-assistant-automations-initial-states-and-boolean-logic-for-netdaemon-rx-3bd5). - -## Logical Operators - -This library has extension methods for logical operators. - -### Stateful observables -All operators except for `Not` are implemented using `CombineLatest`. This means that the first output is only emitted if all inputs have emitted an value after subscribing. For this reason, it makes sense to apply these logical operators to stateful observables. In this context these are observables that emit their current state the moment an observer subscribes to them. This can easily be achieved by using `Prepend`, preferably in combination with `Observable.Defer`. For example: -```csharp -Observable.Defer(() => stateChanges.Prepend(initialState)); -``` - -### Distinctness -Depending on the operator, there are several ways of handling value distinctness. Different forms are explained below. - -### Not - -Returns an observable in which the input is inverted. - -![Not](img/Not.png) - -### And - -Returns an observable that combines the latest of the provided observables using an AND operator. -The `And` method accepts three values to determine distinctness of the output: - -**OutputDistinctUntilChanged (default)** - -DistinctUntilChanged is applied to the returned observable, meaning a "true" can only be followed by a "false" and vice versa. - -![And](img/And.png) - -**InputDistinctUntilChanged** - -DistinctUntilChanged is applied to the inputs only. Meaning that consecutive values on the input do not change the output, but input changes on different inputs can. For example, going from "false", "false" to "true", "false" will emit consecutive "false" values. - -![And (input distinct)](img/And%20(input%20distinct).png) - -**NotDistinct** - -DistinctUntilChanged is never applied. Meaning both consecutive input and output values will be emitted. - -![And (not distinct)](img/And%20(not%20distinct).png) - -### Or - -Returns an observable that combines the latest of the provided observables using an OR operator. -The `Or` method accepts three values to determine distinctness of the output: - -**OutputDistinctUntilChanged (default)** - -DistinctUntilChanged is applied to the returned observable, meaning a "true" can only be followed by a "false" and vice versa. - -![Or](img/Or.png) - -**InputDistinctUntilChanged** - -DistinctUntilChanged is applied to the inputs only. Meaning that consecutive values on the input do not change the output, but input changes on different inputs can. For example, going from "true", "false" to "true", "true" will emit consecutive "true" values. - -![Or (input distinct)](img/Or%20(input%20distinct).png) - -**NotDistinct** - -DistinctUntilChanged is never applied. Meaning both consecutive input and output values will be emitted. - -![Or (not distinct)](img/Or%20(not%20distinct).png) - -### XOr - -Returns an observable that combines the latest results of two observables using an XOR operator. -As changing distinct inputs will always result in a distinct XOR output, the `Xor` method accepts only two values to determine distinctness of the output: - -**distinctUntilChanged = true (default)** - -DistinctUntilChanged is applied to the result. - -![XOr](img/XOr.png) - -**distinctUntilChanged = false** - -DistinctUntilChanged is not applied to the result. - -![XOr (not distinct)](img/XOr%20(not%20distinct).png) - -### Inverted operators - -This library also implements inverted operators `Nand`, `Nor` and `Xnor`. - -## Scheduling - -This library also has extension methods for scheduling: - -### TrueForAtLeast - -Returns an observable that won't emit `false` for at least the provided timespan after an initial `true` is emitted by the source observable. -If a `false` is emitted during the provided timespan, it will be emitted immediately after the timer is completed. - -![TrueForAtLeast](img/TrueForAtLeast.png) - -**Example Use Case** - -Turn on a light for at least 3 seconds after a button was pressed. If 3 seconds are passed, only keep it on if the button is still being pressed, but immediately turn if off if not. -```csharp -// buttonPressed is a IObservable -var buttonPressed = button.StateChanges().Select(s => s.State == "pressed"); -buttonPressed - .TrueForAtLeast(TimeSpan.FromSeconds(3), scheduler) - .SubscribeTrueFalse( - () => light.TurnOn(), - () => light.TurnOff()); -``` - -### PersistTrueFor - -Returns an observable that delays the first `false` that is emitted after a `true` by the source for a duration of a provided timespan. - -![PersistTrueFor](img/PersistTrueFor.png) - -**Example Use Case** - -Keep a light on for 3 more seconds after last motion was detected. -```csharp -// motionDetected is a IObservable -var motionDetected = motionSensor.StateChanges().Select(s => s.State == "motion detected"); -motionDetected - .PersistTrueFor(TimeSpan.FromSeconds(3), scheduler) - .SubscribeTrueFalse( - () => light.TurnOn(), - () => light.TurnOff()); -``` - -### WhenTrueFor - -Returns an observable that emits `true` once the source does not emit `false` for a minimum of the provided timespan. - -![WhenTrueFor](img/WhenTrueFor.png) - -**Example Use Case** - -Send notification when washing machine power has been 0 for at least 1 minute. -```csharp -// washingMachineCurrentIsZero is a IObservable -var washingMachineCurrentIsZero = washingMachineCurrent.StateChanges().Select(s => s.State == 0); -washingMachineCurrentIsZero - .WhenTrueFor(TimeSpan.FromMinutes(1), scheduler) - .SubscribeTrue(() => notification.Send("Washing machine is done!")); -``` - -### LimitTrueDuration - -Returns an observable that will automatically emit `false` if the source does not emit a `false` itself within the provided timespan after emitting `true`. - -![LimitTrueDuration](img/LimitTrueDuration.png) - -**Example Use Case** - -Keep closet lights on for a maximum amount of time. -```csharp -// closetDoorOpen is a IObservable -var closetDoorOpen = closetDoor.StateChanges().Select(s => s.State == "open"); -closetDoorOpen - .LimitTrueDuration(TimeSpan.FromMinutes(2), scheduler) - .SubscribeTrueFalse( - () => closetLight.TurnOn(), - () => closetLight.TurnOff()); -``` - -## Subscribing - -Besides transformations, this library has extension methods that help with common cases of subscribing to implementations of `IObservable`: `SubscribeTrueFalse`, `SubscribeFalse` and `SubscribeTrue`. - -### Example - -```cs -boolObservable.SubscribeTrueFalse( - () => { - // Logic for when observable emits true. - }, - () => { - // Logic for when observable emits false. - } -) -``` \ No newline at end of file +# Reactive.Boolean + +[![GitHub license](https://img.shields.io/github/license/DevJasperNL/Reactive.Boolean?label=License)](https://github.com/DevJasperNL/Reactive.Boolean?tab=MIT-1-ov-file) +[![GitHub release](https://img.shields.io/github/v/release/DevJasperNL/Reactive.Boolean?label=Release)](https://github.com/DevJasperNL/Reactive.Boolean/releases/latest) +[![Build Status](https://github.com/DevJasperNL/Reactive.Boolean/actions/workflows/ci-build-and-test.yml/badge.svg)](https://github.com/DevJasperNL/Reactive.Boolean/actions/workflows/ci-build-and-test.yml) + +Reactive Extensions meant specifically for implementations of `IObservable` + +This documentation uses marble diagrams to explain the transformations of `IObservable`. More on marble diagrams can be found in the documentation of [ReactiveX](https://reactivex.io/documentation/observable.html). + +Article containing examples in relation to home automation: [Article with examples](https://dev.to/devjaspernl/supercharging-home-assistant-automations-initial-states-and-boolean-logic-for-netdaemon-rx-3bd5). + +## Logical Operators + +This library has extension methods for logical operators. + +### Stateful observables +All operators except for `Not` are implemented using `CombineLatest`. This means that the first output is only emitted if all inputs have emitted an value after subscribing. For this reason, it makes sense to apply these logical operators to stateful observables. In this context these are observables that emit their current state the moment an observer subscribes to them. This can easily be achieved by using `Prepend`, preferably in combination with `Observable.Defer`. For example: +```csharp +Observable.Defer(() => stateChanges.Prepend(initialState)); +``` + +### Distinctness +Depending on the operator, there are several ways of handling value distinctness. Different forms are explained below. + +### Not + +Returns an observable in which the input is inverted. + +![Not](img/Not.png) + +### And + +Returns an observable that combines the latest of the provided observables using an AND operator. +The `And` method accepts three values to determine distinctness of the output: + +**OutputDistinctUntilChanged (default)** + +DistinctUntilChanged is applied to the returned observable, meaning a "true" can only be followed by a "false" and vice versa. + +![And](img/And.png) + +**InputDistinctUntilChanged** + +DistinctUntilChanged is applied to the inputs only. Meaning that consecutive values on the input do not change the output, but input changes on different inputs can. For example, going from "false", "false" to "true", "false" will emit consecutive "false" values. + +![And (input distinct)](img/And%20(input%20distinct).png) + +**NotDistinct** + +DistinctUntilChanged is never applied. Meaning both consecutive input and output values will be emitted. + +![And (not distinct)](img/And%20(not%20distinct).png) + +### Or + +Returns an observable that combines the latest of the provided observables using an OR operator. +The `Or` method accepts three values to determine distinctness of the output: + +**OutputDistinctUntilChanged (default)** + +DistinctUntilChanged is applied to the returned observable, meaning a "true" can only be followed by a "false" and vice versa. + +![Or](img/Or.png) + +**InputDistinctUntilChanged** + +DistinctUntilChanged is applied to the inputs only. Meaning that consecutive values on the input do not change the output, but input changes on different inputs can. For example, going from "true", "false" to "true", "true" will emit consecutive "true" values. + +![Or (input distinct)](img/Or%20(input%20distinct).png) + +**NotDistinct** + +DistinctUntilChanged is never applied. Meaning both consecutive input and output values will be emitted. + +![Or (not distinct)](img/Or%20(not%20distinct).png) + +### XOr + +Returns an observable that combines the latest results of two observables using an XOR operator. +As changing distinct inputs will always result in a distinct XOR output, the `Xor` method accepts only two values to determine distinctness of the output: + +**distinctUntilChanged = true (default)** + +DistinctUntilChanged is applied to the result. + +![XOr](img/XOr.png) + +**distinctUntilChanged = false** + +DistinctUntilChanged is not applied to the result. + +![XOr (not distinct)](img/XOr%20(not%20distinct).png) + +### Inverted operators + +This library also implements inverted operators `Nand`, `Nor` and `Xnor`. + +## Scheduling + +This library also has extension methods for scheduling. Every scheduling method takes an `IScheduler`, subscribes to the source exactly once, and behaves the same whether values arrive one at a time from a hot subject or in a burst from a cold observable (for example `Observable.Return(true).Concat(...)` or `SelectMany`). + +### Common options + +All scheduling methods share three optional parameters: + +**resetTimerOnConsecutiveTrue / resetTimerOnConsecutiveFalse (default `false`)** + +A real transition (for example `false` to `true` for `TrueForAtLeast`) always starts the timer. This flag decides whether a *repeated* value, emitted while the timer is running, restarts it. For the symmetric `WhenStableFor` the flag is called `resetTimerOnConsecutiveValue`. + +**distinctUntilChanged (default `true`)** + +When `true`, the resulting observable never emits the same value twice in a row. When `false`, consecutive values from the source are passed through, except where the timer withholds them (details per method below). + +**completionBehavior (default `CompletionBehavior.CompleteImmediately`)** + +Determines what happens when the source completes while the timer is still running: + +- `CompleteImmediately`: the result completes at once. A value that the timer would still have emitted is dropped. +- `CompleteAfterTimer`: the result stays alive until the timer runs out, emits the pending value and completes afterwards. When nothing is pending it completes immediately. For `BlinkWhileTrue` this means finishing the current `true` phase, emitting `false` and completing afterwards. + +Errors from the source are always forwarded immediately. + +### TrueForAtLeast + +Returns an observable that won't emit `false` for at least the provided timespan after an initial `true` is emitted by the source observable. +If a `false` is emitted during the provided timespan, it will be emitted immediately after the timer is completed. With `distinctUntilChanged: false`, multiple `false` values received during the timespan are emitted as a single `false`. A repeated `true` received after the timer ran out only starts a new timer with `resetTimerOnConsecutiveTrue: true`. + +![TrueForAtLeast](img/TrueForAtLeast.png) + +**Example Use Case** + +Turn on a light for at least 3 seconds after a button was pressed. If 3 seconds are passed, only keep it on if the button is still being pressed, but immediately turn if off if not. +```csharp +// buttonPressed is a IObservable +var buttonPressed = button.StateChanges().Select(s => s.State == "pressed"); +buttonPressed + .TrueForAtLeast(TimeSpan.FromSeconds(3), scheduler) + .SubscribeTrueFalse( + () => light.TurnOn(), + () => light.TurnOff()); +``` + +### PersistTrueFor + +Returns an observable that delays the first `false` that is emitted after a `true` by the source for a duration of a provided timespan. A `true` emitted during that time cancels the delayed `false`. With `distinctUntilChanged: false`, multiple `false` values received during the timespan are emitted as a single `false`. + +![PersistTrueFor](img/PersistTrueFor.png) + +**Example Use Case** + +Keep a light on for 3 more seconds after last motion was detected. +```csharp +// motionDetected is a IObservable +var motionDetected = motionSensor.StateChanges().Select(s => s.State == "motion detected"); +motionDetected + .PersistTrueFor(TimeSpan.FromSeconds(3), scheduler) + .SubscribeTrueFalse( + () => light.TurnOn(), + () => light.TurnOff()); +``` + +### WhenTrueFor + +Returns an observable that emits `true` once the source does not emit `false` for a minimum of the provided timespan. The first value emitted is always `false`. With `distinctUntilChanged: false`, `true` values received while the timer is running are not emitted. + +Note that `WhenTrueFor` and `PersistFalseFor` (and likewise `WhenFalseFor` and `PersistTrueFor`) behave identically once the source has emitted both values. They only differ in how the first value is handled: `WhenTrueFor` always emits `false` first, `PersistFalseFor` passes the first value through. + +![WhenTrueFor](img/WhenTrueFor.png) + +**Example Use Case** + +Send notification when washing machine power has been 0 for at least 1 minute. +```csharp +// washingMachineCurrentIsZero is a IObservable +var washingMachineCurrentIsZero = washingMachineCurrent.StateChanges().Select(s => s.State == 0); +washingMachineCurrentIsZero + .WhenTrueFor(TimeSpan.FromMinutes(1), scheduler) + .SubscribeTrue(() => notification.Send("Washing machine is done!")); +``` + +### WhenStableFor + +Returns an observable that only emits a value once the source has held it for a minimum of the provided timespan, in both directions. The first value of the source is emitted immediately. A change that reverts before the timer runs out is never emitted. With `resetTimerOnConsecutiveValue: true`, a repeated pending value restarts the timer. With `distinctUntilChanged: false`, values equal to the current output are passed through, while values received during the timer are not emitted. + +**Example Use Case** + +Ignore a bouncing door contact by only reacting once the door has been open or closed for 2 seconds. +```csharp +// doorOpen is a IObservable +var doorOpen = doorContact.StateChanges().Select(s => s.State == "open"); +doorOpen + .WhenStableFor(TimeSpan.FromSeconds(2), scheduler) + .SubscribeTrueFalse( + () => notification.Send("Door opened"), + () => notification.Send("Door closed")); +``` + +### LimitTrueDuration + +Returns an observable that will automatically emit `false` if the source does not emit a `false` itself within the provided timespan after emitting `true`. +Once the limit is reached, a repeated `true` from the source is ignored until the source emits `false` again. With `resetTimerOnConsecutiveTrue: true`, a repeated `true` instead starts a new limited period and is emitted again. + +![LimitTrueDuration](img/LimitTrueDuration.png) + +**Example Use Case** + +Keep closet lights on for a maximum amount of time. +```csharp +// closetDoorOpen is a IObservable +var closetDoorOpen = closetDoor.StateChanges().Select(s => s.State == "open"); +closetDoorOpen + .LimitTrueDuration(TimeSpan.FromMinutes(2), scheduler) + .SubscribeTrueFalse( + () => closetLight.TurnOn(), + () => closetLight.TurnOff()); +``` + +### PulseTrueFor + +Returns an observable that emits `true` for exactly the provided timespan after the source transitions to `true`, followed by `false`. A `false` emitted by the source during the pulse is withheld until the pulse ends, and a `true` that outlasts the pulse does not extend it. A `true` that follows a `false` always starts a new pulse. With `resetTimerOnConsecutiveTrue: true`, every repeated `true` restarts the pulse (a retriggerable pulse), also after the pulse has ended. With `distinctUntilChanged: false`, `false` values received during the pulse are not emitted; the pulse always ends with a single `false`. + +**Example Use Case** + +Ring the doorbell chime for one second when the button is pressed, no matter how long it is held. +```csharp +// doorbellPressed is a IObservable +var doorbellPressed = doorbell.StateChanges().Select(s => s.State == "pressed"); +doorbellPressed + .PulseTrueFor(TimeSpan.FromSeconds(1), scheduler) + .SubscribeTrueFalse( + () => chime.TurnOn(), + () => chime.TurnOff()); +``` + +### BlinkWhileTrue + +Returns an observable that alternates between `true` and `false` while the source is `true`, starting with `true`. A `false` from the source stops the blinking immediately, also in the middle of a phase. The single-timespan overload uses the same duration for both phases; the `onDuration`/`offDuration` overload lets them differ. A repeated `true` from the source is ignored unless `resetTimerOnConsecutiveTrue: true`, which restarts the `true` phase. Zero or negative durations throw an `ArgumentOutOfRangeException`. + +**Example Use Case** + +Flash a light every 500 milliseconds while the alarm is triggered. +```csharp +// alarmTriggered is a IObservable +var alarmTriggered = alarm.StateChanges().Select(s => s.State == "triggered"); +alarmTriggered + .BlinkWhileTrue(TimeSpan.FromMilliseconds(500), scheduler) + .SubscribeTrueFalse( + () => light.TurnOn(), + () => light.TurnOff()); +``` + +### Inverted scheduling methods + +Every scheduling method except `WhenStableFor` has a `False` mirror (`FalseForAtLeast`, `PersistFalseFor`, `WhenFalseFor`, `LimitFalseDuration`, `PulseFalseFor`, `BlinkWhileFalse`) that applies the same logic to `false` values. + +## Subscribing + +Besides transformations, this library has extension methods that help with common cases of subscribing to implementations of `IObservable`: `SubscribeTrueFalse`, `SubscribeFalse` and `SubscribeTrue`. + +### Example + +```cs +boolObservable.SubscribeTrueFalse( + () => { + // Logic for when observable emits true. + }, + () => { + // Logic for when observable emits false. + } +) +``` \ No newline at end of file diff --git a/src/Reactive.Boolean/BlinkWhileTrueOperator.cs b/src/Reactive.Boolean/BlinkWhileTrueOperator.cs new file mode 100644 index 0000000..62b45ee --- /dev/null +++ b/src/Reactive.Boolean/BlinkWhileTrueOperator.cs @@ -0,0 +1,52 @@ +using System.Reactive.Concurrency; + +namespace Reactive.Boolean; + +internal sealed class BlinkWhileTrueOperator( + IObserver observer, + TimeSpan onDuration, + TimeSpan offDuration, + IScheduler scheduler, + bool distinctUntilChanged, + bool resetTimerOnConsecutiveTrue, + CompletionBehavior completionBehavior) + : TimedBooleanOperator(observer, onDuration, scheduler, distinctUntilChanged, completionBehavior) +{ + // The base timer duration is the "true" phase; only the "false" phase needs its own duration. + + // Under CompleteAfterTimer only the current "true" phase is finished, so an "off" phase has nothing pending. + protected override bool HasPendingValue => TimerRunning && LastEmittedValue == true; + + protected override void OnSourceValue(bool value) + { + if (!value) + { + StopTimer(); + Emit(false); + return; + } + + if (LastSourceValue != true || resetTimerOnConsecutiveTrue) + { + Emit(true); + StartTimer(); + } + } + + protected override void OnTimerElapsed() + { + if (LastEmittedValue == true) + { + Emit(false); + if (!SourceCompleted) + { + StartTimer(offDuration); + } + + return; + } + + Emit(true); + StartTimer(); + } +} diff --git a/src/Reactive.Boolean/BooleanObservableExtensions.Scheduling.cs b/src/Reactive.Boolean/BooleanObservableExtensions.Scheduling.cs index 5bbe9a7..5a90337 100644 --- a/src/Reactive.Boolean/BooleanObservableExtensions.Scheduling.cs +++ b/src/Reactive.Boolean/BooleanObservableExtensions.Scheduling.cs @@ -1,4 +1,4 @@ -using System.Reactive.Concurrency; +using System.Reactive.Concurrency; using System.Reactive.Linq; namespace Reactive.Boolean @@ -13,47 +13,28 @@ public static partial class BooleanObservableExtensions /// /// /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. Note that consecutive "false" values that occur during the timer, will only be emitted as a single "false" once the timer runs out. - /// If "true", every "true" that is emitted by will reset the timer. + /// If "true", every "true" that is emitted by will reset the timer. A "true" that follows a "false" always (re)starts the timer. A repeated "true" received after the timer ran out only starts a new timer when this is set. + /// Determines what happens when completes while a "false" is being withheld: drop it and complete immediately (default), or emit it once the timer runs out and complete afterwards. /// public static IObservable TrueForAtLeast( this IObservable source, TimeSpan timeSpan, IScheduler scheduler, bool distinctUntilChanged = true, - bool resetTimerOnConsecutiveTrue = false) + bool resetTimerOnConsecutiveTrue = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) { - var timer = CreateTimer(source, timeSpan, scheduler, resetTimerOnConsecutiveTrue); - - if (distinctUntilChanged) + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(scheduler); + if (timeSpan <= TimeSpan.Zero) { - return source.CombineLatest(timer) - .Where(t => t.First || !t.Second) - .Select(t => t.First) - .DistinctUntilChanged(); + return source; } - return source.CombineLatest(timer) - .Select(tuple => ((bool?)tuple.First, (bool?)tuple.Second)) // We convert to nullables to allow an initial seed of (null, null) in the pairing method. - .Pairwise((null, null)) // As double true values from the source do need to be emitted. We need the previous item to determine whether the change comes from the source or from the timer. - .Where(tuple => - { - var previous = tuple.Item1; - var current = tuple.Item2; - - var currentValue = current.Item1; - - var previousTimer = previous.Item2; - var currentTimer = current.Item2; - - var timerValueChanged = previousTimer != null && previousTimer != currentTimer; - if (!timerValueChanged) - { - return currentValue == true || currentTimer == false; - } - // Only when the timer is done and the current value is false, we have to emit something based on the timer. - return currentValue == false && currentTimer == false; - }) - .Select(t => t.Item2.Item1!.Value); // We can assume the item is not null as we only use null for the previous value. + return Observable.Create(observer => + new TimedWindowOperator(observer, timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, completionBehavior, + withholdFalseDuringWindow: true, forceFalseAtEnd: false) + .Run(source)); } /// @@ -64,33 +45,39 @@ public static IObservable TrueForAtLeast( /// /// /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. Note that consecutive "true" values that occur during the timer, will only be emitted as a single "true" once the timer runs out. - /// If "true", every "false" that is emitted by will reset the timer. + /// If "true", every "false" that is emitted by will reset the timer. A "false" that follows a "true" always (re)starts the timer. A repeated "false" received after the timer ran out only starts a new timer when this is set. + /// Determines what happens when completes while a "true" is being withheld: drop it and complete immediately (default), or emit it once the timer runs out and complete afterwards. /// public static IObservable FalseForAtLeast( this IObservable source, TimeSpan timeSpan, IScheduler scheduler, bool distinctUntilChanged = true, - bool resetTimerOnConsecutiveFalse = false) => + bool resetTimerOnConsecutiveFalse = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) => source .Not() - .TrueForAtLeast(timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveFalse) + .TrueForAtLeast(timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveFalse, completionBehavior) .Not(); /// /// Returns an observable that delays the first "false" that is emitted after a "true" by for a duration of . - /// Resulting observable is distinct. + /// A "true" emitted during that time cancels the delayed "false". /// /// /// /// - /// If "true", every "false" that is emitted by will reset the timer. + /// If "true", every "false" that is emitted by while the timer runs will reset the timer. + /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. Note that consecutive "false" values that occur during the timer, will only be emitted as a single "false" once the timer runs out. + /// Determines what happens when completes while a "false" is being delayed: drop it and complete immediately (default), or emit it once the timer runs out and complete afterwards. /// public static IObservable PersistTrueFor( this IObservable source, TimeSpan timeSpan, IScheduler scheduler, - bool resetTimerOnConsecutiveFalse = false) + bool resetTimerOnConsecutiveFalse = false, + bool distinctUntilChanged = true, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(scheduler); @@ -99,256 +86,320 @@ public static IObservable PersistTrueFor( return source; } - /* - * Note: Attempting to implement this method with both parameters distinctUntilChanged and resetTimerOnConsecutiveFalse is not impossible, but is difficult to do in a clean way: - * The difficulty with distinctUntilChanged as false, is that consecutive false values should be emitted if the timer is not running. - * The difficulty with resetTimerOnConsecutiveFalse as true, is that consecutive false values should only reset a timer when it's already running. - * Only implementing resetTimerOnConsecutiveFalse was relatively easy, as we can always use a false value to reset the Throttle timer and simply apply DistinctUntilChanged to the end result. - */ - - var trueObservable = source.Where(b => b); - var initialFalseObservable = source.Take(1).Where(b => !b); - - IObservable delayedFalseObservable; - if (resetTimerOnConsecutiveFalse) - { - delayedFalseObservable = source - .Skip(1) // Skip the first value as both the first true and the first false should immediately publish a value. - .Throttle(timeSpan, - scheduler) // This will publish the value after x time. Note that we also pass "true" values here, to prevent a delayed "false" from being released. - .Where(b => !b); // As a "true" is also passed to throttle, we filter it out here. - } - else - { - delayedFalseObservable = source - .Skip(1) // Skip the first value as both the first true and the first false should immediately publish a value. - .DistinctUntilChanged() // Filter out consecutive values to prevent the timer from being reset. - .Throttle(timeSpan, - scheduler) // This will publish the value after x time. Note that we also pass "true" values here, to prevent a delayed "false" from being released. - .Where(b => !b); // As a "true" is also passed to throttle, we filter it out here. - } - - return trueObservable - .Merge(initialFalseObservable) - .Merge(delayedFalseObservable) - .DistinctUntilChanged(); + return Observable.Create(observer => + new DelayedTransitionOperator(observer, timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveFalse, completionBehavior, + delayTrue: false, delayFalse: true, assumedInitialValue: null) + .Run(source)); } /// /// Returns an observable that delays the first "true" that is emitted after a "false" by for a duration of . - /// Resulting observable is distinct. + /// A "false" emitted during that time cancels the delayed "true". /// /// /// /// - /// If "true", every "true" that is emitted by will reset the timer. + /// If "true", every "true" that is emitted by while the timer runs will reset the timer. + /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. Note that consecutive "true" values that occur during the timer, will only be emitted as a single "true" once the timer runs out. + /// Determines what happens when completes while a "true" is being delayed: drop it and complete immediately (default), or emit it once the timer runs out and complete afterwards. /// public static IObservable PersistFalseFor( this IObservable source, TimeSpan timeSpan, IScheduler scheduler, - bool resetTimerOnConsecutiveTrue = false) => + bool resetTimerOnConsecutiveTrue = false, + bool distinctUntilChanged = true, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) => source .Not() - .PersistTrueFor(timeSpan, scheduler, resetTimerOnConsecutiveTrue) + .PersistTrueFor(timeSpan, scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged, completionBehavior) .Not(); /// /// Returns an observable that emits "true" once does not emit "false" for a minimum of . - /// Resulting observable is distinct. + /// The resulting observable emits "false" for the first value of and for every "false". /// /// /// /// - /// If "true", every "true" that is emitted by will reset the timer. + /// If "true", every "true" that is emitted by while the timer runs will reset the timer. + /// If set to "false", the resulting observable will not be distinct. Consecutive "false" values are emitted, as are consecutive "true" values received after the timer ran out. "true" values received while the timer runs are not emitted. + /// Determines what happens when completes while the timer runs: complete immediately without emitting "true" (default), or emit "true" once the timer runs out and complete afterwards. public static IObservable WhenTrueFor( this IObservable source, TimeSpan timeSpan, - IScheduler scheduler, - bool resetTimerOnConsecutiveTrue = false) + IScheduler scheduler, + bool resetTimerOnConsecutiveTrue = false, + bool distinctUntilChanged = true, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(scheduler); - if (timeSpan <= TimeSpan.Zero) { return source; } - /* - * Note: Attempting to implement this method with both parameters distinctUntilChanged and resetTimerOnConsecutiveTrue together is not impossible, but is difficult to do in a clean way: - * The difficulty with distinctUntilChanged as false, is that consecutive true values should be emitted if the timer isn't running anymore. - * The difficulty with resetTimerOnConsecutiveTrue as true, is that consecutive true values should only reset a timer when it's still running. - * Only implementing resetTimerOnConsecutiveTrue was relatively easy, as we can always use a true value to reset the Throttle timer and simply apply DistinctUntilChanged to the end result. - */ - - var falseObservable = source.Where(b => !b); - - var initialFalseObservable = source - .Take(1) - .Where(b => b) // Only when immediately true, we need to publish a false in the beginning as falseObservable won't provide it for us. - .Select(_ => false); - - IObservable delayedTrueObservable; - if (resetTimerOnConsecutiveTrue) - { - delayedTrueObservable = source - .Throttle(timeSpan, scheduler) // This will publish the value after x time. Note that we also pass "false" values here, to prevent a delayed "true" from being released. - .Where(b => b); // As a "false" is also passed to throttle, we filter it out here. - } - else - { - delayedTrueObservable = source - .DistinctUntilChanged() // Filter out consecutive values to prevent the timer from being reset. - .Throttle(timeSpan, scheduler) // This will publish the value after x time. Note that we also pass "false" values here, to prevent a delayed "true" from being released. - .Where(b => b); // As a "false" is also passed to throttle, we filter it out here. - } - - return falseObservable - .Merge(initialFalseObservable) - .Merge(delayedTrueObservable) - .DistinctUntilChanged(); + return Observable.Create(observer => + new DelayedTransitionOperator(observer, timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, completionBehavior, + delayTrue: true, delayFalse: false, assumedInitialValue: false) + .Run(source)); } /// /// Returns an observable that emits "false" once does not emit "true" for a minimum of . - /// Resulting observable is distinct. + /// The resulting observable emits "true" for the first value of and for every "true". /// /// /// /// - /// If "true", every "false" that is emitted by will reset the timer. + /// If "true", every "false" that is emitted by while the timer runs will reset the timer. + /// If set to "false", the resulting observable will not be distinct. Consecutive "true" values are emitted, as are consecutive "false" values received after the timer ran out. "false" values received while the timer runs are not emitted. + /// Determines what happens when completes while the timer runs: complete immediately without emitting "false" (default), or emit "false" once the timer runs out and complete afterwards. public static IObservable WhenFalseFor( this IObservable source, TimeSpan timeSpan, IScheduler scheduler, - bool resetTimerOnConsecutiveFalse = false) => + bool resetTimerOnConsecutiveFalse = false, + bool distinctUntilChanged = true, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) => source .Not() - .WhenTrueFor(timeSpan, scheduler, resetTimerOnConsecutiveFalse) + .WhenTrueFor(timeSpan, scheduler, resetTimerOnConsecutiveFalse, distinctUntilChanged, completionBehavior) .Not(); /// - /// Returns an observable that will automatically emit "false" if does not emit a "false" itself within after emitting "true". + /// Returns an observable that only emits a value once has held it for a minimum of . + /// The first value of is emitted immediately. A change that reverts before the timer runs out is never emitted. /// /// /// /// - /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. - /// If "true", every "true" that is emitted by will reset the timer. + /// If "true", a repeated pending value that is emitted by while the timer runs will reset the timer. + /// If set to "false", the resulting observable will not be distinct. Values equal to the last emitted value are passed through, including one that cancels a pending change. Values received while the timer runs are not emitted. + /// Determines what happens when completes while a change is pending: complete immediately without emitting it (default), or emit it once the timer runs out and complete afterwards. /// - public static IObservable LimitTrueDuration(this IObservable source, TimeSpan timeSpan, IScheduler scheduler, + public static IObservable WhenStableFor( + this IObservable source, + TimeSpan timeSpan, + IScheduler scheduler, + bool resetTimerOnConsecutiveValue = false, bool distinctUntilChanged = true, - bool resetTimerOnConsecutiveTrue = false) + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) { ArgumentNullException.ThrowIfNull(source); ArgumentNullException.ThrowIfNull(scheduler); - - if (distinctUntilChanged) + if (timeSpan <= TimeSpan.Zero) { - var falseWhenTrueFor = source - .WhenTrueFor(timeSpan, scheduler, resetTimerOnConsecutiveTrue) - .Where(b => b) - .Select(_ => false); - - return source.Merge(falseWhenTrueFor).DistinctUntilChanged(); + return source; } - var timer = CreateTimer(source, timeSpan, scheduler, resetTimerOnConsecutiveTrue); - return source.CombineLatest(timer) - .Select(tuple => ((bool?)tuple.First, (bool?)tuple.Second)) // We convert to nullables to allow an initial seed of (null, null) in the pairing method. - .Pairwise((null, null)) // As double true values from the source do need to be emitted. We need the previous item to determine whether the change comes from the source or from the timer. - .Select(tuple => - { - var previous = tuple.Item1; - var current = tuple.Item2; - - var previousValue = previous.Item1; - var currentValue = current.Item1; - - var previousTimer = previous.Item2; - var currentTimer = current.Item2; - - var timerValueChanged = previousTimer != null && previousTimer != currentTimer; - if (!timerValueChanged) - { - // Always emit false, and only emit true if the timer is (still) running. - if (currentValue == false || (currentValue == true && currentTimer == true)) - { - return currentValue; - } - // It can happen that we receive the "true" that initiates the timer before we receive the value indicated that the timer has started. For that reason we also pass every first "true" value if the timer is not running. - if (previousValue != true && currentValue == true && currentTimer == false) - { - return true; - } - - return null; - } + return Observable.Create(observer => + new DelayedTransitionOperator(observer, timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveValue, completionBehavior, + delayTrue: true, delayFalse: true, assumedInitialValue: null) + .Run(source)); + } - // Only when the timer is done and the current value is true, we have to emit false based on the timer. - if (currentValue == true && currentTimer == false) - { - return false; - } + /// + /// Returns an observable that will automatically emit "false" if does not emit a "false" itself within after emitting "true". + /// Once the limit is reached, a repeated "true" from is ignored until it emits "false" again, unless is set. + /// + /// + /// + /// + /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. + /// If "true", every "true" that is emitted by will reset the timer. A "true" received after the limit was reached then re-arms the limit and is emitted again. + /// Determines what happens when completes while the timer runs: complete immediately without emitting the limiting "false" (default), or emit it once the timer runs out and complete afterwards. + /// + public static IObservable LimitTrueDuration( + this IObservable source, + TimeSpan timeSpan, + IScheduler scheduler, + bool distinctUntilChanged = true, + bool resetTimerOnConsecutiveTrue = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(scheduler); - return null; - }) - .Where(b => b != null) - .Select(b => b!.Value); + return Observable.Create(observer => + new TimedWindowOperator(observer, timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, completionBehavior, + withholdFalseDuringWindow: false, forceFalseAtEnd: true) + .Run(source)); } /// /// Returns an observable that will automatically emit "true" if does not emit a "true" itself within after emitting "false". + /// Once the limit is reached, a repeated "false" from is ignored until it emits "true" again, unless is set. /// /// /// /// /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. - /// If "true", every "false" that is emitted by will reset the timer. + /// If "true", every "false" that is emitted by will reset the timer. A "false" received after the limit was reached then re-arms the limit and is emitted again. + /// Determines what happens when completes while the timer runs: complete immediately without emitting the limiting "true" (default), or emit it once the timer runs out and complete afterwards. /// public static IObservable LimitFalseDuration( - this IObservable source, + this IObservable source, TimeSpan timeSpan, IScheduler scheduler, bool distinctUntilChanged = true, - bool resetTimerOnConsecutiveTrue = false) => + bool resetTimerOnConsecutiveFalse = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) => source .Not() - .LimitTrueDuration(timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue) + .LimitTrueDuration(timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveFalse, completionBehavior) .Not(); /// - /// Returns an observable that immediately returns false, emits true when emits true and will emit false again after a duration of . + /// Returns an observable that emits "true" for exactly after transitions to "true", followed by "false". + /// A "false" emitted by during the pulse is withheld until the pulse ends, and a "true" that outlasts the pulse does not extend it. /// - private static IObservable CreateTimer( - IObservable triggerObservable, + /// + /// + /// + /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. Note that "false" values that occur during the pulse are not emitted; the pulse always ends with a single "false". + /// If "true", every "true" that is emitted by will restart the pulse, also after the pulse has ended. A "true" that follows a "false" always (re)starts the pulse. + /// Determines what happens when completes during a pulse: complete immediately without emitting the closing "false" (default), or emit it once the pulse ends and complete afterwards. + /// + public static IObservable PulseTrueFor( + this IObservable source, TimeSpan timeSpan, - IScheduler scheduler, - bool resetTimerOnConsecutiveTrue = false) + IScheduler scheduler, + bool distinctUntilChanged = true, + bool resetTimerOnConsecutiveTrue = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) { - var trueObservable = triggerObservable.Where(b => b); - IObservable delayedFalseObservable; - if (resetTimerOnConsecutiveTrue) - { - delayedFalseObservable = triggerObservable - .Where(b => b) - .Throttle(timeSpan, scheduler) - .Select(b => !b); // We invert the result as we want to turn the timer off x time after the "true" was emitted. - } - else + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(scheduler); + if (timeSpan <= TimeSpan.Zero) { - delayedFalseObservable = triggerObservable - .DistinctUntilChanged() - .Where(b => b) - .Throttle(timeSpan, scheduler) - .Select(b => !b); // We invert the result as we want to turn the timer off x time after the "true" was emitted. + return source; } - return trueObservable - .Merge(delayedFalseObservable) - .Prepend(false) - .DistinctUntilChanged(); + return Observable.Create(observer => + new TimedWindowOperator(observer, timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, completionBehavior, + withholdFalseDuringWindow: true, forceFalseAtEnd: true) + .Run(source)); } + + /// + /// Returns an observable that emits "false" for exactly after transitions to "false", followed by "true". + /// A "true" emitted by during the pulse is withheld until the pulse ends, and a "false" that outlasts the pulse does not extend it. + /// + /// + /// + /// + /// If set to "false", the resulting observable will not be distinct. Both consecutive "true" and "false" values will be emitted. Note that "true" values that occur during the pulse are not emitted; the pulse always ends with a single "true". + /// If "true", every "false" that is emitted by will restart the pulse, also after the pulse has ended. A "false" that follows a "true" always (re)starts the pulse. + /// Determines what happens when completes during a pulse: complete immediately without emitting the closing "true" (default), or emit it once the pulse ends and complete afterwards. + /// + public static IObservable PulseFalseFor( + this IObservable source, + TimeSpan timeSpan, + IScheduler scheduler, + bool distinctUntilChanged = true, + bool resetTimerOnConsecutiveFalse = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) => + source + .Not() + .PulseTrueFor(timeSpan, scheduler, distinctUntilChanged, resetTimerOnConsecutiveFalse, completionBehavior) + .Not(); + + /// + /// Returns an observable that alternates between "true" and "false" while is "true", starting with "true". + /// A "false" emitted by stops the blinking immediately. + /// + /// + /// How long each "true" and each "false" phase lasts. + /// + /// If set to "false", the resulting observable will not be distinct. Consecutive "false" values from are emitted. Repeated "true" values received while blinking are not emitted. + /// If "true", every "true" that is emitted by restarts the "true" phase. + /// Determines what happens when completes while blinking: complete immediately (default), or finish the current "true" phase, emit "false" and complete afterwards. Completion during a "false" phase is always immediate. + /// is zero or negative. + public static IObservable BlinkWhileTrue( + this IObservable source, + TimeSpan interval, + IScheduler scheduler, + bool distinctUntilChanged = true, + bool resetTimerOnConsecutiveTrue = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) => + source.BlinkWhileTrue(interval, interval, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, completionBehavior); + + /// + /// Returns an observable that alternates between "true" and "false" while is "true", starting with "true". + /// A "false" emitted by stops the blinking immediately. + /// + /// + /// How long each "true" phase lasts. + /// How long each "false" phase lasts. + /// + /// If set to "false", the resulting observable will not be distinct. Consecutive "false" values from are emitted. Repeated "true" values received while blinking are not emitted. + /// If "true", every "true" that is emitted by restarts the "true" phase. + /// Determines what happens when completes while blinking: complete immediately (default), or finish the current "true" phase, emit "false" and complete afterwards. Completion during a "false" phase is always immediate. + /// or is zero or negative. + public static IObservable BlinkWhileTrue( + this IObservable source, + TimeSpan onDuration, + TimeSpan offDuration, + IScheduler scheduler, + bool distinctUntilChanged = true, + bool resetTimerOnConsecutiveTrue = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(scheduler); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(onDuration, TimeSpan.Zero); + ArgumentOutOfRangeException.ThrowIfLessThanOrEqual(offDuration, TimeSpan.Zero); + + return Observable.Create(observer => + new BlinkWhileTrueOperator(observer, onDuration, offDuration, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, completionBehavior) + .Run(source)); + } + + /// + /// Returns an observable that alternates between "false" and "true" while is "false", starting with "false". + /// A "true" emitted by stops the blinking immediately. + /// + /// + /// How long each "false" and each "true" phase lasts. + /// + /// If set to "false", the resulting observable will not be distinct. Consecutive "true" values from are emitted. Repeated "false" values received while blinking are not emitted. + /// If "true", every "false" that is emitted by restarts the "false" phase. + /// Determines what happens when completes while blinking: complete immediately (default), or finish the current "false" phase, emit "true" and complete afterwards. Completion during a "true" phase is always immediate. + /// is zero or negative. + public static IObservable BlinkWhileFalse( + this IObservable source, + TimeSpan interval, + IScheduler scheduler, + bool distinctUntilChanged = true, + bool resetTimerOnConsecutiveFalse = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) => + source.BlinkWhileFalse(interval, interval, scheduler, distinctUntilChanged, resetTimerOnConsecutiveFalse, completionBehavior); + + /// + /// Returns an observable that alternates between "false" and "true" while is "false", starting with "false". + /// A "true" emitted by stops the blinking immediately. + /// + /// + /// How long each "false" phase lasts. + /// How long each "true" phase lasts. + /// + /// If set to "false", the resulting observable will not be distinct. Consecutive "true" values from are emitted. Repeated "false" values received while blinking are not emitted. + /// If "true", every "false" that is emitted by restarts the "false" phase. + /// Determines what happens when completes while blinking: complete immediately (default), or finish the current "false" phase, emit "true" and complete afterwards. Completion during a "true" phase is always immediate. + /// or is zero or negative. + public static IObservable BlinkWhileFalse( + this IObservable source, + TimeSpan offDuration, + TimeSpan onDuration, + IScheduler scheduler, + bool distinctUntilChanged = true, + bool resetTimerOnConsecutiveFalse = false, + CompletionBehavior completionBehavior = CompletionBehavior.CompleteImmediately) => + source + .Not() + .BlinkWhileTrue(offDuration, onDuration, scheduler, distinctUntilChanged, resetTimerOnConsecutiveFalse, completionBehavior) + .Not(); } } diff --git a/src/Reactive.Boolean/CompletionBehavior.cs b/src/Reactive.Boolean/CompletionBehavior.cs new file mode 100644 index 0000000..6277e6a --- /dev/null +++ b/src/Reactive.Boolean/CompletionBehavior.cs @@ -0,0 +1,18 @@ +namespace Reactive.Boolean; + +/// +/// Specifies how a scheduling operator completes when its source completes while the operator's timer is still running. +/// +public enum CompletionBehavior +{ + /// + /// The resulting observable completes immediately. A value that was still pending on the timer is never emitted. + /// + CompleteImmediately, + + /// + /// The resulting observable stays alive until the timer elapses, emits the pending value and completes afterwards. + /// It completes immediately when no value is pending. + /// + CompleteAfterTimer +} diff --git a/src/Reactive.Boolean/DelayedTransitionOperator.cs b/src/Reactive.Boolean/DelayedTransitionOperator.cs new file mode 100644 index 0000000..57afd3a --- /dev/null +++ b/src/Reactive.Boolean/DelayedTransitionOperator.cs @@ -0,0 +1,57 @@ +using System.Reactive.Concurrency; + +namespace Reactive.Boolean; + +/// +/// Delays a transition of the source by the timespan and cancels it when the source reverts before the timer runs out. +/// (delay "true", assume an initial "false"), +/// (delay "false", pass the first value through) and +/// (delay both, pass the first value through) are all instances of this machine. +/// +internal sealed class DelayedTransitionOperator( + IObserver observer, + TimeSpan timeSpan, + IScheduler scheduler, + bool distinctUntilChanged, + bool resetTimerOnConsecutiveValue, + CompletionBehavior completionBehavior, + bool delayTrue, + bool delayFalse, + bool? assumedInitialValue) + : TimedBooleanOperator(observer, timeSpan, scheduler, distinctUntilChanged, completionBehavior) +{ + protected override void OnSourceValue(bool value) + { + if (LastEmittedValue == null) + { + if (assumedInitialValue == null) + { + Emit(value); + return; + } + + // The assumed value is the current state; a differing first value is a transition like any other. + Emit(assumedInitialValue.Value); + if (value == assumedInitialValue) + { + return; + } + } + + var delayed = value ? delayTrue : delayFalse; + if (value == LastEmittedValue || !delayed) + { + StopTimer(); + Emit(value); + return; + } + + if (!TimerRunning || resetTimerOnConsecutiveValue) + { + StartTimer(); + } + } + + // The timer only runs while the latest source value differs from the emitted one, so it is the pending change. + protected override void OnTimerElapsed() => Emit(LastSourceValue!.Value); +} diff --git a/src/Reactive.Boolean/ObservableExtensions.cs b/src/Reactive.Boolean/ObservableExtensions.cs deleted file mode 100644 index 6f652e2..0000000 --- a/src/Reactive.Boolean/ObservableExtensions.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Reactive.Linq; - -namespace Reactive.Boolean -{ - internal static class ObservableExtensions - { - /// - /// Returns an observable that represents pairs of previous and current values of the source observable. First pair that is emitted will use as previous value. - /// - /// - /// - /// - /// - internal static IObservable<(T TPrevious, T TCurrent)> Pairwise(this IObservable source, T seed) - { - return source - .StartWith(seed) // Prepend the seed value - .Buffer(2, 1) // Create overlapping pairs - .Where(pair => pair.Count == 2) // Prevent completing the observable from emitting a single item - .Select(pair => (pair[0], pair[1])); // Convert to (Previous, Current) tuple - } - } -} diff --git a/src/Reactive.Boolean/TimedBooleanOperator.cs b/src/Reactive.Boolean/TimedBooleanOperator.cs new file mode 100644 index 0000000..fabbe0f --- /dev/null +++ b/src/Reactive.Boolean/TimedBooleanOperator.cs @@ -0,0 +1,168 @@ +using System.Reactive.Concurrency; +using System.Reactive.Disposables; + +namespace Reactive.Boolean; + +/// +/// Base class for the time-based operators. Subscribes to the source exactly once and drives a single restartable timer +/// on the provided scheduler, so that source values and timer expiry are handled as one ordered stream of events under +/// one lock. Subclasses only decide what to emit and when to (re)start the timer. +/// +internal abstract class TimedBooleanOperator( + IObserver observer, + TimeSpan timeSpan, + IScheduler scheduler, + bool distinctUntilChanged, + CompletionBehavior completionBehavior) +{ + private readonly object _gate = new(); + private readonly SerialDisposable _timer = new(); + + private int _timerGeneration; + private bool? _lastEmittedValue; + private bool _sourceCompleted; + private bool _terminated; + + /// + /// Inside : the value that preceded the one being handled. + /// Inside : the latest value. Null until the source has emitted. + /// + protected bool? LastSourceValue { get; private set; } + + protected bool TimerRunning { get; private set; } + + /// + /// The value most recently handed to the observer. Null until something has been emitted. + /// + protected bool? LastEmittedValue => _lastEmittedValue; + + /// + /// Whether the source has completed while completion is deferred until the timer elapses. + /// + protected bool SourceCompleted => _sourceCompleted; + + /// + /// Whether the running timer will emit a value when it elapses. Decides whether + /// has anything to wait for. + /// + protected virtual bool HasPendingValue => TimerRunning; + + protected abstract void OnSourceValue(bool value); + + protected abstract void OnTimerElapsed(); + + public IDisposable Run(IObservable source) + { + var subscription = source.Subscribe(OnNext, OnError, OnCompleted); + var stop = Disposable.Create(() => + { + lock (_gate) + { + _terminated = true; + } + }); + return new CompositeDisposable(subscription, _timer, stop); + } + + protected void Emit(bool value) + { + if (distinctUntilChanged && _lastEmittedValue == value) + { + return; + } + + _lastEmittedValue = value; + observer.OnNext(value); + } + + protected void StartTimer() => StartTimer(timeSpan); + + protected void StartTimer(TimeSpan dueTime) + { + var generation = ++_timerGeneration; + TimerRunning = true; + _timer.Disposable = scheduler.Schedule(dueTime, () => TimerElapsed(generation)); + } + + protected void StopTimer() + { + _timerGeneration++; + TimerRunning = false; + _timer.Disposable = Disposable.Empty; + } + + private void OnNext(bool value) + { + lock (_gate) + { + if (_terminated) + { + return; + } + + OnSourceValue(value); + LastSourceValue = value; + } + } + + private void OnError(Exception error) + { + lock (_gate) + { + if (_terminated) + { + return; + } + + Terminate(); + observer.OnError(error); + } + } + + private void OnCompleted() + { + lock (_gate) + { + if (_terminated) + { + return; + } + + if (completionBehavior == CompletionBehavior.CompleteAfterTimer && HasPendingValue) + { + _sourceCompleted = true; + return; + } + + Terminate(); + observer.OnCompleted(); + } + } + + private void TimerElapsed(int generation) + { + lock (_gate) + { + // A stale callback can still fire when it was dequeued just before the timer was restarted or stopped. + if (_terminated || generation != _timerGeneration) + { + return; + } + + TimerRunning = false; + OnTimerElapsed(); + + if (_sourceCompleted && !TimerRunning) + { + Terminate(); + observer.OnCompleted(); + } + } + } + + private void Terminate() + { + _terminated = true; + StopTimer(); + } +} diff --git a/src/Reactive.Boolean/TimedWindowOperator.cs b/src/Reactive.Boolean/TimedWindowOperator.cs new file mode 100644 index 0000000..fe68d0f --- /dev/null +++ b/src/Reactive.Boolean/TimedWindowOperator.cs @@ -0,0 +1,77 @@ +using System.Reactive.Concurrency; + +namespace Reactive.Boolean; + +/// +/// Opens a window of the timespan on every rising edge of the source and decides what "false" means while it is open +/// and when it ends. withholds a "false" until the window ends, +/// forces a "false" when it ends and +/// does both. +/// +internal sealed class TimedWindowOperator( + IObserver observer, + TimeSpan timeSpan, + IScheduler scheduler, + bool distinctUntilChanged, + bool resetTimerOnConsecutiveTrue, + CompletionBehavior completionBehavior, + bool withholdFalseDuringWindow, + bool forceFalseAtEnd) + : TimedBooleanOperator(observer, timeSpan, scheduler, distinctUntilChanged, completionBehavior) +{ + protected override bool HasPendingValue => TimerRunning && (forceFalseAtEnd || LastSourceValue == false); + + protected override void OnSourceValue(bool value) + { + if (!value) + { + if (withholdFalseDuringWindow && TimerRunning) + { + return; + } + + StopTimer(); + Emit(false); + return; + } + + // A rising edge always opens a window; a repeated "true" only restarts it when asked to. + if (LastSourceValue != true) + { + Emit(true); + StartTimer(); + return; + } + + if (TimerRunning) + { + Emit(true); + if (resetTimerOnConsecutiveTrue) + { + StartTimer(); + } + + return; + } + + // The window ended and the source merely repeats "true". + if (resetTimerOnConsecutiveTrue) + { + Emit(true); + StartTimer(); + } + else if (!forceFalseAtEnd) + { + // Nothing was forced, so the output still tracks the source. + Emit(true); + } + } + + protected override void OnTimerElapsed() + { + if (forceFalseAtEnd || LastSourceValue == false) + { + Emit(false); + } + } +} diff --git a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.BlinkWhileTrue.Tests.cs b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.BlinkWhileTrue.Tests.cs new file mode 100644 index 0000000..5a3fe61 --- /dev/null +++ b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.BlinkWhileTrue.Tests.cs @@ -0,0 +1,446 @@ +using Microsoft.Reactive.Testing; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Reactive.Boolean.Tests +{ + [TestClass] + public class BooleanObservableExtensionsSchedulingBlinkWhileTrueTests + { + private static readonly TimeSpan On = TimeSpan.FromTicks(2); + private static readonly TimeSpan Off = TimeSpan.FromTicks(3); + + [TestMethod] + [DataRow(false, false, true)] + [DataRow(false, false, false)] + [DataRow(true, false, true)] + [DataRow(true, false, false)] + [DataRow(false, true, true)] + [DataRow(false, true, false)] + [DataRow(true, true, true)] + [DataRow(true, true, false)] + public void BlinkWhileTrue_InitialValue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + blinkObservable.Subscribe(b => result = b); + + // Act + subject.OnNext(initialValue); + + // Assert + Assert.AreEqual(initialValue, result); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void BlinkWhileTrue_Toggles(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false, true }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false, true, false }, results); + } + + [TestMethod] + public void BlinkWhileTrue_SingleInterval_Toggles() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, scheduler); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false, true }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false, true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void BlinkWhileTrue_FalseDuringOnPhase_StopsImmediately(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, false }, results); + + scheduler.AdvanceBy(10); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void BlinkWhileTrue_FalseDuringOffPhase_StopsImmediately(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(false); + var expected = distinctUntilChanged ? new[] { true, false } : new[] { true, false, false }; + CollectionAssert.AreEqual(expected, results); + + scheduler.AdvanceBy(10); + CollectionAssert.AreEqual(expected, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void BlinkWhileTrue_RepeatedTrue_IgnoredWithoutReset(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnNext(true); // Neither emitted nor restarting the phase. + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void BlinkWhileTrue_RepeatedTrueDuringOnPhase_RestartsWithReset(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnNext(true); + var expected = distinctUntilChanged ? new[] { true } : new[] { true, true }; + CollectionAssert.AreEqual(expected, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(expected, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(expected.Append(false).ToArray(), results); + } + + [TestMethod] + public void BlinkWhileTrue_RepeatedTrueDuringOffPhase_RestartsWithReset() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true, false, true }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false, true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void BlinkWhileTrue_ConsecutiveFalse_NotDistinct_Emitted(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(false); + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { false, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void BlinkWhileTrue_CompleteImmediately_MidOnPhase_CompletesAtOnce(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteImmediately); + + var results = new List(); + var completed = false; + blinkObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(10); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void BlinkWhileTrue_CompleteAfterTimer_MidOnPhase_FinishesPhaseThenCompletes(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + blinkObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsFalse(completed); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(10); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void BlinkWhileTrue_CompleteAfterTimer_MidOffPhase_CompletesImmediately(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + blinkObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(10); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void BlinkWhileTrue_Error(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + Exception? receivedException = null; + blinkObservable.Subscribe(b => result = b, e => receivedException = e); + + subject.OnNext(true); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + var exception = new InvalidOperationException("This is a test"); + subject.OnError(exception); + Assert.AreEqual(receivedException, exception); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void BlinkWhileTrue_DisposeCancelsTimer(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + var subscription = blinkObservable.Subscribe(results.Add); + + subject.OnNext(true); + subscription.Dispose(); + + scheduler.AdvanceBy(10); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void BlinkWhileTrue_SubscribesToSourceOnce(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subscriptions = 0; + var source = Observable.Defer(() => + { + subscriptions++; + return Observable.Never(); + }); + + source.BlinkWhileTrue(On, Off, new TestScheduler(), distinctUntilChanged, resetTimerOnConsecutiveTrue).Subscribe(); + + Assert.AreEqual(1, subscriptions); + } + + [TestMethod] + [DataRow(SourceShape.Subject, false, false)] + [DataRow(SourceShape.Subject, true, false)] + [DataRow(SourceShape.Subject, false, true)] + [DataRow(SourceShape.Subject, true, true)] + [DataRow(SourceShape.SelectManyBurst, false, false)] + [DataRow(SourceShape.SelectManyBurst, true, false)] + [DataRow(SourceShape.SelectManyBurst, false, true)] + [DataRow(SourceShape.SelectManyBurst, true, true)] + [DataRow(SourceShape.ColdArray, false, false)] + [DataRow(SourceShape.ColdArray, true, false)] + [DataRow(SourceShape.ColdArray, false, true)] + [DataRow(SourceShape.ColdArray, true, true)] + [DataRow(SourceShape.ColdConcat, false, false)] + [DataRow(SourceShape.ColdConcat, true, false)] + [DataRow(SourceShape.ColdConcat, false, true)] + [DataRow(SourceShape.ColdConcat, true, true)] + [DataRow(SourceShape.DeferPrepend, false, false)] + [DataRow(SourceShape.DeferPrepend, true, false)] + [DataRow(SourceShape.DeferPrepend, false, true)] + [DataRow(SourceShape.DeferPrepend, true, true)] + public void BlinkWhileTrue_FalseThenTrue_SameForEverySourceShape(SourceShape shape, bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var scheduler = new TestScheduler(); + var (source, start) = SourceShapes.Create(shape, false, true); + var blinkObservable = source.BlinkWhileTrue(On, Off, scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + start(); + CollectionAssert.AreEqual(new[] { false, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false, true }, results); + + // The cold shapes complete here after finishing the "true" phase, so only the first phase is common to every shape. + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false, true, false }, results); + } + + [TestMethod] + public void BlinkWhileTrue_ZeroOrNegativeDuration_Throws() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + + Assert.ThrowsExactly(() => subject.BlinkWhileTrue(TimeSpan.Zero, scheduler)); + Assert.ThrowsExactly(() => subject.BlinkWhileTrue(TimeSpan.FromTicks(-1), scheduler)); + Assert.ThrowsExactly(() => subject.BlinkWhileTrue(TimeSpan.Zero, Off, scheduler)); + Assert.ThrowsExactly(() => subject.BlinkWhileTrue(On, TimeSpan.Zero, scheduler)); + } + + [TestMethod] + public void BlinkWhileFalse_IsInverse() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var blinkObservable = subject.BlinkWhileFalse(On, Off, scheduler); + + var results = new List(); + blinkObservable.Subscribe(results.Add); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { false, true }, results); + + scheduler.AdvanceBy(3); + CollectionAssert.AreEqual(new[] { false, true, false }, results); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { false, true, false, true }, results); + } + } +} diff --git a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.LimitTrueDuration.Tests.cs b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.LimitTrueDuration.Tests.cs index 61f9fa5..8f88bfe 100644 --- a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.LimitTrueDuration.Tests.cs +++ b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.LimitTrueDuration.Tests.cs @@ -1,315 +1,525 @@ -using Microsoft.Reactive.Testing; -using System.Reactive.Subjects; - -namespace Reactive.Boolean.Tests -{ - [TestClass] - public class BooleanObservableExtensionsSchedulingTests - { - [TestMethod] - [DataRow(false, false, true)] - [DataRow(false, false, false)] - [DataRow(true, false, true)] - [DataRow(true, false, false)] - [DataRow(false, true, true)] - [DataRow(false, true, false)] - [DataRow(true, true, true)] - [DataRow(true, true, false)] - public void LimitTrueDuration_InitialValue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - // Act - subject.OnNext(initialValue); - - // Assert - Assert.AreEqual(initialValue, result); - } - - [TestMethod] - [DataRow(false, true)] - [DataRow(false, false)] - [DataRow(true, true)] - [DataRow(true, false)] - public void LimitTrueDuration_InitialValue_Distinct(bool resetTimerOnConsecutiveTrue, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue: resetTimerOnConsecutiveTrue); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - // Act - subject.OnNext(initialValue); - subject.OnNext(initialValue); - - // Assert - CollectionAssert.AreEqual(new[] { initialValue }, results); - } - - [TestMethod] - [DataRow(false, true)] - [DataRow(false, false)] - [DataRow(true, true)] - [DataRow(true, false)] - public void LimitTrueDuration_InitialValue_NotDistinct(bool resetTimerOnConsecutiveTrue, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = - subject.LimitTrueDuration(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - // Act - subject.OnNext(initialValue); - subject.OnNext(initialValue); - - // Assert - CollectionAssert.AreEqual(new[] { initialValue, initialValue }, results); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void LimitTrueDuration_FalseAfterTimeSpan(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void LimitTrueDuration_FalseAfterTimeSpan_Repeat(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - subject.OnNext(false); - subject.OnNext(true); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void LimitTrueDuration_FalseIsImmediate(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - subject.OnNext(false); - Assert.IsFalse(result); - } - - [TestMethod] - public void LimitTrueDuration_TimerNotResetOnConsecutiveTrue_Distinct() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should not reset timer - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, false }, results); - - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, false }, results); - } - - [TestMethod] - public void LimitTrueDuration_TimerNotResetOnConsecutiveTrue_NotDistinct() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should not reset timer - CollectionAssert.AreEqual(new[] { true, true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, true, false }, results); - - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, true, false, false }, results); - } - - [TestMethod] - public void LimitTrueDuration_TimerResetOnConsecutiveTrue_Distinct() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue: true); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should reset timer - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, false }, results); - - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, false }, results); - } - - [TestMethod] - public void LimitTrueDuration_TimerResetOnConsecutiveTrue_NotDistinct() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue: true); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should reset timer - CollectionAssert.AreEqual(new[] { true, true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, true, false }, results); - - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, true, false, false }, results); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void LimitTrueDuration_CompleteIsImmediate(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - var completed = false; - memoryObservable.Subscribe(b => result = b, _ => { }, () => completed = true); - - subject.OnNext(true); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - subject.OnCompleted(); - Assert.IsTrue(completed); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void LimitTrueDuration_Error(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - Exception? receivedException = null; - memoryObservable.Subscribe(b => result = b, e => receivedException = e); - - subject.OnNext(true); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - var exception = new InvalidOperationException("This is a test"); - subject.OnError(exception); - Assert.AreEqual(receivedException, exception); - } - } -} \ No newline at end of file +using Microsoft.Reactive.Testing; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Reactive.Boolean.Tests +{ + [TestClass] + public class BooleanObservableExtensionsSchedulingLimitTrueDurationTests + { + [TestMethod] + [DataRow(false, false, true)] + [DataRow(false, false, false)] + [DataRow(true, false, true)] + [DataRow(true, false, false)] + [DataRow(false, true, true)] + [DataRow(false, true, false)] + [DataRow(true, true, true)] + [DataRow(true, true, false)] + public void LimitTrueDuration_InitialValue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + // Act + subject.OnNext(initialValue); + + // Assert + Assert.AreEqual(initialValue, result); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void LimitTrueDuration_InitialValue_Distinct(bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue: resetTimerOnConsecutiveTrue); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + // Act + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + // Assert + CollectionAssert.AreEqual(new[] { initialValue }, results); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void LimitTrueDuration_InitialValue_NotDistinct(bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = + subject.LimitTrueDuration(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + // Act + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + // Assert + CollectionAssert.AreEqual(new[] { initialValue, initialValue }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_FalseAfterTimeSpan(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_FalseAfterTimeSpan_Repeat(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + subject.OnNext(false); + subject.OnNext(true); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_FalseIsImmediate(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnNext(false); + Assert.IsFalse(result); + } + + [TestMethod] + public void LimitTrueDuration_TimerNotResetOnConsecutiveTrue_Distinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should not reset timer + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + public void LimitTrueDuration_TimerNotResetOnConsecutiveTrue_NotDistinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should not reset timer + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, true, false, false }, results); + } + + [TestMethod] + public void LimitTrueDuration_TimerResetOnConsecutiveTrue_Distinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should reset timer + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + public void LimitTrueDuration_TimerResetOnConsecutiveTrue_NotDistinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should reset timer + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, true, false, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_CompleteIsImmediate(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + var completed = false; + memoryObservable.Subscribe(b => result = b, _ => { }, () => completed = true); + + subject.OnNext(true); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnCompleted(); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_Error(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + Exception? receivedException = null; + memoryObservable.Subscribe(b => result = b, e => receivedException = e); + + subject.OnNext(true); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + var exception = new InvalidOperationException("This is a test"); + subject.OnError(exception); + Assert.AreEqual(receivedException, exception); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void LimitTrueDuration_RepeatedTrueAfterLimit_IgnoredWithoutReset(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(true); // The source is still "true" for too long. + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(false); + subject.OnNext(true); // Only a real "false" to "true" transition starts a new limited period. + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true, false, true } : new[] { true, false, false, true }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true, false, true, false } : new[] { true, false, false, true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void LimitTrueDuration_RepeatedTrueAfterLimit_RetriggersWithReset(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true, false, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false, true, false }, results); + } + + [TestMethod] + [DataRow(SourceShape.Subject, false, false)] + [DataRow(SourceShape.Subject, true, false)] + [DataRow(SourceShape.Subject, false, true)] + [DataRow(SourceShape.Subject, true, true)] + [DataRow(SourceShape.SelectManyBurst, false, false)] + [DataRow(SourceShape.SelectManyBurst, true, false)] + [DataRow(SourceShape.SelectManyBurst, false, true)] + [DataRow(SourceShape.SelectManyBurst, true, true)] + [DataRow(SourceShape.ColdArray, false, false)] + [DataRow(SourceShape.ColdArray, true, false)] + [DataRow(SourceShape.ColdArray, false, true)] + [DataRow(SourceShape.ColdArray, true, true)] + [DataRow(SourceShape.ColdConcat, false, false)] + [DataRow(SourceShape.ColdConcat, true, false)] + [DataRow(SourceShape.ColdConcat, false, true)] + [DataRow(SourceShape.ColdConcat, true, true)] + [DataRow(SourceShape.DeferPrepend, false, false)] + [DataRow(SourceShape.DeferPrepend, true, false)] + [DataRow(SourceShape.DeferPrepend, false, true)] + [DataRow(SourceShape.DeferPrepend, true, true)] + public void LimitTrueDuration_TrueThenTrue_SameForEverySourceShape(SourceShape shape, bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var scheduler = new TestScheduler(); + var (source, start) = SourceShapes.Create(shape, true, true); + var memoryObservable = source.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + start(); + var expected = distinctUntilChanged ? new[] { true } : new[] { true, true }; + CollectionAssert.AreEqual(expected, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(expected, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(expected.Append(false).ToArray(), results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_SubscribesToSourceOnce(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subscriptions = 0; + var source = Observable.Defer(() => + { + subscriptions++; + return Observable.Never(); + }); + + source.LimitTrueDuration(TimeSpan.FromTicks(2), new TestScheduler(), distinctUntilChanged, resetTimerOnConsecutiveTrue).Subscribe(); + + Assert.AreEqual(1, subscriptions); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_CompleteImmediately_DoesNotEmitLimitingFalse(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteImmediately); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_CompleteAfterTimer_EmitsLimitingFalseThenCompletes(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsFalse(completed); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_CompleteAfterTimer_NoTimerRunning_CompletesImmediately(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); // Stops the timer. + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void LimitTrueDuration_DisposeCancelsTimer(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.LimitTrueDuration(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + var subscription = memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + subscription.Dispose(); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true }, results); + } + } +} \ No newline at end of file diff --git a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.PersistTrueFor.Tests.cs b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.PersistTrueFor.Tests.cs index 9b78214..195bc23 100644 --- a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.PersistTrueFor.Tests.cs +++ b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.PersistTrueFor.Tests.cs @@ -1,279 +1,480 @@ -using Microsoft.Reactive.Testing; -using System.Reactive.Subjects; - -namespace Reactive.Boolean.Tests -{ - [TestClass] - public class BooleanObservableExtensionsSchedulingPersistTrueForTests - { - [TestMethod] - [DataRow(true, true)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(false, false)] - public void PersistTrueFor_InitialValue(bool resetTimerOnConsecutiveFalse, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveFalse); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - // Act - subject.OnNext(initialValue); - - // Assert - Assert.AreEqual(initialValue, result); - } - - [TestMethod] - [DataRow(false, true)] - [DataRow(false, false)] - [DataRow(true, true)] - [DataRow(true, false)] - public void PersistTrueFor_InitialValue_IsDistinct(bool resetTimerOnConsecutiveFalse, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveFalse); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - // Act - subject.OnNext(initialValue); - subject.OnNext(initialValue); - - // Assert - CollectionAssert.AreEqual(new[] { initialValue }, results); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void PersistTrueFor_RemainsTrue(bool resetTimerOnConsecutiveFalse) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveFalse); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - - // Act - subject.OnNext(false); - - // Assert - Assert.IsTrue(result); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void PersistTrueFor_RemainsTrueForTimeSpan(bool resetTimerOnConsecutiveFalse) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - subject.OnNext(false); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void PersistTrueFor_RemainsTrueForTimeSpan_Repeat(bool resetTimerOnConsecutiveFalse) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - subject.OnNext(false); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - subject.OnNext(true); - subject.OnNext(false); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void PersistTrueFor_RemainsTrueForTimeSpanAfterFalse(bool resetTimerOnConsecutiveFalse) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - subject.OnNext(false); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void PersistTrueFor_TrueResetsTimer(bool resetTimerOnConsecutiveFalse) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - subject.OnNext(false); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - subject.OnNext(true); - subject.OnNext(false); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - } - - [TestMethod] - public void PersistTrueFor_TimerNotResetOnConsecutiveFalse() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(false); // Should not reset timer - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, false }, results); - } - - [TestMethod] - public void PersistTrueFor_TimerResetOnConsecutiveFalse() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse: true); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(false); // Should reset timer - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, false }, results); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void PersistTrueFor_CompleteIsImmediate(bool resetTimerOnConsecutiveFalse) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); - - bool? result = null; - bool completed = false; - memoryObservable.Subscribe(b => result = b, _ => { }, () => completed = true); - - subject.OnNext(true); - subject.OnNext(false); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - subject.OnCompleted(); - Assert.IsTrue(completed); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void PersistTrueFor_Error(bool resetTimerOnConsecutiveFalse) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); - - bool? result = null; - Exception? receivedException = null; - memoryObservable.Subscribe(b => result = b, e => receivedException = e); - - subject.OnNext(true); - subject.OnNext(false); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - var exception = new InvalidOperationException("This is a test"); - subject.OnError(exception); - Assert.AreEqual(receivedException, exception); - } - } -} \ No newline at end of file +using Microsoft.Reactive.Testing; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Reactive.Boolean.Tests +{ + [TestClass] + public class BooleanObservableExtensionsSchedulingPersistTrueForTests + { + [TestMethod] + [DataRow(true, true)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(false, false)] + public void PersistTrueFor_InitialValue(bool resetTimerOnConsecutiveFalse, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveFalse); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + // Act + subject.OnNext(initialValue); + + // Assert + Assert.AreEqual(initialValue, result); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void PersistTrueFor_InitialValue_IsDistinct(bool resetTimerOnConsecutiveFalse, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveFalse); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + // Act + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + // Assert + CollectionAssert.AreEqual(new[] { initialValue }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PersistTrueFor_RemainsTrue(bool resetTimerOnConsecutiveFalse) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveFalse); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + + // Act + subject.OnNext(false); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PersistTrueFor_RemainsTrueForTimeSpan(bool resetTimerOnConsecutiveFalse) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + subject.OnNext(false); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PersistTrueFor_RemainsTrueForTimeSpan_Repeat(bool resetTimerOnConsecutiveFalse) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + subject.OnNext(false); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + subject.OnNext(true); + subject.OnNext(false); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PersistTrueFor_RemainsTrueForTimeSpanAfterFalse(bool resetTimerOnConsecutiveFalse) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + subject.OnNext(false); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PersistTrueFor_TrueResetsTimer(bool resetTimerOnConsecutiveFalse) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + subject.OnNext(false); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + subject.OnNext(true); + subject.OnNext(false); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + } + + [TestMethod] + public void PersistTrueFor_TimerNotResetOnConsecutiveFalse() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(false); // Should not reset timer + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + public void PersistTrueFor_TimerResetOnConsecutiveFalse() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse: true); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(false); // Should reset timer + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PersistTrueFor_CompleteIsImmediate(bool resetTimerOnConsecutiveFalse) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); + + bool? result = null; + bool completed = false; + memoryObservable.Subscribe(b => result = b, _ => { }, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnCompleted(); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PersistTrueFor_Error(bool resetTimerOnConsecutiveFalse) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse); + + bool? result = null; + Exception? receivedException = null; + memoryObservable.Subscribe(b => result = b, e => receivedException = e); + + subject.OnNext(true); + subject.OnNext(false); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + var exception = new InvalidOperationException("This is a test"); + subject.OnError(exception); + Assert.AreEqual(receivedException, exception); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void PersistTrueFor_InitialValue_NotDistinct(bool resetTimerOnConsecutiveFalse, bool initialValue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveFalse, distinctUntilChanged: false); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + CollectionAssert.AreEqual(new[] { initialValue, initialValue }, results); + } + + [TestMethod] + public void PersistTrueFor_ConsecutiveFalseDuringTimer_NotDistinct_EmitsSingleFalse() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(false); // Timer is no longer running, so consecutive "false" values are emitted again. + CollectionAssert.AreEqual(new[] { true, false, false }, results); + } + + [TestMethod] + [DataRow(SourceShape.Subject, false, false)] + [DataRow(SourceShape.Subject, true, false)] + [DataRow(SourceShape.Subject, false, true)] + [DataRow(SourceShape.Subject, true, true)] + [DataRow(SourceShape.SelectManyBurst, false, false)] + [DataRow(SourceShape.SelectManyBurst, true, false)] + [DataRow(SourceShape.SelectManyBurst, false, true)] + [DataRow(SourceShape.SelectManyBurst, true, true)] + [DataRow(SourceShape.ColdArray, false, false)] + [DataRow(SourceShape.ColdArray, true, false)] + [DataRow(SourceShape.ColdArray, false, true)] + [DataRow(SourceShape.ColdArray, true, true)] + [DataRow(SourceShape.ColdConcat, false, false)] + [DataRow(SourceShape.ColdConcat, true, false)] + [DataRow(SourceShape.ColdConcat, false, true)] + [DataRow(SourceShape.ColdConcat, true, true)] + [DataRow(SourceShape.DeferPrepend, false, false)] + [DataRow(SourceShape.DeferPrepend, true, false)] + [DataRow(SourceShape.DeferPrepend, false, true)] + [DataRow(SourceShape.DeferPrepend, true, true)] + public void PersistTrueFor_TrueThenFalse_SameForEverySourceShape(SourceShape shape, bool resetTimerOnConsecutiveFalse, bool distinctUntilChanged) + { + var scheduler = new TestScheduler(); + var (source, start) = SourceShapes.Create(shape, true, false); + var memoryObservable = source.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + start(); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PersistTrueFor_SubscribesToSourceOnce(bool resetTimerOnConsecutiveFalse, bool distinctUntilChanged) + { + var subscriptions = 0; + var source = Observable.Defer(() => + { + subscriptions++; + return Observable.Never(); + }); + + source.PersistTrueFor(TimeSpan.FromTicks(2), new TestScheduler(), resetTimerOnConsecutiveFalse, distinctUntilChanged).Subscribe(); + + Assert.AreEqual(1, subscriptions); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PersistTrueFor_CompleteImmediately_DropsDelayedFalse(bool resetTimerOnConsecutiveFalse, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse, distinctUntilChanged, CompletionBehavior.CompleteImmediately); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PersistTrueFor_CompleteAfterTimer_EmitsDelayedFalseThenCompletes(bool resetTimerOnConsecutiveFalse, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsFalse(completed); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PersistTrueFor_CompleteAfterTimer_NothingDelayed_CompletesImmediately(bool resetTimerOnConsecutiveFalse, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + subject.OnNext(true); // Cancels the delayed "false". + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true } : new[] { true, true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PersistTrueFor_DisposeCancelsTimer(bool resetTimerOnConsecutiveFalse, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.PersistTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveFalse, distinctUntilChanged); + + var results = new List(); + var subscription = memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + subscription.Dispose(); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true }, results); + } + } +} \ No newline at end of file diff --git a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.PulseTrueFor.Tests.cs b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.PulseTrueFor.Tests.cs new file mode 100644 index 0000000..cca4af6 --- /dev/null +++ b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.PulseTrueFor.Tests.cs @@ -0,0 +1,507 @@ +using Microsoft.Reactive.Testing; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Reactive.Boolean.Tests +{ + [TestClass] + public class BooleanObservableExtensionsSchedulingPulseTrueForTests + { + [TestMethod] + [DataRow(false, false, true)] + [DataRow(false, false, false)] + [DataRow(true, false, true)] + [DataRow(true, false, false)] + [DataRow(false, true, true)] + [DataRow(false, true, false)] + [DataRow(true, true, true)] + [DataRow(true, true, false)] + public void PulseTrueFor_InitialValue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + pulseObservable.Subscribe(b => result = b); + + // Act + subject.OnNext(initialValue); + + // Assert + Assert.AreEqual(initialValue, result); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_EndsAfterTimeSpanWhileSourceStillTrue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_FalseDuringPulseIsWithheld(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_FalseAfterPulse(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true, false } : new[] { true, false, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_TrueAfterFalseDuringPulse_RestartsPulse(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnNext(false); + subject.OnNext(true); + var expected = distinctUntilChanged ? new[] { true } : new[] { true, true }; + CollectionAssert.AreEqual(expected, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(expected, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(expected.Append(false).ToArray(), results); + } + + [TestMethod] + public void PulseTrueFor_TimerNotResetOnConsecutiveTrue_Distinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should not reset timer + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + public void PulseTrueFor_TimerNotResetOnConsecutiveTrue_NotDistinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should not reset timer + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true, false }, results); + } + + [TestMethod] + public void PulseTrueFor_TimerResetOnConsecutiveTrue_Distinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should reset timer + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + public void PulseTrueFor_TimerResetOnConsecutiveTrue_NotDistinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should reset timer + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PulseTrueFor_RepeatedTrueAfterPulse_IgnoredWithoutReset(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(true); // The source is still "true" after the pulse ended. + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(false); + subject.OnNext(true); // Only a real "false" to "true" transition starts a new pulse. + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true, false, true } : new[] { true, false, false, true }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true, false, true, false } : new[] { true, false, false, true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void PulseTrueFor_RepeatedTrueAfterPulse_RetriggersWithReset(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true, false, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false, true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_CompleteIsImmediate(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + var completed = false; + pulseObservable.Subscribe(b => result = b, _ => { }, () => completed = true); + + subject.OnNext(true); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnCompleted(); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_Error(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + Exception? receivedException = null; + pulseObservable.Subscribe(b => result = b, e => receivedException = e); + + subject.OnNext(true); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + var exception = new InvalidOperationException("This is a test"); + subject.OnError(exception); + Assert.AreEqual(receivedException, exception); + } + + [TestMethod] + [DataRow(SourceShape.Subject, false, false)] + [DataRow(SourceShape.Subject, true, false)] + [DataRow(SourceShape.Subject, false, true)] + [DataRow(SourceShape.Subject, true, true)] + [DataRow(SourceShape.SelectManyBurst, false, false)] + [DataRow(SourceShape.SelectManyBurst, true, false)] + [DataRow(SourceShape.SelectManyBurst, false, true)] + [DataRow(SourceShape.SelectManyBurst, true, true)] + [DataRow(SourceShape.ColdArray, false, false)] + [DataRow(SourceShape.ColdArray, true, false)] + [DataRow(SourceShape.ColdArray, false, true)] + [DataRow(SourceShape.ColdArray, true, true)] + [DataRow(SourceShape.ColdConcat, false, false)] + [DataRow(SourceShape.ColdConcat, true, false)] + [DataRow(SourceShape.ColdConcat, false, true)] + [DataRow(SourceShape.ColdConcat, true, true)] + [DataRow(SourceShape.DeferPrepend, false, false)] + [DataRow(SourceShape.DeferPrepend, true, false)] + [DataRow(SourceShape.DeferPrepend, false, true)] + [DataRow(SourceShape.DeferPrepend, true, true)] + public void PulseTrueFor_TrueThenFalse_SameForEverySourceShape(SourceShape shape, bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var scheduler = new TestScheduler(); + var (source, start) = SourceShapes.Create(shape, true, false); + var pulseObservable = source.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + start(); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_SubscribesToSourceOnce(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subscriptions = 0; + var source = Observable.Defer(() => + { + subscriptions++; + return Observable.Never(); + }); + + source.PulseTrueFor(TimeSpan.FromTicks(2), new TestScheduler(), distinctUntilChanged, resetTimerOnConsecutiveTrue).Subscribe(); + + Assert.AreEqual(1, subscriptions); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_CompleteImmediately_DoesNotEmitClosingFalse(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteImmediately); + + var results = new List(); + var completed = false; + pulseObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_CompleteAfterTimer_EmitsClosingFalseThenCompletes(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + pulseObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsFalse(completed); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_CompleteAfterTimer_NoTimerRunning_CompletesImmediately(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + pulseObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(2); // The pulse has ended. + subject.OnCompleted(); + Assert.IsTrue(completed); + + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void PulseTrueFor_DisposeCancelsTimer(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseTrueFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + var subscription = pulseObservable.Subscribe(results.Add); + + subject.OnNext(true); + subscription.Dispose(); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + public void PulseTrueFor_ZeroTimeSpan_ReturnsSource() + { + var subject = new Subject(); + + Assert.AreSame(subject, subject.PulseTrueFor(TimeSpan.Zero, new TestScheduler())); + } + + [TestMethod] + public void PulseFalseFor_IsInverse() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var pulseObservable = subject.PulseFalseFor(TimeSpan.FromTicks(2), scheduler); + + var results = new List(); + pulseObservable.Subscribe(results.Add); + + subject.OnNext(false); + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { false, true }, results); + } + } +} diff --git a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.TrueForAtLeast.Tests.cs b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.TrueForAtLeast.Tests.cs index bb892d6..a2f76b9 100644 --- a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.TrueForAtLeast.Tests.cs +++ b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.TrueForAtLeast.Tests.cs @@ -1,346 +1,570 @@ -using Microsoft.Reactive.Testing; -using System.Reactive.Linq; -using System.Reactive.Subjects; - -namespace Reactive.Boolean.Tests -{ - [TestClass] - public class BooleanObservableExtensionsSchedulingTrueForAtLeastTests - { - [TestMethod] - [DataRow(false, false, true)] - [DataRow(false, false, false)] - [DataRow(true, false, true)] - [DataRow(true, false, false)] - [DataRow(false, true, true)] - [DataRow(false, true, false)] - [DataRow(true, true, true)] - [DataRow(true, true, false)] - public void TrueForAtLeast_InitialValue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - // Act - subject.OnNext(initialValue); - - // Assert - Assert.AreEqual(initialValue, result); - } - - [TestMethod] - [DataRow(false, true)] - [DataRow(false, false)] - [DataRow(true, true)] - [DataRow(true, false)] - public void TrueForAtLeast_InitialValue_Distinct(bool resetTimerOnConsecutiveTrue, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue: resetTimerOnConsecutiveTrue); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - // Act - subject.OnNext(initialValue); - subject.OnNext(initialValue); - - // Assert - CollectionAssert.AreEqual(new[] { initialValue }, results); - } - - [TestMethod] - [DataRow(false, true)] - [DataRow(false, false)] - [DataRow(true, true)] - [DataRow(true, false)] - public void TrueForAtLeast_InitialValue_NotDistinct(bool resetTimerOnConsecutiveTrue, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = - subject.TrueForAtLeast(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - // Act - subject.OnNext(initialValue); - subject.OnNext(initialValue); - - // Assert - CollectionAssert.AreEqual(new[] { initialValue, initialValue }, results); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void TrueForAtLeast_RemainsTrue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - subject.OnNext(true); - - // Act - subject.OnNext(false); - - // Assert - Assert.IsTrue(result); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void TrueForAtLeast_RemainsTrueForTimeSpan(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - subject.OnNext(false); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void TrueForAtLeast_RemainsTrueForTimeSpan_Repeat(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - subject.OnNext(false); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - subject.OnNext(true); - subject.OnNext(false); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void TrueForAtLeast_CanTurnFalseAfterTimeSpan(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - Assert.IsTrue(result); - - scheduler.AdvanceBy(1); - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - subject.OnNext(false); - Assert.IsFalse(result); - } - - [TestMethod] - public void TrueForAtLeast_TimerNotResetOnConsecutiveTrue_Distinct() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should not reset timer - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, false }, results); - - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, false }, results); - } - - [TestMethod] - public void TrueForAtLeast_TimerNotResetOnConsecutiveTrue_NotDistinct() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should not reset timer - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, true, false }, results); - - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, true, false, false }, results); - } - - [TestMethod] - public void TrueForAtLeast_TimerResetOnConsecutiveTrue_Distinct() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue: true); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should reset timer - subject.OnNext(false); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, false }, results); - - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, false }, results); - } - - [TestMethod] - public void TrueForAtLeast_TimerResetOnConsecutiveTrue_NotDistinct() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue: true); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { true }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should reset timer - subject.OnNext(false); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, true }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { true, true, false }, results); - - subject.OnNext(false); - CollectionAssert.AreEqual(new[] { true, true, false, false }, results); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void TrueForAtLeast_CompleteIsImmediate(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - var completed = false; - memoryObservable.Subscribe(b => result = b, _ => { }, () => completed = true); - - subject.OnNext(true); - subject.OnNext(false); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - subject.OnCompleted(); - Assert.IsTrue(completed); - } - - [TestMethod] - [DataRow(false, false)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(true, true)] - public void TrueForAtLeast_Error(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); - - bool? result = null; - Exception? receivedException = null; - memoryObservable.Subscribe(b => result = b, e => receivedException = e); - - subject.OnNext(true); - subject.OnNext(false); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - var exception = new InvalidOperationException("This is a test"); - subject.OnError(exception); - Assert.AreEqual(receivedException, exception); - } - } -} \ No newline at end of file +using Microsoft.Reactive.Testing; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Reactive.Boolean.Tests +{ + [TestClass] + public class BooleanObservableExtensionsSchedulingTrueForAtLeastTests + { + [TestMethod] + [DataRow(false, false, true)] + [DataRow(false, false, false)] + [DataRow(true, false, true)] + [DataRow(true, false, false)] + [DataRow(false, true, true)] + [DataRow(false, true, false)] + [DataRow(true, true, true)] + [DataRow(true, true, false)] + public void TrueForAtLeast_InitialValue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + // Act + subject.OnNext(initialValue); + + // Assert + Assert.AreEqual(initialValue, result); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void TrueForAtLeast_InitialValue_Distinct(bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue: resetTimerOnConsecutiveTrue); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + // Act + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + // Assert + CollectionAssert.AreEqual(new[] { initialValue }, results); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void TrueForAtLeast_InitialValue_NotDistinct(bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = + subject.TrueForAtLeast(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + // Act + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + // Assert + CollectionAssert.AreEqual(new[] { initialValue, initialValue }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_RemainsTrue(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromMinutes(1), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + subject.OnNext(true); + + // Act + subject.OnNext(false); + + // Assert + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_RemainsTrueForTimeSpan(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + subject.OnNext(false); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_RemainsTrueForTimeSpan_Repeat(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + subject.OnNext(false); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + subject.OnNext(true); + subject.OnNext(false); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_CanTurnFalseAfterTimeSpan(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + Assert.IsTrue(result); + + scheduler.AdvanceBy(1); + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnNext(false); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TrueForAtLeast_RepeatedTrueAfterTimer_DoesNotRestartWithoutReset(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + subject.OnNext(true); // The source is still "true" after the minimum passed. + subject.OnNext(false); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true, false } : new[] { true, true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TrueForAtLeast_RepeatedTrueAfterTimer_RestartsWithReset(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + subject.OnNext(true); + subject.OnNext(false); + var expected = distinctUntilChanged ? new[] { true } : new[] { true, true }; + CollectionAssert.AreEqual(expected, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(expected.Append(false).ToArray(), results); + } + + [TestMethod] + public void TrueForAtLeast_TimerNotResetOnConsecutiveTrue_Distinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should not reset timer + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + public void TrueForAtLeast_TimerNotResetOnConsecutiveTrue_NotDistinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should not reset timer + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, true, false, false }, results); + } + + [TestMethod] + public void TrueForAtLeast_TimerResetOnConsecutiveTrue_Distinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should reset timer + subject.OnNext(false); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + public void TrueForAtLeast_TimerResetOnConsecutiveTrue_NotDistinct() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: false, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should reset timer + subject.OnNext(false); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, true, false }, results); + + subject.OnNext(false); + CollectionAssert.AreEqual(new[] { true, true, false, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_CompleteIsImmediate(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + var completed = false; + memoryObservable.Subscribe(b => result = b, _ => { }, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnCompleted(); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_Error(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + bool? result = null; + Exception? receivedException = null; + memoryObservable.Subscribe(b => result = b, e => receivedException = e); + + subject.OnNext(true); + subject.OnNext(false); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + var exception = new InvalidOperationException("This is a test"); + subject.OnError(exception); + Assert.AreEqual(receivedException, exception); + } + + [TestMethod] + [DataRow(SourceShape.Subject, false, false)] + [DataRow(SourceShape.Subject, true, false)] + [DataRow(SourceShape.Subject, false, true)] + [DataRow(SourceShape.Subject, true, true)] + [DataRow(SourceShape.SelectManyBurst, false, false)] + [DataRow(SourceShape.SelectManyBurst, true, false)] + [DataRow(SourceShape.SelectManyBurst, false, true)] + [DataRow(SourceShape.SelectManyBurst, true, true)] + [DataRow(SourceShape.ColdArray, false, false)] + [DataRow(SourceShape.ColdArray, true, false)] + [DataRow(SourceShape.ColdArray, false, true)] + [DataRow(SourceShape.ColdArray, true, true)] + [DataRow(SourceShape.ColdConcat, false, false)] + [DataRow(SourceShape.ColdConcat, true, false)] + [DataRow(SourceShape.ColdConcat, false, true)] + [DataRow(SourceShape.ColdConcat, true, true)] + [DataRow(SourceShape.DeferPrepend, false, false)] + [DataRow(SourceShape.DeferPrepend, true, false)] + [DataRow(SourceShape.DeferPrepend, false, true)] + [DataRow(SourceShape.DeferPrepend, true, true)] + public void TrueForAtLeast_TrueThenFalse_SameForEverySourceShape(SourceShape shape, bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var scheduler = new TestScheduler(); + var (source, start) = SourceShapes.Create(shape, true, false); + var memoryObservable = source.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + start(); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_SubscribesToSourceOnce(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subscriptions = 0; + var source = Observable.Defer(() => + { + subscriptions++; + return Observable.Never(); + }); + + source.TrueForAtLeast(TimeSpan.FromTicks(2), new TestScheduler(), distinctUntilChanged, resetTimerOnConsecutiveTrue).Subscribe(); + + Assert.AreEqual(1, subscriptions); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void TrueForAtLeast_TrueAfterFalseInsideTimer_RestartsTimer(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(3), scheduler, distinctUntilChanged); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnNext(true); // Not consecutive: a "true" after a "false" restarts the timer. + subject.OnNext(false); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true } : new[] { true, true }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true, false } : new[] { true, true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_CompleteImmediately_DropsWithheldFalse(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteImmediately); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_CompleteAfterTimer_EmitsWithheldFalseThenCompletes(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsFalse(completed); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_CompleteAfterTimer_NothingWithheld_CompletesImmediately(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void TrueForAtLeast_DisposeCancelsTimer(bool distinctUntilChanged, bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.TrueForAtLeast(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged, resetTimerOnConsecutiveTrue); + + var results = new List(); + var subscription = memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + subscription.Dispose(); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true }, results); + } + } +} \ No newline at end of file diff --git a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.WhenStableFor.Tests.cs b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.WhenStableFor.Tests.cs new file mode 100644 index 0000000..545e5aa --- /dev/null +++ b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.WhenStableFor.Tests.cs @@ -0,0 +1,459 @@ +using Microsoft.Reactive.Testing; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Reactive.Boolean.Tests +{ + [TestClass] + public class BooleanObservableExtensionsSchedulingWhenStableForTests + { + [TestMethod] + [DataRow(false, false, true)] + [DataRow(false, false, false)] + [DataRow(true, false, true)] + [DataRow(true, false, false)] + [DataRow(false, true, true)] + [DataRow(false, true, false)] + [DataRow(true, true, true)] + [DataRow(true, true, false)] + public void WhenStableFor_InitialValueIsImmediate(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged); + + bool? result = null; + stableObservable.Subscribe(b => result = b); + + // Act + subject.OnNext(initialValue); + + // Assert + Assert.AreEqual(initialValue, result); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void WhenStableFor_InitialValue_Distinct(bool resetTimerOnConsecutiveValue, bool initialValue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveValue); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + CollectionAssert.AreEqual(new[] { initialValue }, results); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void WhenStableFor_InitialValue_NotDistinct(bool resetTimerOnConsecutiveValue, bool initialValue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged: false); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + CollectionAssert.AreEqual(new[] { initialValue, initialValue }, results); + } + + [TestMethod] + [DataRow(false, false, true)] + [DataRow(false, false, false)] + [DataRow(true, false, true)] + [DataRow(true, false, false)] + [DataRow(false, true, true)] + [DataRow(false, true, false)] + [DataRow(true, true, true)] + [DataRow(true, true, false)] + public void WhenStableFor_ChangeAfterTimeSpan(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged, bool initialValue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(initialValue); + subject.OnNext(!initialValue); + CollectionAssert.AreEqual(new[] { initialValue }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { initialValue }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { initialValue, !initialValue }, results); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void WhenStableFor_RevertDuringTimer_Cancels_Distinct(bool resetTimerOnConsecutiveValue, bool initialValue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(initialValue); + subject.OnNext(!initialValue); + scheduler.AdvanceBy(1); + subject.OnNext(initialValue); + CollectionAssert.AreEqual(new[] { initialValue }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { initialValue }, results); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void WhenStableFor_RevertDuringTimer_Cancels_NotDistinct(bool resetTimerOnConsecutiveValue, bool initialValue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged: false); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(initialValue); + subject.OnNext(!initialValue); + scheduler.AdvanceBy(1); + subject.OnNext(initialValue); + CollectionAssert.AreEqual(new[] { initialValue, initialValue }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { initialValue, initialValue }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenStableFor_TimerNotResetOnConsecutiveValue(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: distinctUntilChanged); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnNext(false); // Should not reset timer + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenStableFor_TimerResetOnConsecutiveValue(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue: true, distinctUntilChanged: distinctUntilChanged); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnNext(false); // Should reset timer + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenStableFor_ConsecutiveCurrentValue_NotDistinct_Emitted(bool resetTimerOnConsecutiveValue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged: false); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { true, true }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, true }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenStableFor_ChangesBackAndForth(bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, distinctUntilChanged: distinctUntilChanged); + + var results = new List(); + stableObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false }, results); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true, false, true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenStableFor_CompleteIsImmediate(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged); + + bool? result = null; + var completed = false; + stableObservable.Subscribe(b => result = b, _ => { }, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnCompleted(); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenStableFor_Error(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged); + + bool? result = null; + Exception? receivedException = null; + stableObservable.Subscribe(b => result = b, e => receivedException = e); + + subject.OnNext(true); + subject.OnNext(false); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + var exception = new InvalidOperationException("This is a test"); + subject.OnError(exception); + Assert.AreEqual(receivedException, exception); + } + + [TestMethod] + [DataRow(SourceShape.Subject, false, false)] + [DataRow(SourceShape.Subject, true, false)] + [DataRow(SourceShape.Subject, false, true)] + [DataRow(SourceShape.Subject, true, true)] + [DataRow(SourceShape.SelectManyBurst, false, false)] + [DataRow(SourceShape.SelectManyBurst, true, false)] + [DataRow(SourceShape.SelectManyBurst, false, true)] + [DataRow(SourceShape.SelectManyBurst, true, true)] + [DataRow(SourceShape.ColdArray, false, false)] + [DataRow(SourceShape.ColdArray, true, false)] + [DataRow(SourceShape.ColdArray, false, true)] + [DataRow(SourceShape.ColdArray, true, true)] + [DataRow(SourceShape.ColdConcat, false, false)] + [DataRow(SourceShape.ColdConcat, true, false)] + [DataRow(SourceShape.ColdConcat, false, true)] + [DataRow(SourceShape.ColdConcat, true, true)] + [DataRow(SourceShape.DeferPrepend, false, false)] + [DataRow(SourceShape.DeferPrepend, true, false)] + [DataRow(SourceShape.DeferPrepend, false, true)] + [DataRow(SourceShape.DeferPrepend, true, true)] + public void WhenStableFor_TrueThenFalse_SameForEverySourceShape(SourceShape shape, bool resetTimerOnConsecutiveValue, bool distinctUntilChanged) + { + var scheduler = new TestScheduler(); + var (source, start) = SourceShapes.Create(shape, true, false); + var stableObservable = source.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + stableObservable.Subscribe(results.Add); + start(); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenStableFor_SubscribesToSourceOnce(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged) + { + var subscriptions = 0; + var source = Observable.Defer(() => + { + subscriptions++; + return Observable.Never(); + }); + + source.WhenStableFor(TimeSpan.FromTicks(2), new TestScheduler(), resetTimerOnConsecutiveValue, distinctUntilChanged).Subscribe(); + + Assert.AreEqual(1, subscriptions); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenStableFor_CompleteImmediately_DoesNotEmitPendingValue(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged, CompletionBehavior.CompleteImmediately); + + var results = new List(); + var completed = false; + stableObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenStableFor_CompleteAfterTimer_EmitsPendingValueThenCompletes(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + stableObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsFalse(completed); + CollectionAssert.AreEqual(new[] { true }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { true, false }, results); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenStableFor_CompleteAfterTimer_NoTimerRunning_CompletesImmediately(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + stableObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); + subject.OnNext(true); // Cancels the pending change. + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { true } : new[] { true, true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenStableFor_DisposeCancelsTimer(bool resetTimerOnConsecutiveValue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var stableObservable = subject.WhenStableFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveValue, distinctUntilChanged); + + var results = new List(); + var subscription = stableObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(false); + subscription.Dispose(); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { true }, results); + } + + [TestMethod] + public void WhenStableFor_ZeroTimeSpan_ReturnsSource() + { + var subject = new Subject(); + + Assert.AreSame(subject, subject.WhenStableFor(TimeSpan.Zero, new TestScheduler())); + } + } +} diff --git a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.WhenTrueFor.Tests.cs b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.WhenTrueFor.Tests.cs index 2910d59..1776144 100644 --- a/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.WhenTrueFor.Tests.cs +++ b/tests/Reactive.Boolean.Tests/BooleanObservableExtensions.Scheduling.WhenTrueFor.Tests.cs @@ -1,264 +1,477 @@ -using Microsoft.Reactive.Testing; -using System.Reactive.Subjects; - -namespace Reactive.Boolean.Tests -{ - [TestClass] - public class BooleanObservableExtensionsSchedulingWhenTrueForTests - { - [TestMethod] - [DataRow(true, true)] - [DataRow(true, false)] - [DataRow(false, true)] - [DataRow(false, false)] - public void WhenTrueFor_InitialValueAlwaysFalse(bool resetTimerOnConsecutiveTrue, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - // Act - subject.OnNext(initialValue); - - // Assert - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(false, true)] - [DataRow(false, false)] - [DataRow(true, true)] - [DataRow(true, false)] - public void WhenTrueFor_InitialValue_IsDistinct(bool resetTimerOnConsecutiveTrue, bool initialValue) - { - // Arrange - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - // Act - subject.OnNext(initialValue); - subject.OnNext(initialValue); - - // Assert - CollectionAssert.AreEqual(new[] { false }, results); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void WhenTrueFor_TrueAfterTimeSpan(bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void WhenTrueFor_TrueAfterTimeSpan_Repeat(bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - subject.OnNext(false); - subject.OnNext(true); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void WhenTrueFor_FalseIsImmediate(bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(1), scheduler, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - - subject.OnNext(false); - Assert.IsFalse(result); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void WhenTrueFor_RemainsTrueForTimeSpanAfterTrue(bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - scheduler.AdvanceBy(1); - subject.OnNext(true); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - } - - [TestMethod] - public void WhenTrueFor_FalseResetsTimer() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler); - - bool? result = null; - memoryObservable.Subscribe(b => result = b); - - subject.OnNext(true); - - scheduler.AdvanceBy(1); - subject.OnNext(false); - subject.OnNext(true); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - scheduler.AdvanceBy(1); - Assert.IsTrue(result); - } - - [TestMethod] - public void WhenTrueFor_TimerNotResetOnConsecutiveTrue() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { false }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should not reset timer - CollectionAssert.AreEqual(new[] { false }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { false, true }, results); - } - - [TestMethod] - public void WhenTrueFor_TimerResetOnConsecutiveTrue() - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue: true); - - var results = new List(); - memoryObservable.Subscribe(results.Add); - - subject.OnNext(true); - CollectionAssert.AreEqual(new[] { false }, results); - - scheduler.AdvanceBy(1); - subject.OnNext(true); // Should reset timer - CollectionAssert.AreEqual(new[] { false }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { false }, results); - - scheduler.AdvanceBy(1); - CollectionAssert.AreEqual(new[] { false, true }, results); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void WhenTrueFor_CompleteIsImmediate(bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); - - bool? result = null; - bool completed = false; - memoryObservable.Subscribe(b => result = b, _ => { }, () => completed = true); - - subject.OnNext(true); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - subject.OnCompleted(); - Assert.IsTrue(completed); - } - - [TestMethod] - [DataRow(true)] - [DataRow(false)] - public void WhenTrueFor_Error(bool resetTimerOnConsecutiveTrue) - { - var subject = new Subject(); - var scheduler = new TestScheduler(); - var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); - - bool? result = null; - Exception? receivedException = null; - memoryObservable.Subscribe(b => result = b, e => receivedException = e); - - subject.OnNext(true); - - scheduler.AdvanceBy(1); - Assert.IsFalse(result); - - var exception = new InvalidOperationException("This is a test"); - subject.OnError(exception); - Assert.AreEqual(receivedException, exception); - } - } -} \ No newline at end of file +using Microsoft.Reactive.Testing; +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Reactive.Boolean.Tests +{ + [TestClass] + public class BooleanObservableExtensionsSchedulingWhenTrueForTests + { + [TestMethod] + [DataRow(true, true)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(false, false)] + public void WhenTrueFor_InitialValueAlwaysFalse(bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + // Act + subject.OnNext(initialValue); + + // Assert + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(false, true)] + [DataRow(false, false)] + [DataRow(true, true)] + [DataRow(true, false)] + public void WhenTrueFor_InitialValue_IsDistinct(bool resetTimerOnConsecutiveTrue, bool initialValue) + { + // Arrange + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + // Act + subject.OnNext(initialValue); + subject.OnNext(initialValue); + + // Assert + CollectionAssert.AreEqual(new[] { false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_TrueAfterTimeSpan(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_TrueAfterTimeSpan_Repeat(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnNext(false); + subject.OnNext(true); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_FalseIsImmediate(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(1), scheduler, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + + subject.OnNext(false); + Assert.IsFalse(result); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_RemainsTrueForTimeSpanAfterTrue(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + scheduler.AdvanceBy(1); + subject.OnNext(true); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + } + + [TestMethod] + public void WhenTrueFor_FalseResetsTimer() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler); + + bool? result = null; + memoryObservable.Subscribe(b => result = b); + + subject.OnNext(true); + + scheduler.AdvanceBy(1); + subject.OnNext(false); + subject.OnNext(true); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + scheduler.AdvanceBy(1); + Assert.IsTrue(result); + } + + [TestMethod] + public void WhenTrueFor_TimerNotResetOnConsecutiveTrue() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should not reset timer + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false, true }, results); + } + + [TestMethod] + public void WhenTrueFor_TimerResetOnConsecutiveTrue() + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue: true); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(1); + subject.OnNext(true); // Should reset timer + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false, true }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_CompleteIsImmediate(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); + + bool? result = null; + bool completed = false; + memoryObservable.Subscribe(b => result = b, _ => { }, () => completed = true); + + subject.OnNext(true); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + subject.OnCompleted(); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_Error(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue); + + bool? result = null; + Exception? receivedException = null; + memoryObservable.Subscribe(b => result = b, e => receivedException = e); + + subject.OnNext(true); + + scheduler.AdvanceBy(1); + Assert.IsFalse(result); + + var exception = new InvalidOperationException("This is a test"); + subject.OnError(exception); + Assert.AreEqual(receivedException, exception); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_ConsecutiveFalse_NotDistinct(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromMinutes(1), scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged: false); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(false); + subject.OnNext(false); + + CollectionAssert.AreEqual(new[] { false, false }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_ConsecutiveTrueDuringTimer_NotDistinct_NotEmitted(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged: false); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { false, true }, results); + } + + [TestMethod] + [DataRow(true)] + [DataRow(false)] + public void WhenTrueFor_ConsecutiveTrueAfterTimer_NotDistinct_Emitted(bool resetTimerOnConsecutiveTrue) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged: false); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { false, true }, results); + + subject.OnNext(true); + CollectionAssert.AreEqual(new[] { false, true, true }, results); + } + + [TestMethod] + [DataRow(SourceShape.Subject, false, false)] + [DataRow(SourceShape.Subject, true, false)] + [DataRow(SourceShape.Subject, false, true)] + [DataRow(SourceShape.Subject, true, true)] + [DataRow(SourceShape.SelectManyBurst, false, false)] + [DataRow(SourceShape.SelectManyBurst, true, false)] + [DataRow(SourceShape.SelectManyBurst, false, true)] + [DataRow(SourceShape.SelectManyBurst, true, true)] + [DataRow(SourceShape.ColdArray, false, false)] + [DataRow(SourceShape.ColdArray, true, false)] + [DataRow(SourceShape.ColdArray, false, true)] + [DataRow(SourceShape.ColdArray, true, true)] + [DataRow(SourceShape.ColdConcat, false, false)] + [DataRow(SourceShape.ColdConcat, true, false)] + [DataRow(SourceShape.ColdConcat, false, true)] + [DataRow(SourceShape.ColdConcat, true, true)] + [DataRow(SourceShape.DeferPrepend, false, false)] + [DataRow(SourceShape.DeferPrepend, true, false)] + [DataRow(SourceShape.DeferPrepend, false, true)] + [DataRow(SourceShape.DeferPrepend, true, true)] + public void WhenTrueFor_FalseThenTrue_SameForEverySourceShape(SourceShape shape, bool resetTimerOnConsecutiveTrue, bool distinctUntilChanged) + { + var scheduler = new TestScheduler(); + var (source, start) = SourceShapes.Create(shape, false, true); + var memoryObservable = source.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + memoryObservable.Subscribe(results.Add); + start(); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false, true }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenTrueFor_SubscribesToSourceOnce(bool resetTimerOnConsecutiveTrue, bool distinctUntilChanged) + { + var subscriptions = 0; + var source = Observable.Defer(() => + { + subscriptions++; + return Observable.Never(); + }); + + source.WhenTrueFor(TimeSpan.FromTicks(2), new TestScheduler(), resetTimerOnConsecutiveTrue, distinctUntilChanged).Subscribe(); + + Assert.AreEqual(1, subscriptions); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenTrueFor_CompleteImmediately_DoesNotEmitTrue(bool resetTimerOnConsecutiveTrue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged, CompletionBehavior.CompleteImmediately); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenTrueFor_CompleteAfterTimer_EmitsTrueThenCompletes(bool resetTimerOnConsecutiveTrue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsFalse(completed); + CollectionAssert.AreEqual(new[] { false }, results); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(new[] { false, true }, results); + Assert.IsTrue(completed); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenTrueFor_CompleteAfterTimer_NoTimerRunning_CompletesImmediately(bool resetTimerOnConsecutiveTrue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged, CompletionBehavior.CompleteAfterTimer); + + var results = new List(); + var completed = false; + memoryObservable.Subscribe(results.Add, () => completed = true); + + subject.OnNext(true); + subject.OnNext(false); // Stops the timer. + scheduler.AdvanceBy(1); + subject.OnCompleted(); + Assert.IsTrue(completed); + + scheduler.AdvanceBy(1); + CollectionAssert.AreEqual(distinctUntilChanged ? new[] { false } : new[] { false, false }, results); + } + + [TestMethod] + [DataRow(false, false)] + [DataRow(true, false)] + [DataRow(false, true)] + [DataRow(true, true)] + public void WhenTrueFor_DisposeCancelsTimer(bool resetTimerOnConsecutiveTrue, bool distinctUntilChanged) + { + var subject = new Subject(); + var scheduler = new TestScheduler(); + var memoryObservable = subject.WhenTrueFor(TimeSpan.FromTicks(2), scheduler, resetTimerOnConsecutiveTrue, distinctUntilChanged); + + var results = new List(); + var subscription = memoryObservable.Subscribe(results.Add); + + subject.OnNext(true); + subscription.Dispose(); + + scheduler.AdvanceBy(2); + CollectionAssert.AreEqual(new[] { false }, results); + } + } +} \ No newline at end of file diff --git a/tests/Reactive.Boolean.Tests/SourceShapes.cs b/tests/Reactive.Boolean.Tests/SourceShapes.cs new file mode 100644 index 0000000..e8ec276 --- /dev/null +++ b/tests/Reactive.Boolean.Tests/SourceShapes.cs @@ -0,0 +1,39 @@ +using System.Reactive.Linq; +using System.Reactive.Subjects; + +namespace Reactive.Boolean.Tests +{ + /// + /// Ways of delivering the same two values to an operator. A hot subject hands each value to every subscriber before + /// producing the next one; the other shapes do not, and the cold shapes also complete. Operators must behave the same for all of them. + /// + public enum SourceShape + { + Subject, + SelectManyBurst, + ColdArray, + ColdConcat, + DeferPrepend + } + + internal static class SourceShapes + { + /// + /// Creates a source that emits followed by once Start is invoked + /// (or upon subscription for the cold shapes). + /// + public static (IObservable Source, Action Start) Create(SourceShape shape, bool first, bool second) + { + var subject = new Subject(); + return shape switch + { + SourceShape.Subject => (subject, () => { subject.OnNext(first); subject.OnNext(second); }), + SourceShape.SelectManyBurst => (subject.SelectMany(_ => new[] { first, second }), () => subject.OnNext(true)), + SourceShape.ColdArray => (new[] { first, second }.ToObservable(), () => { }), + SourceShape.ColdConcat => (Observable.Return(first).Concat(Observable.Return(second)), () => { }), + SourceShape.DeferPrepend => (Observable.Defer(() => subject.Prepend(first)), () => subject.OnNext(second)), + _ => throw new ArgumentOutOfRangeException(nameof(shape)) + }; + } + } +}