HTF 不测试 TH 生成的道具
HTF does not test props generated by TH
我想对我的图书馆中的各种类型做一些类似的测试。
为了简化事情,假设我有许多向量类型实现 Num
class,并且我想生成相同的 QuickCheck 属性 检查 prop_absNorm x y = abs x + abs y >= abs (x+y)
处理库中的所有类型。
我使用 TH:
生成这样的属性
$(writeTests
(\t ->
[d| prop_absNorm :: $(t) -> $(t) -> Bool
prop_absNorm x y = abs x + abs y >= abs (x+y)
|])
)
我生成测试的函数具有以下签名:
writeTests :: (TypeQ -> Q [Dec]) -> Q [Dec]
此函数通过 reify ''VectorMath
查找我的向量 class VectorMath (n::Nat) t
的所有实例(同时查找 Num
的实例)并生成所有 prop相应地发挥作用。
-ddump-splices
显示如下:
prop_absNormIntX4 :: Vector 4 Int -> Vector 4 Int -> Bool
prop_absNormIntX4 x y = abs x + abs y >= abs (x+y)
prop_absNormCIntX4 :: Vector 4 CInt -> Vector 4 CInt -> Bool
prop_absNormCIntX4 x y = abs x + abs y >= abs (x+y)
...
prop_absNormFloatX4 :: Vector 4 Float -> Vector 4 Float -> Bool
prop_absNormFloatX4 x y = abs x + abs y >= abs (x+y)
prop_absNormFloatX3 :: Vector 3 Float -> Vector 3 Float -> Bool
prop_absNormFloatX3 x y = abs x + abs y >= abs (x+y)
问题是所有手动编写的属性都被检查,但是生成的没有。
注意 1:我在同一文件中生成和未生成属性(即 TH 表达式 $(..)
与其他道具在同一文件中)。
注意 2:用于创建 prop 函数的类型列表是可变的 - 我想稍后添加 VectorMath
的其他实例,因此它们会自动添加到测试列表中。
我认为问题在于 HTF(可能也使用 TH)解析原始文件,而不是生成代码的文件 - 但我不明白为什么会这样。
所以我的问题是:如何解决这个问题?如果不能使用 TH 生成的道具,那么是否可以对各种类型进行 QuickCheck 测试(即将它们替换为 prop_absNorm :: Vector 4 a -> Vector 4 a -> Bool
)?
另一种方法可能是进一步使用 TH 手动将测试条目添加到 htf_Main,但我还没有想出如何做到这一点;
它看起来不像一个干净的解决方案。
如果您事先知道生成的 属性 测试的名称是什么,那么您总是可以手动定义存根以便 HTF 看到它们,例如:
$(generate prop test for Int)
$(generate prop test for CInt)
prop_p1 = prop_absNormInt
prop_p2 = prop_absNormCInt
HTF 会将测试视为 prop_p1
和 prop_p2
。您不必在这些存根上放置类型签名。
另一个想法是创建您自己的源预处理器来为您添加这些存根(并给它们更好的名字)。您的源预处理器会自动调用 htfpp
来完成预处理。
如果你告诉我你的 TH 是如何调用的,我可以告诉你如何编写预处理器。
更新:
根据您的评论,我会考虑执行以下操作:
- 编写程序生成测试模块源码。
- 在您的 cabal 项目中包含该程序及其生成的输出。
- 如果用户想要更新测试模块,请告诉他们 运行 程序。
因此 - 测试用例保持不变,直到程序 运行 重新生成测试模块。
拥有静态测试模块的好处是您可以准确判断正在测试的内容。
有了重新创建测试模块的程序,您就可以在新的 Num 实例可用时轻松更新它。
好的,我设法解决了这个问题。
这个想法是使用 TH 聚合测试并将它们插入 htfMain
。
除了我的问题之外,这还包括以下步骤:
- 将所有可测试属性转换为
IO
操作 运行 QuickCheck 测试;
- 将所有测试汇总到
TestSuite
;
- 将所有测试套件聚合到一个列表中并放入
htfMain
。
为了使用第 1 步,我不得不使用名为 qcAssertion :: (QCAssertion t) => t -> Assertion
的 HTF 的半内部函数。
该功能可用,但不建议外用;它允许 运行 QuickCheck 测试很好,并将它们集成到报告中。
为了继续第 2 步,我使用了 HTF 中的两个函数:makeTestSuite
和 makeQuickCheckTest
。
我还使用 TH 中的 location
函数来提供文件名和插入带有测试模板的拼接位置的行(为了更好的测试日志)。
第 3 步是一个棘手的步骤:为此我们需要找到所有生成的测试套件。
问题是 TH 不允许浏览模块中的所有函数(包括生成的函数)。
为了克服这个问题,我添加了以下类型 class:
class MultitypeTestSuite name where
multitypeTestSuite :: name -> TestSuite
所以我的函数 writeTests
生成了一个新数据类型 data MTS[prop_name]
和该数据类型的 MultitypeTestSuite
实例。
这允许我稍后在 htfMain 中使用另一个拼接函数,它将使用 reify
:
从 class 的实例中生成测试套件列表
aggregateTests :: ExpQ
aggregateTests = do
ClassI _ instances <- reify ''MultitypeTestSuite
liftM ListE . forM instances
$ \... -> [e| multitypeTestSuite $(...) |]
最后,包括所有生成的测试以及手动编写的测试看起来非常简单:
main :: IO ()
main = htfMain $ htf_importedTests ++ $(aggregateTests)
因此,通过调整函数 $(writeTests)
,我现在能够生成和测试参数类型不同的属性 - 对于同一类型范围内可用的所有类型。
测试结果和日志的包含方式与原始测试相同。
至此问题完全解决
HTF 不使用 TemplateHaskell 来收集测试,这会显着减慢编译时间。相反,HTF 使用名为 htfpp
的自定义预处理器。 htfpp
在 编译器之前运行(因此在扩展 TemplateHaskell 拼接之前)。这意味着在使用 TemplateHaskell 生成测试时,您不能使用 htfpp
的自动测试发现。
我的建议:无论如何,当您使用 TemplateHaskell 时,只需使用 TemplateHaskell 来收集您生成的测试用例。 HTF 没有内置此功能,但实现这样的功能并不困难。在这里:
-- file TH.hs
{-# LANGUAGE TemplateHaskell #-}
module TH ( genTestSuiteFromQcProps ) where
import Language.Haskell.TH
import Test.Framework
import Test.Framework.Location
genTestSuiteFromQcProps :: String -> [Name] -> Q Exp
genTestSuiteFromQcProps suiteName names =
[| makeTestSuite $(stringE suiteName) $(listE genTests) |]
where
genTests :: [ExpQ]
genTests =
map genTest names
genTest :: Name -> Q Exp
genTest name =
[| makeQuickCheckTest $(stringE (show name)) unknownLocation
(qcAssertion $(varE name)) |]
函数 genTestSuiteFromQcProps
获取要生成的测试套件的名称和名称列表,参考您的 QC 属性。 genTestSuiteFromQcProps
returns 类型 TestSuite
的表达式。 TestSuite 是 HTF 用来组织测试的一种类型。
(htfpp
预处理器 als 在其输出中使用 TestSuite
类型。)
这里是你如何使用 genTestSuiteFromQcProps
:
-- file Main.hs
{-# OPTIONS_GHC -F -pgmF htfpp #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where
import TH
import Test.Framework
import {-@ HTF_TESTS @-} OtherTests
prop_additionCommutative :: Int -> Int -> Bool
prop_additionCommutative x y = (x + y) == (y + x)
prop_reverseReverseIdentity :: [Int] -> Bool
prop_reverseReverseIdentity l = l == reverse (reverse l)
myTestSuite :: TestSuite
myTestSuite =
$(genTestSuiteFromQcProps
"MyTestSuite"
['prop_additionCommutative
,'prop_reverseReverseIdentity])
main :: IO ()
main = htfMain (myTestSuite : htf_importedTests)
对于您的情况,您将传递 genTestSuiteFromQcProps
您使用 TemplateHaskell 生成的 QC 属性的名称。
该示例还表明,您可以将使用 TemplateHaskell 函数生成的测试用例与 htfpp
收集的测试用例混合使用。为了完整起见,这里是OtherTests
的内容:
{-# OPTIONS_GHC -F -pgmF htfpp #-}
module OtherTests ( htf_thisModulesTests) where
import Test.Framework
test_someOtherTest :: IO ()
test_someOtherTest =
assertEqual 1 1
我想对我的图书馆中的各种类型做一些类似的测试。
为了简化事情,假设我有许多向量类型实现 Num
class,并且我想生成相同的 QuickCheck 属性 检查 prop_absNorm x y = abs x + abs y >= abs (x+y)
处理库中的所有类型。
我使用 TH:
生成这样的属性$(writeTests
(\t ->
[d| prop_absNorm :: $(t) -> $(t) -> Bool
prop_absNorm x y = abs x + abs y >= abs (x+y)
|])
)
我生成测试的函数具有以下签名:
writeTests :: (TypeQ -> Q [Dec]) -> Q [Dec]
此函数通过 reify ''VectorMath
查找我的向量 class VectorMath (n::Nat) t
的所有实例(同时查找 Num
的实例)并生成所有 prop相应地发挥作用。
-ddump-splices
显示如下:
prop_absNormIntX4 :: Vector 4 Int -> Vector 4 Int -> Bool
prop_absNormIntX4 x y = abs x + abs y >= abs (x+y)
prop_absNormCIntX4 :: Vector 4 CInt -> Vector 4 CInt -> Bool
prop_absNormCIntX4 x y = abs x + abs y >= abs (x+y)
...
prop_absNormFloatX4 :: Vector 4 Float -> Vector 4 Float -> Bool
prop_absNormFloatX4 x y = abs x + abs y >= abs (x+y)
prop_absNormFloatX3 :: Vector 3 Float -> Vector 3 Float -> Bool
prop_absNormFloatX3 x y = abs x + abs y >= abs (x+y)
问题是所有手动编写的属性都被检查,但是生成的没有。
注意 1:我在同一文件中生成和未生成属性(即 TH 表达式 $(..)
与其他道具在同一文件中)。
注意 2:用于创建 prop 函数的类型列表是可变的 - 我想稍后添加 VectorMath
的其他实例,因此它们会自动添加到测试列表中。
我认为问题在于 HTF(可能也使用 TH)解析原始文件,而不是生成代码的文件 - 但我不明白为什么会这样。
所以我的问题是:如何解决这个问题?如果不能使用 TH 生成的道具,那么是否可以对各种类型进行 QuickCheck 测试(即将它们替换为 prop_absNorm :: Vector 4 a -> Vector 4 a -> Bool
)?
另一种方法可能是进一步使用 TH 手动将测试条目添加到 htf_Main,但我还没有想出如何做到这一点; 它看起来不像一个干净的解决方案。
如果您事先知道生成的 属性 测试的名称是什么,那么您总是可以手动定义存根以便 HTF 看到它们,例如:
$(generate prop test for Int)
$(generate prop test for CInt)
prop_p1 = prop_absNormInt
prop_p2 = prop_absNormCInt
HTF 会将测试视为 prop_p1
和 prop_p2
。您不必在这些存根上放置类型签名。
另一个想法是创建您自己的源预处理器来为您添加这些存根(并给它们更好的名字)。您的源预处理器会自动调用 htfpp
来完成预处理。
如果你告诉我你的 TH 是如何调用的,我可以告诉你如何编写预处理器。
更新:
根据您的评论,我会考虑执行以下操作:
- 编写程序生成测试模块源码。
- 在您的 cabal 项目中包含该程序及其生成的输出。
- 如果用户想要更新测试模块,请告诉他们 运行 程序。
因此 - 测试用例保持不变,直到程序 运行 重新生成测试模块。
拥有静态测试模块的好处是您可以准确判断正在测试的内容。
有了重新创建测试模块的程序,您就可以在新的 Num 实例可用时轻松更新它。
好的,我设法解决了这个问题。
这个想法是使用 TH 聚合测试并将它们插入 htfMain
。
除了我的问题之外,这还包括以下步骤:
- 将所有可测试属性转换为
IO
操作 运行 QuickCheck 测试; - 将所有测试汇总到
TestSuite
; - 将所有测试套件聚合到一个列表中并放入
htfMain
。
为了使用第 1 步,我不得不使用名为 qcAssertion :: (QCAssertion t) => t -> Assertion
的 HTF 的半内部函数。
该功能可用,但不建议外用;它允许 运行 QuickCheck 测试很好,并将它们集成到报告中。
为了继续第 2 步,我使用了 HTF 中的两个函数:makeTestSuite
和 makeQuickCheckTest
。
我还使用 TH 中的 location
函数来提供文件名和插入带有测试模板的拼接位置的行(为了更好的测试日志)。
第 3 步是一个棘手的步骤:为此我们需要找到所有生成的测试套件。 问题是 TH 不允许浏览模块中的所有函数(包括生成的函数)。 为了克服这个问题,我添加了以下类型 class:
class MultitypeTestSuite name where
multitypeTestSuite :: name -> TestSuite
所以我的函数 writeTests
生成了一个新数据类型 data MTS[prop_name]
和该数据类型的 MultitypeTestSuite
实例。
这允许我稍后在 htfMain 中使用另一个拼接函数,它将使用 reify
:
aggregateTests :: ExpQ
aggregateTests = do
ClassI _ instances <- reify ''MultitypeTestSuite
liftM ListE . forM instances
$ \... -> [e| multitypeTestSuite $(...) |]
最后,包括所有生成的测试以及手动编写的测试看起来非常简单:
main :: IO ()
main = htfMain $ htf_importedTests ++ $(aggregateTests)
因此,通过调整函数 $(writeTests)
,我现在能够生成和测试参数类型不同的属性 - 对于同一类型范围内可用的所有类型。
测试结果和日志的包含方式与原始测试相同。
至此问题完全解决
HTF 不使用 TemplateHaskell 来收集测试,这会显着减慢编译时间。相反,HTF 使用名为 htfpp
的自定义预处理器。 htfpp
在 编译器之前运行(因此在扩展 TemplateHaskell 拼接之前)。这意味着在使用 TemplateHaskell 生成测试时,您不能使用 htfpp
的自动测试发现。
我的建议:无论如何,当您使用 TemplateHaskell 时,只需使用 TemplateHaskell 来收集您生成的测试用例。 HTF 没有内置此功能,但实现这样的功能并不困难。在这里:
-- file TH.hs
{-# LANGUAGE TemplateHaskell #-}
module TH ( genTestSuiteFromQcProps ) where
import Language.Haskell.TH
import Test.Framework
import Test.Framework.Location
genTestSuiteFromQcProps :: String -> [Name] -> Q Exp
genTestSuiteFromQcProps suiteName names =
[| makeTestSuite $(stringE suiteName) $(listE genTests) |]
where
genTests :: [ExpQ]
genTests =
map genTest names
genTest :: Name -> Q Exp
genTest name =
[| makeQuickCheckTest $(stringE (show name)) unknownLocation
(qcAssertion $(varE name)) |]
函数 genTestSuiteFromQcProps
获取要生成的测试套件的名称和名称列表,参考您的 QC 属性。 genTestSuiteFromQcProps
returns 类型 TestSuite
的表达式。 TestSuite 是 HTF 用来组织测试的一种类型。
(htfpp
预处理器 als 在其输出中使用 TestSuite
类型。)
这里是你如何使用 genTestSuiteFromQcProps
:
-- file Main.hs
{-# OPTIONS_GHC -F -pgmF htfpp #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where
import TH
import Test.Framework
import {-@ HTF_TESTS @-} OtherTests
prop_additionCommutative :: Int -> Int -> Bool
prop_additionCommutative x y = (x + y) == (y + x)
prop_reverseReverseIdentity :: [Int] -> Bool
prop_reverseReverseIdentity l = l == reverse (reverse l)
myTestSuite :: TestSuite
myTestSuite =
$(genTestSuiteFromQcProps
"MyTestSuite"
['prop_additionCommutative
,'prop_reverseReverseIdentity])
main :: IO ()
main = htfMain (myTestSuite : htf_importedTests)
对于您的情况,您将传递 genTestSuiteFromQcProps
您使用 TemplateHaskell 生成的 QC 属性的名称。
该示例还表明,您可以将使用 TemplateHaskell 函数生成的测试用例与 htfpp
收集的测试用例混合使用。为了完整起见,这里是OtherTests
的内容:
{-# OPTIONS_GHC -F -pgmF htfpp #-}
module OtherTests ( htf_thisModulesTests) where
import Test.Framework
test_someOtherTest :: IO ()
test_someOtherTest =
assertEqual 1 1