Skip to content

Commit ece2d8b

Browse files
committed
Move TaskSeq.PocTests.fs to SmokeTests, it is the only file with a TaskResult dependency
We want to remove the dependency in our tests on FsToolkit.ErrorHandling in PR #181 because it uses a high version of FSharp.Core. In other words, it disallows us to properly test TaskSeq with the lowest-denominator FSharp.Core it was compiled with, which we should do, to ensure stability in the widest range of forward-and-backward compatibility. The SmokeTests library, on the other hand, uses the highest RTM release of FSharp.Core and we don't really care about other deps there.
1 parent 850c36f commit ece2d8b

File tree

4 files changed

+133
-1
lines changed

4 files changed

+133
-1
lines changed

src/FSharp.Control.TaskSeq.SmokeTests/FSharp.Control.TaskSeq.SmokeTests.fsproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,16 @@
99
</ItemGroup>
1010

1111
<ItemGroup>
12+
<Compile Include="TestUtils.fs" />
1213
<Compile Include="SmokeTests.fs" />
14+
<Compile Include="TaskSeq.PocTests.fs" />
1315
</ItemGroup>
1416

1517
<ItemGroup>
1618
<!-- our smoketests use the highest RTM (non-alpha) FSharp.Core, contrary to the base test project, which uses the lowest possible denominator -->
1719
<PackageReference Update="FSharp.Core" Version="7.0.401" />
1820
<PackageReference Include="FSharp.Control.TaskSeq" Version="0.4.0-alpha.1" />
21+
<PackageReference Include="FsToolkit.ErrorHandling.TaskResult" Version="3.2.0" />
1922
<PackageReference Include="FsUnit.xUnit" Version="5.5.0" />
2023
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
2124
<PackageReference Include="xunit" Version="2.5.3" />
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
namespace TaskSeq.Tests
2+
3+
open System
4+
open System.Threading
5+
open System.Threading.Tasks
6+
open System.Diagnostics
7+
open System.Collections.Generic
8+
9+
open Xunit
10+
open Xunit.Abstractions
11+
open FsUnit.Xunit
12+
13+
open FSharp.Control
14+
15+
/// Milliseconds
16+
[<Measure>]
17+
type ms
18+
19+
/// Microseconds
20+
[<Measure>]
21+
type µs
22+
23+
/// Helpers for short waits, as Task.Delay has about 15ms precision.
24+
/// Inspired by IoT code: https://github.com/dotnet/iot/pull/235/files
25+
module DelayHelper =
26+
27+
/// <summary>
28+
/// Delay for at least the specified <paramref name="microseconds"/>.
29+
/// </summary>
30+
/// <param name="microseconds">The number of microseconds to delay.</param>
31+
/// <param name="allowThreadYield">
32+
/// True to allow yielding the thread. If this is set to false, on single-proc systems
33+
/// this will prevent all other code from running.
34+
/// </param>
35+
let spinWaitDelay (microseconds: int64<µs>) (allowThreadYield: bool) =
36+
let start = Stopwatch.GetTimestamp()
37+
let minimumTicks = int64 microseconds * Stopwatch.Frequency / 1_000_000L
38+
39+
// FIXME: though this is part of official IoT code, the `allowThreadYield` version is extremely slow
40+
// slower than would be expected from a simple SpinOnce. Though this may be caused by scenarios with
41+
// many tasks at once. Have to investigate. See perf smoke tests.
42+
if allowThreadYield then
43+
let spinWait = SpinWait()
44+
45+
while Stopwatch.GetTimestamp() - start < minimumTicks do
46+
spinWait.SpinOnce(1)
47+
48+
else
49+
while Stopwatch.GetTimestamp() - start < minimumTicks do
50+
Thread.SpinWait(1)
51+
52+
let delayTask (µsecMin: int64<µs>) (µsecMax: int64<µs>) f = task {
53+
let rnd = Random()
54+
let rnd () = rnd.NextInt64(int64 µsecMin, int64 µsecMax) * 1L<µs>
55+
56+
// ensure unequal running lengths and points-in-time for assigning the variable
57+
// DO NOT use Thead.Sleep(), it's blocking!
58+
// WARNING: Task.Delay only has a 15ms timer resolution!!!
59+
60+
// TODO: check this! The following comment may not be correct
61+
// this creates a resume state, which seems more efficient than SpinWait.SpinOnce, see DelayHelper.
62+
let! _ = Task.Delay 0
63+
let delay = rnd ()
64+
65+
// typical minimum accuracy of Task.Delay is 15.6ms
66+
// for delay-cases shorter than that, we use SpinWait
67+
if delay < 15_000L<µs> then
68+
do spinWaitDelay (rnd ()) false
69+
else
70+
do! Task.Delay(int <| float delay / 1_000.0)
71+
72+
return f ()
73+
}
74+
75+
/// <summary>
76+
/// Creates dummy backgroundTasks with a randomized delay and a mutable state,
77+
/// to ensure we properly test whether processing is done ordered or not.
78+
/// Default for <paramref name="µsecMin" /> and <paramref name="µsecMax" />
79+
/// are 10,000µs and 30,000µs respectively (or 10ms and 30ms).
80+
/// </summary>
81+
type DummyTaskFactory(µsecMin: int64<µs>, µsecMax: int64<µs>) =
82+
let mutable x = 0
83+
84+
/// <summary>
85+
/// Creates dummy tasks with a randomized delay and a mutable state,
86+
/// to ensure we properly test whether processing is done ordered or not.
87+
/// Uses the defaults for <paramref name="µsecMin" /> and <paramref name="µsecMax" />
88+
/// with 10,000µs and 30,000µs respectively (or 10ms and 30ms).
89+
/// </summary>
90+
new() = new DummyTaskFactory(10_000L<µs>, 30_000L<µs>)
91+
92+
93+
/// Bunch of delayed tasks that randomly have a yielding delay of 10-30ms, therefore having overlapping execution times.
94+
member _.CreateDelayedTasks_SideEffect total = [
95+
for i in 0 .. total - 1 do
96+
fun () -> DelayHelper.delayTask µsecMin µsecMax (fun _ -> Interlocked.Increment &x)
97+
]
98+
99+
/// Just some dummy task generators, copied over from the base test project, with artificial delays,
100+
/// mostly to ensure sequential async operation of side effects.
101+
module Gen =
102+
/// Joins two tasks using merely BCL methods. This approach is what you can use to
103+
/// properly, sequentially execute a chain of tasks in a non-blocking, non-overlapping way.
104+
let joinWithContinuation tasks =
105+
let simple (t: unit -> Task<_>) (source: unit -> Task<_>) : unit -> Task<_> =
106+
fun () ->
107+
source()
108+
.ContinueWith((fun (_: Task) -> t ()), TaskContinuationOptions.OnlyOnRanToCompletion)
109+
.Unwrap()
110+
:?> Task<_>
111+
112+
let rec combine acc (tasks: (unit -> Task<_>) list) =
113+
match tasks with
114+
| [] -> acc
115+
| t :: tail -> combine (simple t acc) tail
116+
117+
match tasks with
118+
| first :: rest -> combine first rest
119+
| [] -> failwith "oh oh, no tasks given!"
120+
121+
let joinIdentityHotStarted tasks () = task { return tasks |> List.map (fun t -> t ()) }
122+
123+
let joinIdentityDelayed tasks () = task { return tasks }
124+
125+
let createAndJoinMultipleTasks total joiner : Task<_> =
126+
// the actual creation of tasks
127+
let tasks = DummyTaskFactory().CreateDelayedTasks_SideEffect total
128+
let combinedTask = joiner tasks
129+
// start the combined tasks
130+
combinedTask ()

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
<Compile Include="TaskSeq.Tests.CE.fs" />
4343
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
4444
<Compile Include="TaskSeq.StateTransitionBug-delayed.Tests.CE.fs" />
45-
<Compile Include="TaskSeq.PocTests.fs" />
4645
<Compile Include="TaskSeq.Realworld.fs" />
4746
<Compile Include="TaskSeq.AsyncExtensions.Tests.fs" />
4847
<Compile Include="TaskSeq.TaskExtensions.Tests.fs" />

0 commit comments

Comments
 (0)