packages feed

grpc-spec-1.0.0: test-grpc-spec/Test/Util/Awkward.hs

module Test.Util.Awkward (
    Awkward(..)
    -- * Support for defining instances
  , awkward
  , arbitraryAwkward
  , shrinkAwkward
  , shrinkRegular
  ) where

import Control.Monad
import Data.ByteString qualified as BS.Strict
import Data.ByteString qualified as Strict (ByteString)
import Data.ByteString.Char8 qualified as Strict.BS.Char8
import Data.Char (ord, chr, isSpace)
import Data.List.NonEmpty (NonEmpty)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Word
import Test.QuickCheck
import Test.QuickCheck.Instances ()

{-------------------------------------------------------------------------------
  \"Awkward\" instances
-------------------------------------------------------------------------------}

-- | Newtype wrapper for \"awkward\" 'Arbitrary' instances
--
-- In most property tests we don't explore edge cases, preferring for example
-- to use only simple header names, rather than check encoding issues. But in
-- these serialization tests the edge cases are of course important.
newtype Awkward a = Awkward { getAwkward :: a }
  deriving stock (Show, Functor)

awkward :: Arbitrary (Awkward a) => Gen a
awkward = getAwkward <$> arbitrary

arbitraryAwkward :: Arbitrary (Awkward a) => (a -> b) -> Gen (Awkward b)
arbitraryAwkward f = Awkward . f <$> awkward

shrinkAwkward ::
     Arbitrary (Awkward a)
  => (a -> b)
  -> (b -> a)
  -> Awkward b -> [Awkward b]
shrinkAwkward f g =
      map (Awkward . f . getAwkward)
    . shrink
    . Awkward . g . getAwkward

-- | Non-awkward fields of awkward types
shrinkRegular ::
     Arbitrary a
  => (a -> b)
  -> (b -> a)
  -> Awkward b -> [Awkward b]
shrinkRegular f g = map (Awkward . f) . shrink . g . getAwkward

{-------------------------------------------------------------------------------
  Standard instances
-------------------------------------------------------------------------------}

distrib :: Functor f => f (Awkward a) -> Awkward (f a)
distrib = Awkward . fmap getAwkward

undistrib :: Functor f => Awkward (f a) -> f (Awkward a)
undistrib = fmap Awkward . getAwkward

instance Arbitrary (Awkward a) => Arbitrary (Awkward (Maybe a)) where
  arbitrary = distrib <$> arbitrary
  shrink    = map distrib . shrink . undistrib

instance Arbitrary (Awkward a) => Arbitrary (Awkward [a]) where
  arbitrary = distrib <$> arbitrary
  shrink    = map distrib . shrink . undistrib

instance Arbitrary (Awkward a) => Arbitrary (Awkward (NonEmpty a)) where
  arbitrary = distrib <$> arbitrary
  shrink    = map distrib . shrink . undistrib

instance ( Arbitrary (Awkward a)
         , Arbitrary (Awkward b)
         ) => Arbitrary (Awkward (a, b)) where
  arbitrary = fmap Awkward $ (,) <$> awkward <*> awkward
  shrink pair@(Awkward (x, y)) = concat [
        shrinkAwkward (\x' -> (x', y )) fst pair
      , shrinkAwkward (\y' -> (x , y')) snd pair
      ]

instance ( Arbitrary (Awkward k)
         , Arbitrary (Awkward v)
         , Ord k
         ) => Arbitrary (Awkward (Map k v)) where
  arbitrary = arbitraryAwkward Map.fromList
  shrink    = shrinkAwkward    Map.fromList Map.toList

instance Arbitrary (Awkward Strict.ByteString) where
  arbitrary = sized $ \sz -> Awkward <$> do
      n <- choose (0, sz)
      trimByteString . BS.Strict.pack <$> replicateM n genChar
    where
      -- Generate commas with relatively high probability, so that the tests
      -- for duplicate headers have enough material to work with.
      genChar :: Gen Word8
      genChar = frequency [
            (9, arbitrary)
          , (1, pure $ fromIntegral (ord ','))
          ]

  shrink =
        map (Awkward . trimByteString . BS.Strict.pack)
      . shrinkList shrinkChar
      . BS.Strict.unpack . getAwkward
    where
      -- shrink as 'Char'
      shrinkChar :: Word8 -> [Word8]
      shrinkChar = map (fromIntegral . ord) . shrink . (chr . fromIntegral)

instance {-# OVERLAPPING #-} Arbitrary (Awkward String) where
  arbitrary = Awkward . trim <$> arbitrary
  shrink    = map (Awkward . trim) . shrink . getAwkward

instance Arbitrary (Awkward Text) where
  arbitrary = Awkward . Text.pack . trim . getAwkward <$> arbitrary
  shrink    = map (Awkward . Text.pack . trim) . shrink . (Text.unpack . getAwkward)

instance Arbitrary (Awkward Int) where
  arbitrary = Awkward <$> arbitrary
  shrink    = map Awkward . shrink . getAwkward

instance Arbitrary (Awkward Double) where
  arbitrary = Awkward <$> arbitrary
  shrink    = map Awkward . shrink . getAwkward

{-------------------------------------------------------------------------------
  Trimming

  Generate only trimmed strings; if we generate a value that has leading or
  trailing whitespace, we might not be able to parse that value, as parsing may
  trim the value.
-------------------------------------------------------------------------------}

trim :: String -> String
trim =
      dropWhile isSpace
    . reverse
    . dropWhile isSpace
    . reverse

trimByteString :: Strict.ByteString -> Strict.ByteString
trimByteString =
      Strict.BS.Char8.dropWhile    isSpace
    . Strict.BS.Char8.dropWhileEnd isSpace