哪些测试输入会导致QuickCheck属性挂起?QC.within无法终止循环
问题排查:找出导致测试挂起的输入
我有一个复杂函数的测试用例,不明原因地在部分平台上出现挂起情况,希望找出导致该挂起行为的测试输入。我尝试使用QuickCheck的within函数使测试用例超时,但即使使用-O1编译,它也无法中断循环。请问还有什么方法可以让我的测试套件上报存在问题的测试输入?
简化后的测试代码 (test/Main.hs)
module Main (main) where import Test.Tasty import Test.Tasty.QuickCheck as QC main :: IO () main = defaultMain props props :: TestTree props = testGroup "properties" [ QC.testProperty "hangy" hangy {- Unfortunately `within` isn't able to disrupt to the hang in my actual testcase. , QC.testProperty "within" $ \n -> QC.within 10000 $ hangy n -} ] hangy :: Int -> Bool hangy n | n `mod` 5 == 0 = let x = x in x | otherwise = True
测试依赖
build-depends: base, tasty, tasty-quickcheck
解决方案
1. 禁用导致异常屏蔽的GHC优化
GHC在-O1/-O2优化下,会把某些无限递归(比如示例中的let x = x in x)编译成不会检查异步异常的死循环,导致QuickCheck的within无法中断。可以通过以下方式解决:
- 给挂起的函数添加
NOINLINEpragma,阻止GHC过度优化:{-# NOINLINE hangy #-} hangy :: Int -> Bool hangy n | n `mod` 5 == 0 = let x = x in x | otherwise = True - 或者在cabal文件的测试目标中添加编译选项:
ghc-options: -fno-full-laziness -fno-state-hack
2. 使用Tasty的全局/单测超时机制
Tasty自带的超时机制不依赖函数内部的异常处理,直接通过进程管理终止超时测试,同时会记录触发超时的输入:
- 给单个测试属性设置超时(单位是微秒,示例为10秒):
props :: TestTree props = testGroup "properties" [ QC.testProperty "hangy" $ withOption (\opts -> opts { mkTimeout = Just 10000000 }) hangy ] - 或者在运行测试时通过命令行参数设置全局超时:
cabal test --test-option="--timeout 10s"
3. 手动记录测试输入
修改测试逻辑,在执行测试前先打印输入,挂起时最后输出的输入就是问题输入:
hangyTest :: Int -> IO Bool hangyTest n = do putStrLn $ "Testing input: " ++ show n return $ hangy n props :: TestTree props = testGroup "properties" [ QC.testProperty "hangy" $ QC.ioProperty . hangyTest ]
4. 切换到QuickCheck原生测试Runner
使用QuickCheck的原生runner,它的超时处理对某些优化场景更有效:
main :: IO () main = verboseCheck hangy
运行时添加超时参数:
cabal test --test-option="--timeout 10s"
内容的提问来源于stack exchange,提问作者sjakobi
相关产品推荐
相关产品推荐

