moonlight-triangulation-0.1.0.0: test/parallel/Moonlight/Triangulation/ParallelSpec.hs
-- | The one module in the package that spawns threads. Its whole contract is
-- that scheduling is invisible: the same tournament, the same value, at every
-- worker count. That is what is asserted here, because a concurrent join whose
-- result depended on the worker count would be wrong in a way no amount of
-- validity checking on a single run could see.
module Moonlight.Triangulation.ParallelSpec
( tests
) where
import qualified Data.List.NonEmpty as NE
import Moonlight.Triangulation (unions)
import Moonlight.Triangulation.AlgebraFixtures (Mesh, assertMesh, operands)
import Moonlight.Triangulation.Parallel (unionsConcurrently)
import Support (requireRight)
tests :: IO ()
tests = do
testConcurrentJoinAgreesWithSequential
testConcurrentJoinIsWorkerCountInvariant
putStrLn "parallel: ok"
-- Counts on both sides of the operand count, so the tournament is starved at
-- one end and saturated at the other, and 1 exercises the sequential-collapse
-- branch that a purely concurrent test would never reach.
workerCounts :: [Int]
workerCounts = [1, 2, 3, 5, 16]
concurrentJoin :: Int -> NE.NonEmpty Mesh -> IO Mesh
concurrentJoin workers shards = do
outcome <- unionsConcurrently workers shards
requireRight ("concurrent unions at " <> show workers <> " workers") outcome
-- The sequential tournament is the oracle. It is a different interpreter over
-- the same plan, not a second copy of this one, so agreement is evidence.
testConcurrentJoinAgreesWithSequential :: IO ()
testConcurrentJoinAgreesWithSequential = do
shards <- fmap (map snd) operands
nonEmptyShards <- case NE.nonEmpty shards of
Nothing -> fail "concurrent join agreement: no operands"
Just present -> pure present
sequential <- requireRight "sequential unions" (unions shards)
mapM_
( \workers -> do
concurrent <- concurrentJoin workers nonEmptyShards
assertMesh
("concurrent tournament at " <> show workers <> " workers")
sequential
concurrent
)
workerCounts
-- Stated separately from agreement: even if both interpreters were wrong in
-- the same way, a result that moved with the worker count would still be a
-- defect, and this is the assertion that would catch it.
testConcurrentJoinIsWorkerCountInvariant :: IO ()
testConcurrentJoinIsWorkerCountInvariant = do
shards <- fmap (map snd) operands
nonEmptyShards <- case NE.nonEmpty shards of
Nothing -> fail "worker-count invariance: no operands"
Just present -> pure present
results <- traverse (`concurrentJoin` nonEmptyShards) workerCounts
case results of
[] -> fail "worker-count invariance: no worker counts"
(reference : rest) ->
mapM_
( \(workers, result) ->
assertMesh
("worker count " <> show workers <> " agrees with the first")
reference
result
)
(zip (drop 1 workerCounts) rest)