{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE QuasiQuotes #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE FlexibleContexts #-} module Main where -- Monoid import Data.Monoid ((<>)) -- Text import Data.Text (Text) import qualified Data.Text as T import qualified Data.Text.Lazy as TL import NeatInterpolation -- Various data structures import qualified Data.Vector as V import Data.Vector (Vector) import qualified Data.Map as M import Data.Map (Map) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as BSL -- Generics import GHC.Generics -- Call stacks import Data.CallStack -- Tests import Test.Hspec -- Fmt import Fmt ---------------------------------------------------------------------------- -- Constants for testing (to avoid “ambiguous type” errors) ---------------------------------------------------------------------------- n :: Integer n = 25 s :: String s = "!" ---------------------------------------------------------------------------- -- List of tests ---------------------------------------------------------------------------- main :: IO () main = hspec $ do test_operators test_outputTypes describe "formatters" $ do test_indent test_baseConversion test_floatingPoint test_hex test_ADTs test_conditionals test_padding test_lists test_maps test_tuples test_generic ---------------------------------------------------------------------------- -- Testing that operators work (when used as intended) ---------------------------------------------------------------------------- test_operators :: Spec test_operators = do it "simple examples" $ do ("a"+|n|+"b") ==%> "a25b" ("a"+|n|+"b"+|s|+"") ==%> "a25b!" (""+|n|++|s|+"") ==%> "25!" (""+|negate n|++|s|+"") ==%> "-25!" (""+|Just n|++|s|+"") ==%> "25!" describe "examples with Show/mixed" $ do it "copy of Buildable examples" $ do ("a"+||n||+"b") ==%> "a25b" ("a"+||n||++||n||+"b") ==%> "a2525b" -- These are mixed, i.e. both Buildable and Show versions are used ("a"+||n||+"b"+|s|+"") ==%> "a25b!" (""+||n||++|s|+"") ==%> "25!" (""+||negate n||++|s|+"") ==%> "-25!" it "examples that don't work with Buildable" $ do (""+||Just n||+"") ==%> "Just 25" (""+||(n,n)||+"") ==%> "(25,25)" it "plays nice with other operators" $ do -- If precedence is bad these won't compile (""+|n-1|++|n+1|+"") ==%> "2426" (id $ ""+|n-1|++|n+1|+"") ==%> "2426" it "works with <>" $ do ("number: "+|n|+"\n"<> "string: "+|s|+"") ==%> "number: 25\nstring: !" ---------------------------------------------------------------------------- -- Testing that different output types work ---------------------------------------------------------------------------- test_outputTypes :: Spec test_outputTypes = describe "output as" $ do it "String" $ ("a"+|n|+"b" :: String) `shouldBe` "a25b" it "Text" $ ("a"+|n|+"b" :: Text) `shouldBe` "a25b" it "Lazy Text" $ ("a"+|n|+"b" :: TL.Text) `shouldBe` "a25b" it "Builder" $ ("a"+|n|+"b" :: Builder) `shouldBe` "a25b" ---------------------------------------------------------------------------- -- Tests for various simple formatters ---------------------------------------------------------------------------- test_indent :: Spec test_indent = describe "'indentF'" $ do it "simple examples" $ do indentF 0 "hi" ==#> "hi\n" indentF 0 "\nhi\n\n" ==#> "\nhi\n\n" indentF 2 "hi" ==#> " hi\n" indentF 2 "hi\n" ==#> " hi\n" indentF 2 "" ==#> " \n" indentF 2 "hi\nbye" ==#> " hi\n bye\n" indentF 2 "hi\nbye\n" ==#> " hi\n bye\n" it "formatting a block" $ do ("Some numbers:\n"<> indentF 2 ( "odd: "+|n|+"\n"<> "even: "+|n+1|+"")) ==#> "Some numbers:\n odd: 25\n even: 26\n" test_baseConversion :: Spec test_baseConversion = describe "conversion to bases" $ do it "octF" $ do octF n ==#> "31" it "binF" $ do binF n ==#> "11001" it "baseF" $ do baseF 36 (n^n) ==#> "54kbbzw21jhueg5jb0ggr4p" it "-baseF" $ do baseF 36 (-(n^n)) ==#> "-54kbbzw21jhueg5jb0ggr4p" test_floatingPoint :: Spec test_floatingPoint = describe "floating-point" $ do let f1_3 = 1.2999999999999998 :: Double it "floatF" $ do floatF f1_3 ==#> "1.2999999999999998" it "exptF" $ do exptF 2 f1_3 ==#> "1.30e0" it "fixedF" $ do fixedF 2 f1_3 ==#> "1.30" test_hex :: Spec test_hex = describe "'hexF'" $ do it "Int" $ do hexF n ==#> "19" it "-Int" $ do hexF (-n) ==#> "-19" it "strict ByteString" $ do hexF (BS.pack [15,250]) ==#> "0ffa" it "lazy ByteString" $ do hexF (BSL.pack [15,250]) ==#> "0ffa" test_ADTs :: Spec test_ADTs = describe "ADTs" $ do it "maybeF" $ do maybeF (Nothing :: Maybe Int) ==#> "" maybeF (Just 3 :: Maybe Int) ==#> "3" it "eitherF" $ do eitherF (Left 1 :: Either Int Int) ==#> "" eitherF (Right 1 :: Either Int Int) ==#> "" test_conditionals :: Spec test_conditionals = describe "conditionals" $ do it "whenF" $ do whenF True "hi" ==#> "hi" whenF False "hi" ==#> "" it "unlessF" $ do unlessF True "hi" ==#> "" unlessF False "hi" ==#> "hi" ---------------------------------------------------------------------------- -- Tests for padding ---------------------------------------------------------------------------- test_padding :: Spec test_padding = describe "padding" $ do it "prefixF" $ do prefixF (-1) ("hello" :: Text) ==#> "" prefixF 0 ("hello" :: Text) ==#> "" prefixF 1 ("hello" :: Text) ==#> "h" prefixF 2 ("hello" :: Text) ==#> "he" prefixF 3 ("hello" :: Text) ==#> "hel" prefixF 5 ("hello" :: Text) ==#> "hello" prefixF 1000 ("hello" :: Text) ==#> "hello" prefixF 1000 ("" :: Text) ==#> "" it "suffixF" $ do suffixF (-1) ("hello" :: Text) ==#> "" suffixF 0 ("hello" :: Text) ==#> "" suffixF 1 ("hello" :: Text) ==#> "o" suffixF 2 ("hello" :: Text) ==#> "lo" suffixF 3 ("hello" :: Text) ==#> "llo" suffixF 5 ("hello" :: Text) ==#> "hello" suffixF 1000 ("hello" :: Text) ==#> "hello" suffixF 1000 ("" :: Text) ==#> "" it "padLeftF" $ do padLeftF (-1) '!' ("hello" :: Text) ==#> "hello" padLeftF 0 '!' ("hello" :: Text) ==#> "hello" padLeftF 1 '!' ("hello" :: Text) ==#> "hello" padLeftF 5 '!' ("hello" :: Text) ==#> "hello" padLeftF 6 '!' ("hello" :: Text) ==#> "!hello" padLeftF 7 '!' ("hello" :: Text) ==#> "!!hello" padLeftF 7 '!' ("" :: Text) ==#> "!!!!!!!" it "padRightF" $ do padRightF (-1) '!' ("hello" :: Text) ==#> "hello" padRightF 0 '!' ("hello" :: Text) ==#> "hello" padRightF 1 '!' ("hello" :: Text) ==#> "hello" padRightF 5 '!' ("hello" :: Text) ==#> "hello" padRightF 6 '!' ("hello" :: Text) ==#> "hello!" padRightF 7 '!' ("hello" :: Text) ==#> "hello!!" padRightF 7 '!' ("" :: Text) ==#> "!!!!!!!" it "padBothF" $ do padBothF (-1) '!' ("hello" :: Text) ==#> "hello" padBothF 0 '!' ("hello" :: Text) ==#> "hello" padBothF 1 '!' ("hello" :: Text) ==#> "hello" padBothF 5 '!' ("hello" :: Text) ==#> "hello" padBothF 6 '!' ("hello" :: Text) ==#> "!hello" padBothF 7 '!' ("hello" :: Text) ==#> "!hello!" padBothF 7 '!' ("hell" :: Text) ==#> "!!hell!" padBothF 7 '!' ("hel" :: Text) ==#> "!!hel!!" padBothF 8 '!' ("hell" :: Text) ==#> "!!hell!!" padBothF 8 '!' ("hel" :: Text) ==#> "!!!hel!!" padBothF 8 '!' ("" :: Text) ==#> "!!!!!!!!" ---------------------------------------------------------------------------- -- Tests for lists ---------------------------------------------------------------------------- test_lists :: Spec test_lists = describe "lists" $ do test_listF test_blockListF test_jsonListF test_listF :: Spec test_listF = describe "'listF'" $ do it "simple examples" $ do listF ([] :: [Int]) ==#> "[]" listF [n] ==#> "[25]" listF [n,n+1] ==#> "[25, 26]" listF [s,s<>s,"",s<>s<>s] ==#> "[!, !!, , !!!]" it "different Foldables" $ do listF ([1,2,3] :: [Int]) ==#> "[1, 2, 3]" listF (V.fromList [1,2,3] :: Vector Int) ==#> "[1, 2, 3]" listF (M.fromList [(1,2),(3,4),(5,6)] :: Map Int Int) ==#> "[2, 4, 6]" test_blockListF :: Spec test_blockListF = describe "'blockListF'" $ do emptyList nullElements singleLineElements multiLineElements mixed where emptyList = it "empty list" $ do blockListF ([] :: [Int]) ==#> "[]\n" nullElements = it "null elements" $ do blockListF ([""] :: [Text]) ==#> [text| - |] blockListF (["",""] :: [Text]) ==#> [text| - - |] blockListF (["","a",""] :: [Text]) ==#> [text| - - a - |] singleLineElements = it "single-line elements" $ do blockListF (["a"] :: [Text]) ==#> [text| - a |] blockListF (["a","b"] :: [Text]) ==#> [text| -_a -_b |] blockListF (["a","b","ccc"] :: [Text]) ==#> [text| - a - b - ccc |] multiLineElements = it "multi-line elements" $ do blockListF (["a\nx"] :: [Text]) ==#> [text| - a x |] blockListF (["a\n x"] :: [Text]) ==#> [text| - a x |] blockListF (["a\n x"," b\nxx\ny y ","c\n\n"] :: [Text]) ==#> [text| - a x - b xx y y_ - c __ |] mixed = it "mix of single-line and multi-line" $ do blockListF (["a\nx","b"] :: [Text]) ==#> [text| - a x - b |] blockListF (["a\nx","b\n"] :: [Text]) ==#> [text| - a x - b |] blockListF (["a"," b\nxx\ny y ","c\n\n"] :: [Text]) ==#> [text| - a - b xx y y_ - c __ |] test_jsonListF :: Spec test_jsonListF = describe "'jsonListF'" $ do emptyList nullElements singleLineElements multiLineElements mixed where emptyList = it "empty list" $ do jsonListF ([] :: [Int]) ==#> "[]\n" nullElements = it "null elements" $ do jsonListF ([""] :: [Text]) ==#> [text| [ ] |] jsonListF (["",""] :: [Text]) ==#> [text| [ , ] |] jsonListF (["","a",""] :: [Text]) ==#> [text| [ , a , ] |] singleLineElements = it "single-line elements" $ do jsonListF (["a"] :: [Text]) ==#> [text| [ a ] |] jsonListF (["a","b"] :: [Text]) ==#> [text| [ a , b ] |] jsonListF (["a","b","ccc"] :: [Text]) ==#> [text| [ a , b , ccc ] |] multiLineElements = it "multi-line elements" $ do jsonListF (["a\nx"] :: [Text]) ==#> [text| [ a x ] |] jsonListF (["a\n x"] :: [Text]) ==#> [text| [ a x ] |] jsonListF (["a\n x"," b\nxx\ny y ","c\n\n"] :: [Text]) ==#> [text| [ a x , b xx y y_ , c __ ] |] mixed = it "mix of single-line and multi-line" $ do jsonListF (["a\nx","b"] :: [Text]) ==#> [text| [ a x , b ] |] jsonListF (["a\nx","b\n"] :: [Text]) ==#> [text| [ a x , b ] |] jsonListF (["a"," b\nxx\ny y ","c\n\n"] :: [Text]) ==#> [text| [ a , b xx y y_ , c __ ] |] ---------------------------------------------------------------------------- -- Tests for maps ---------------------------------------------------------------------------- test_maps :: Spec test_maps = describe "maps" $ do test_mapF test_blockMapF test_jsonMapF test_mapF :: Spec test_mapF = describe "'mapF'" $ do it "simple examples" $ do mapF ([] :: [(Int, Int)]) ==#> "{}" mapF [(n,n+1)] ==#> "{25: 26}" mapF [(s,n)] ==#> "{!: 25}" mapF [('a',True),('b',False),('c',True)] ==#> "{a: True, b: False, c: True}" it "different map types" $ do let m = [('a',True),('b',False),('d',False),('c',True)] mapF m ==#> "{a: True, b: False, d: False, c: True}" mapF (M.fromList m) ==#> "{a: True, b: False, c: True, d: False}" test_blockMapF :: Spec test_blockMapF = describe "'blockMapF'" $ do it "empty map" $ do blockMapF ([] :: [(Int, Int)]) ==#> "{}\n" it "complex example" $ do blockMapF ([("hi", ""), ("foo"," a\n b"), ("bar","a"), ("baz","a\ng")] :: [(Text, Text)]) ==#> [text| hi: foo: a b bar: a baz: a g |] test_jsonMapF :: Spec test_jsonMapF = describe "'jsonMapF'" $ do it "empty map" $ do jsonMapF ([] :: [(Int, Int)]) ==#> "{}\n" it "complex example" $ do jsonMapF ([("hi", ""), ("foo"," a\n b"), ("bar","a"), ("baz","a\ng")] :: [(Text, Text)]) ==#> [text| { hi: , foo: a b , bar: a , baz: a g } |] ---------------------------------------------------------------------------- -- Tests for tuples ---------------------------------------------------------------------------- test_tuples :: Spec test_tuples = describe "tuples" $ do test_tupleSimple describe "tupleF on lists" $ do test_tupleOneLine test_tupleMultiline test_tupleSimple :: Spec test_tupleSimple = it "tupleF" $ do -- we don't need complicated tests here, they're all tested in -- "tupleF on lists" tests tupleF (n, s) ==#> "(25, !)" tupleF (n, s, n, s) ==#> "(25, !, 25, !)" tupleF (n, s, n, s, 'a', 'b', 'c', 'd') ==#> "(25, !, 25, !, a, b, c, d)" test_tupleOneLine :: Spec test_tupleOneLine = describe "one-line" $ do it "()" $ do tupleF ([] :: [Builder]) ==#> "()" it "('')" $ do tupleF ([""] :: [Builder]) ==#> "()" it "(a)" $ do tupleF (["a"] :: [Builder]) ==#> "(a)" it "(a,b)" $ do tupleF (["a", "b"] :: [Builder]) ==#> "(a, b)" it "(a,'')" $ do tupleF (["a", ""] :: [Builder]) ==#> "(a, )" it "('',b)" $ do tupleF (["", "b"] :: [Builder]) ==#> "(, b)" it "('','')" $ do tupleF (["", ""] :: [Builder]) ==#> "(, )" it "(a,b,c)" $ do tupleF (["a", "ba", "caba"] :: [Builder]) ==#> "(a, ba, caba)" test_tupleMultiline :: Spec test_tupleMultiline = describe "multiline" $ do allNonEmpty someEmpty it "weird case" $ do -- not sure whether I should fix it or not tupleF ["a\n" :: Builder] ==#> "(a\n)" where allNonEmpty = describe "all non-empty" $ do it "1 element (2 lines)" $ do tupleF ["a\nx" :: Builder] ==#> [text| ( a x ) |] tupleF ["a\n x" :: Builder] ==#> [text| ( a x ) |] tupleF [" a\nx\n" :: Builder] ==#> [text| ( a x ) |] it "1 element (3 lines)" $ do tupleF ["a\nb\nc" :: Builder] ==#> [text| ( a b c ) |] it "2 elements (1 line + 2 lines)" $ do tupleF ["a", "b\nc" :: Builder] ==#> [text| ( a , b c ) |] it "2 elements (2 lines + 1 line)" $ do tupleF ["a\nb", "c" :: Builder] ==#> [text| ( a b , c ) |] it "3 elements (each has 2 lines)" $ do tupleF ["a\nb", "c\nd", "e\nf" :: Builder] ==#> [text| ( a b , c d , e f ) |] someEmpty = describe "some empty" $ do it "2 elements (0 + 2)" $ do tupleF ["", "a\nb" :: Builder] ==#> [text| ( , a b ) |] it "2 elements (2 + 0)" $ do tupleF ["a\nb", "" :: Builder] ==#> [text| ( a b , ) |] it "3 elements (0 + 2 + 0)" $ do tupleF ["", "a\nb", "" :: Builder] ==#> [text| ( , a b , ) |] it "3 elements (2 + 0 + 2)" $ do tupleF ["a\nb", "", "c\nd" :: Builder] ==#> [text| ( a b , , c d ) |] it "4 elements (2 + 0 + 0 + 2)" $ do tupleF ["a\nb", "", "", "c\nd" :: Builder] ==#> [text| ( a b , , , c d ) |] ---------------------------------------------------------------------------- -- Tests for 'genericF' ---------------------------------------------------------------------------- data Foo a = Foo a deriving Generic data Bar a = Bar a a deriving Generic data Qux a = Qux {q1 :: Int, q2 :: a, q3 :: Text} deriving Generic data Op = (:|:) Int | (:||:) Int Int | (:|||:) Int Int Int | Int :-: Int | Int `O` Int deriving Generic test_generic :: Spec test_generic = describe "'genericF'" $ do it "Maybe" $ do genericF (Nothing :: Maybe Int) ==#> "Nothing" genericF (Just 25 :: Maybe Int) ==#> "" it "Either" $ do genericF (Left 25 :: Either Int Bool) ==#> "" genericF (Right True :: Either Int Bool) ==#> "" it "tuples" $ do genericF (n, s) ==#> "(25, !)" genericF (n, s, -n, s ++ s) ==#> "(25, !, -25, !!)" describe "custom types" $ do it "ordinary constructors" $ do genericF (Foo n) ==#> "" genericF (Bar (n-1) (n+1)) ==#> "" it "records" $ do genericF (Qux 1 True "hi") ==#> [text| Qux: q1: 1 q2: True q3: hi |] it "operators and infix constructors" $ do genericF ((:|:) 1) ==#> "<(:|:): 1>" genericF ((:||:) 1 2) ==#> "<(:||:): 1, 2>" genericF ((:|||:) 1 2 3) ==#> "<(:|||:): 1, 2, 3>" genericF ((:-:) 1 2) ==#> "(1 :-: 2)" genericF (O 1 2) ==#> "(1 `O` 2)" describe "types with non-Buildables inside" $ do it "functions" $ do genericF (Foo not) ==#> ">" it "lists" $ do genericF (Foo [n, n+1]) ==#> "" it "maps" $ do genericF (Foo (M.singleton n s)) ==#> "" it "tuples" $ do genericF (Foo (n, s)) ==#> "" genericF (Foo (n, s, n+1)) ==#> "" it "tuple with a string inside" $ do -- strings have to be handled differently from lists -- so we test this case separately genericF (Foo (n, s)) ==#> "" it "Either" $ do genericF (Foo (Left 25 :: Either Int Int)) ==#> ">" it "Maybe with a non-buildable" $ do genericF (Foo (Just [n])) ==#> "" genericF (Foo (Nothing :: Maybe ())) ==#> ">" ---------------------------------------------------------------------------- -- Utilities ---------------------------------------------------------------------------- (==%>) :: HasCallStack => Text -> Text -> Expectation (==%>) = shouldBe (==#>) :: HasCallStack => Builder -> Text -> Expectation (==#>) a b = a `shouldBe` build (T.replace "_" " " b)