packages feed

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)