night-light/NightLight.Core.Tests/NightLightTests.fs

157 lines
6.6 KiB
FSharp

namespace NightLight.Core.Tests
open NightLight.Core.Core
open NightLight.Core.Tests.InteractionListGenerators
open NightLight.Core.Tests.InteractionListHelpers
open NightLight.Core.Models
open FsCheck.Xunit
open FsCheck.FSharp
type private BedroomLightsCycle =
| BothOff
| BothOn
| LeftOn
| RightOn
type NightLightTests() =
let createFakeHomeWithNightLightAndInteract (interactions: Interaction list) =
let mutable nightLightStateMachine = NightLightStateMachine()
let fakeHome = FakeHome()
fakeHome.OnEventPublished.Add(fun event ->
match event |> nightLightStateMachine.OnEventReceived with
| Ok(newState, commands) ->
commands |> Seq.iter fakeHome.ProcessCommand
nightLightStateMachine <- newState
| Error error -> failwith $"Unexpected error {error}")
fakeHome.Interact interactions
fakeHome
[<Property(Arbitrary = [| typeof<ArbitraryInteractions> |])>]
let ``All lights should either be off or have the right color`` (interactions: Interaction list) =
let fakeHome = createFakeHomeWithNightLightAndInteract interactions
let partOfDay = getPartOfDayAfterInteractions interactions
fakeHome.LightStates
|> Seq.forall (function
| _, Off -> true
| _, On(_, color) ->
match partOfDay with
| Day -> color = White || color = Yellow
| Night -> color = Red)
|> Prop.collect partOfDay
|> Prop.collect $"{fakeHome.LightsThatAreOn.Length} light(s) on"
|> Prop.label fakeHome.Label
|> Prop.trivial (fakeHome.LightsThatAreOn.Length = 0)
[<Property(Arbitrary = [| typeof<ArbitraryInteractions> |], MaxTest = 500)>]
let ``All lights should either be off or have a brightness that fits its color`` (interactions: Interaction list) =
let fakeHome = createFakeHomeWithNightLightAndInteract interactions
let time = getTimeAfterInteractions interactions |> _.TimeOfDay
let alarm =
hasNewDayStartedSince interactions (tryGetLastBedroomControllingRemoteInteraction interactions)
&& startOfDay <= time
&& time <= endOfAlarm
let scaledForAlarm light brightness =
if (light = RightBedroomLamp || light = LeftBedroomLamp) && alarm then
float brightness * ((time - startOfDay) / (endOfAlarm - startOfDay)) |> byte
else
brightness
fakeHome.LightStates
|> Seq.forall (fun (light, state) ->
let maybeExpectedBrightness =
match (lightProps light).Bulb, state with
| _, Off -> None
| IkeaBulb, On(_, White) -> Some 254uy
| IkeaBulb, On(_, Yellow) -> Some 210uy
| IkeaBulb, On(_, Red) -> Some 254uy
| PaulmannBulb, On(_, White) -> Some 35uy
| PaulmannBulb, On(_, Yellow) -> Some 35uy
| PaulmannBulb, On(_, Red) -> Some 80uy
|> Option.map (scaledForAlarm light)
let maybeActualBrightness =
match state with
| Off -> None
| On(brightness, _) -> Some brightness
maybeExpectedBrightness = maybeActualBrightness)
|> Prop.collect $"{fakeHome.LightsThatAreOn.Length} light(s) on"
|> Prop.classify alarm "alarm"
|> Prop.label fakeHome.Label
|> Prop.trivial (fakeHome.LightsThatAreOn.Length = 0)
[<Property(Arbitrary = [| typeof<ArbitraryInteractions> |], MaxTest = 500)>]
let ``All lights with power should have the correct state`` (interactions: Interaction list) =
let fakeHome = createFakeHomeWithNightLightAndInteract interactions
let lightsWithPower =
fakeHome.LightStates
|> Seq.filter (fun (light, _) -> doesLightHavePowerAfterInteractions light interactions)
|> Seq.toList
let lastBedroomControllingRemoteInteraction =
tryGetLastBedroomControllingRemoteInteraction interactions
let newDayStartedSinceLastBedroomControllingRemoteInteraction =
hasNewDayStartedSince interactions lastBedroomControllingRemoteInteraction
let livingRoomLightsToggledOn =
interactions
|> Seq.choose (function
| Interaction.LivingRoomControllingRemoteInteraction interaction -> Some interaction
| _ -> None)
|> Seq.fold
(fun state interaction ->
match interaction with
| RemotePressedRightButton -> not state
| LivingRoomRemotePressedOnButton -> true
| LivingRoomRemotePressedOffButton -> false)
true
let bedroomLightsCycle =
interactions
|> Seq.choose (function
| Interaction.BedroomControllingRemoteInteraction interaction -> Some interaction
| _ -> None)
|> Seq.fold
(fun state interaction ->
match state, interaction with
| _, RemotePressedOffButton -> BothOff
| BothOff, RemotePressedOnButton -> BothOn
| BothOn, RemotePressedOnButton -> LeftOn
| LeftOn, RemotePressedOnButton -> RightOn
| RightOn, RemotePressedOnButton -> BothOn
| _, RemotePressedLeftButton -> LeftOn)
BothOn
let isExpectedOn light =
match light with
| LeftBedroomLamp ->
newDayStartedSinceLastBedroomControllingRemoteInteraction
|| bedroomLightsCycle = BothOn
|| bedroomLightsCycle = LeftOn
| RightBedroomLamp ->
newDayStartedSinceLastBedroomControllingRemoteInteraction
|| bedroomLightsCycle = BothOn
|| bedroomLightsCycle = RightOn
| LivingRoomWallLamp
| LivingRoomFloorLamp -> livingRoomLightsToggledOn
| BathroomCeilingLamp -> true
lightsWithPower
|> Seq.forall (fun (light, state) -> state.IsOn = isExpectedOn light)
|> Prop.collect $"{lightsWithPower.Length} light(s) with power"
|> Prop.collect $"bedroom lights cycle = {bedroomLightsCycle}"
|> Prop.classify livingRoomLightsToggledOn "living room lights toggled on"
|> Prop.classify
newDayStartedSinceLastBedroomControllingRemoteInteraction
"new day started since last bedroom controlling remote interaction"
|> Prop.label fakeHome.Label
|> Prop.trivial (lightsWithPower.Length = 0)