packages feed

zifter-0.0.1.5: test/Zifter/ZiftSpec.hs

{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleContexts #-}

module Zifter.ZiftSpec
    ( spec
    ) where

import Test.Hspec
import Test.QuickCheck
import Test.Validity

import Data.GenValidity.Path ()
import Data.Maybe
import Data.Monoid

import Control.Concurrent.STM

import Path.IO

import Zifter
import Zifter.OptParse
import Zifter.Zift

import Zifter.Gen ()
import Zifter.OptParse.Gen ()
import Zifter.Zift.Gen ()

spec :: Spec
spec = do
    describe "ZiftOutput" $ eqSpec @ZiftOutput
    describe "ZiftResult" $ do
        eqSpec @(ZiftResult Int)
        genValiditySpec @(ZiftResult Double)
        functorSpec @ZiftResult
        applicativeSpec @ZiftResult
        monoidSpec @(ZiftResult String)
        monadSpec @ZiftResult
    describe "ziftRunner" $ do
        it "pure () outputs nothing" $
            pure () `outputShouldBe` [ZiftToken [] Nothing]
        it "pure () twice outputs two tokens" $
            let func = do
                    pure ()
                    pure ()
            in func `outputShouldBe`
               [ZiftToken [L] Nothing, ZiftToken [R] Nothing]
        it "printZift outputs one message" $
            printZift "hello" `outputShouldBe`
            [ ZiftToken
                  []
                  (Just ZiftOutput {outputColors = [], outputMessage = "hello"})
            ]
        it "printZift twice outputs two messages and two tokens" $
            let func = do
                    printZift "hello"
                    printZift "world"
            in func `outputShouldBe`
               [ ZiftToken
                     [L]
                     (Just
                          ZiftOutput
                          {outputColors = [], outputMessage = "hello"})
               , ZiftToken
                     [R]
                     (Just
                          ZiftOutput
                          {outputColors = [], outputMessage = "world"})
               ]
    describe "addState" $ do
        it "stores the first output on the left for [L]" $
            forAllUnchecked $ \mzo ->
                addState LinearUnknown (ZiftToken [L] mzo) `shouldBe`
                Just (LinearBranch (LinearLeaf mzo) LinearUnknown)
        it "stores the first output on the Right for [R]" $
            forAllUnchecked $ \mzo ->
                addState LinearUnknown (ZiftToken [R] mzo) `shouldBe`
                Just (LinearBranch LinearUnknown (LinearLeaf mzo))
    describe "flushState" $ do
        let l = LinearLeaf
            u = LinearUnknown
            d = LinearDone
            b = LinearBranch
            ln = l Nothing
            t bs es eb =
                let (as, ab) = flushState bs
                in do as `shouldBe` es
                      ab `shouldBe` eb
        it "flushes a simple branch at the top level" $
            forAllUnchecked $ \(hello, world) ->
                t
                    (b (l (Just hello)) (l (Just world)))
                    (b d d)
                    (BufReady [hello, world])
        it
            "flushes and prunes the left side of a branch if the right side is unknown" $
            forAllUnchecked $ \msg ->
                t (b (l (Just msg)) u) (b d u) (BufReady [msg])
        it
            "does not flush the right side of a branch if the left side is unknown" $
            forAllUnchecked $ \msg ->
                let s = b u (l (Just msg))
                in t s s BufNotReady
        it "flushes a branch with two leaves" $
            forAllUnchecked $ \(hello, world) ->
                t
                    (b (l (Just hello)) (l (Just world)))
                    (b d d)
                    (BufReady [hello, world])
        it
            "flushes the entire state when the left side is done and the right side is one level deep" $
            forAllUnchecked $ \(hello, world) ->
                t
                    (b ln (b (l (Just hello)) (l (Just world))))
                    (b d (b d d))
                    (BufReady [hello, world])
        it
            "flushes the entire state when the left side is done and the right side is two levels deep" $
            forAllUnchecked $ \(hello, big, beautiful, world) ->
                t
                    (b (l Nothing)
                         (b (b (l (Just hello)) (l (Just big)))
                              (b (l (Just beautiful)) (l (Just world)))))
                    (b d (b (b d d) (b d d)))
                    (BufReady [hello, big, beautiful, world])
        it
            "flushes the entire left half of a complete binary tree of size two if the entire left part is done" $
            forAllUnchecked $ \(hello, world) ->
                t
                    (b (b (l (Just hello)) (l (Just world))) (b u u))
                    (b (b d d) (b u u))
                    (BufReady [hello, world])
        it
            "flushes the correct part of the right half of the state when the left part is done and the right side isn't" $
            forAllUnchecked $ \(hello, world) ->
                t
                    (b (l (Just hello)) (b (l (Just world)) u))
                    (b d (b d u))
                    (BufReady [hello, world])
        it
            "flushes and the entire left half of a complete binary tree of size two if the entire left part is done" $
            forAllUnchecked $ \(hello, beautiful, world) ->
                t
                    (b (b (l (Just hello)) (l (Just beautiful)))
                         (b (l (Just world)) u))
                    (b (b d d) (b d u))
                    (BufReady [hello, beautiful, world])
        it "flushes the entire tree for any done tree" $
            forAll doneTree $ \st ->
                let (s', _) = flushState st
                in s' `shouldBe` makeForceFlushed st
        it "flushes the entire left tree for any tree whose left part is done" $
            forAllShrink doneTree (map makeForceFlushed . shrinkUnchecked) $ \dt ->
                forAllUnchecked $ \ut ->
                    let s = b dt ut
                        (rs', b2) = flushState ut
                    in t s
                           (b (makeForceFlushed dt) rs')
                           (flushStateAll dt <> b2)
        it "can only grow the depth of the state" $
            forAll
                (genUnchecked `suchThat`
                 (\(st, token) -> isJust $ processToken st token)) $ \(st, token) ->
                case processToken st token of
                    Nothing -> pure () -- fine
                    Just (t', _) -> depth t' `shouldSatisfy` (>= depth st)

depth :: LinearState -> Int
depth LinearUnknown = 1
depth LinearDone = 1
depth (LinearLeaf _) = 1
depth (LinearBranch t1 t2) = max (depth t1) (depth t2)

doneTree :: Gen LinearState
doneTree =
    sized $ \s ->
        oneof
            [ LinearLeaf <$> genUnchecked
            , pure LinearDone
            , do (ls, rs) <- genSplit s
                 LinearBranch <$> resize ls doneTree <*> resize rs doneTree
            ]

makeForceFlushed :: LinearState -> LinearState
makeForceFlushed LinearUnknown = LinearUnknown
makeForceFlushed LinearDone = LinearDone
makeForceFlushed (LinearLeaf _) = LinearDone
makeForceFlushed (LinearBranch s1 s2) =
    LinearBranch (makeForceFlushed s1) (makeForceFlushed s2)

outputShouldBe :: Zift () -> [ZiftToken] -> Expectation
outputShouldBe func ls = outputShouldSatisfy func (== ls)

outputShouldSatisfy :: Zift () -> ([ZiftToken] -> Bool) -> Expectation
outputShouldSatisfy func predicate = do
    rd <- resolveDir' "/tmp/zifter"
    td <- resolveDir rd ".zifter"
    pchan <- newTChanIO
    let ctx =
            ZiftContext
            { rootdir = rd
            , tmpdir = td
            , settings =
                  Settings
                  {setsOutputColor = False, setsOutputMode = OutputFast}
            , printChan = pchan
            , recursionList = []
            }
    fmvar <- newEmptyTMVarIO
    ec <- ziftRunner ctx fmvar func
    ec `shouldBe` ZiftSuccess ()
    atomically (takeTMVar fmvar) `shouldReturn` ()
    outs <- readAllFrom pchan
    outs `shouldSatisfy` predicate

readAllFrom :: TChan a -> IO [a]
readAllFrom chan = do
    mr <- atomically $ tryReadTChan chan
    case mr of
        Nothing -> pure []
        Just r -> do
            rest <- readAllFrom chan
            pure (r : rest)