aihc-cpp-1.0.0.0: test/Spec.hs
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Aihc.Cpp (Config (..), Result (..), Step (..), defaultConfig, preprocess)
import qualified Data.Map.Strict as M
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Test.Progress (CaseMeta (..), Outcome (..), evaluateCase, loadManifest)
import Test.Tasty (TestTree, defaultMain, testGroup)
import Test.Tasty.HUnit (Assertion, assertFailure, testCase)
import qualified Test.Tasty.QuickCheck as QC
main :: IO ()
main = do
cases <- loadManifest
checks <- mapM mkCase cases
defaultMain
( testGroup
"cpp-oracle"
( checks
<> [linePragmaTest, dateTimeTest, functionMacroArgumentTest, functionMacroUnclosedCallTest, definedConditionSpacingTest, stringContinuationTests, tokenPastingTests, ccallLineCommentTest]
<> [pragmaOnceTest]
<> [QC.testProperty "dummy quickcheck property" prop_dummy]
)
)
-- | Dummy QuickCheck property that always passes.
-- Added so that --quickcheck-tests flag is accepted by the test suite.
prop_dummy :: Bool
prop_dummy = True
dateTimeTest :: TestTree
dateTimeTest =
testGroup
"__DATE__ and __TIME__"
[ testCase "expands to provided values" $ do
let cfg =
defaultConfig
{ configMacros =
M.fromList
[ ("__DATE__", "\"Mar 15 2026\""),
("__TIME__", "\"12:00:00\"")
]
}
input = TE.encodeUtf8 "__DATE__ __TIME__"
case preprocess cfg input of
Done result ->
resultOutput result @?= "#line 1 \"<input>\"\n\"Mar 15 2026\" \"12:00:00\"\n"
_ -> assertFailure "expected Done",
testCase "defaults to unix epoch" $ do
let cfg = defaultConfig
input = TE.encodeUtf8 "__DATE__ __TIME__"
case preprocess cfg input of
Done result ->
resultOutput result @?= "#line 1 \"<input>\"\n\"Jan 1 1970\" \"00:00:00\"\n"
_ -> assertFailure "expected Done"
]
(@?=) :: (Eq a, Show a) => a -> a -> Assertion
actual @?= expected =
if actual == expected
then pure ()
else assertFailure ("expected: " <> show expected <> "\n but got: " <> show actual)
mkCase :: CaseMeta -> IO TestTree
mkCase meta =
pure $ testCase (caseId meta) (assertCase meta)
assertCase :: CaseMeta -> Assertion
assertCase meta = do
(_, outcome, details) <- evaluateCase meta
case outcome of
OutcomeFail ->
assertFailure
( "cpp regression in "
<> caseId meta
<> " ["
<> caseCategory meta
<> "]: "
<> details
)
_ -> pure ()
linePragmaTest :: TestTree
linePragmaTest =
testCase "include emits line pragmas" $
case preprocess defaultConfig {configInputFile = "root.hs"} (TE.encodeUtf8 "before\n#include \"nested.inc\"\nafter") of
NeedInclude _ k ->
case k (Just "inside") of
Done result -> do
let out = T.lines (resultOutput result)
hasIncludePragma = any (T.isSuffixOf "nested.inc\"") out
if hasIncludePragma && "#line 3 \"root.hs\"" `elem` out
then pure ()
else assertFailure "expected include line pragmas in output"
NeedInclude {} -> assertFailure "unexpected nested include in line pragma test"
Done _ -> assertFailure "expected include continuation step"
functionMacroArgumentTest :: TestTree
functionMacroArgumentTest =
testCase "function-like macro keeps nested argument text" $
case preprocess defaultConfig (TE.encodeUtf8 "#define PAIR(x,y) x + y\nPAIR((1 + 2), 3)") of
Done result ->
resultOutput result @?= "#line 1 \"<input>\"\n\n(1 + 2) + 3\n"
_ -> assertFailure "expected Done"
functionMacroUnclosedCallTest :: TestTree
functionMacroUnclosedCallTest =
testCase "unterminated function-like call does not expand macro" $
case preprocess defaultConfig (TE.encodeUtf8 "#define ID() replaced\nID(") of
Done result ->
resultOutput result @?= "#line 1 \"<input>\"\n\nID(\n"
_ -> assertFailure "expected Done"
definedConditionSpacingTest :: TestTree
definedConditionSpacingTest =
testCase "defined handles whitespace around parenthesized name" $
case preprocess defaultConfig (TE.encodeUtf8 "#define FLAG 1\n#if defined ( FLAG )\nok\n#else\nbad\n#endif") of
Done result ->
if "ok\n" `T.isInfixOf` resultOutput result && not ("bad\n" `T.isInfixOf` resultOutput result)
then pure ()
else assertFailure ("expected ok branch to be active, output was: " <> show (resultOutput result))
_ -> assertFailure "expected Done"
stringContinuationTests :: TestTree
stringContinuationTests =
testGroup
"Haskell string continuations"
[ testCase "ordinary string accepts GCC double-backslash continuation" $
assertPreprocessOutput
gccStringContinuationInput
(T.unlines ["#line 1 \"<input>\"", "x = \"a\\ \\b\""]),
testCase "ordinary string preserves GHC single-backslash gap" $
assertPreprocessOutput
ghcStringGapInput
(T.unlines ["#line 1 \"<input>\"", "x = \"a\\", " \\b\""]),
testCase "function macro argument accepts GCC double-backslash continuation" $
assertPreprocessOutput
gccStringContinuationMacroInput
(T.unlines ["#line 1 \"<input>\"", "", "x = \"a\\ \\b\""]),
testCase "function macro argument preserves GHC single-backslash gap" $
assertPreprocessOutput
ghcStringGapMacroInput
(T.unlines ["#line 1 \"<input>\"", "", "x = \"a\\", " \\b\""]),
testCase "GCC continuation tracks string gaps across concatenation" $
assertPreprocessOutput
gccStringContinuationConcatInput
(T.unlines ["#line 1 \"<input>\"", "x = \"a\\ \\\" <> y <> \"\\n\\ \\b\""]),
testCase "double backslash outside strings is not line-spliced" $
assertPreprocessOutput
nonStringDoubleBackslashInput
(T.unlines ["#line 1 \"<input>\"", "", "x = foo \\\\", " bar"])
]
assertPreprocessOutput :: T.Text -> T.Text -> Assertion
assertPreprocessOutput input expected =
case preprocess defaultConfig (TE.encodeUtf8 input) of
Done result -> resultOutput result @?= expected
_ -> assertFailure "expected Done"
tokenPastingTests :: TestTree
tokenPastingTests =
testGroup
"token pasting"
[ testCase "CCALL macro expands stringizing and token pasting" $
case preprocess defaultConfig (TE.encodeUtf8 ccallMacroInput) of
Done result ->
if "foreign import ccall unsafe \"foo\"" `T.isInfixOf` resultOutput result
&& "c_foo :: Int -> IO Int" `T.isInfixOf` resultOutput result
then pure ()
else assertFailure ("expected CCALL expansion in output, got: " <> show (resultOutput result))
_ -> assertFailure "expected Done",
testCase "token pasting joins both sides without expanding arguments first" $
case preprocess defaultConfig (TE.encodeUtf8 tokenPasteRawArgInput) of
Done result ->
resultOutput result @?= "#line 1 \"<input>\"\n\n\nXY\n"
_ -> assertFailure "expected Done",
testCase "token pasting result is rescanned for further macro expansion" $
case preprocess defaultConfig (TE.encodeUtf8 tokenPasteRescanInput) of
Done result ->
resultOutput result @?= "#line 1 \"<input>\"\n\n\n42\n"
_ -> assertFailure "expected Done",
testCase "token pasting supports prefix and suffix forms" $
case preprocess defaultConfig (TE.encodeUtf8 tokenPasteAffixInput) of
Done result ->
resultOutput result @?= "#line 1 \"<input>\"\n\n\nleft right\n"
_ -> assertFailure "expected Done",
testCase "token pasting supports chained concatenation" $
case preprocess defaultConfig (TE.encodeUtf8 tokenPasteChainedInput) of
Done result ->
resultOutput result @?= "#line 1 \"<input>\"\n\nfoobar\n"
_ -> assertFailure "expected Done",
testCase "token pasting survives Haskell block comments in arguments" $
case preprocess defaultConfig (TE.encodeUtf8 tokenPasteHsCommentInput) of
Done result ->
resultOutput result
@?= "#line 1 \"<input>\"\n\n{-# INLINE _bar #-}; _bar :: LensP Foo Baz{-comment-}; _bar = lens bar $ \\ Foo {..} bar_ -> Foo {bar = bar_, ..}\n"
_ -> assertFailure "expected Done"
]
ccallLineCommentTest :: TestTree
ccallLineCommentTest =
testCase "CCALL macro with -- comment in argument list" $
case preprocess defaultConfig (TE.encodeUtf8 ccallLineCommentInput) of
Done result ->
if "foreign import ccall unsafe \"xls_wb_sheetcount\"" `T.isInfixOf` resultOutput result
&& "c_xls_wb_sheetcount :: XLSWorkbook -> IO CInt" `T.isInfixOf` resultOutput result
&& " -- Int32" `T.isInfixOf` resultOutput result
then pure ()
else assertFailure ("expected CCALL expansion with line comment, got: " <> show (resultOutput result))
_ -> assertFailure "expected Done"
pragmaOnceTest :: TestTree
pragmaOnceTest =
testCase "#pragma once skips repeated includes" $
case preprocess defaultConfig {configInputFile = "root.hs"} (TE.encodeUtf8 "#include \"guarded.inc\"\n#include \"guarded.inc\"\nafter") of
NeedInclude _ k1 ->
case k1 (Just "#pragma once\ninside") of
NeedInclude {} -> assertFailure "second include should be skipped"
Done result -> do
let output = resultOutput result
if T.count "inside" output == 1 && "after\n" `T.isSuffixOf` output
then pure ()
else assertFailure ("expected guarded include once, got: " <> show output)
Done _ -> assertFailure "expected include continuation step"
ccallLineCommentInput :: T.Text
ccallLineCommentInput =
T.unlines
[ "#define CCALL(name,signature) \\",
"foreign import ccall unsafe #name \\",
" c_##name :: signature",
"",
"CCALL(xls_wb_sheetcount, XLSWorkbook -> IO CInt -- Int32)"
]
ccallMacroInput :: T.Text
ccallMacroInput =
T.unlines
[ "#define CCALL(name,signature) \\",
"foreign import ccall unsafe #name \\",
" c_##name :: signature",
"",
"CCALL(foo, Int -> IO Int)"
]
tokenPasteRawArgInput :: T.Text
tokenPasteRawArgInput =
T.unlines
[ "#define X Y",
"#define JOIN(a,b) a##b",
"JOIN(X,Y)"
]
tokenPasteRescanInput :: T.Text
tokenPasteRescanInput =
T.unlines
[ "#define VALUE 42",
"#define JOIN(a,b) a##b",
"JOIN(VAL,UE)"
]
tokenPasteAffixInput :: T.Text
tokenPasteAffixInput =
T.unlines
[ "#define PREFIX(name) left##name",
"#define SUFFIX(name) name##right",
"PREFIX() SUFFIX()"
]
tokenPasteChainedInput :: T.Text
tokenPasteChainedInput =
T.unlines
[ "#define CHAIN(a,b,c) a##b##c",
"CHAIN(foo,bar,)"
]
tokenPasteHsCommentInput :: T.Text
tokenPasteHsCommentInput =
T.unlines
[ "#define LENS(S,F,A) {-# INLINE _/**/F #-}; _/**/F :: LensP S A; _/**/F = lens F $ \\ S {..} F/**/_ -> S {F = F/**/_, ..}",
"LENS(Foo,bar,Baz{-comment-})"
]
gccStringContinuationInput :: T.Text
gccStringContinuationInput =
T.unlines
[ "x = \"a\\\\",
" \\b\""
]
ghcStringGapInput :: T.Text
ghcStringGapInput =
T.unlines
[ "x = \"a\\",
" \\b\""
]
gccStringContinuationMacroInput :: T.Text
gccStringContinuationMacroInput =
T.unlines
[ "#define ID(x) x",
"x = ID(\"a\\\\",
" \\b\")"
]
ghcStringGapMacroInput :: T.Text
ghcStringGapMacroInput =
T.unlines
[ "#define ID(x) x",
"x = ID(\"a\\",
" \\b\")"
]
gccStringContinuationConcatInput :: T.Text
gccStringContinuationConcatInput =
T.unlines
[ "x = \"a\\\\",
" \\\" <> y <> \"\\n\\\\",
" \\b\""
]
nonStringDoubleBackslashInput :: T.Text
nonStringDoubleBackslashInput =
T.unlines
[ "#define ID(x) x",
"x = ID(foo \\\\",
" bar)"
]