c-expr-dsl-0.2.0.0: test/Test/CExpr/Parse/Golden.hs
-- | Golden integration tests for 'C.Expr.Parse.parseMacroBody'
--
-- These tests use @libclang@ to tokenise the macros defined in
-- @test/fixtures/macros.h@, split each definition into its formal parameters
-- and its body, feed the bodies to 'parseMacroBody', and compare the results
-- against the golden file @test/fixtures/macros.golden@.
--
-- Golden file can be regenerated using the @--accept@ CLI option.
module Test.CExpr.Parse.Golden (tests) where
import Data.ByteString.Lazy.Char8 qualified as LBS
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Vec.Lazy qualified as Vec
import System.FilePath ((</>))
import System.IO.Unsafe (unsafePerformIO)
import Test.Tasty (TestName, TestTree, testGroup)
import Test.Tasty.Golden (goldenVsString)
import C.Expr.Parse
import C.Expr.Syntax
import Clang.Args
import Clang.CStandard
import Clang.Enum.Bitfield
import Clang.Enum.Simple
import Clang.HighLevel qualified as HighLevel
import Clang.HighLevel.Types
import Clang.LowLevel.Core
import Clang.Version
import Paths_c_expr_dsl (getDataDir)
{-------------------------------------------------------------------------------
Top-level
-------------------------------------------------------------------------------}
data TestCStandard = CExprC17 | CExprC23
testCStandardToCStandard :: TestCStandard -> CStandard
testCStandardToCStandard = \case
CExprC17 -> C17
CExprC23 -> C23
testCStandardToClangArg :: TestCStandard -> String
testCStandardToClangArg = \case
CExprC17 -> "-std=c17"
CExprC23 -> "-std=c2x"
tests :: TestTree
tests = testGroup "Parse.Golden" $ goldenWith CExprC17 : mbC23
where
mbC23 =
case runtimeClangVersion of
ClangVersion x | x >= (15,0,0) -> [goldenWith CExprC23]
_ -> []
goldenWith :: TestCStandard -> TestTree
goldenWith testCStd =
goldenDynamic ("macros-" <> show cStd)
(datadir </> "macros." <> show cStd <> ".golden")
(parseMacrosFixture testCStd (datadir </> "macros.h"))
where
cStd = testCStandardToCStandard testCStd
{-# NOINLINE datadir #-}
datadir :: FilePath
datadir = unsafePerformIO getDataDir
-- | Minimal golden test using an 'IO' action to resolve the golden file path.
--
-- If the golden file does not yet exist it is created and the test fails so
-- that the developer can inspect and commit it. If it does exist the actual
-- output is compared byte-for-byte; on a mismatch the test fails with a hint
-- about how to regenerate the file.
goldenDynamic ::
TestName
-> FilePath -- ^ path to the golden file
-> IO LBS.ByteString -- ^ action producing the actual output
-> TestTree
goldenDynamic = goldenVsString
{-------------------------------------------------------------------------------
Run the parser on all macros in the fixture file
-------------------------------------------------------------------------------}
parseMacrosFixture :: TestCStandard -> FilePath -> IO LBS.ByteString
parseMacrosFixture testCStd fixturePath = do
macroTokens <- collectMacroTokens testCStd fixturePath
return $ LBS.pack $ unlines $ map formatEntry macroTokens
where
cStd :: ClangCStandard
cStd = ClangCStandard (testCStandardToCStandard testCStd) DisableGnu
-- A split failure is reported separately from a parse failure: the splitter
-- is test-local, so a regression in it must not masquerade as a parser
-- result.
formatEntry :: (Text, [Token SourcePath TokenSpelling]) -> String
formatEntry (name, tokens) =
Text.unpack name ++ ": " ++
case splitMacro tokens of
Nothing -> "Left <split error>"
Just (params, body) ->
Vec.reifyList params $ \params' ->
case runParser "<parseMacrosFixture" (parseMacroBody cStd params') body of
Right expr -> "Right " ++ show expr
Left _ -> "Left <parse error>"
{-------------------------------------------------------------------------------
Splitting macro definitions
Test-local and deliberately minimal: @hs-bindgen@ owns the real splitter
(@HsBindgen.Macro.Syntax.splitMacro@), which reports errors and handles
variadic macros. This one exists only so that these golden tests can keep
driving real @#define@s through libclang. Do not promote it into the library.
-------------------------------------------------------------------------------}
-- | Split a macro definition into its formal parameters and its body
--
-- The parameters are returned in source order. 'Nothing' means the definition
-- is not one we can represent: a name that is neither an identifier nor a
-- keyword, a malformed parameter list, or a variadic macro.
splitMacro ::
[Token SourcePath TokenSpelling]
-> Maybe ([Identifier], [Token SourcePath TokenSpelling])
splitMacro [] = Nothing
splitMacro (name:tokens)
| not (isMacroName name) = Nothing
| otherwise =
case tokens of
-- A macro is function-like only when the opening parenthesis follows
-- the name with no whitespace in between. See issue #1903:
-- <https://github.com/well-typed/hs-bindgen/issues/1903>
t:ts | adjacent name t, isPunctuation "(" t -> paramList ts
_otherwise -> Just ([], tokens)
where
paramList ::
[Token SourcePath TokenSpelling]
-> Maybe ([Identifier], [Token SourcePath TokenSpelling])
paramList (t:ts) | isPunctuation ")" t = Just ([], ts)
paramList ts = go [] ts
go ::
[Identifier]
-> [Token SourcePath TokenSpelling]
-> Maybe ([Identifier], [Token SourcePath TokenSpelling])
go acc (t:u:us)
| Just param <- macroParam t
= if | isPunctuation "," u -> go (param:acc) us
| isPunctuation ")" u -> Just (reverse (param:acc), us)
| otherwise -> Nothing
go _ _
= Nothing
-- | Macro names may be keywords (@#define bool int@ is valid C)
isMacroName :: Token SourcePath TokenSpelling -> Bool
isMacroName t = case fromSimpleEnum (tokenKind t) of
Right CXToken_Identifier -> True
Right CXToken_Keyword -> True
_otherwise -> False
-- | Parameter names may be keywords, too (@#define F(bool) bool@)
--
-- Which spellings @libclang@ classifies as 'CXToken_Keyword' depends on the C
-- standard in force, so the kind must not decide what counts as a parameter.
macroParam :: Token SourcePath TokenSpelling -> Maybe Identifier
macroParam t = case fromSimpleEnum (tokenKind t) of
Right CXToken_Identifier -> Just name
Right CXToken_Keyword -> Just name
_otherwise -> Nothing
where
name = Identifier (getTokenSpelling (tokenSpelling t))
isPunctuation :: String -> Token SourcePath TokenSpelling -> Bool
isPunctuation expected t =
fromSimpleEnum (tokenKind t) == Right CXToken_Punctuation
&& removeMultilines (Text.unpack (getTokenSpelling (tokenSpelling t))) == expected
-- | Are the two tokens adjacent in the source, with no whitespace in between?
adjacent ::
Token SourcePath TokenSpelling
-> Token SourcePath TokenSpelling
-> Bool
adjacent prev next =
singleLocPath end == singleLocPath start
&& singleLocLine end == singleLocLine start
&& singleLocColumn end == singleLocColumn start
where
end = rangeEnd $ multiLocExpansion <$> tokenExtent prev
start = rangeStart $ multiLocExpansion <$> tokenExtent next
-- | Drop line continuations, which libclang sometimes leaves inside a token
-- spelling
removeMultilines :: String -> String
removeMultilines = \case
'\\':'\n':cs -> removeMultilines cs
c:cs -> c : removeMultilines cs
[] -> []
{-------------------------------------------------------------------------------
Collect macro definitions from a C header file via libclang
-------------------------------------------------------------------------------}
collectMacroTokens ::
TestCStandard
-> FilePath
-> IO [(Text, [Token SourcePath TokenSpelling])]
collectMacroTokens testCStd path =
HighLevel.withIndex DontDisplayDiagnostics $ \index ->
HighLevel.withTranslationUnit index src noArgs [] flags $ \unit -> do
root <- clang_getTranslationUnitCursor unit
HighLevel.clang_visitChildren root (macroFold unit)
where
src :: Maybe SourcePath
src = Just $ SourcePath $ Text.pack path
noArgs :: ClangArgs
noArgs = ClangArgs [testCStandardToClangArg testCStd]
flags :: BitfieldEnum CXTranslationUnit_Flags
flags = bitfieldEnum [CXTranslationUnit_DetailedPreprocessingRecord]
macroFold ::
CXTranslationUnit
-> Fold IO (Text, [Token SourcePath TokenSpelling])
macroFold unit = simpleFold $ \cursor -> do
loc <- clang_getCursorLocation cursor
inMain <- clang_Location_isFromMainFile loc
if not inMain
then foldContinue
else do
kind <- fromSimpleEnum <$> clang_getCursorKind cursor
case kind of
Right CXCursor_MacroDefinition -> do
name <- clang_getCursorSpelling cursor
tokens <- HighLevel.clang_tokenize unit =<< clang_getCursorExtent cursor
foldContinueWith (name, tokens)
_ ->
foldContinue