-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
947 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"$schema": "http://json.schemastore.org/template", | ||
"author": "@jet @bartelink", | ||
"classifications": [ | ||
"Equinox", | ||
"Event Sourcing", | ||
"Closing Books", | ||
"Epoch", | ||
"Series", | ||
"Period" | ||
], | ||
"tags": { | ||
"language": "F#", | ||
"type": "project" | ||
}, | ||
"identity": "Equinox.Patterns", | ||
"name": "Equinox Patterns Sample", | ||
"shortName": "eqxPatterns", | ||
"sourceName": "Patterns", | ||
"preferNameDirectory": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<TargetFramework>netcoreapp3.1</TargetFramework> | ||
<WarningLevel>5</WarningLevel> | ||
<IsPackable>false</IsPackable> | ||
<OutputType>Library</OutputType> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="Infrastructure.fs" /> | ||
<Compile Include="PeriodsCarryingForward.fs" /> | ||
<Compile Include="ItemIngesterTests.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" /> | ||
|
||
<PackageReference Include="FsCheck.Xunit" Version="2.14.2" /> | ||
<PackageReference Include="unquote" Version="5.0.0" /> | ||
<PackageReference Include="xunit" Version="2.4.1" /> | ||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<ProjectReference Include="..\Domain\Domain.fsproj" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
[<AutoOpen>] | ||
module Patterns.Domain.Tests.Infrastructure | ||
|
||
open FSharp.UMX | ||
open System | ||
open FsCheck | ||
open Patterns.Domain | ||
|
||
(* Generic FsCheck helpers *) | ||
|
||
let (|Id|) (x : Guid) = x.ToString "N" |> UMX.tag | ||
let inline mkId () = Guid.NewGuid() |> (|Id|) | ||
let (|Ids|) (xs : Guid[]) = xs |> Array.map (|Id|) | ||
|
||
type DomainArbs() = | ||
|
||
static member Item : Arbitrary<ItemEpoch.Events.Item> = Arb.fromGen <| gen { | ||
let! r = Arb.Default.Derive() |> Arb.toGen | ||
let id = mkId () // TODO why doesnt `let (Id id) = Arb.generate` generate fresh every time? | ||
return { r with id = id } | ||
} | ||
|
||
type DomainProperty() = inherit FsCheck.Xunit.PropertyAttribute(Arbitrary=[|typeof<DomainArbs>|], QuietOnSuccess=true) | ||
|
||
/// Inspired by AutoFixture.XUnit's AutoDataAttribute - generating test data without the full Property Based Tests experience | ||
/// By using this instead of Property, the developer has | ||
/// a) asserted by using this property instead of [<DomainProperty>] | ||
/// b) indirectly validated by running the tests frequently locally in DEBUG mode | ||
/// that running the test multiple times is not a useful thing to do | ||
#if !DEBUG | ||
type AutoDataAttribute() = inherit FsCheck.Xunit.PropertyAttribute(Arbitrary=[|typeof<DomainArbs>|], MaxTest=1, QuietOnSuccess=true) | ||
#else | ||
type AutoDataAttribute() = inherit FsCheck.Xunit.PropertyAttribute(Arbitrary=[|typeof<DomainArbs>|], MaxTest=5, QuietOnSuccess=true) | ||
#endif |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
module Patterns.Domain.Tests.ItemIngesterTests | ||
|
||
open Patterns.Domain | ||
open Patterns.Domain.ItemIngester | ||
open FsCheck.Xunit | ||
open FSharp.UMX | ||
open Swensen.Unquote | ||
|
||
let linger, lookBackLimit, maxPickTicketsPerBatch = System.TimeSpan.FromMilliseconds 1., 2, 5 | ||
|
||
let createSut store trancheId = | ||
// While we use ~ 200ms when hitting Cosmos, there's no value in doing so in the context of these property based tests | ||
let service = MemoryStore.Create(store, linger=linger, maxItemsPerEpoch=maxPickTicketsPerBatch, lookBackLimit=lookBackLimit) | ||
service.ForTranche trancheId | ||
|
||
let [<Property>] properties shouldInitialize shouldUseSameSut (Id trancheId) initialItems items = | ||
let store = Equinox.MemoryStore.VolatileStore() | ||
Async.RunSynchronously <| async { | ||
// Initialize with some items | ||
let initialSut = createSut store trancheId | ||
if shouldInitialize then do! initialSut.Initialize() | ||
let! initialResult = initialSut.IngestMany(initialItems) | ||
let initialExpected = initialItems |> Seq.map ItemEpoch.itemId |> Array.ofSeq | ||
test <@ set initialExpected = set initialResult @> | ||
|
||
// Add some extra | ||
let sut = if shouldUseSameSut then initialSut else createSut store trancheId | ||
if shouldInitialize then do! sut.Initialize() | ||
let! result = sut.IngestMany items | ||
let expected = items |> Seq.map ItemEpoch.itemId |> Seq.except initialExpected |> Seq.distinct | ||
test <@ set expected = set result @> | ||
|
||
// Add the same stuff for a different tranche; the data should be completely independent from an ingestion perspective | ||
let differentTranche = %(sprintf "%s2" %trancheId) | ||
let differentSutSameStore = createSut store differentTranche | ||
let! independentResult = differentSutSameStore.IngestMany(Array.append initialItems items) | ||
test <@ set initialResult + set result = set independentResult @> | ||
} | ||
|
||
let [<AutoData>] ``lookBack is limited`` (Id trancheId) genItem = | ||
let store = Equinox.MemoryStore.VolatileStore() | ||
Async.RunSynchronously <| async { | ||
// Initialize with more items than the lookBack accommodates | ||
let initialSut = createSut store trancheId | ||
let itemCount = | ||
// Fill up lookBackLimit batches, and another one as batch 0 that we will not look include in the load | ||
(lookBackLimit+1) * maxPickTicketsPerBatch | ||
// Add one more so we end up with an active batchId = lookBackLimit | ||
+ 1 | ||
let items = Array.init itemCount (fun _ -> genItem () ) | ||
test <@ Array.distinct items = items @> | ||
let batch0 = Array.take maxPickTicketsPerBatch items | ||
let batchesInLookBack = Array.skip maxPickTicketsPerBatch items | ||
let! b0Added = initialSut.IngestMany batch0 | ||
let b0Added = Array.ofSeq b0Added | ||
test <@ maxPickTicketsPerBatch = Array.length b0Added @> | ||
let! batchesInLookBackAdded = initialSut.IngestMany batchesInLookBack | ||
test <@ itemCount = Set.count (set b0Added + set batchesInLookBackAdded) @> | ||
|
||
// Now try to add the same items - the first batch worth should not be deduplicated | ||
let sut = createSut store trancheId | ||
let! result = sut.IngestMany items | ||
let result = Array.ofSeq result | ||
test <@ itemCount = itemCount | ||
&& result.Length = maxPickTicketsPerBatch | ||
&& set result = set b0Added @> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/// Integration suite for `Period` | ||
module Patterns.Domain.Tests.PeriodsCarryingForward | ||
|
||
open Patterns.Domain | ||
open Patterns.Domain.Period | ||
open FSharp.UMX | ||
open Swensen.Unquote | ||
open Xunit | ||
|
||
[<Fact>] | ||
let ``Happy path`` () = | ||
let store = Equinox.MemoryStore.VolatileStore() | ||
let service = MemoryStore.create store | ||
let decide items _state = | ||
let apply = Array.truncate 2 items | ||
let overflow = Array.skip apply.Length items | ||
(match overflow with [||] -> None | xs -> Some xs), // Apply max of two events | ||
(), // result | ||
[Events.Added {items = apply }] | ||
let add period events = service.Transact(PeriodId.parse period, decide, events) |> Async.RunSynchronously | ||
let read period = service.Read(PeriodId.parse period) |> Async.RunSynchronously | ||
add 0 [| %"a"; %"b" |] | ||
test <@ Fold.Open [| %"a"; %"b"|] = read 0 @> | ||
add 1 [| %"c"; %"d" |] | ||
test <@ Fold.Closed ([| %"a"; %"b"|], [| %"a"; %"b"|]) = read 0 @> | ||
test <@ Fold.Open [| %"a"; %"b"; %"c"; %"d" |] = read 1 @> | ||
let items period = read period |> Fold.(|Items|) | ||
add 1 [| %"e"; %"f"; %"g" |] // >2 items, therefore triggers an overflow | ||
test <@ [| %"a"; %"b"; %"c"; %"d"; %"e"; %"f" |] = items 1 @> | ||
test <@ [| %"a"; %"b"; %"c"; %"d"; %"e"; %"f"; %"g" |] = items 2 @> | ||
test <@ Fold.Initial = read 3 @> | ||
add 3 [| %"h" |] | ||
test <@ Fold.Open [| %"a"; %"b"; %"c"; %"d"; %"e"; %"f"; %"g"; %"h" |] = read 3 @> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
|
||
<PropertyGroup> | ||
<!-- We could target netstandard2.0 but we're slipping in a CosmosStore reference --> | ||
<TargetFramework>netcoreapp3.1</TargetFramework> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<Compile Include="Infrastructure.fs" /> | ||
<Compile Include="Types.fs" /> | ||
<Compile Include="Period.fs" /> | ||
<Compile Include="ItemEpoch.fs" /> | ||
<Compile Include="ItemSeries.fs" /> | ||
<Compile Include="ItemIngester.fs" /> | ||
</ItemGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Equinox.MemoryStore" Version="3.0.0-beta.4" /> | ||
<PackageReference Include="Equinox.CosmosStore" Version="3.0.0-beta.4" /> | ||
<PackageReference Include="FsCodec.NewtonsoftJson" Version="2.0.1" /> | ||
</ItemGroup> | ||
|
||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
[<AutoOpen>] | ||
module Patterns.Domain.Infrastructure | ||
|
||
module Equinox = | ||
|
||
let createDecider stream = | ||
Equinox.Decider(Serilog.Log.Logger, stream, maxAttempts = 3) | ||
|
||
/// Buffers events accumulated from a series of decisions while evolving the presented `state` to reflect said proposed `Events` | ||
type Accumulator<'event, 'state>(originState, fold : 'state -> 'event seq -> 'state) = | ||
let pendingEvents = ResizeArray() | ||
let mutable state = originState | ||
|
||
let apply (events : 'event seq) = | ||
pendingEvents.AddRange events | ||
state <- fold state events | ||
|
||
/// Run a decision function, buffering and applying any Events yielded | ||
member _.Transact decide = | ||
let r, events = decide state | ||
apply events | ||
r | ||
|
||
/// Run a decision function that does not yield a result | ||
// member x.Transact decide = | ||
// x.Transact(fun state -> (), decide state) | ||
|
||
/// Run an Async decision function, buffering and applying any Events yielded | ||
member _.TransactAsync decide = async { | ||
let! r, events = decide state | ||
apply events | ||
return r } | ||
|
||
/// Run an Async decision function that does not yield a result | ||
member x.TransactAsync decide = async { | ||
let! events = decide state | ||
apply events } | ||
|
||
// /// Projects from the present state including accumulated events | ||
// member _.Query f = f state | ||
|
||
/// Accumulated events based on the Decisions applied to date | ||
member _.Events = List.ofSeq pendingEvents |
Oops, something went wrong.