packages feed

cachix-1.9.1: test/Daemon/TaskQueueSpec.hs

module Daemon.TaskQueueSpec where

import Cachix.Daemon.TaskQueue
import Cachix.Daemon.Types.TaskQueue (Prioritized (..))
import Protolude
import Test.Hspec

data PriorityTask = PriorityTask
  { priority :: Int,
    taskId :: Int
  }
  deriving stock (Eq, Show)

instance Ord PriorityTask where
  compare t1 t2 = compare (priority t1) (priority t2)

spec :: Spec
spec = do
  describe "TaskQueue" $ do
    describe "basic operations" $ do
      it "creates an empty queue" $ do
        queue <- atomically newTaskQueue
        result <- atomically $ tryWriteTask queue (PriorityTask 1 1)
        result `shouldBe` Just True

      it "writes and reads a single task" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          writeTask q (PriorityTask 1 1)
          return q
        task <- atomically $ readTask queue
        task `shouldBe` Just (PriorityTask 1 1)

      it "tryWriteTask returns Just True on success" $ do
        queue <- atomically newTaskQueue
        result <- atomically $ tryWriteTask queue (PriorityTask 1 1)
        result `shouldBe` Just True

      it "tryWriteTask returns Just False when closed" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          closeTaskQueue q
          return q
        result <- atomically $ tryWriteTask queue (PriorityTask 1 1)
        result `shouldBe` Just False

    describe "priority ordering" $ do
      it "returns higher priority items first" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          writeTask q (PriorityTask 1 1)
          writeTask q (PriorityTask 3 2)
          writeTask q (PriorityTask 2 3)
          return q

        task1 <- atomically $ readTask queue
        task1 `shouldBe` Just (PriorityTask 3 2)

        task2 <- atomically $ readTask queue
        task2 `shouldBe` Just (PriorityTask 2 3)

        task3 <- atomically $ readTask queue
        task3 `shouldBe` Just (PriorityTask 1 1)

      it "maintains FIFO ordering within same priority" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          writeTask q (PriorityTask 1 1)
          writeTask q (PriorityTask 1 2)
          writeTask q (PriorityTask 1 3)
          return q

        task1 <- atomically $ readTask queue
        task1 `shouldBe` Just (PriorityTask 1 1)

        task2 <- atomically $ readTask queue
        task2 `shouldBe` Just (PriorityTask 1 2)

        task3 <- atomically $ readTask queue
        task3 `shouldBe` Just (PriorityTask 1 3)

      it "handles mixed priorities with FIFO within each priority level" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          writeTask q (PriorityTask 2 1)
          writeTask q (PriorityTask 1 2)
          writeTask q (PriorityTask 2 3)
          writeTask q (PriorityTask 3 4)
          writeTask q (PriorityTask 1 5)
          return q

        task1 <- atomically $ readTask queue
        task1 `shouldBe` Just (PriorityTask 3 4)

        task2 <- atomically $ readTask queue
        task2 `shouldBe` Just (PriorityTask 2 1)

        task3 <- atomically $ readTask queue
        task3 `shouldBe` Just (PriorityTask 2 3)

        task4 <- atomically $ readTask queue
        task4 `shouldBe` Just (PriorityTask 1 2)

        task5 <- atomically $ readTask queue
        task5 `shouldBe` Just (PriorityTask 1 5)

    describe "queue lifecycle" $ do
      it "writeTask is a no-op when queue is closed" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          closeTaskQueue q
          writeTask q (PriorityTask 1 1)
          return q
        task <- atomically $ readTask queue
        task `shouldBe` Nothing

      it "readTask returns Nothing when queue is closed and empty" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          closeTaskQueue q
          return q
        task <- atomically $ readTask (queue :: TaskQueue PriorityTask)
        task `shouldBe` Nothing

      it "can read existing items after queue is closed" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          writeTask q (PriorityTask 1 1)
          writeTask q (PriorityTask 2 2)
          closeTaskQueue q
          return q

        task1 <- atomically $ readTask queue
        task1 `shouldBe` Just (PriorityTask 2 2)

        task2 <- atomically $ readTask queue
        task2 `shouldBe` Just (PriorityTask 1 1)

        task3 <- atomically $ readTask queue
        task3 `shouldBe` Nothing

      it "close is idempotent" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          closeTaskQueue q
          closeTaskQueue q
          closeTaskQueue q
          return q
        task <- atomically $ readTask (queue :: TaskQueue PriorityTask)
        task `shouldBe` Nothing

    describe "edge cases" $ do
      it "handles single item queue correctly" $ do
        queue <- atomically $ do
          q <- newTaskQueue
          writeTask q (PriorityTask 5 1)
          return q
        task <- atomically $ readTask queue
        task `shouldBe` Just (PriorityTask 5 1)

      it "maintains ordering with large number of items" $ do
        let items = [1 .. 100] :: [Int]
        queue <- atomically $ do
          q <- newTaskQueue
          forM_ items $ \i -> writeTask q (PriorityTask (i `mod` 5) i)
          return q

        results <- replicateM 100 (atomically $ readTask queue)
        let justResults = catMaybes results
        length justResults `shouldBe` 100

        let grouped = groupBy (\a b -> priority a == priority b) justResults
        forM_ grouped $ \grp -> do
          let priorities = map priority grp
          priorities `shouldSatisfy` \case
            [] -> True
            (p : rest) -> all (== p) rest

    describe "sequence number overflow" $ do
      it "handles wraparound from maxBound to minBound correctly" $ do
        let maxSeq = maxBound :: Int32
            minSeq = minBound :: Int32
            taskOld = Prioritized (PriorityTask 1 1) maxSeq
            taskNew = Prioritized (PriorityTask 1 2) minSeq
        taskOld `compare` taskNew `shouldBe` GT

      it "maintains FIFO across overflow boundary" $ do
        let nearMax = maxBound - 2 :: Int32
            tasks =
              [ Prioritized (PriorityTask 1 4) (nearMax + 3),
                Prioritized (PriorityTask 1 2) (nearMax + 1),
                Prioritized (PriorityTask 1 1) nearMax,
                Prioritized (PriorityTask 1 3) (nearMax + 2)
              ]
        let sorted = sortBy (comparing Down) tasks
        map (taskId . pTask) sorted `shouldBe` [1, 2, 3, 4]

      it "circular comparison works for sequences far apart" $ do
        let s1 = 100 :: Int32
            s2 = s1 + 1000000
            taskOld = Prioritized (PriorityTask 1 1) s1
            taskNew = Prioritized (PriorityTask 1 2) s2
        taskOld `compare` taskNew `shouldBe` GT