packages feed

hs-bindgen-1.0.0.0: test/hs-bindgen/Test/HsBindgen/Prop/Selection.hs

{-# OPTIONS_GHC -Wno-orphans #-}

module Test.HsBindgen.Prop.Selection (tests) where

import Data.String (IsString (fromString))
import Data.Text qualified as Text
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (Assertion, HasCallStack, testCase, (@?=))
import Test.Tasty.QuickCheck (Arbitrary (arbitrary), CoArbitrary (coarbitrary),
                              Fun, Function (function), Property,
                              coarbitraryShow, elements, functionMap, oneof,
                              pattern Fn, testProperty, (=/=), (===))

import Clang.Paths

import HsBindgen.Errors (panicPure)
import HsBindgen.Frontend.Predicate
import HsBindgen.IR.C qualified as C

tests :: TestTree
tests = testGroup "Test.HsBindgen.Prop.Selection" [
      testGroup "matchSelect" [
          testProperty "true"                  prop_selectTrue
        , testProperty "false"                 prop_selectFalse
        , testProperty "and"                   prop_selectAnd
        , testProperty "or"                    prop_selectOr
        , testProperty "not"                   prop_selectNot
        , testProperty "no-header"             prop_selectNoHeader
        , testProperty "from-main-headers"     prop_selectFromMainHeaders
        , testProperty "from-main-header-dirs" prop_selectFromMainHeaderDirs
        , testProperty "from-all-headers"      prop_selectFromAllHeaders
        , testProperty "header-path/all"       prop_selectHeaderPathMatchesAll
        , testProperty "header-path/needle"    prop_selectHeaderPathMatchesNeedle
        , testProperty "decl-name/all"         prop_selectDeclNameMatchesAll
        , testProperty "decl-name/needle"      prop_selectDeclNameMatchesNeedle
        , testProperty "decl-deprecated"       prop_selectDeclMatchDeprecated
        ]
    , testGroup "mergeBooleans" [
          testProperty "select/false"     (prop_mergeFalse    @SelectionPredicate)
        , testProperty "select/add/true"  (prop_mergeAddTrue  @SelectionPredicate)
        , testProperty "select/add/false" (prop_mergeAddFalse @SelectionPredicate)
        , testCase     "true/pos"         mergeTruePos
        , testCase     "true/neg"         mergeTrueNeg
        , testCase     "deselect/one"     mergeDeselectOne
        , testCase     "deselect/two"     mergeDeselectTwo
        ]
    ]

{-------------------------------------------------------------------------------
  Select pass selection properties
-------------------------------------------------------------------------------}

prop_selectTrue :: C.DeclPath -> C.DeclName -> C.Availability -> Bool
prop_selectTrue path name availability =
  matchSelect (const True) (const True) path name availability BTrue

prop_selectFalse :: C.DeclPath -> C.DeclName -> C.Availability -> Bool
prop_selectFalse path name availability =
    not $ matchSelect (const True) (const True) path name availability BFalse

prop_selectAnd
  :: Fun RealPath Bool -> Fun RealPath Bool
  -> C.DeclPath -> C.DeclName -> C.Availability
  -> Boolean SelectionPredicate -> Boolean SelectionPredicate -> Bool
prop_selectAnd (Fn isMainHeader) (Fn isInMainHeaderDir) path name availability p1 p2 =
    let p1Res = matchSelect isMainHeader isInMainHeaderDir path name availability p1
        p2Res = matchSelect isMainHeader isInMainHeaderDir path name availability p2
        p1AndP2Res =
          matchSelect isMainHeader isInMainHeaderDir path name availability (BAnd p1 p2)
     in (p1Res && p2Res) == p1AndP2Res

prop_selectOr
  :: Fun RealPath Bool -> Fun RealPath Bool
  -> C.DeclPath -> C.DeclName -> C.Availability
  -> Boolean SelectionPredicate -> Boolean SelectionPredicate -> Bool
prop_selectOr (Fn isMainHeader) (Fn isInMainHeaderDir) path name availability p1 p2 =
    let p1Res = matchSelect isMainHeader isInMainHeaderDir path name availability p1
        p2Res = matchSelect isMainHeader isInMainHeaderDir path name availability p2
        p1OrP2Res =
          matchSelect isMainHeader isInMainHeaderDir path name availability (BOr p1 p2)
     in (p1Res || p2Res) == p1OrP2Res

prop_selectNot
  :: Fun RealPath Bool -> Fun RealPath Bool
  -> C.DeclPath -> C.DeclName -> C.Availability
  -> Boolean SelectionPredicate -> Property
prop_selectNot (Fn isMainHeader) (Fn isInMainHeaderDir) path name availability p =
      matchSelect isMainHeader isInMainHeaderDir path name availability p
  =/= matchSelect isMainHeader isInMainHeaderDir path name availability (BNot p)

-- | Outside a header, no header predicate matches
prop_selectNoHeader ::
     Fun RealPath Bool -> Fun RealPath Bool
  -> C.DeclName -> C.Availability -> HeaderPathPredicate -> Bool
prop_selectNoHeader (Fn isMainHeader) (Fn isInMainHeaderDir) name availability p =
    not $ any
      (\path -> matchSelect isMainHeader isInMainHeaderDir path name availability $
        BIf (SelectHeader p))
      [C.InRootHeader, C.OnCommandLine]

prop_selectFromMainHeaders
  :: Fun RealPath Bool -> RealPath -> C.DeclName -> C.Availability -> Bool
prop_selectFromMainHeaders (Fn isMainHeader) rp name availability =
  let p = BIf $ SelectHeader FromMainHeaders
   in matchSelect isMainHeader unused (C.InHeader rp) name availability p == isMainHeader rp

prop_selectFromMainHeaderDirs
  :: Fun RealPath Bool -> RealPath -> C.DeclName -> C.Availability -> Bool
prop_selectFromMainHeaderDirs (Fn isInMainHeaderDir) rp name availability =
  let p = BIf $ SelectHeader FromMainHeaderDirs
   in matchSelect unused isInMainHeaderDir (C.InHeader rp) name availability p
        == isInMainHeaderDir rp

prop_selectFromAllHeaders :: RealPath -> C.DeclName -> C.Availability -> Bool
prop_selectFromAllHeaders rp name availability =
  let p = BIf $ SelectHeader FromAllHeaders
   in matchSelect unused unused (C.InHeader rp) name availability p

prop_selectHeaderPathMatchesAll ::
  RealPath -> C.DeclName -> C.Availability -> Bool
prop_selectHeaderPathMatchesAll rp name availability =
  let p = BIf $ SelectHeader (HeaderPathMatches ".*")
   in matchSelect unused unused (C.InHeader rp) name availability p

prop_selectHeaderPathMatchesNeedle ::
  RealPath -> C.DeclName -> C.Availability -> Bool
prop_selectHeaderPathMatchesNeedle rp name availability =
  let pathT = getRealPathText rp
      rp' = RealPath $ pathT <> "NEEDLE" <> pathT
      p = BIf $ SelectHeader (HeaderPathMatches "NEEDLE")
   in matchSelect unused unused (C.InHeader rp') name availability p

prop_selectDeclNameMatchesAll ::
  C.DeclPath -> C.DeclName -> C.Availability -> Bool
prop_selectDeclNameMatchesAll path name availability =
  let p = BIf $ SelectDecl (DeclNameMatches ".*")
   in matchSelect unused unused path name availability p

prop_selectDeclNameMatchesNeedle ::
  C.DeclPath -> C.DeclName -> C.Availability -> Bool
prop_selectDeclNameMatchesNeedle path declName availability =
  let name  = declName.text
      name' = C.DeclName (name <> "NEEDLE" <> name) declName.kind
      p     = BIf $ SelectDecl (DeclNameMatches "NEEDLE")
   in matchSelect unused unused path name' availability p

prop_selectDeclMatchDeprecated ::
  C.DeclPath -> C.DeclName -> C.Availability -> Bool
prop_selectDeclMatchDeprecated path name availability =
  let p = BIf $ SelectDecl DeclDeprecated
   in matchSelect unused unused path name availability p
        == (availability == C.Deprecated)

{-------------------------------------------------------------------------------
  Match tests and properties
-------------------------------------------------------------------------------}

prop_mergeFalse ::
  (Eq a, Show a) => [Boolean a] -> Property
prop_mergeFalse ps = mergeBooleans ps [] === BFalse

prop_mergeAddTrue ::
  (Eq a, Show a) => [Boolean a] -> [Boolean a] -> Property
prop_mergeAddTrue ps qs =
  mergeBooleans ps [BTrue] === mergeBooleans ps (BTrue : qs)

prop_mergeAddFalse ::
  (Eq a, Show a) => [Boolean a] -> [Boolean a] -> Property
prop_mergeAddFalse ps qs =
  mergeBooleans ps qs === mergeBooleans (BFalse : ps) qs

mergeTruePos, mergeTrueNeg :: Assertion
mergeTruePos =
  mergeBooleans @HeaderPathPredicate []       [BTrue] @?= BTrue
mergeTrueNeg =
  mergeBooleans @HeaderPathPredicate [BFalse] [BTrue] @?= BTrue

mergeDeselectOne :: Assertion
mergeDeselectOne = mergeBooleans [p] [BTrue] @?= BNot p
  where
    p :: Boolean SelectionPredicate
    p = BIf $ SelectDecl (DeclNameMatches "a")

mergeDeselectTwo :: Assertion
mergeDeselectTwo = mergeBooleans [pa, pb] [BTrue] @?= BAnd (BNot pa) (BNot pb)
  where
    pa, pb :: Boolean SelectionPredicate
    pa = BIf $ SelectDecl (DeclNameMatches "a")
    pb = BIf $ SelectDecl (DeclNameMatches "b")

{-------------------------------------------------------------------------------
  Helpers
-------------------------------------------------------------------------------}

instance Arbitrary RealPath where
  arbitrary = RealPath . Text.pack <$> arbitrary

instance Function RealPath where
  function = functionMap
               (Text.unpack . getRealPathText)
               (RealPath . Text.pack)

instance CoArbitrary RealPath where
  coarbitrary = coarbitraryShow

instance Arbitrary C.DeclPath where
  arbitrary = oneof [
      pure C.OnCommandLine
    , pure C.InRootHeader
    , C.InHeader <$> arbitrary
    ]

instance Arbitrary C.NameKind where
  arbitrary = elements [minBound .. maxBound]

instance Arbitrary C.DeclName where
  arbitrary = C.DeclName <$> (Text.pack <$> arbitrary) <*> arbitrary

instance Arbitrary C.Availability where
  arbitrary = elements [minBound .. maxBound]

instance Arbitrary a => Arbitrary (Boolean a) where
  arbitrary = oneof [
      pure BTrue
    , pure BFalse
    , BAnd <$> arbitrary <*> arbitrary
    , BOr <$> arbitrary <*> arbitrary
    , BNot <$> arbitrary
    , BIf <$> arbitrary
    ]

instance Arbitrary SelectionPredicate where
  arbitrary = oneof [
      SelectHeader <$> arbitrary
    , SelectDecl . DeclNameMatches <$> elements regexPatterns
    , pure (SelectDecl DeclDeprecated)
    ]

instance Arbitrary HeaderPathPredicate where
  arbitrary = oneof [
      pure FromMainHeaders
    , pure FromMainHeaderDirs
    , pure FromAllHeaders
    , HeaderPathMatches <$> elements regexPatterns
    ]

regexPatterns :: [Regex]
regexPatterns = map fromString
  [ -- Basic literals and character classes
    "abc"
  , "\\d+"
  , "\\w*"
  , "\\s?"
  , "."
  , "[aeiou]"
  , "[^0-9]"
  , "[a-z0-9]+"

    -- Anchors and word boundaries
  , "^start"
  , "end$"
  , "\\bword\\b"

    -- Quantifiers
  , "colou?r"
  , "a{3}"
  , "b{1,3}"

    -- Alternation and grouping
  , "cat|dog"
  , "gr(a|e)y"
  , "(abc)+"

    -- Escaping and specific common patterns
  , "foo\\.bar"
  , "https?://"
  , "[A-Za-z]+"
  , "\\d{4}-\\d{2}-\\d{2}"

    -- Edge cases
  , ""
  , "^$"
  ]

unused :: HasCallStack => a
unused = panicPure "Unexpected use"