packages feed

tilia-0.0.1.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.Text (Text)
import Data.Text qualified as T
import Test.Hspec
import Tilia.Cpp (withoutRuledOut)
import Tilia.Cpp.Macros

-- | A plan with one dependency at 1.2.3 and a compiler at 9.10.3.
macros :: Macros
macros =
  Macros
    { 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)
          ]
    }

-- | What this guard comes to, given the plan above.
answer :: Text -> Maybe Bool
answer = answerTo 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" $
      map 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" $
      map 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" $
      map 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" $
      map answer ["if __GLASGOW_HASKELL__ >= 902", "if __GLASGOW_HASKELL__ >= 912"]
        `shouldBe` [Just True, Just False]

    it "takes the four-part macro apart the same way" $
      map
        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 "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" $
      map
        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" $
      map 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" $
      map answer ["if 0", "if 1"] `shouldBe` [Just False, Just True]

    it "says nothing about a flag" $
      map answer ["ifdef FOO", "ifndef FOO", "if defined FOO"]
        `shouldBe` [Nothing, Nothing, Nothing]

    it "says a macro it has a value for is defined" $
      map 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" $
      map answer ["else", "endif", "define FOO 1"]
        `shouldBe` [Nothing, Nothing, Nothing]

  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 "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