Initial commit of structure

This commit is contained in:
Smaug123
2021-02-17 16:07:42 +00:00
commit 1a28f01551
7 changed files with 388 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
namespace PulsingServer.Test
open PulsingServer
open NUnit.Framework
open FsUnitTyped
type AgentIndex = AgentIndex of int
type ReadIndex = ReadIndex of int
type Action<'info> =
| ChangeData of 'info
| BeginRead of AgentIndex
| AwaitRead of ReadIndex
[<TestFixture>]
module TestProperties =
let executeAction
(ext : ExternalInfoProvider<'info>)
(agents : ServerAgent<'info> array)
((readNumber : int), (awaitingRead : Map<ReadIndex, Async<'info>>))
(action : Action<'info>)
=
match action with
| BeginRead (AgentIndex i) ->
let mutable answer = None
let result = ServerAgent.giveNextResponse (fun resp -> answer <- Some resp) agents.[i]
let output =
async {
do! result
return Option.get answer
}
ext, agents, (readNumber + 1, Map.add (ReadIndex readNumber) output awaitingRead)
| AwaitRead index ->
awaitingRead.[index]
|> Async.RunSynchronously

23
Test/Test.fsproj Normal file
View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="TestServer.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FsCheck" Version="2.14.4" />
<PackageReference Include="FsUnit" Version="4.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="NUnit3TestAdapter" Version="3.16.1" />
<PackageReference Include="NUnit" Version="3.13.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\PulsingServer\PulsingServer.fsproj" />
</ItemGroup>
</Project>

172
Test/TestServer.fs Normal file
View File

@@ -0,0 +1,172 @@
namespace PulsingServer.Test
open System
open System.Diagnostics
open PulsingServer
open NUnit.Framework
open FsUnitTyped
[<TestFixture>]
module TestPulsingServer =
[<Test>]
let ``Example test scenario`` () =
let responder1 = ServerAgent.make "hi"
let responder2 = ServerAgent.make "hi"
let mutable info = "original info"
let count = ref 0
let getInfo = async {
System.Threading.Interlocked.Increment count |> ignore
let info = lock info (fun () -> sprintf "%s" info)
return info
}
let dontSleep (_ : TimeSpan) = async { return () }
let infoProvider = ExternalInfoProvider.make dontSleep getInfo 10<ms> [| responder1 ; responder2 |]
// We're not getting new info, because we didn't await the construction of ExternalInfoProvider
count.Value |> shouldEqual 0
// The two responders are ready, but have not received anything yet.
do
let response = ServerAgent.giveNextResponse responder1
response
|> Async.RunSynchronously
|> shouldEqual "hi"
// Now start off the ExternalInfoProvider!
let _ =
infoProvider
|> Async.RunSynchronously
// Now we have definitely started pinging...
count.Value |> shouldBeGreaterThan 0
// ... and at some point soon, the first responder will act on the info it receives.
do
let response = ServerAgent.giveNextResponse responder1
response
|> Async.RunSynchronously
|> shouldEqual "original info"
// Update the info. responder1 is not going to fail on the `received` check, because that
// was one-shot.
lock info (fun () ->
info <- "new info!"
)
// Get responder2 ready to act in a couple of different ways.
let response2 = ServerAgent.giveNextResponse responder2
let response2' = ServerAgent.giveNextResponse responder2
// At some point soon, the infoProvider picks up the change and propagates it.
response2
|> Async.RunSynchronously
|> fun info ->
// By design, we can't distinguish between these two cases.
(info = "new info!" || info = "original info")
|> shouldEqual true
response2'
|> Async.RunSynchronously
|> fun info ->
// By design, we can't distinguish between these two cases.
(info = "new info!" || info = "original info")
|> shouldEqual true
// Eventually, responder2 does pick up the new info.
let rec go () =
let response = ServerAgent.giveNextResponse responder2 |> Async.RunSynchronously
if response <> "new info!" then go ()
go ()
[<TestCase (10000, 1)>]
[<TestCase (10000, 3)>]
let ``Stress test`` (n : int, queues : int) =
let responders = Array.init queues (fun _ -> ServerAgent.make "uninitialised")
let mutable data = ""
let getInfo =
async {
// Simulate a slow network call
do! Async.Sleep (TimeSpan.FromSeconds 1.)
let result = lock data (fun () -> sprintf "%s" data)
return result
}
let _infoProvider =
ExternalInfoProvider.make Async.Sleep getInfo 10<ms> responders
|> Async.RunSynchronously
let time = Stopwatch ()
// Restart it a couple of times to warm it up
time.Restart ()
time.Restart ()
// n requests come in - note that we don't start them off yet,
// because we want to time them separately
let requests =
Array.init n (fun i ->
async {
let! answer = ServerAgent.giveNextResponse responders.[i % queues]
if answer <> "" then failwith "unexpected response!"
return ()
}
)
|> Async.Parallel
|> Async.Ignore
time.Stop ()
printfn "Time to construct requests: %i ms" time.ElapsedMilliseconds
time.Restart ()
requests
|> Async.RunSynchronously
time.Stop ()
printfn "Time to execute: %i ms" time.ElapsedMilliseconds
// Now prepare n more requests, but halfway through, we'll be changing the data.
// Again, don't kick them off right now; wait for the timer.
time.Restart ()
let requests =
Array.init n (fun i ->
if i = n / 2 then
async {
lock data (fun () -> data <- "new data")
return None
}
else
async {
do! Async.Sleep (TimeSpan.FromMilliseconds (float i))
let! response = ServerAgent.giveNextResponse (responders.[i % queues])
return Some response
}
)
|> Async.Parallel
time.Stop ()
printfn "Time to construct requests: %i ms" time.ElapsedMilliseconds
time.Restart ()
let results =
requests
|> Async.RunSynchronously
time.Stop ()
printfn "Time to execute: %i ms" time.ElapsedMilliseconds
let grouped =
results
|> Array.countBy id
|> Map.ofArray
grouped.[None] |> shouldEqual 1
let pre = Map.tryFind (Some "") grouped |> Option.defaultValue 0
let post = Map.tryFind (Some "new data") grouped |> Option.defaultValue 0
pre + post |> shouldEqual (n - 1)
printfn "Got old data: %i. Got new data: %i." pre post