wireform-proto-0.1.0.0: test-integration/Test/Hooks.hs
module Test.Hooks (hooksTests) where
import Data.Map.Strict qualified as Map
import Data.Text (Text)
import Data.Text qualified as T
import Proto.CodeGen
import Proto.CodeGen.Hooks
import Proto.IDL.AST
import Proto.IDL.Parser
import Test.Tasty
import Test.Tasty.HUnit
hooksTests :: TestTree
hooksTests =
testGroup
"CodeGen Hooks"
[ testGroup
"Hook context construction"
[ testCase "message hook receives correct type name" $ do
let hook =
mempty
{ onMessageCodeGen = \ctx ->
["-- type: " <> mhcHsTypeName ctx]
}
opts = defaultGenerateOpts {genHooks = hook}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "package test;"
, "message Person {"
, " string name = 1;"
, "}"
]
code = generateModuleText opts Map.empty "<test>" pf
assertBool
"Hook output should appear in generated code"
(T.isInfixOf "-- type: Person" code)
, testCase "message hook receives FQ proto name" $ do
let hook =
mempty
{ onMessageCodeGen = \ctx ->
["-- fq: " <> mhcFqProtoName ctx]
}
opts = defaultGenerateOpts {genHooks = hook}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "package my.pkg;"
, "message Foo {"
, " int32 x = 1;"
, "}"
]
code = generateModuleText opts Map.empty "<test>" pf
assertBool
"Hook output should contain FQ name"
(T.isInfixOf "-- fq: my.pkg.Foo" code)
, testCase "enum hook receives correct type name" $ do
let hook =
mempty
{ onEnumCodeGen = \ctx ->
["-- enum: " <> ehcHsTypeName ctx]
}
opts = defaultGenerateOpts {genHooks = hook}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "enum Status {"
, " UNKNOWN = 0;"
, " ACTIVE = 1;"
, "}"
]
code = generateModuleText opts Map.empty "<test>" pf
assertBool
"Hook output should appear for enum"
(T.isInfixOf "-- enum: Status" code)
, testCase "file hook receives module name" $ do
let hook =
mempty
{ onFileCodeGen = \ctx ->
["-- module: " <> fhcModuleName ctx]
}
opts = defaultGenerateOpts {genHooks = hook}
pf = parseOrDie "syntax = \"proto3\";\n"
code = generateModuleText opts Map.empty "<test>" pf
assertBool
"Hook output should appear at file level"
(T.isInfixOf "-- module: Proto.Gen" code)
]
, testGroup
"Attribute-driven hooks"
[ testCase "onMessageAttribute fires when attribute present" $ do
let hook = onMessageAttribute "my_custom" $ \val ctx ->
case val of
CBool True -> ["-- custom triggered on " <> mhcHsTypeName ctx]
_ -> []
opts = defaultGenerateOpts {genHooks = hook}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Annotated {"
, " option (my_custom) = true;"
, " string name = 1;"
, "}"
]
code = generateModuleText opts Map.empty "<test>" pf
assertBool
"Attribute hook should fire"
(T.isInfixOf "-- custom triggered on Annotated" code)
, testCase "onMessageAttribute does not fire without attribute" $ do
let hook = onMessageAttribute "my_custom" $ \_ ctx ->
["-- should not appear for " <> mhcHsTypeName ctx]
opts = defaultGenerateOpts {genHooks = hook}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Plain {"
, " string name = 1;"
, "}"
]
code = generateModuleText opts Map.empty "<test>" pf
assertBool
"Attribute hook should not fire"
(not (T.isInfixOf "-- should not appear" code))
, testCase "onEnumAttribute fires with matching option" $ do
let hook = onEnumAttribute "special_enum" $ \val ctx ->
case val of
CString s -> ["-- special: " <> s <> " on " <> ehcHsTypeName ctx]
_ -> []
opts = defaultGenerateOpts {genHooks = hook}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "enum Color {"
, " option (special_enum) = \"rainbow\";"
, " RED = 0;"
, " GREEN = 1;"
, "}"
]
code = generateModuleText opts Map.empty "<test>" pf
assertBool
"Enum attribute hook should fire"
(T.isInfixOf "-- special: rainbow on Color" code)
, testCase "onFileAttribute fires with matching file option" $ do
let hook = onFileAttribute "codegen_extra" $ \val ctx ->
case val of
CBool True -> ["-- extra codegen for " <> fhcModuleName ctx]
_ -> []
opts = defaultGenerateOpts {genHooks = hook}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "option (codegen_extra) = true;"
]
code = generateModuleText opts Map.empty "<test>" pf
assertBool
"File attribute hook should fire"
(T.isInfixOf "-- extra codegen for" code)
]
, testGroup
"Hook composition"
[ testCase "composed hooks both produce output" $ do
let hook1 =
mempty
{ onMessageCodeGen = \ctx ->
["-- hook1: " <> mhcHsTypeName ctx]
}
hook2 =
mempty
{ onMessageCodeGen = \ctx ->
["-- hook2: " <> mhcHsTypeName ctx]
}
opts = defaultGenerateOpts {genHooks = hook1 <> hook2}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg {"
, " int32 x = 1;"
, "}"
]
code = generateModuleText opts Map.empty "<test>" pf
assertBool "hook1 output present" (T.isInfixOf "-- hook1: Msg" code)
assertBool "hook2 output present" (T.isInfixOf "-- hook2: Msg" code)
, testCase "mempty produces no extra output" $ do
let opts1 = defaultGenerateOpts
opts2 = defaultGenerateOpts {genHooks = mempty}
pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message M { int32 x = 1; }"
]
code1 = generateModuleText opts1 Map.empty "<test>" pf
code2 = generateModuleText opts2 Map.empty "<test>" pf
code1 @?= code2
]
, testGroup
"Attribute query helpers"
[ testCase "lookupAttribute finds extension option" $ do
let opts = [OptionDef () (OptionName [ExtensionOption "my_opt"]) (CInt 42)]
lookupAttribute "my_opt" opts @?= Just (CInt 42)
, testCase "lookupAttribute returns Nothing for missing" $ do
let opts = [OptionDef () (OptionName [SimpleOption "java_package"]) (CString "com.example")]
lookupAttribute "java_package" opts @?= Nothing
, testCase "hasAttribute" $ do
let opts = [OptionDef () (OptionName [ExtensionOption "present"]) (CBool True)]
hasAttribute "present" opts @?= True
hasAttribute "absent" opts @?= False
, testCase "attributeAsText" $ do
let opts = [OptionDef () (OptionName [ExtensionOption "tag"]) (CString "hello")]
attributeAsText "tag" opts @?= Just "hello"
attributeAsText "missing" opts @?= Nothing
, testCase "attributeAsBool" $ do
let opts = [OptionDef () (OptionName [ExtensionOption "flag"]) (CBool True)]
attributeAsBool "flag" opts @?= Just True
, testCase "attributeAsInt" $ do
let opts = [OptionDef () (OptionName [ExtensionOption "count"]) (CInt 99)]
attributeAsInt "count" opts @?= Just 99
, testCase "attributeAsFloat" $ do
let opts = [OptionDef () (OptionName [ExtensionOption "rate"]) (CFloat 3.14)]
attributeAsFloat "rate" opts @?= Just 3.14
, testCase "attributeAsAggregate" $ do
let agg = [("key", CString "val")]
opts = [OptionDef () (OptionName [ExtensionOption "meta"]) (CAggregate agg)]
attributeAsAggregate "meta" opts @?= Just agg
, testCase "type mismatch returns Nothing" $ do
let opts = [OptionDef () (OptionName [ExtensionOption "x"]) (CInt 1)]
attributeAsText "x" opts @?= Nothing
attributeAsBool "x" opts @?= Nothing
]
, testGroup
"messageOptions extraction"
[ testCase "extracts MEOption from message elements" $ do
let pf =
parseOrDie $
T.unlines
[ "syntax = \"proto3\";"
, "message Msg {"
, " option (my_opt) = true;"
, " option deprecated = true;"
, " string name = 1;"
, "}"
]
case protoTopLevels pf of
[TLMessage msg] -> do
let opts = messageOptions msg
length opts @?= 2
_ -> assertFailure "expected one message"
]
, testGroup
"THHooks construction"
[ testCase "defaultTHHooks is mempty" $ do
let h1 = defaultTHHooks
h2 = mempty :: THHooks
-- Both should be constructible (type-checks)
assertBool "defaultTHHooks should be the identity" True
, testCase "THHooks compose with <>" $ do
let h1 = defaultTHHooks
h2 = defaultTHHooks
combined = h1 <> h2
assertBool "THHooks should compose" True
, testCase "thOnMessageAttribute constructs without error" $ do
let hook = thOnMessageAttribute "my_attr" $ \_val _ctx ->
pure []
assertBool "thOnMessageAttribute should construct" True
, testCase "thOnEnumAttribute constructs without error" $ do
let hook = thOnEnumAttribute "my_attr" $ \_val _ctx ->
pure []
assertBool "thOnEnumAttribute should construct" True
, testCase "thOnFileAttribute constructs without error" $ do
let hook = thOnFileAttribute "my_attr" $ \_val _ctx ->
pure []
assertBool "thOnFileAttribute should construct" True
]
]
parseOrDie :: Text -> ProtoFile
parseOrDie src = case parseProtoFile "<test>" src of
Left err -> error ("Parse failed: " <> show err)
Right pf -> pf