hs-di-0.3.0: test/MainSpec.hs
module MainSpec where
import Test.Hspec as Hspec hiding (specify, it)
import qualified Test.Hspec as Hspec (specify, it)
import qualified Test.Hspec.Core.Runner as HC
import Data.List
import Language.Haskell.TH
import DI
import SimpleDefs
import NotSoEasyToTestCode
import Data.Maybe
import Data.Time
import Data.IORef
import Data.String.Utils
import Common
import qualified GradualSpec as G ()
import Control.Exception
import NeatInterpolation
import qualified Data.Text as T
import Control.DeepSeq (force)
import Language.Haskell.Meta
import Assert
import Language.Haskell.Ghcid
import Control.Concurrent
import System.IO.Unsafe (unsafePerformIO)
import Control.Concurrent.MVar
import Foreign.Store
-- import Data.IORef
import qualified Data.Text.IO as T
-- import qualified Test.HUnit as HUnit
import Test.Hspec.Expectations
import Text.InterpolatedString.Perl6
import Data.String.Interpolate.Util
import SpecCommon
import Data.Monoid
import Text.Regex.TDFA
import Test.HUnit.Lang (HUnitFailure(..))
inj
testIdiomaticImportMock = 44
inj
a :: Int
a = 1
injG
bI :: Int
bI = 2
mainColor = HC.hspecWith
HC.defaultConfig{HC.configColorMode=HC.ColorAlways}
$ specWith setUpGhcidCached
main = hspec spec
spec = specWith setUpGhcidUncached
specWith setUpGhcid = do
let
dep n c = Dep n Original Pure c
rep n c = Dep n Replaced Pure c
specify "mapDepNames" $ do
let l x = dep x []
mapDepNames (const "2" ) (dep "1" []) `shouldBe` (dep "2" [])
mapDepNames (const 2) (dep "1" []) `shouldBe` (dep 2 [])
specify "convertDepsToExp" $ do
-- let ppsB = shouldBeF pprint
-- let ppsB = shouldBeF show
let ppsB = shouldBe
convertDepsToExp (dep "a" []) `ppsB` (VarE $ mkName "a")
convertDepsToExp (dep "a" [dep "b" []]) `ppsB`
(AppE (VarE $ mkName "a") (VarE $ mkName "b"))
convertDepsToExp (dep "a" [dep "b" [], dep "c" []]) `ppsB`
(AppE (AppE (VarE $ mkName "a") (VarE $ mkName "b")) (VarE $ mkName "c"))
specify "override fn" $ do
override "a" "b" (dep "a" []) `shouldBe` rep "b" []
override "a" "c" (dep "a" []) `shouldBe` rep "c" []
-- override "b" "c" (dep "a" []) `shouldBe` dep "a" []
evaluate (override "b" "c" (dep "a" [])) `shouldThrow` anyException
-- override "x" "c" (dep "b" [dep "a" []]) `shouldBe` (dep "b" [dep "a" []])
evaluate (override "x" "c" (dep "b" [dep "a" []])) `shouldThrow` anyException
override "b" "c" (dep "b" [dep "a" []]) `shouldBe` (rep "c" [dep "a" []])
override "a" "c" (dep "b" [dep "a" []]) `shouldBe` (dep "b" [rep "c" []])
override "a" "c" (dep "b" [dep "a" [], dep "a" []]) `shouldBe`
(dep "b" [rep "c" [], rep "c" []])
specify "assemble" $ do
$(assemble barD) `shouldBe` 2
specify "mocking" $ do
let
fooDMock = dep "fooMock" []
fooMock = 33
$(assemble $ override "foo" "fooMock" barD) `shouldBe` 34
specify "type variable support" $ do
$(assemble $ idTestD) `shouldBe` 3
let
idDMock = dep "idMock" []
idMock = (+1)
$(assemble $ override "id" "idMock" idTestD) `shouldBe` 4
specify "module support" $ do
$(assemble $ testModuleD) `shouldBe` 12
-- specify "qualified names" $ do
-- $(assemble $ dep "Prelude.id" []) 1 `shouldBe` 1
-- $(assemble $ dep "Prelude.*" []) 2 3 `shouldBe` 6
specify "code that is more real-life" $ do
let parseTime t =
fromJust $ parseTimeM True defaultTimeLocale "%Y-%m-%d %H:%M:%S%Q" t
mockConsole <- newIORef []
cTime <- newIORef $ parseTime "2016-01-01 14:00:00"
let
-- $(inj)
putStrLnMockD = dep "putStrLnMock" []
putStrLnMockT = putStrLnMock
putStrLnMock a = modifyIORef mockConsole (a :)
-- readMockConsole = readIORef mockConsole >>= fmap reverse
readMockConsole = do
readIORef mockConsole >>= (reverse .> return)
-- readIORef mockConsole $> (fmap reverse)
-- $(inj)
getCurrentTimeMockD = dep "getCurrentTimeMock" []
getCurrentTimeMockT = getCurrentTimeMock
getCurrentTimeMock = readIORef cTime
setUpThen cont = do
writeIORef mockConsole []
writeIORef cTime $ parseTime "2016-01-01 14:00:00"
timer <- $(makeTimerD
$> override "putStrLn" "putStrLnMock"
$> override "getCurrentTime" "getCurrentTimeMock"
$> assemble
)
timer
cont timer
readMockConsole >>= (`shouldBe` [])
setUpThen $ \timer -> do
readMockConsole >>= (`shouldBe` ["2016-01-01 14:00:00 UTC"])
setUpThen $ \timer -> do
timer
readMockConsole >>= (`shouldBe`
["2016-01-01 14:00:00 UTC", "2016-01-01 14:00:00 UTC, diff: 0s"])
setUpThen $ \timer -> do
writeIORef cTime $ parseTime "2016-01-01 14:00:01"
timer
readMockConsole >>= (`shouldBe`
[ "2016-01-01 14:00:00 UTC", "2016-01-01 14:00:01 UTC, diff: 1s"])
-- [x] TODO figure out a way to branch out just like with Jasmine-Given JS testing framework
setUpThen $ \timer -> do
writeIORef cTime $ parseTime "2016-01-01 14:00:00.00002"
timer
readMockConsole >>= (`shouldBe`
[ "2016-01-01 14:00:00 UTC", "2016-01-01 14:00:00.00002 UTC, diff: 0.00002s"])
describe "automatic deps declaration" $ do
-- deps "makeTimer" $> runQ >>= (pprint .> (`shouldBe` "dep \"makeTimer\" [putStrLnD, getCurrentTimeD]"))
-- putStrLn $( (fmap show $ location) >>= ( StringL .> LitE .> return) )
-- let asd = fmap (LitE $ StringL) getContentOfNextLine
specify "" $ do
strip $(getContentOfNextLineLit) `shouldBe` "let asd foo = foo + 1"
let asd foo = foo + 1
strip $(getContentOfNextLineLit) `shouldBe` "let asd foo = foo + 2"
let asd foo = foo + 2
let
a = strip $(getContentOfNextLineLit) `shouldBe` "asd foo = foo + 2"
asd foo = foo + 2
a
-- parseLineToDeps "foo = 1" `shouldBe` dep "foo" []
parseLineToDeps "a = 1" `shouldBe` ("a", "aD", [], [])
parseLineToDeps "b = 1" `shouldBe` ("b", "bD", [], [])
parseLineToDeps "b a = 1" `shouldBe` ("b", "bD", ["aD"], ["a"])
[aa| (injectableI (return "asd = 2") $> runQ $> fmap pprint) >>=
(`shouldSatisfy` ("asdD = Dep \"asd\" Original Pure []" `isPrefixOf`)) |]
[aa| (injectableI (return "asd a = 2") $> runQ $> fmap pprint) >>=
(`shouldSatisfy` ("asdD = Dep \"asd\" Original Pure [aD]" `isPrefixOf`)) |]
[aa| (injLeaf "asdasd" $> runQ $> fmap pprint) >>=
(`shouldSatisfy` ("asdasdD = Dep \"asdasd\" Original Pure []" `isPrefixOf`)) |]
describe "idiomatic module support" $ do
specify "tuplePattern" $ do
(tuplePattern (dep "a" []) $> pprint) `shouldBe` "a"
(tuplePattern (dep "a" [dep "b" []]) $> pprint) `shouldBe` "(a, b)"
(tuplePattern (dep "a" [dep "b" [], dep "c" []]) $> pprint) `shouldBe` "(a, b, c)"
-- (tuplePattern (dep "a" [dep "b" [], dep "b" []]) $> pprint) `shouldBe` "(a, b, _)"
-- (tuplePattern (dep "a" [dep "b" [], dep "b" []]) $> pprint) `shouldSatisfy`
-- ((=~ "b") .> (== 1))
(tuplePattern (dep "a" [dep "b" [], dep "b" []]) $> pprint $>
(=~ "b")) `shouldBe` (1 :: Int)
specify "utils" $ do
-- (convertDepsViaTuple (dep "a" []) $> runQ $> fmap pprint) `shouldReturn` "let a = aT in a"
(convertDepsViaTuple (dep "a" []) $> pprint) `shouldBe` "let a = aT\n in a"
(convertDepsViaTuple (dep "a" [dep "b" []]) $> pprint) `shouldBe`
"let (a, b) = aT\n in a b"
(convertDepsViaTuple (dep "a" [dep "b" [dep "d" []], dep "c" []]) $> pprint)
`shouldBe` "let (a, (b, d), c) = aT\n in a (b d) c"
(convertDepsViaTuple (rep "a" []) $> pprint) `shouldBe` "let _ = aT\n in a"
[aa| (convertDepsViaTuple (dep "a" [rep "b" []]) $> pprint) `shouldBe`
"let (a, _) = aT\n in a b" |]
-- [x] TODO: warn or error if override didn't match anything
-- e.g. compare before with after to check for EQ
specify "the real deal" $ do
$(assemble testIdiomaticModuleD) `shouldBe` 23
$( testIdiomaticModuleD
$> override "testIdimoaticImport" "testIdiomaticImportMock"
$> assemble) `shouldBe` 48
specify "Still support clumsy fallback" $ do
let
aD = dep "a" []
aT = a
a = 1
$( testIdiomaticModuleD
$> override "testIdimoaticImport" "a"
$> assemble) `shouldBe` 5
specify "less clumsy, requires more imports though" $ do
let
aD = dep "a" []
a = 2
$( testIdiomaticModuleD
$> override "testIdimoaticImport" "a"
$> assembleSimple) `shouldBe` 6
specify "make sure that inj also declares a value that does not require `assemble`" $ do
testIdiomaticModuleA `shouldBe` 23
describe "" $ do
let f (n, _, _, _, ds) = (n, ds)
specify "Support for type declaration be in between inj and fn decl" $ do
([text|
aI b = 1
|] $> T.unpack $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"])
("\naI b = 1" $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"])
("\n\naI b = 1" $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"])
([text|
aI :: Int
aI b = 1
|] $> T.unpack $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"])
-- ("" $> parseLineToDepsG $> f) `shouldBe` ("", [])
("" $> parseLineToDepsG $> f $> force $> evaluate) `shouldThrow` anyException
([text|
aI :: x => Int
aI b = 1
|] $> T.unpack $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"])
a `shouldBe` 1
b `shouldBe` 2
[aa| ("aI = \\n -> f $ g n" $> parseLineToDepsG $> f) `shouldBe` ("a", []) |]
[aa| ("aI = \\n -> f $ g n" $> parseLineToDepsG $> f) `shouldBe` ("a", []) |]
[aa| ("aI = \\x -> f $ g x" $> parseLineToDepsG $> f) `shouldBe` ("a", []) |]
[aa| ("aI = 1" $> parseLineToDepsG $> f) `shouldBe` ("a", []) |]
[aa| ("aI b = 1" $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"]) |]
[aa| ("aI b = \\ x -> 1" $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"]) |]
[aa| ("aI = 1 > 1" $> parseLineToDepsG $> f) `shouldBe` ("a", []) |]
context "support @ notation" $ do
[aa| ("aI f@longName = 1" $> parseLineToDepsG $> f) `shouldBe` ("a", ["longName"]) |]
-- [aa| ("aI f @ longName = 1" $> parseLineToDepsG $> f) `shouldBe` ("a", ["longName"]) |]
-- [aa| ("aI (f@longName) = 1" $> parseLineToDepsG $> f) `shouldBe` ("a", ["longName"]) |]
context "multiline support" $ do
-- [aa| ([text|
-- aI
-- b
-- = 1
-- |\] $> T.unpack $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"]) |]
it "" $ do
([text|
aI
b
= 1
|] $> T.unpack $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"])
it "" $ do
([text|
a :: Int -> Int
aI
b
= 1
|] $> T.unpack $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"])
it "" $ do
([text|
a
:: Int
-> Int
aI
b
= 1
|] $> T.unpack $> parseLineToDepsG $> f) `shouldBe` ("a", ["b"])
it "" $ do
-- threadDelay 1000000
([text|
a
:: Int
-> Int
aI
b@longAssName
= 1
|] $> T.unpack $> parseLineToDepsG $> f) `shouldBe` ("a", ["longAssName"])
-- let loadModule' g modName = do
-- result <- exec g $ ":load test/" ++ modName ++ ".hs"
-- -- if result $> last $> ("Ok, modules loaded" `isPrefixOf`)
-- if result $> any ("Ok, modules loaded" `isPrefixOf`)
-- then map printForward result $> sequence_
-- else error $ "\n" ++ unlines result
context "ghcid" $ beforeAll setUpGhcid $ do
it "ghcid test 1" $ \g -> do
exec g "1+2" `shouldReturn` ["3"]
it "ghcid test 2" $ \g -> do
-- exec g "import GhcidTest"
-- exec g ":load test/GhcidTest.hs" >>= print
-- exec g ":m + GhcidTest"
loadModule' g "GhcidTest"
exec g "xxx" `shouldReturn` ["123"]
exec g "xxx2" `shouldReturn` ["34"]
specify "common dependency" $ \g -> do
-- pendingWith [qx|
-- s
-- d
-- |]
T.writeFile "test/Scenarios/CommonDependency.hs" [text|
import DI
aI = 1
bI a = a + 2
cI a = a + 4
dI b c = b + c
injAllG
|]
loadModule' g "Scenarios/CommonDependency"
execAssert g "$(assemble dD)" (`shouldBeStr` unlines ["8"])
-- let
-- (_, (_, aT), _) = dT
-- aA = aT
specify "common dependency 2" $ \g -> do
T.writeFile "test/Scenarios/CommonDependency2.hs" [text|
import DI
xI = 1
aI x = x + 1
bI a = a + 2
cI a = a + 4
dI b c = b + c
injAllG
|]
loadModule' g "Scenarios/CommonDependency2"
execAssert g "$(assemble dD)" (`shouldBeStr` unlines ["10"])
-- failDetails "foobar" $ 1 `shouldBe` 2
-- 1 `shouldBe` 2
-- $> failDetails "foobar"
-- $> failDetails "asdasd"
specify "injAllG" $ \g -> do
T.writeFile "test/Scenarios/injAll.hs" [text|
import DI
aI = 1
bI a = a + 2
injAllG
|]
loadModule' g "Scenarios/injAll"
execAssert g "$(assemble bD)" (`shouldBeStr` unlines ["3"])
specify "injAllG out-of-order" $ \g -> do
T.writeFile "test/Scenarios/injAll2.hs" [text|
import DI
bI a = a + 2
aI = 1
cI b = b + 3
injAllG
|]
loadModule' g "Scenarios/injAll2"
execAssert g "$(assemble cD)" (`shouldBeStr` unlines ["6"])
-- it ">>= support" $ \g -> do
-- -- exec g ":load test/IOScenario.hs" >>= print
-- loadModule' g "IOScenario"
-- -- (exec g "$(assemble startupTimeStringD)" $> fmap unlines) >>= putStrLn
-- -- (exec g "startupTimeStringD" $> fmap unlines) >>= putStrLn
-- -- (exec g "$(assemble startupTimeStringD)" $> fmap unlines)
-- -- `shouldReturn` unlines ["123"]
-- (exec g "$(assemble xxxD)") >>= (unlines .> putStrLn)
-- loadModule' g "IOScenarioMain"
-- -- exec g "xxx" >>= unlines .> putStrLn
-- -- exec g "$(assemble xxxD)" >>= unlines .> putStrLn
-- -- exec g "1+1+12323" >>= map printForward .> sequence_
-- -- print 1
-- -- 1 `shouldBe` 2
-- -- exec g "import Asd"
-- -- exec g ":set -XTemplateHsaskell"
-- exec g ":load test/Asd.hs"
-- -- exec g "import Asd"
-- -- exec g ":t xxxD" `shouldReturn` ["123"]
-- exec g "xxx" `shouldReturn` ["123"]
-- exec g "xxx2" `shouldReturn` ["34"]
-- -- return ()
-- -- print 1
-- TODO: Handle splices in aa
-- [aa| $(assemble $ override "foo" "33" barD) `shouldBe` 34 |]
specify "override with simple expressions" $ do
$(assemble $ override "foo" "33" barD) `shouldBe` 34
$(assemble $ override "foo" "1 + 2" barD) `shouldBe` 4
specify "transpose [Dec] to `(TupP [Pat], TupP [Exp])`" $ do
("a = 1; b = 2" $> pd $> transposeDecsToPE $> fst)
`shouldBe` ("(a, b)" $> pp)
("a = 1; b = 2" $> pd $> transposeDecsToPE $> snd)
`shouldBe` ("(1, 2)" $> pe)
-- -- [ ] TODO: Overcome GHC stage restriction:
-- -- home/wizek/sandbox/exp-xml/exp-xml/hs-di/test/MainSpec.hs:289:18:
-- -- GHC stage restriction:
-- -- ‘aD’ is used in a top-level splice or annotation,
-- -- and must be imported, not defined locally
-- -- In the splice: $(assemble aD)
-- specify "allow defining injectable value at non-top-level" $ do
-- let
-- $(injP) = $(injE)
-- aI = 453
-- $(assemble aD) `shouldBe` 453
-- specify "override a non-leaf" $ do
-- -- pending
-- specify "override, change deps" $ do
-- pending
context "unindent" $ do
[aa| unindent (unlines [
"a"
]) `shouldBe` (unlines [
"a"
]) |]
[aa| unindent (unlines [
" a"
]) `shouldBe` (unlines [
"a"
]) |]
[aa| unindent (unlines [
" a"
, "a"
]) `shouldBe` (unlines [
" a"
, "a"
]) |]
[aa| unindent (unlines [
" a"
, " a"
]) `shouldBe` (unlines [
" a"
, "a"
]) |]
loadModule' g modName = do
result <- exec g $ ":load test/" ++ modName ++ ".hs"
-- if result $> last $> ("Ok, modules loaded" `isPrefixOf`)
if result $> any ("Ok, modules loaded" `isPrefixOf`)
-- then map printForward result $> sequence_
then return ()
else error $ "\n" ++ unlines result
-- runOnlyPrefix = ["!"]
-- runOnlyPrefix = ["unindent"]
runOnlyPrefix = [""]
specify a = if (any (`isPrefixOf` a) runOnlyPrefix)
then Hspec.specify a
else (\_->return ())
it a = if (any (`isPrefixOf` a) runOnlyPrefix)
then Hspec.specify a
else (\_->return ())
pd = parseDecs .> fromRight
pp = parsePat .> fromRight
pe = parseExp .> fromRight
fromRight (Right a) = a
printForward = (prefix ++) .> putStrLn
prefix = " "
noop :: Monad m => m ()
noop = return ()
dont _ = noop
xcontext n _ = context n $ it "xcontext" pending
xit n _ = it n pending
xspecify n _ = specify n pending
displayLoadingInfo = id
.> map (\case
-- l@(Loading _ _) -> l $> show $> (prefix ++) $> putStrLn
-- (Message{loadMessage=msg}) -> putStrLn $ unlines $ map (prefix ++) msg
(Message{loadMessage=msg, loadSeverity=Error}) -> error $ ("\n" ++) $ unlines $ map (prefix ++) msg
(Message{loadMessage=msg, loadSeverity=Warning}) -> error $ ("\n" ++) $ unlines $ map (prefix ++) msg
_ -> return ()
)
.> sequence_
where
prefix = " "
-- setUpGhcidCached = do
-- (g, ls) <- startGhci "stack ghci hs-di:exe:hs-di-cases" (Just ".") (const $ const (return ()))
-- displayLoadingInfo ls
-- return g
-- ghcid = unsafePerformIO $ newIORef Nothing
-- ghcid = unsafePerformIO $ newMVar Nothing
setUpGhcidUncached = do
(g, ls) <- startGhci "stack ghci hs-di:exe:hs-di-cases" (Just ".") (const $ const (return ()))
displayLoadingInfo ls
return g
setUpGhcidCached = {-Hspec.runIO $-} do
(lookupStore 0 $> handle) >>= \case
Nothing -> do
g <- setUpGhcidUncached
(newStore g >> return ()) `catch` (\(e :: SomeException) -> print (2, e))
-- writeIORef ghcid $ Just g
return g
Just s -> readStore s
where
handle = (`catch` f)
f (e :: SomeException) = do
print (1, e)
return Nothing
-- [x] TODO Handle:
-- Test suite failure for package hs-di-0.2.2
-- hs-di-test: exited with: ExitFailure (-11)
-- Logs printed to console
shouldBeStr :: String -> String -> IO ()
actual `shouldBeStr` expected
| actual == expected = return ()
| otherwise = expectationFailure failMsg
where
failMsg =
[qx|
Actual: {singleLineOrIndent actual}
Expected: {singleLineOrIndent expected}
|]
-- failMsg = T.unpack
-- [text|
-- $(T.pack expected)
-- ${T.pack actual}
-- |]
-- (actual `shouldReturn` expected) `catch` (\(e :: SomeException) ->
-- putStrLn actual
-- )
singleLineOrIndent = f
where
f str@(isMultiline -> True) = "\n" <> indent 2 str
f str = str
isMultiline = ('\n' `elem`)
indent n str =
str
$> lines
$> map (replicate n ' ' ++)
$> unlines
removeInteractive = id
.> groupByIndentation
.> filter (concat .> ("<interactive>:" `isPrefixOf`) .> not)
.> concat
exec' g cmd = do
stdoutB <- newIORef []
stderrB <- newIORef []
let
f Stdout str = modifyIORef stdoutB (<> [str])
f Stderr str = modifyIORef stderrB (<> [str])
execStream g cmd f
return (,) <*> readIORef stdoutB <*> readIORef stderrB
failDetails details assert = do
assert `catch` \(HUnitFailure loc msg) -> do
throw $ HUnitFailure loc $ msg ++ "\n" ++ details
execAssert g cmd assert = exec g cmd
>>= (\full -> full $> removeInteractive $> unlines $> assert
$> failDetails ("Full: " <> (full $> unlines $> singleLineOrIndent)))