packages feed

fmt-0.6: tests/Main.hs

{-# 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) ==#> "<Nothing>"
    maybeF (Just 3 :: Maybe Int) ==#> "3"
  it "eitherF" $ do
    eitherF (Left 1 :: Either Int Int) ==#> "<Left: 1>"
    eitherF (Right 1 :: Either Int Int) ==#> "<Right: 1>"

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) ==#> "<Just: 25>"
  it "Either" $ do
    genericF (Left 25 :: Either Int Bool) ==#> "<Left: 25>"
    genericF (Right True :: Either Int Bool) ==#> "<Right: True>"
  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) ==#> "<Foo: 25>"
      genericF (Bar (n-1) (n+1)) ==#> "<Bar: 24, 26>"
    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) ==#> "<Foo: <function>>"
      it "lists" $ do
        genericF (Foo [n, n+1]) ==#> "<Foo: [25, 26]>"
      it "maps" $ do
        genericF (Foo (M.singleton n s)) ==#> "<Foo: {25: !}>"
      it "tuples" $ do
        genericF (Foo (n, s)) ==#> "<Foo: (25, !)>"
        genericF (Foo (n, s, n+1)) ==#> "<Foo: (25, !, 26)>"
      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)) ==#> "<Foo: (25, !)>"
      it "Either" $ do
        genericF (Foo (Left 25 :: Either Int Int)) ==#> "<Foo: <Left: 25>>"
      it "Maybe with a non-buildable" $ do
        genericF (Foo (Just [n])) ==#> "<Foo: [25]>"
        genericF (Foo (Nothing :: Maybe ())) ==#> "<Foo: <Nothing>>"

----------------------------------------------------------------------------
-- Utilities
----------------------------------------------------------------------------

(==%>) :: HasCallStack => Text -> Text -> Expectation
(==%>) = shouldBe

(==#>) :: HasCallStack => Builder -> Text -> Expectation
(==#>) a b = a `shouldBe` build (T.replace "_" " " b)