Skip to content

Commit

Permalink
eqxShipping: Memory store tests (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
bartelink authored Jun 11, 2020
1 parent 9792bf4 commit 842c22e
Show file tree
Hide file tree
Showing 18 changed files with 360 additions and 15 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ The `Unreleased` section name is replaced by the expected version of next releas
## [Unreleased]

### Added

- `eqxShipping`: Unit and integration tests [#70](https://github.com/jet/dotnet-templates/pull/70)

### Changed
### Removed
### Fixed
Expand Down
14 changes: 14 additions & 0 deletions dotnet-templates.sln
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Watchdog", "equinox-shippin
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Equinox.Templates.Tests", "tests\Equinox.Templates.Tests\Equinox.Templates.Tests.fsproj", "{26BFE6BC-5887-4E40-8CFD-F15332F5A104}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain.Tests", "equinox-shipping\Domain.Tests\Domain.Tests.fsproj", "{5A45EF21-576B-4B40-86BD-F5960ECD66BF}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Watchdog.Integration", "equinox-shipping\Watchdog.Integration\Watchdog.Integration.fsproj", "{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -157,6 +161,14 @@ Global
{26BFE6BC-5887-4E40-8CFD-F15332F5A104}.Debug|Any CPU.Build.0 = Debug|Any CPU
{26BFE6BC-5887-4E40-8CFD-F15332F5A104}.Release|Any CPU.ActiveCfg = Release|Any CPU
{26BFE6BC-5887-4E40-8CFD-F15332F5A104}.Release|Any CPU.Build.0 = Release|Any CPU
{5A45EF21-576B-4B40-86BD-F5960ECD66BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5A45EF21-576B-4B40-86BD-F5960ECD66BF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5A45EF21-576B-4B40-86BD-F5960ECD66BF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5A45EF21-576B-4B40-86BD-F5960ECD66BF}.Release|Any CPU.Build.0 = Release|Any CPU
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{F66A5BFE-7C81-44DC-97DE-3FD8C83B8F06} = {B72FFAAE-7801-41B2-86F5-FD90E97A30F7}
Expand All @@ -172,5 +184,7 @@ Global
{36C2D70A-F292-4481-8ADA-5066A80F92B2} = {1F3C9245-F973-43A3-97C9-5E527B93060C}
{7B96FCF8-0BB5-4494-A143-628882A6E50A} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{9AFF6138-B63B-4EBF-B86B-4F626E1F1ADF} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{5A45EF21-576B-4B40-86BD-F5960ECD66BF} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
{83BA87C3-6288-40F4-BC4F-EC3A54586CDF} = {DAE9E2B9-EDA2-4064-B0CE-FD5294549374}
EndGlobalSection
EndGlobal
12 changes: 12 additions & 0 deletions equinox-shipping/Domain.Tests/ContainerTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Shipping.Domain.Tests.ContainerTests

open Shipping.Domain.Container

open FsCheck.Xunit
open Swensen.Unquote

let [<Property>] ``events roundtrip`` (x : Events.Event) =
let ee = Events.codec.Encode(None, x)
let e = FsCodec.Core.TimelineEvent.Create(0L, ee.EventType, ee.Data)
let des = Events.codec.TryDecode e
test <@ des = Some x @>
33 changes: 33 additions & 0 deletions equinox-shipping/Domain.Tests/Domain.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<WarningLevel>5</WarningLevel>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>

<ItemGroup>
<Compile Include="Infrastructure.fs" />
<Compile Include="Fixtures.fs" />
<Compile Include="ContainerTests.fs" />
<Compile Include="ShipmentTests.fs" />
<Compile Include="FinalizationTransactionTests.fs" />
<Compile Include="FinalizationProcessManagerTests.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />

<PackageReference Include="Equinox.MemoryStore" Version="2.1.0" />
<PackageReference Include="Unquote" Version="5.0.0" />
<PackageReference Include="FsCheck.Xunit" Version="2.14.2" />
<PackageReference Include="xunit" Version="2.4.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Domain\Domain.fsproj" />
</ItemGroup>

</Project>
43 changes: 43 additions & 0 deletions equinox-shipping/Domain.Tests/FinalizationProcessManagerTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
module Shipping.Domain.Tests.FinalizationProcessManagerTests

open Shipping.Domain

open FsCheck.Xunit
open Swensen.Unquote

[<Property>]
let ``FinalizationProcessManager properties`` (Id transId1, Id transId2, Id containerId1, Id containerId2, IdsAtLeastOne shipmentIds1, IdsAtLeastOne shipmentIds2, Id shipment3) =
let store = Equinox.MemoryStore.VolatileStore()
let buffer = EventAccumulator()
use __ = store.Committed.Subscribe buffer.Record
let eventTypes = seq { for e in buffer.All() -> e.EventType }
let processManager = createProcessManager 16 store
Async.RunSynchronously <| async {
(* First, run the happy path - should pass through all stages of the lifecycle *)
let requestedShipmentIds = Array.append shipmentIds1 shipmentIds2
let! res1 = processManager.TryFinalizeContainer(transId1, containerId1, requestedShipmentIds)
let expectedEvents =
[ "FinalizationRequested"; "ReservationCompleted"; "AssignmentCompleted"; "Completed" // Transaction
"Reserved"; "Assigned" // Shipment
"Finalized"] // Container
test <@ res1 && set eventTypes = set expectedEvents @>
let containerEvents =
buffer.Queue(Container.streamName containerId1)
|> Seq.choose Container.Events.codec.TryDecode
|> List.ofSeq
test <@ match containerEvents with
| [ Container.Events.Finalized e ] -> e.shipmentIds = requestedShipmentIds
| xs -> failwithf "Unexpected %A" xs @>
(* Next, we run an overlapping finalize - this should
a) yield a fail result
b) result in triggering of Revert flow with associated Shipment revoke events *)
buffer.Clear()
let! res2 = processManager.TryFinalizeContainer(transId2, containerId2, Array.append shipmentIds2 [|shipment3|])
let expectedEvents =
[ "FinalizationRequested"; "RevertCommenced"; "Completed" // Transaction
"Reserved"; "Revoked" ] // Shipment
test <@ not res2
&& set eventTypes = set expectedEvents @>
}

module Dummy = let [<EntryPoint>] main _argv = 0
12 changes: 12 additions & 0 deletions equinox-shipping/Domain.Tests/FinalizationTransactionTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module Shipping.Domain.Tests.FinalizationTransactionTests

open Shipping.Domain.FinalizationTransaction

open FsCheck.Xunit
open Swensen.Unquote

let [<Property>] ``events roundtrip`` (x : Events.Event) =
let ee = Events.codec.Encode(None, x)
let e = FsCodec.Core.TimelineEvent.Create(0L, ee.EventType, ee.Data)
let des = Events.codec.TryDecode e
test <@ des = Some x @>
39 changes: 39 additions & 0 deletions equinox-shipping/Domain.Tests/Fixtures.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[<AutoOpen>]
module Shipping.Domain.Tests.Fixtures

open Shipping.Domain

module FinalizationTransaction =
open FinalizationTransaction
module MemoryStore =
open Equinox.MemoryStore
let create store =
let resolver = Resolver(store, Events.codec, Fold.fold, Fold.initial)
create resolver.Resolve
module Container =
open Container
module MemoryStore =
open Equinox.MemoryStore
let create store =
let resolver = Resolver(store, Events.codec, Fold.fold, Fold.initial)
create resolver.Resolve
module Shipment =
open Shipment
module MemoryStore =
open Equinox.MemoryStore
let create store =
let resolver = Resolver(store, Events.codec, Fold.fold, Fold.initial)
create resolver.Resolve

let createProcessManager maxDop store =
let transactions = FinalizationTransaction.MemoryStore.create store
let containers = Container.MemoryStore.create store
let shipments = Shipment.MemoryStore.create store
FinalizationProcessManager.Service(transactions, containers, shipments, maxDop=maxDop)

(* Generic FsCheck helpers *)

let (|Id|) (x : System.Guid) = x.ToString "N" |> FSharp.UMX.UMX.tag
let (|Ids|) (xs : System.Guid[]) = xs |> Array.map (|Id|)
let (|IdsAtLeastOne|) (Ids xs, Id x) = [| yield x; yield! xs |]
let (|AtLeastOne|) (x, xs) = x::xs
22 changes: 22 additions & 0 deletions equinox-shipping/Domain.Tests/Infrastructure.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[<AutoOpen>]
module Shipping.Domain.Tests.Infrastructure

open System.Collections.Concurrent

type EventAccumulator<'E>() =
let messages = ConcurrentDictionary<FsCodec.StreamName, ConcurrentQueue<'E>>()

member __.Record(stream, events : 'E seq) =
let initStreamQueue _ = ConcurrentQueue events
let appendToQueue _ (queue : ConcurrentQueue<'E>) = events |> Seq.iter queue.Enqueue; queue
messages.AddOrUpdate(stream, initStreamQueue, appendToQueue) |> ignore

member __.Queue stream =
match messages.TryGetValue stream with
| false, _ -> Seq.empty<'E>
| true, xs -> xs :> _

member __.All() = seq { for KeyValue (_, xs) in messages do yield! xs }

member __.Clear() =
messages.Clear()
17 changes: 17 additions & 0 deletions equinox-shipping/Domain.Tests/ShipmentTests.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Shipping.Domain.Tests.ShipmentTests

open Shipping.Domain.Shipment

open FsCheck.Xunit
open Swensen.Unquote

// We use Optional string values in the event representations
// However, FsCodec.NewtonsoftJson.OptionConverter maps `Some null` to `None`, which does not roundtrip
// In order to avoid having to special case the assertion in the roundtrip test, we stub out such values
let (|ReplaceSomeNullWithNone|) = TypeShape.Generic.map (function Some (null : string) -> None | x -> x)

let [<Property>] ``events roundtrip`` (ReplaceSomeNullWithNone (x : Events.Event)) =
let ee = Events.codec.Encode(None, x)
let e = FsCodec.Core.TimelineEvent.Create(0L, ee.EventType, ee.Data)
let des = Events.codec.TryDecode e
test <@ des = Some x @>
6 changes: 3 additions & 3 deletions equinox-shipping/Domain/Container.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ let streamName (containerId : ContainerId) = FsCodec.StreamName.create Category
module Events =

type Event =
| Finalized of {| shipmentIds : ShipmentId[] |}
| Finalized of {| shipmentIds : ShipmentId[] |}
| Snapshotted of {| shipmentIds : ShipmentId[] |}
interface TypeShape.UnionContract.IUnionContract

Expand All @@ -28,15 +28,15 @@ module Fold =
let toSnapshot (state : State) = Events.Snapshotted {| shipmentIds = state.shipmentIds |}

let interpretFinalize shipmentIds (state : Fold.State): Events.Event list =
[ if (not << Array.isEmpty) state.shipmentIds then yield Events.Finalized {| shipmentIds = shipmentIds |} ]
[ if Array.isEmpty state.shipmentIds then yield Events.Finalized {| shipmentIds = shipmentIds |} ]

type Service internal (resolve : ContainerId -> Equinox.Stream<Events.Event, Fold.State>) =

member __.Finalize(containerId, shipmentIds) : Async<unit> =
let stream = resolve containerId
stream.Transact(interpretFinalize shipmentIds)

let private create resolve =
let create resolve =
let resolve id = Equinox.Stream(Serilog.Log.ForContext<Service>(), resolve (streamName id), maxAttempts=3)
Service(resolve)

Expand Down
2 changes: 1 addition & 1 deletion equinox-shipping/Domain/FinalizationProcessManager.fs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type Service

| Action.AssignShipments (shipmentIds, containerId) ->
let! _ = Async.Parallel(seq { for sId in shipmentIds -> shipments.Assign(sId, containerId, transactionId) }, maxDop)
return! loop Events.Completed
return! loop Events.AssignmentCompleted

| Action.FinalizeContainer (containerId, shipmentIds) ->
do! containers.Finalize(containerId, shipmentIds)
Expand Down
7 changes: 4 additions & 3 deletions equinox-shipping/Domain/FinalizationTransaction.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ module Fold =
// The implementation trusts (does not spend time double checking) that events have passed an isValidTransition check
let evolve (state : State) (event : Events.Event) : State =
match state, event with
| _, Events.FinalizationRequested event -> State.Assigning {| container = event.container; shipments = event.shipments |}
| State.Assigning state, Events.RevertCommenced event -> State.Reverting {| shipments = event.shipments |}
| _, Events.FinalizationRequested event -> State.Reserving {| container = event.container; shipments = event.shipments |}
| State.Reserving state, Events.ReservationCompleted -> State.Assigning {| container = state.container; shipments = state.shipments |}
| State.Reserving _state, Events.RevertCommenced event -> State.Reverting {| shipments = event.shipments |}
| State.Reverting _state, Events.Completed -> State.Completed {| success = false |}
| State.Assigning state, Events.AssignmentCompleted -> State.Assigned {| container = state.container; shipments = state.shipments |}
| State.Assigned _, Events.Completed -> State.Completed {| success = true |}
Expand Down Expand Up @@ -99,7 +100,7 @@ type Service internal (resolve : TransactionId -> Equinox.Stream<Events.Event, F
let stream = resolve transactionId
stream.Transact(decide update)

let private create resolve =
let create resolve =
let resolve id = Equinox.Stream(Serilog.Log.ForContext<Service>(), resolve (streamName id), maxAttempts=3)
Service(resolve)

Expand Down
12 changes: 5 additions & 7 deletions equinox-shipping/Domain/Shipment.fs
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,12 @@ module Fold =
let toSnapshot (state : State) = Events.Snapshotted {| reservation = state.reservation; association = state.association |}

let decideReserve transactionId : Fold.State -> bool * Events.Event list = function
| { reservation = r; association = a } ->
// validity is dependent on whether its a) open or b) an idempotent retry by the same transaction
let isValid = Option.forall (fun x -> x = transactionId) r
// if it's already Reserved (and/or assigned), there's no work to do
isValid, if isValid && Option.isNone a then [ Events.Reserved {| transaction = transactionId |} ] else []
| { reservation = Some r } when r = transactionId -> true, []
| { reservation = None } -> true, [ Events.Reserved {| transaction = transactionId |} ]
| _ -> false, []

let interpretRevoke transactionId : Fold.State -> Events.Event list = function
| { reservation = Some current; association = None } when current = transactionId ->
| { reservation = Some r; association = None } when r = transactionId ->
[ Events.Revoked ]
| _ -> [] // Ignore if a) already revoked/never reserved b) not reserved for this transactionId

Expand All @@ -61,7 +59,7 @@ type Service internal (resolve : ShipmentId -> Equinox.Stream<Events.Event, Fold
let stream = resolve shipmentId
stream.Transact(interpretAssign transactionId containerId)

let private create resolve =
let create resolve =
let resolve id = Equinox.Stream(Serilog.Log.ForContext<Service>(), resolve (streamName id), maxAttempts=3)
Service(resolve)

Expand Down
12 changes: 12 additions & 0 deletions equinox-shipping/Shipping.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ ProjectSection(SolutionItems) = preProject
README.md = README.md
EndProjectSection
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Domain.Tests", "Domain.Tests\Domain.Tests.fsproj", "{4F3C949A-0D13-4ED1-841E-B74AC500D0E7}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Watchdog.Integration", "Watchdog.Integration\Watchdog.Integration.fsproj", "{DAF9BD44-E012-4986-9820-5A3732845384}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -23,5 +27,13 @@ Global
{222D612E-51E0-4ED1-BB34-AF08E3BBF741}.Debug|Any CPU.Build.0 = Debug|Any CPU
{222D612E-51E0-4ED1-BB34-AF08E3BBF741}.Release|Any CPU.ActiveCfg = Release|Any CPU
{222D612E-51E0-4ED1-BB34-AF08E3BBF741}.Release|Any CPU.Build.0 = Release|Any CPU
{4F3C949A-0D13-4ED1-841E-B74AC500D0E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F3C949A-0D13-4ED1-841E-B74AC500D0E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4F3C949A-0D13-4ED1-841E-B74AC500D0E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F3C949A-0D13-4ED1-841E-B74AC500D0E7}.Release|Any CPU.Build.0 = Release|Any CPU
{DAF9BD44-E012-4986-9820-5A3732845384}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DAF9BD44-E012-4986-9820-5A3732845384}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DAF9BD44-E012-4986-9820-5A3732845384}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DAF9BD44-E012-4986-9820-5A3732845384}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
Loading

0 comments on commit 842c22e

Please sign in to comment.