packages feed

hs-bindgen-1.0.0.0: test/hs-bindgen/Test/HsBindgen/Fixtures/TestCases.hs

{-# LANGUAGE OverloadedRecordDot #-}

-- | Shared fixture compilation status and skip logic
--
-- This module provides the common 'FixtureStatus' type and shared skip
-- reasons used by both 'Test.HsBindgen.THFixtures.TestCases' and
-- 'Test.HsBindgen.PPFixtures.TestCases'.
--
-- To add a new skip reason that applies to /both/ TH and PP fixture
-- compilation, add it to 'commonFixtureStatus'. For mode-specific skips,
-- add them in the respective @determineTHStatus@ or @determinePPStatus@.
--
module Test.HsBindgen.Fixtures.TestCases (
    FixtureStatus(..)
  , isCompile
  , commonFixtureStatus
    -- * Shared skip lists
  , commonKnownEmpty
    -- * Tasty options
  , HaddockMode(..)
  , haddockOutputDir
    -- * Tasty test helpers
  , SimpleTest(..)
  , mkFixtureTest
  , passedResult
  , skippedResult
    -- * Helpers
  , isSuccess
  , isVariantTest
  ) where

import Data.List (isPrefixOf)
import Data.Map.Strict qualified as Map
import Data.Proxy (Proxy (..))
import System.FilePath ((</>))
import Test.Tasty
import Test.Tasty.Options (IsOption (..), OptionDescription (Option),
                           flagCLParser, safeReadBool)
import Test.Tasty.Providers (IsTest (..), singleTest, testFailed)
import Test.Tasty.Providers.ConsoleFormat (noResultDetails)
import Test.Tasty.Runners (Result (..))
import Test.Tasty.Runners qualified as Tasty

import Test.HsBindgen.Fixtures.Utils (sanitizeLibName)
import Test.HsBindgen.Golden.Infra.TestCase

{-------------------------------------------------------------------------------
  Fixture status
-------------------------------------------------------------------------------}

-- | Compilation status for a fixture
data FixtureStatus
    = FixtureCompile
      -- ^ Compile this fixture (with @-optc-Werror@ where applicable)
    | FixtureCompileNoOptcWerror
      -- ^ Compile without @-optc-Werror@ (PP only; TH treats as 'FixtureCompile')
    | FixtureSkip String
      -- ^ Skip with reason
  deriving stock (Eq, Show)

-- | Does this status indicate compilation?
isCompile :: FixtureStatus -> Bool
isCompile FixtureCompile             = True
isCompile FixtureCompileNoOptcWerror = True
isCompile FixtureSkip{}              = False

{-------------------------------------------------------------------------------
  Common skip logic
-------------------------------------------------------------------------------}

-- | Determine fixture status based on skip reasons common to both TH and PP
--
-- Returns @Just status@ if a common rule applies, @Nothing@ if mode-specific
-- logic should decide.
--
commonFixtureStatus :: TestCase -> Maybe FixtureStatus
commonFixtureStatus tc
  -- Expected failure tests produce no valid output
  | not (isSuccess tc.outcome)
      = Just $ FixtureSkip "Expected failure test"
  -- Tests with external binding specs not yet supported
  | "binding-specs/fun_arg/" `isPrefixOf` tc.name
    || "macros/macro_ext_binding_dep" `isPrefixOf` tc.name
    || "types/anonymous/edge-cases/drop_indirect_fields" `isPrefixOf` tc.name
      = Just $ FixtureSkip "External binding specs not yet supported (issue #1495)"
  -- Trans-dep and relative-include fixtures deliberately reference a
  -- missing module to exercise external binding spec resolution; they
  -- are not meant to compile.
  | "binding-specs/trans_dep/" `isPrefixOf` tc.name
    || "binding-specs/relative_include/" `isPrefixOf` tc.name
      = Just $ FixtureSkip "Generated bindings reference intentionally-missing module"
  -- Apple block extension requires clang
  | tc.name == "edge-cases/iterator"
      = Just $ FixtureSkip "Apple block extension requires clang (issue #913)"
  -- Unusable struct
  | tc.name == "functions/decls_in_signature"
      = Just $ FixtureSkip "Unusable struct (issue #1128)"
  -- Issue #1490
  | tc.name `elem` issue1490
      = Just $ FixtureSkip "Issue #1490"
  -- Issue #1679
  | tc.name `elem` issue1679
      = Just $ FixtureSkip "Issue #1679"
  -- Issue #1956
  | tc.name == "macros/undef"
      = Just $ FixtureSkip "Issue #1956"
  -- @typedef int bool@ / @#define bool int@ is invalid in C23
  | tc.name == "types/primitives/bool_typedef_override"
 || tc.name == "types/primitives/bool_macro_override"
      = Just $ FixtureSkip "bool override is invalid in C23"
  -- Empty output fixtures (shared between TH and PP)
  | tc.name `elem` commonKnownEmpty
      = Just $ FixtureSkip "Empty fixture (no bindings generated)"
  | otherwise
      = Nothing
  where
    issue1490 :: [String]
    issue1490 = [
        "functions/heap_types/struct_const_member"
      , "functions/heap_types/struct_const_typedef"
      , "functions/heap_types/struct_const"
      , "functions/heap_types/union_const_member"
      , "functions/heap_types/union_const_typedef"
      , "functions/heap_types/union_const"
      ]

    issue1679 :: [String]
    issue1679 = [
        "program-analysis/program-slicing/macro_selected"
      , "program-analysis/program-slicing/macro_unselected"
      ]

{-------------------------------------------------------------------------------
  Shared skip lists
-------------------------------------------------------------------------------}

-- | Empty output fixtures shared between TH and PP
--
commonKnownEmpty :: [String]
commonKnownEmpty = [
    "declarations/declaration_unselected_b"
  , "declarations/name_collision"
  , "edge-cases/clang_generated_collision"
  , "edge-cases/duplicate"
  , "edge-cases/headers"
  , "edge-cases/select_no_match"
  , "edge-cases/thread_local"
  , "edge-cases/unsupported_builtin"
  , "functions/not_visible_decl"
  , "macros/macro_type_void"
  , "macros/redeclaration/different_value"
  , "program-analysis/delay_traces"
  , "program-analysis/selection_foo"
  , "program-analysis/selection_merge_traces"
  , "program-analysis/selection_omit_prescriptive"
  , "program-analysis/selection_squash_typedef"
  , "types/anonymous/edge-cases/empty_anon"
  , "types/anonymous/edge-cases/unnamed_bitfield"
  , "types/special/long_double"
  , "types/structs/untagged_struct"
  , "types/typedefs/typenames"
  ]

{-------------------------------------------------------------------------------
  Tasty options
-------------------------------------------------------------------------------}

-- | Whether to generate Haddock documentation for fixtures
data HaddockMode = NoHaddock | WithHaddock
  deriving stock (Eq, Ord)

instance IsOption HaddockMode where
    defaultValue = NoHaddock
    parseValue s = case safeReadBool s of
        Just True  -> Just WithHaddock
        Just False -> Just NoHaddock
        Nothing    -> Nothing
    optionName = return "haddock"
    optionHelp = return "Generate Haddock documentation for fixtures"
    optionCLParser = flagCLParser Nothing WithHaddock

-- | Convert 'HaddockMode' to an optional output directory
haddockOutputDir :: HaddockMode -> FilePath -> Maybe FilePath
haddockOutputDir NoHaddock   _        = Nothing
haddockOutputDir WithHaddock buildDir = Just $ buildDir </> "haddock-fixtures"

{-------------------------------------------------------------------------------
  Tasty test helpers
-------------------------------------------------------------------------------}

-- | A simple test that runs an IO action returning a 'Result'
newtype SimpleTest = SimpleTest (IO Result)

instance IsTest SimpleTest where
    run _ (SimpleTest action) _ = action
    testOptions = return [Option (Proxy :: Proxy HaddockMode)]

-- | Create a test tree entry for a single fixture
--
-- Skipped fixtures show their reason; compiled fixtures look up their
-- result in the shared batch-build results map.
mkFixtureTest ::
     String                              -- ^ Library name prefix (e.g. @\"fixture-\"@)
  -> String                              -- ^ Label (e.g. @\"PP compilation\"@)
  -> (TestCase -> FixtureStatus)         -- ^ Status function
  -> IO (Map.Map String (Either String ()))  -- ^ Batch build results
  -> TestCase
  -> TestTree
mkFixtureTest prefix label determineStatus getResults tc =
    case determineStatus tc of
      FixtureSkip reason ->
          singleTest tc.name $
            SimpleTest $ return $ skippedResult reason
      _ ->
          singleTest tc.name $ SimpleTest $ do
              results <- getResults
              let libName = prefix ++ sanitizeLibName tc.name
              case Map.lookup libName results of
                  Just (Right ()) -> return $ passedResult $
                      label ++ " succeeded"
                  Just (Left err) -> return $ testFailed $
                      label ++ " failed:\n" ++ err
                  Nothing -> return $ testFailed
                      "Library not found in batch build results"

passedResult :: String -> Result
passedResult msg = Result Tasty.Success msg "OK" 0 noResultDetails

skippedResult :: String -> Result
skippedResult reason = Result Tasty.Success ("Skipped: " ++ reason) ("Skipped: " ++ reason) 0 noResultDetails

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

-- | Check if an outcome is 'Success'
isSuccess :: Outcome -> Bool
isSuccess Success = True
isSuccess _       = False

-- | Check if a test name is a variant test (e.g., "foo.1.bar")
isVariantTest :: String -> Bool
isVariantTest name = any isVariantSuffix (suffixes name)
  where
    suffixes :: String -> [String]
    suffixes []     = []
    suffixes (_:xs) = xs : suffixes xs

    isVariantSuffix :: String -> Bool
    isVariantSuffix ('.':c:_) = c `elem` ['0'..'9']
    isVariantSuffix _         = False