tilia-0.1.0.0: tests/Tilia/Cpp/MacrosSpec.hs
{-# LANGUAGE OverloadedStrings #-}
-- | What a build plan settles about a module's conditionals.
module Tilia.Cpp.MacrosSpec (spec) where
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Text qualified as T
import Test.Hspec
import Tilia.Cpp.Directives (withoutRuledOut)
import Tilia.Cpp.Macros
-- | A plan with one dependency at 1.2.3 and GHC at 9.10.3.
macros :: Macros
macros =
mempty
{ macroVersions =
Map.fromList
[ ("MIN_VERSION_thing", [1, 2, 3]),
("MIN_VERSION_GLASGOW_HASKELL", [9, 10, 3, 0])
],
macroNumbers =
Map.fromList
[ ("__GLASGOW_HASKELL__", 910),
("__GLASGOW_HASKELL_PATCHLEVEL1__", 3),
("__GLASGOW_HASKELL_PATCHLEVEL2__", 0)
],
macroUndefined = Set.fromList ["__MHS__", "__HUGS__"]
}
-- | What this guard comes to, given the plan above.
answer :: Text -> Maybe Bool
answer = guardHolds macros
spec :: Spec
spec = do
describe "a guard about a version the plan fixed" $ do
it "is true where the plan is at least what it asks for" $
fmap answer ["if MIN_VERSION_thing(1,2,3)", "if MIN_VERSION_thing(1,0,0)"]
`shouldBe` [Just True, Just True]
it "is false where the plan is short of it" $
fmap answer ["if MIN_VERSION_thing(1,2,4)", "if MIN_VERSION_thing(2,0,0)"]
`shouldBe` [Just False, Just False]
it "compares the parts as numbers and not as text" $
answer "if MIN_VERSION_thing(1,10,0)" `shouldBe` Just False
it "pads the shorter side with zeros" $
fmap answer ["if MIN_VERSION_thing(1,2)", "if MIN_VERSION_thing(1,2,3,1)"]
`shouldBe` [Just True, Just False]
it "says nothing about a package the plan does not name" $
answer "if MIN_VERSION_other(1,0,0)" `shouldBe` Nothing
describe "a guard about the compiler" $ do
it "reads its version as the compiler spells it" $
fmap answer ["if __GLASGOW_HASKELL__ >= 902", "if __GLASGOW_HASKELL__ >= 912"]
`shouldBe` [Just True, Just False]
it "takes the four-part macro apart the same way" $
fmap
answer
[ "if MIN_VERSION_GLASGOW_HASKELL(9,10,1,0)",
"if MIN_VERSION_GLASGOW_HASKELL(9,2,0,0)",
"if MIN_VERSION_GLASGOW_HASKELL(9,12,1,0)"
]
`shouldBe` [Just True, Just True, Just False]
describe "a guard about another compiler" $ do
it "says its macros are not defined" $
fmap
answer
[ "if defined(__MHS__)",
"if defined __HUGS__",
"ifdef __MHS__",
"ifndef __HUGS__",
"if !defined(__MHS__)"
]
`shouldBe` [Just False, Just False, Just False, Just True, Just True]
it "reads them as zero where a number is asked for" $
fmap answer ["if __MHS__", "if __HUGS__ >= 200", "if __HUGS__ == 0"]
`shouldBe` [Just False, Just False, Just True]
it "answers a guard that names this compiler alongside" $
fmap
answer
[ "if defined(__GLASGOW_HASKELL__) && !defined(__MHS__)",
"if defined(__MHS__) || __GLASGOW_HASKELL__ >= 902"
]
`shouldBe` [Just True, Just True]
it "says nothing where a flag it does not know decides" $
fmap answer ["if defined(__MHS__) || defined(FOO)", "if !defined(__MHS__) && defined(FOO)"]
`shouldBe` [Nothing, Nothing]
describe "an answer that needs more than one question settled" $ do
it "carries a false through a conjunction whatever else is in it" $
answer "if defined(SOMETHING) && MIN_VERSION_thing(2,0,0)"
`shouldBe` Just False
it "carries a true through a disjunction the same way" $
answer "if defined(SOMETHING) || MIN_VERSION_thing(1,0,0)"
`shouldBe` Just True
it "gives up where what is left over decides it" $
fmap
answer
[ "if defined(SOMETHING) && MIN_VERSION_thing(1,0,0)",
"if defined(SOMETHING) || MIN_VERSION_thing(2,0,0)"
]
`shouldBe` [Nothing, Nothing]
it "answers a version test behind a defined of the same macro" $
answer "if defined(MIN_VERSION_thing) && MIN_VERSION_thing(1,0,0)"
`shouldBe` Just True
it "negates what it knows and nothing else" $
fmap answer ["if !MIN_VERSION_thing(2,0,0)", "if !defined(SOMETHING)"]
`shouldBe` [Just True, Nothing]
it "reads brackets" $
answer "if (MIN_VERSION_thing(1,0,0) || defined(X)) && !MIN_VERSION_thing(9,0,0)"
`shouldBe` Just True
describe "a guard that is not about a version at all" $ do
it "answers a bare number, which is how a branch is turned off" $
fmap answer ["if 0", "if 1"] `shouldBe` [Just False, Just True]
it "says nothing about a flag" $
fmap answer ["ifdef FOO", "ifndef FOO", "if defined FOO"]
`shouldBe` [Nothing, Nothing, Nothing]
it "says a macro it has a value for is defined" $
fmap answer ["ifdef MIN_VERSION_thing", "ifndef MIN_VERSION_thing"]
`shouldBe` [Just True, Just False]
it "says nothing about arithmetic, which it does not read" $
answer "if __GLASGOW_HASKELL__ + 1 > 900" `shouldBe` Nothing
it "says nothing about a guard whose keyword asks nothing" $
fmap answer ["else", "endif", "define FOO 1"]
`shouldBe` [Nothing, Nothing, Nothing]
describe "a guard about a macro known to be defined or not" $ do
let known = mempty{macroDefined = Set.fromList ["ON"], macroUndefined = Set.fromList ["OFF"]}
knownAnswer = guardHolds known
it "takes one that is not defined for 0" $
fmap knownAnswer ["if OFF", "if !OFF", "if OFF > 1"]
`shouldBe` [Just False, Just True, Just False]
it "says whether each is defined" $
fmap knownAnswer ["ifdef ON", "ifndef ON", "ifdef OFF", "if defined(OFF)"]
`shouldBe` [Just True, Just False, Just False, Just False]
it "says nothing about the value of one that is defined" $
knownAnswer "if ON" `shouldBe` Nothing
describe "what a guard implies about which macros are defined" $ do
let defined n = mempty{macroDefined = Set.singleton n}
notDefined n = mempty{macroUndefined = Set.singleton n}
it "reads it off a guard asking whether one is" $
fmap
(impliedBy True)
["ifdef A", "ifndef A", "if defined(A)", "if defined A", "if !defined(A)"]
`shouldBe` [ defined "A",
notDefined "A",
defined "A",
defined "A",
notDefined "A"
]
it "takes a failing guard to say the opposite" $
fmap (impliedBy False) ["ifdef A", "if !defined(A)"]
`shouldBe` [notDefined "A", defined "A"]
it "takes a macro that is not 0 for one that is defined" $
fmap (impliedBy True) ["if A", "elif A"] `shouldBe` [defined "A", defined "A"]
it "learns nothing from a macro that is 0, which it may be by not being defined" $
(impliedBy False "if A", impliedBy True "if !A") `shouldBe` (mempty, mempty)
it "reads both sides of a conjunction that holds and of a disjunction that fails" $
( impliedBy True "if defined(A) && !defined(B)",
impliedBy False "if defined(A) || defined(B)"
)
`shouldBe` (defined "A" <> notDefined "B", notDefined "A" <> notDefined "B")
it "learns nothing from a conjunction that fails or a disjunction that holds" $
(impliedBy False "if defined(A) && defined(B)", impliedBy True "if defined(A) || defined(B)")
`shouldBe` (mempty, mempty)
it "learns nothing from a call of a macro other than a version test" $
fmap (impliedBy True) ["if CHECK(1,2)", "if MIN_VERSION_base(x)", "if 1"]
`shouldBe` [mempty, mempty, mempty]
describe "a guard under what other guards imply about versions" $ do
let knownAnswer =
guardHolds
( impliedBy True "if MIN_VERSION_base(4,10,0) && !(MIN_VERSION_base(4,12,0))"
<> impliedBy True "if MIN_VERSION_base(4,9)"
)
it "is settled where the bounds they put on the version decide it" $
fmap
knownAnswer
[ "if MIN_VERSION_base(4,9,0)",
"if MIN_VERSION_base(4,10)",
"if MIN_VERSION_base(4,12)",
"if MIN_VERSION_base(5,0,0)",
"if MIN_VERSION_base(4,11,0)"
]
`shouldBe` [Just True, Just True, Just False, Just False, Nothing]
it "reads each comparison of a macro with a number as bounds on it" $
fmap
( \op ->
fmap
(guardHolds (impliedBy True ("if __GLASGOW_HASKELL__ " <> op <> " 908")))
["if __GLASGOW_HASKELL__ >= 908", "if __GLASGOW_HASKELL__ >= 909"]
)
[">=", ">", "<", "<=", "==", "!="]
`shouldBe` [ [Just True, Nothing],
[Just True, Just True],
[Just False, Just False],
[Nothing, Just False],
[Just True, Just False],
[Nothing, Nothing]
]
it "is settled by a failing guard as well" $
fmap
(guardHolds (impliedBy False "if __GLASGOW_HASKELL__ >= 908"))
["if __GLASGOW_HASKELL__ < 908", "if __GLASGOW_HASKELL__ > 910"]
`shouldBe` [Just True, Just False]
describe "blanking the branches a plan rules out" $ do
it "leaves the taken branch and blanks the rest" $
ruledOut
[ "#if MIN_VERSION_thing(1,0,0)",
"import New",
"#else",
"import Old",
"#endif"
]
`shouldBe` ["", "import New", "", "", ""]
it "takes the #else where the condition fails" $
ruledOut
[ "#if MIN_VERSION_thing(2,0,0)",
"import New",
"#else",
"import Old",
"#endif"
]
`shouldBe` ["", "", "", "import Old", ""]
it "leaves nothing where the condition fails and there is no #else" $
ruledOut
[ "#if MIN_VERSION_thing(2,0,0)",
"import New",
"#endif"
]
`shouldBe` ["", "", ""]
it "takes the first branch of an #elif chain that holds" $
ruledOut
[ "#if MIN_VERSION_thing(2,0,0)",
"import Newest",
"#elif MIN_VERSION_thing(1,0,0)",
"import New",
"#else",
"import Old",
"#endif"
]
`shouldBe` ["", "", "", "import New", "", "", ""]
it "leaves a conditional it cannot answer exactly as it was" $
ruledOut
[ "#ifdef FOO",
"import One",
"#else",
"import Two",
"#endif"
]
`shouldBe` ["#ifdef FOO", "import One", "#else", "import Two", "#endif"]
it "leaves a chain alone from the first question it cannot answer" $
ruledOut
[ "#if defined(FOO)",
"import One",
"#elif MIN_VERSION_thing(1,0,0)",
"import Two",
"#endif"
]
`shouldBe` ["#if defined(FOO)", "import One", "#elif MIN_VERSION_thing(1,0,0)", "import Two", "#endif"]
it "leaves the branches before a question it cannot answer as well" $
ruledOut
[ "#if MIN_VERSION_thing(2,0,0)",
"import One",
"#elif defined(FOO)",
"import Two",
"#endif"
]
`shouldBe` [ "#if MIN_VERSION_thing(2,0,0)",
"import One",
"#elif defined(FOO)",
"import Two",
"#endif"
]
it "blanks what only another compiler would see" $
ruledOut
[ "#if defined(__MHS__)",
"import Data.ZipList",
"#endif"
]
`shouldBe` ["", "", ""]
it "rules out a shim defining a macro the plan already has" $
ruledOut
[ "#ifndef MIN_VERSION_thing",
"#define MIN_VERSION_thing(a,b,c) 1",
"#endif"
]
`shouldBe` ["", "", ""]
it "reaches a conditional nested inside the branch that is taken" $
ruledOut
[ "#if MIN_VERSION_thing(1,0,0)",
"#if MIN_VERSION_thing(2,0,0)",
"import Newest",
"#else",
"import New",
"#endif",
"#endif"
]
`shouldBe` ["", "", "", "", "import New", "", ""]
it "keeps every line where it was written" $
let source = T.unlines ["module M where", "#if MIN_VERSION_thing(2,0,0)", "x = 1", "#endif"]
in length (T.lines (withoutRuledOut macros source))
`shouldBe` length (T.lines source)
it "leaves a module with no conditionals in it alone" $
withoutRuledOut macros "module M where\n" `shouldBe` "module M where\n"
-- | The lines of a module once the plan has ruled out what it can.
ruledOut :: [Text] -> [Text]
ruledOut = T.lines . withoutRuledOut macros . T.unlines