{-# OPTIONS -Wno-orphans #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PackageImports #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE FlexibleContexts #-}
import Data.Word
import Data.Char ( chr, isSpace )
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LB
import qualified Data.ByteString.Builder as LB
import Data.List ( sort )
import Data.Semigroup
import Data.Foldable ( foldMap )
import qualified Data.HashMap.Strict as HM
import Control.Monad.IO.Class ( liftIO )
import System.Random.Shuffle
import "hspec" Test.Hspec
import "hspec" Test.Hspec.QuickCheck
import "QuickCheck" Test.QuickCheck
import "QuickCheck" Test.QuickCheck.Monadic
import "quickcheck-instances" Test.QuickCheck.Instances.ByteString ()
import "quickcheck-unicode" Test.QuickCheck.Unicode
import Data.String.Interpolate ( i, iii )
import Data.String.Interpolate.Conversion
main :: IO ()
main = hspec $ parallel $ do
describe "i" $ modifyMaxSuccess (const 10000) $ modifyMaxSize (const 500) $ do
context "when using String as a parameter" $ do
prop "just interpolating should be id" $
\(UTF8S str) -> [i|#{str}|] == str
prop "should passthrough a conversion to strict Text and back unchanged" $
\(UTF8S str) -> iID @String @T.Text str
prop "should passthrough a conversion to lazy Text and back unchanged" $
\(UTF8S str) -> iID @String @LT.Text str
prop "should passthrough a conversion to strict ByteString and back unchanged" $
\(UTF8S str) -> iID @String @B.ByteString str
prop "should passthrough a conversion to lazy ByteString and back unchanged" $
\(UTF8S str) -> iID @String @LB.ByteString str
context "when using strict Text as a parameter" $ do
prop "just interpolating should be id" $
\(t :: T.Text) -> [i|#{t}|] == t
prop "should passthrough a conversion to String and back unchanged" $ iID @T.Text @String
prop "should passthrough a conversion to lazy Text and back unchanged" $ iID @T.Text @LT.Text
prop "should passthrough a conversion to strict ByteString and back unchanged" $ iID @T.Text @B.ByteString
prop "should passthrough a conversion to lazy ByteString and back unchanged" $ iID @T.Text @LB.ByteString
context "when using lazy Text as a parameter" $ do
prop "just interpolating should be id" $
\(lt :: LT.Text) -> [i|#{lt}|] == lt
prop "should passthrough a conversion to String and back unchanged" $ iID @LT.Text @String
prop "should passthrough a conversion to strict Text and back unchanged" $ iID @LT.Text @T.Text
prop "should passthrough a conversion to strict ByteString and back unchanged" $ iID @LT.Text @B.ByteString
prop "should passthrough a conversion to lazy ByteString and back unchanged" $ iID @LT.Text @LB.ByteString
context "when using strict ByteString as a parameter" $ do
prop "just interpolating should be id" $
\(b :: B.ByteString) -> [i|#{b}|] == b
prop "should passthrough a conversion to lazy ByteString and back unchanged" $ iID @B.ByteString @LB.ByteString
context "and the ByteString is valid UTF8" $ do
prop "should passthrough a conversion to String and back unchanged" $ do
\(UTF8BS b) -> iID @B.ByteString @String b
prop "should passthrough a conversion to strict Text and back unchanged" $ do
\(UTF8BS b) -> iID @B.ByteString @T.Text b
prop "should passthrough a conversion to lazy Text and back unchanged" $ do
\(UTF8BS b) -> iID @B.ByteString @LT.Text b
context "when using lazy ByteString as a parameter" $ do
prop "just interpolating should be id" $
\(lb :: LB.ByteString) -> [i|#{lb}|] == lb
prop "should passthrough a conversion to strict ByteString and back unchanged" $ iID @LB.ByteString @B.ByteString
context "and the ByteString is valid UTF8" $ do
prop "should passthrough a conversion to String and back unchanged" $
\(UTF8LBS lb) -> iID @LB.ByteString @String lb
prop "should passthrough a conversion to strict Text and back unchanged" $
\(UTF8LBS lb) -> iID @LB.ByteString @T.Text lb
prop "should passthrough a conversion to lazy Text and back unchanged" $
\(UTF8LBS lb) -> iID @LB.ByteString @LT.Text lb
context "when using Char as a parameter" $ do
prop "interpolating into a String shouldn't have quotes" $
\(UTF8C c) -> [i|#{c}|] == [c]
prop "interpolating into strict Text shouldn't have quotes" $
\(UTF8C c) -> [i|#{c}|] == T.singleton c
prop "interpolating into lazy Text shouldn't have quotes" $
\(UTF8C c) -> [i|#{c}|] == LT.singleton c
prop "interpolating into strict ByteString shouldn't have quotes" $
\(UTF8C c) -> [i|#{c}|] == (LB.toStrict $ LB.toLazyByteString $ LB.charUtf8 c)
prop "interpolating into lazy ByteString shouldn't have quotes" $
\(UTF8C c) -> [i|#{c}|] == (LB.toLazyByteString $ LB.charUtf8 c)
context "when interpolating into strict ByteString" $ do
it "should handle literal Unicode strings correctly" $ do
let interpolated :: B.ByteString = [i|λ|]
expected :: B.ByteString = "\xCE\xBB"
interpolated `shouldBe` expected
context "when interpolating into lazy ByteString" $ do
it "should handle literal Unicode strings correctly" $ do
let interpolated :: LB.ByteString = [i|λ|]
expected :: LB.ByteString = "\xCE\xBB"
interpolated `shouldBe` expected
-- describe "__i" $ modifyMaxSuccess (const 10000) $ modifyMaxSize (const 500) $ do
-- context "when there are no newlines" $ do
-- prop "is the same as i" $
-- \(NonwhitespaceText t) ->
-- let iResult :: T.Text = [i|#{t}|]
-- __iResult :: T.Text = [__i|#{t}|]
-- in iResult == __iResult
-- context "when there are newlines" $ do
-- it "handles a small code snippet correctly" $ do
-- let interpolated :: T.Text =
-- [__i|
-- id :: a -> a
-- id x = y
-- where y = x
-- |]
-- expected :: T.Text = "id :: a -> a\nid x = y\n where y = x"
-- interpolated `shouldBe` expected
-- prop "produces the same output for different indentation levels" $
-- \(lines :: [(Word8, T.Text)]) (indent :: Word8) ->
-- let unindented = flip fmap (unshift lines) $ \(level, line) ->
-- leftPad (fromIntegral level) ' ' line
-- indented = (leftPad (fromIntegral indent) ' ') <$> unindented
-- unindentedResult :: T.Text = [__i|#{T.unlines unindented}|]
-- indentedResult :: T.Text = [__i|#{T.unlines indented}|]
-- in unindentedResult == indentedResult
-- context "is idempotent" $ do
-- prop "into String" $ __iIdempotent @String
-- prop "into strict Text" $ __iIdempotent @T.Text
-- prop "into lazy Text" $ __iIdempotent @LT.Text
-- prop "into strict ByteString" $ __iIdempotent @B.ByteString
-- prop "into lazy ByteString" $ __iIdempotent @LB.ByteString
-- context "is idempotently its own inverse" $ do
-- context "from String" $ do
-- prop "into strict Text" $ __iIdempotentInverse @String @T.Text
-- prop "into lazy Text" $ __iIdempotentInverse @String @LT.Text
-- prop "into strict ByteString" $ __iIdempotentInverse @String @B.ByteString
-- prop "into lazy ByteString" $ __iIdempotentInverse @String @LB.ByteString
-- context "from strict Text" $ do
-- prop "into String" $ __iIdempotentInverse @T.Text @String
-- prop "into lazy Text" $ __iIdempotentInverse @T.Text @LT.Text
-- prop "into strict ByteString" $ __iIdempotentInverse @T.Text @B.ByteString
-- prop "into lazy ByteString" $ __iIdempotentInverse @T.Text @LB.ByteString
-- context "from lazy Text" $ do
-- prop "into String" $ __iIdempotentInverse @LT.Text @String
-- prop "into strict Text" $ __iIdempotentInverse @LT.Text @T.Text
-- prop "into strict ByteString" $ __iIdempotentInverse @LT.Text @B.ByteString
-- prop "into lazy ByteString" $ __iIdempotentInverse @LT.Text @LB.ByteString
-- context "from strict ByteString" $ do
-- prop "into String" $ __iIdempotentInverse @B.ByteString @String
-- prop "into strict Text" $ __iIdempotentInverse @B.ByteString @T.Text
-- prop "into lazy Text" $ __iIdempotentInverse @B.ByteString @LT.Text
-- prop "into lazy ByteString" $ __iIdempotentInverse @B.ByteString @LB.ByteString
-- context "from lazy ByteString" $ do
-- prop "into String" $ __iIdempotentInverse @LB.ByteString @String
-- prop "into strict Text" $ __iIdempotentInverse @LB.ByteString @T.Text
-- prop "into lazy Text" $ __iIdempotentInverse @LB.ByteString @LT.Text
-- prop "into strict ByteString" $ __iIdempotentInverse @LB.ByteString @B.ByteString
-- -- I'm not sure whether these laws actually hold, because of tabs. Will
-- -- have to look at this more closely.
-- prop "is commutative with reversing lines" $
-- \(SpaceyText t) ->
-- [__i|#{T.unlines (reverse (T.lines t))}|] == T.unlines (reverse (T.lines [__i|#{t}|]))
-- prop "is commutative with sorting lines" $
-- \(SpaceyText t) ->
-- [__i|#{T.unlines (sort (T.lines t))}|] == T.unlines (sort (T.lines [__i|#{t}|]))
-- prop "removes same indentation when lines rearranged" $
-- \(SpaceyText t) ->
-- monadicIO $ do
-- shuffled <- T.unlines <$> liftIO (shuffleM $ T.lines t)
-- assert $ sort (T.lines [__i|#{shuffled}|]) == sort (T.lines [__i|#{t}|])
-- prop "non-whitespace chars in output same as in input" $
-- \(SpaceyText t) -> charFrequencies [__i|#{t}|] == charFrequencies t
-- prop "output string length <= input string length" $
-- \(SpaceyText t) -> T.length [__i|#{t}|] <= T.length t
-- prop "output words = input words" $
-- \(SpaceyText t) -> T.words t == T.words [__i|#{t}|]
describe "iii" $ modifyMaxSuccess (const 10000) $ modifyMaxSize (const 500) $ do
context "when there isn't any whitespace" $ do
prop "is the same as i" $
\(NonwhitespaceText t) ->
let iResult :: T.Text = [i|#{t}|]
iiiResult :: T.Text = [iii|#{t}|]
in iResult == iiiResult
context "when there is whitespace" $ do
it "collapses a small example of whitespace" $ do
let interpolated :: T.Text = [iii| foo bar baz |]
expected :: T.Text = "foo bar baz"
interpolated `shouldBe` expected
it "collapses a small example of newlines" $ do
let interpolated :: T.Text =
[iii|
Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
Aenean congue iaculis dui,
at iaculis sapien interdum nec.
|]
expected :: T.Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean congue iaculis dui, at iaculis sapien interdum nec."
interpolated `shouldBe` expected
prop "never has any newlines" $
\(SpaceyText t) -> T.all (/= '\n') [iii|#{t}|]
prop "never has more than one consecutive space" $
\(SpaceyText t) ->
let chunks = T.groupBy (\c1 c2 -> isSpace c1 == isSpace c2) [iii|#{t}|]
in all (\chunk -> T.all (not . isSpace) chunk || T.length chunk <= 1) chunks
prop "never has leading whitespace" $
\(SpaceyText t) -> T.null $ T.takeWhile isSpace [iii|#{t}|]
prop "never has trailing whitespace" $
\(SpaceyText t) -> T.null $ T.takeWhileEnd isSpace [iii|#{t}|]
context "is idempotent" $ do
prop "into String" $ iiiIdempotent @String
prop "into strict Text" $ iiiIdempotent @T.Text
prop "into lazy Text" $ iiiIdempotent @LT.Text
prop "into strict ByteString" $ iiiIdempotent @B.ByteString
prop "into lazy ByteString" $ iiiIdempotent @LB.ByteString
context "is idempotently its own inverse" $ do
context "from String" $ do
prop "into strict Text" $ iiiIdempotentInverse @String @T.Text
prop "into lazy Text" $ iiiIdempotentInverse @String @LT.Text
prop "into strict ByteString" $ iiiIdempotentInverse @String @B.ByteString
prop "into lazy ByteString" $ iiiIdempotentInverse @String @LB.ByteString
context "from strict Text" $ do
prop "into String" $ iiiIdempotentInverse @T.Text @String
prop "into lazy Text" $ iiiIdempotentInverse @T.Text @LT.Text
prop "into strict ByteString" $ iiiIdempotentInverse @T.Text @B.ByteString
prop "into lazy ByteString" $ iiiIdempotentInverse @T.Text @LB.ByteString
context "from lazy Text" $ do
prop "into String" $ iiiIdempotentInverse @LT.Text @String
prop "into strict Text" $ iiiIdempotentInverse @LT.Text @T.Text
prop "into strict ByteString" $ iiiIdempotentInverse @LT.Text @B.ByteString
prop "into lazy ByteString" $ iiiIdempotentInverse @LT.Text @LB.ByteString
context "from strict ByteString" $ do
prop "into String" $ iiiIdempotentInverse @B.ByteString @String
prop "into strict Text" $ iiiIdempotentInverse @B.ByteString @T.Text
prop "into lazy Text" $ iiiIdempotentInverse @B.ByteString @LT.Text
prop "into lazy ByteString" $ iiiIdempotentInverse @B.ByteString @LB.ByteString
context "from lazy ByteString" $ do
prop "into String" $ iiiIdempotentInverse @LB.ByteString @String
prop "into strict Text" $ iiiIdempotentInverse @LB.ByteString @T.Text
prop "into lazy Text" $ iiiIdempotentInverse @LB.ByteString @LT.Text
prop "into strict ByteString" $ iiiIdempotentInverse @LB.ByteString @B.ByteString
prop "is commutative with string reversal" $
\(SpaceyText t) -> [iii|#{T.reverse t}|] == T.reverse [iii|#{t}|]
prop "non-whitespace chars in output same as in input" $
\(SpaceyText t) -> charFrequencies [iii|#{t}|] == charFrequencies t
prop "output string length <= input string length" $
\(SpaceyText t) -> T.length [iii|#{t}|] <= T.length t
prop "output words = input words" $
\(SpaceyText t) -> T.words t == T.words [iii|#{t}|]
iID :: forall from to fromflag toflag.
( Eq from
, Interpolatable fromflag to from
, Interpolatable toflag from to
)
=> from
-> Bool
iID from =
let to :: to = [i|#{from}|]
from' :: from = [i|#{to}|]
in from == from'
-- __iIdempotent :: forall to toflag.
-- ( Eq to
-- , Interpolatable toflag to to
-- , Interpolatable toflag T.Text to
-- )
-- => SpaceyText
-- -> Bool
-- __iIdempotent (SpaceyText t) =
-- let x :: to = [__i|#{t}|]
-- x' :: to = [__i|#{x}|]
-- in x == x'
iiiIdempotent :: forall to toflag.
( Eq to
, Interpolatable toflag to to
, Interpolatable toflag T.Text to
, SpaceChompable to
)
=> SpaceyText
-> Bool
iiiIdempotent (SpaceyText t) =
let x :: to = [iii|#{t}|]
x' :: to = [iii|#{x}|]
in x == x'
-- __iIdempotentInverse :: forall from to fromflag toflag.
-- ( Eq from
-- , Interpolatable fromflag T.Text from
-- , Interpolatable toflag from to
-- , Interpolatable fromflag to from
-- )
-- => SpaceyText
-- -> Bool
-- __iIdempotentInverse (SpaceyText t) =
-- let x :: from = [__i|#{t}|]
-- x' :: to = [__i|#{x}|]
-- x'' :: from = [__i|#{x'}|]
-- in x == x''
iiiIdempotentInverse :: forall from to fromflag toflag.
( Eq from
, Interpolatable fromflag T.Text from
, Interpolatable toflag from to
, Interpolatable fromflag to from
, SpaceChompable from
, SpaceChompable to
)
=> SpaceyText
-> Bool
iiiIdempotentInverse (SpaceyText t) =
let x :: from = [iii|#{t}|]
x' :: to = [iii|#{x}|]
x'' :: from = [iii|#{x'}|]
in x == x''
-- -- |
-- -- Reduce each index by the minimum index in the array.
-- unshift :: (Ord a, Num a) => [(a, b)] -> [(a, b)]
-- unshift [] = []
-- unshift l@((x, _) : xs) =
-- let min = getMin $ foldr (\(x, _) m -> Min x <> m) (Min x) xs
-- in (\(x, y) -> (x - min, y)) <$> l
-- -- |
-- -- Add the given number of the specific characters to the left.
-- leftPad :: Int -> Char -> T.Text -> T.Text
-- leftPad amt c t = T.replicate amt (T.singleton c) <> t
-- |
-- The default Arbitrary for Char generates U+FFFF and U+FFFE, which aren't
-- valid Unicode. Sigh...
newtype UTF8Char = UTF8C { unUTF8C :: Char }
deriving newtype (Eq, Show)
newtype UTF8String = UTF8S { unUTF8S :: String }
deriving newtype (Eq, Show)
newtype UTF8ByteString = UTF8BS B.ByteString
deriving newtype (Eq, Show)
newtype UTF8LazyByteString = UTF8LBS LB.ByteString
deriving newtype (Eq, Show)
newtype SpaceyText = SpaceyText T.Text
deriving newtype (Eq, Show)
newtype NonwhitespaceText = NonwhitespaceText T.Text
deriving newtype (Eq, Show)
instance Arbitrary UTF8Char where
arbitrary = UTF8C <$> unicodeChar
shrink (UTF8C c) = UTF8C <$> shrinkChar c
instance Arbitrary UTF8String where
arbitrary = do
chars <- listOf arbitrary
pure $ UTF8S (unUTF8C <$> chars)
shrink (UTF8S str) = case str of
[] -> []
(_:[]) -> []
_ -> let mid = length str `div` 2
in [UTF8S $ take mid str, UTF8S $ drop mid str]
instance Arbitrary T.Text where
arbitrary = T.pack . unUTF8S <$> arbitrary
shrink t = if T.null t || T.length t == 1
then []
else let mid = T.length t `div` 2
in [T.take mid t, T.drop mid t]
instance Arbitrary LT.Text where
arbitrary = LT.pack . unUTF8S <$> arbitrary
shrink lt = if LT.null lt || LT.length lt == 1
then []
else let mid = LT.length lt `div` 2
in [LT.take mid lt, LT.drop mid lt]
instance Arbitrary UTF8ByteString where
arbitrary = UTF8BS . LB.toStrict . LB.toLazyByteString . foldMap LB.charUtf8 . unUTF8S
<$> arbitrary
instance Arbitrary UTF8LazyByteString where
arbitrary = UTF8LBS . LB.toLazyByteString . foldMap LB.charUtf8 . unUTF8S
<$> arbitrary
-- Basically, we want this to be an 'alternation' of sequences of printable
-- characters and whitespace characters.
instance Arbitrary SpaceyText where
arbitrary = SpaceyText . foldMap id
<$> scale
(round . sqrt @Double . fromIntegral)
(listOf (oneof [whitespace, nonwhitespace]))
instance Arbitrary NonwhitespaceText where
arbitrary = NonwhitespaceText <$> nonwhitespace
charFrequencies :: T.Text -> HM.HashMap Char Int
charFrequencies = T.foldl' (flip $ HM.alter increment) HM.empty . T.filter (not . isSpace)
where increment :: Maybe Int -> Maybe Int
increment Nothing = Just 1
increment (Just x) = Just (x + 1)
whitespace :: Gen T.Text
whitespace = T.pack
<$> listOf1 (elements [' ', '\r', '\t', '\n', '\x1680', '\x2000', '\x2006'])
nonwhitespace :: Gen T.Text
nonwhitespace = T.pack
<$> listOf1 nonwhitespaceChar
nonwhitespaceChar :: Gen Char
nonwhitespaceChar = unicodeChar `suchThat` (not . isSpace)
unicodeChar :: Gen Char
unicodeChar = chr `fmap` points
where points = flip suchThat (not . reserved) $ oneof
[ ascii
, plane0
, plane1
, plane2
, plane14
]