packages feed

nonempty-vector-0.3: test/Test/Data/Vector/NonEmpty/Util.hs

{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-orphans #-}
-- |
-- Module      : Test.Data.Vector.NonEmpty.Util
-- Copyright   : (c) 2019-2026 Emily Pillmore
-- License     : BSD-style
--
-- Maintainer  : Emily Pillmore <emilypi@cohomolo.gy>
-- Stability   : experimental
-- Portability : non-portable
--
-- Generators, list models, and property helpers shared by the
-- non-empty vector test suite.
--
-- Vectors are built and observed through the representation exposed by
-- "Data.Vector.NonEmpty.Internal", so that the models never depend on
-- the API under test.
--
module Test.Data.Vector.NonEmpty.Util
( -- * Building and observing vectors
  vec
, elems
, elems1
, whenNonEmpty
  -- * Generators
, genIndex
, genIndices
, genOutOfBounds
, genSlice
, genUpdates
  -- * Effects
, logged
, unfoldStep
  -- * List models
, adjustAt
, lookupList
, swapAt
, unfoldrMList
, unfoldrNMList
  -- * Exceptions
, throws
, throwsIO
) where


import Control.DeepSeq (NFData, force)
import Control.Exception (SomeException, evaluate, try)

import Data.List.NonEmpty (NonEmpty(..))
import Data.Maybe (fromMaybe, listToMaybe)
import qualified Data.Vector as V
import Data.Vector.NonEmpty.Internal (NonEmptyVector(..))

import Test.QuickCheck


instance Arbitrary a => Arbitrary (NonEmptyVector a) where
    arbitrary = vec . getNonEmpty <$> arbitrary
    shrink = fmap (vec . getNonEmpty) . shrink . NonEmpty . elems

-- ---------------------------------------------------------------------- --
-- Building and observing vectors

-- | Build a non-empty vector from its representation. The caller
-- guarantees that the list is non-empty.
--
vec :: [a] -> NonEmptyVector a
vec = NonEmptyVector . V.fromList

-- | The elements of a non-empty vector, read from its representation.
--
elems :: NonEmptyVector a -> [a]
elems = V.toList . _neVec

-- | The elements of a non-empty vector as a non-empty list, read from its
-- representation.
--
elems1 :: NonEmptyVector a -> NonEmpty a
elems1 (NonEmptyVector v) = V.unsafeHead v :| V.toList (V.unsafeTail v)

-- | Model of the 'Maybe'-valued constructors: 'Nothing' exactly when the
-- result would be empty.
--
whenNonEmpty :: [a] -> Maybe [a]
whenNonEmpty [] = Nothing
whenNonEmpty as = Just as

-- ---------------------------------------------------------------------- --
-- Generators

-- | A valid index into the vector.
--
genIndex :: NonEmptyVector a -> Gen Int
genIndex (NonEmptyVector v) = choose (0, V.length v - 1)

-- | A non-empty vector of valid indices into the vector.
--
genIndices :: NonEmptyVector a -> Gen (NonEmptyVector Int)
genIndices v = vec <$> listOf1 (genIndex v)

-- | An index outside the bounds of the vector, on either side.
--
genOutOfBounds :: NonEmptyVector a -> Gen Int
genOutOfBounds (NonEmptyVector v) = oneof
    [ choose (-10, -1)
    , choose (n, n + 10)
    ]
  where
    n = V.length v

-- | A valid @(offset, length)@ slice of the vector, possibly empty.
--
genSlice :: NonEmptyVector a -> Gen (Int, Int)
genSlice (NonEmptyVector v) = do
    i <- choose (0, n)
    m <- choose (0, n - i)
    pure (i, m)
  where
    n = V.length v

-- | A list of updates at valid indices of the vector.
--
genUpdates :: Arbitrary b => NonEmptyVector a -> Gen [(Int, b)]
genUpdates v = listOf ((,) <$> genIndex v <*> arbitrary)

-- ---------------------------------------------------------------------- --
-- Effects

-- | A writer-style action that records @w@ and yields @a@. Running monadic
-- operations in @([w], -)@ exposes the order and number of effects.
--
logged :: w -> a -> ([w], a)
logged w a = ([w], a)

-- | A terminating unfold step: counts the seed down to zero, emitting
-- @f seed@ at each step.
--
unfoldStep :: (Int -> a) -> Int -> Maybe (a, Int)
unfoldStep f b
    | b <= 0 = Nothing
    | otherwise = Just (f b, b - 1)

-- ---------------------------------------------------------------------- --
-- List models

-- | Apply a function at an index, leaving the list unchanged when the index
-- is out of bounds.
--
adjustAt :: Int -> (a -> a) -> [a] -> [a]
adjustAt i f as = [ if j == i then f a else a | (j, a) <- zip [0 ..] as ]

-- | Total list indexing.
--
lookupList :: Int -> [a] -> Maybe a
lookupList i as
    | i < 0 = Nothing
    | otherwise = listToMaybe (drop i as)

-- | Exchange the elements at two indices.
--
swapAt :: Int -> Int -> [a] -> [a]
swapAt i j as = [ pick k a | (k, a) <- zip [0 ..] as ]
  where
    pick k a
      | k == i = fromMaybe a (lookupList j as)
      | k == j = fromMaybe a (lookupList i as)
      | otherwise = a

-- | Monadic 'Data.List.unfoldr'.
--
unfoldrMList :: Monad m => (b -> m (Maybe (a, b))) -> b -> m [a]
unfoldrMList f = go
  where
    go b = f b >>= maybe (pure []) (\(a, b') -> (a :) <$> go b')

-- | Monadic 'Data.List.unfoldr' producing at most @n@ elements. The step
-- function is not run once @n@ elements have been produced.
--
unfoldrNMList :: Monad m => Int -> (b -> m (Maybe (a, b))) -> b -> m [a]
unfoldrNMList n0 f = go n0
  where
    go n b
      | n <= 0 = pure []
      | otherwise = f b >>= maybe (pure []) (\(a, b') -> (a :) <$> go (n - 1) b')

-- ---------------------------------------------------------------------- --
-- Exceptions

-- | The value throws when fully evaluated.
--
throws :: NFData a => a -> Property
throws = throwsIO . evaluate . force

-- | The action throws when run.
--
throwsIO :: IO a -> Property
throwsIO act = ioProperty $ do
    r <- try act
    pure $ case r of
      Left (_ :: SomeException) -> property True
      Right _ -> counterexample "expected an exception" (property False)