packages feed

chiasma-test-0.12.0.0: integration/Chiasma/Test/ConcurrentTest.hs

module Chiasma.Test.ConcurrentTest where

import qualified Conc
import Conc (withAsyncGated_, withAsync_)
import qualified Data.Text as Text
import Data.Text (stripPrefix)
import Exon (exon)
import Gate (Gate, signal)
import Polysemy.Test (Hedgehog, UnitTest, assert, (===))
import qualified Queue
import Queue (QueueResult (..))
import qualified Time
import Time (MilliSeconds (MilliSeconds))

import Chiasma.Codec.Data.Window (Window (..))
import Chiasma.Data.CodecError (CodecError)
import Chiasma.Data.TmuxCommand (TmuxCommand (ListWindows, NewWindow))
import Chiasma.Data.TmuxError (TmuxError)
import Chiasma.Data.TmuxNotification (TmuxNotification (..))
import qualified Chiasma.Effect.TmuxApi as TmuxApi
import Chiasma.Effect.TmuxApi (Tmux)
import Chiasma.Effect.TmuxClient (NativeTmux)
import Chiasma.Test.Tmux (tmuxTest)
import Chiasma.Tmux (withTmux)

-- | Run a loop of create-window then list-windows commands, collecting results.
worker ::
  Members [Tmux, AtomicState [Text]] r =>
  Text ->
  Sem r ()
worker label =
  for_ @[] [1 .. 10 :: Int] \ i -> do
    win <- TmuxApi.send (NewWindow def)
    wins <- TmuxApi.send (ListWindows def)
    atomicModify' (<> [[exon|#{label}-#{show i}: created @#{show win.windowId}, total #{show (length wins)}|]])

-- | Stress test for concurrent TmuxClient access.
-- Two threads run inside a single @withTmux@ scope, each sending 20 commands
-- (10 iterations × create + list). The Lock in tmuxRequest serializes
-- send+receive pairs, ensuring correct response routing.
test_concurrent :: UnitTest
test_concurrent =
  tmuxTest do
    restop @TmuxError @NativeTmux $ withTmux @TmuxCommand @CodecError do
      restop @CodecError @Tmux do
        Conc.interpretAtomic @[Text] [] do
          withAsync_ (worker "A") (worker "B")
          wins <- TmuxApi.send (ListWindows def)
          log' <- atomicGet
          for_ log' (embed . putStrLn . toString)
          -- 10 windows from each thread + 1 initial = 21
          length wins === 21

-- | Test that tmux control mode notifications are captured and accessible
-- via the ReceiveNotification action.
--
-- A background thread consumes notifications and writes them to a Queue.
-- Three phases exercise different consumption patterns:
-- 1. Send 3 requests, take from queue after each
-- 2. Send 3 requests, take 3 after the last one
-- 3. Run 3 concurrent threads that each send 3 requests, take 9 after all finish
--
-- All notifications are asserted to be valid (non-empty, starting with '%').
test_notifications :: UnitTest
test_notifications =
  tmuxTest do
    restop @TmuxError @NativeTmux $ withTmux @TmuxCommand @CodecError do
      restop @CodecError @Tmux do
        Conc.interpretQueueTB @TmuxNotification 64 do
          Conc.interpretGates do
            withAsyncGated_ consumeNotifications do
              -- Phase 1: interleaved send/take
              phase1 <- for @[] [1 .. 3 :: Int] \ _ -> do
                void $ TmuxApi.send (NewWindow def)
                Time.sleep (MilliSeconds 10)
                takeNotifications 1
              let notes1 = concat phase1
              assertOrdered notes1

              -- Phase 2: batch send, then batch take
              for_ @[] [1 .. 3 :: Int] \ _ -> do
                void $ TmuxApi.send (NewWindow def)
                Time.sleep (MilliSeconds 10)
              notes2 <- takeNotifications 3
              assertOrdered notes2

              -- Phase 3: concurrent senders, batch take
              withAsync_ sender $ withAsync_ sender sender
              notes3 <- takeNotifications 9
              assertOrdered notes3

              -- Global ordering: each phase's notifications should follow the previous
              assertOrdered (notes1 <> notes2 <> notes3)
  where
    consumeNotifications ::
      Members [Tmux, Queue TmuxNotification, Gate] r =>
      Sem r ()
    consumeNotifications = do
      signal
      forever do
        n <- TmuxApi.receiveNotification
        Queue.write n

    sender ::
      Members [Tmux, Time t d] r =>
      Sem r ()
    sender =
      for_ @[] [1 .. 3 :: Int] \ _ -> do
        void $ TmuxApi.send (NewWindow def)
        Time.sleep (MilliSeconds 10)

    takeNotifications ::
      Member (Queue TmuxNotification) r =>
      Int ->
      Sem r [TmuxNotification]
    takeNotifications n =
      catMaybes <$> replicateM n takeOne

    takeOne ::
      Member (Queue TmuxNotification) r =>
      Sem r (Maybe TmuxNotification)
    takeOne =
      Queue.readTimeout (MilliSeconds 500) <&> \case
        Success a -> Just a
        _ -> Nothing

    assertOrdered ::
      Member (Hedgehog IO) r =>
      [TmuxNotification] ->
      Sem r ()
    assertOrdered notes = do
      assert (not (null notes))
      for_ notes \ n ->
        assert (not (Text.null n.name))
      assertNonDecreasing (mapMaybe notificationId notes)

    assertNonDecreasing ::
      Member (Hedgehog IO) r =>
      [Int] ->
      Sem r ()
    assertNonDecreasing = \case
      [] -> unit
      [_] -> unit
      a : b : rest -> do
        assert (a <= b)
        assertNonDecreasing (b : rest)

    notificationId :: TmuxNotification -> Maybe Int
    notificationId n =
      listToMaybe n.args >>= extractId

    extractId :: Text -> Maybe Int
    extractId = readMaybe . toString <=< stripPrefix "@"