5

我有一个玩具程序:

$ cat a.hs
main = putStrLn "Toy example"
$ runghc a.hs
Toy example

让我们添加一些 Template Haskell 到它:

$ cat b.hs
{-# LANGUAGE TemplateHaskell #-}
id [d|
main = putStrLn "Toy example"
|]
$ runghc b.hs

b.hs:3:0: parse error (possibly incorrect indentation)

那么,让我们修复缩进:

$ cat c.hs
{-# LANGUAGE TemplateHaskell #-}
id [d|
 main = putStrLn "Toy example"
 |]
$ runghc c.hs
Toy example

一个空格就足够了,但我必须缩进两条尾随行。

我可以避免缩进我的大部分模块吗?(我的真实模块不仅仅是一行代码。)(不使用{ ; ; }符号?)

我确实希望在引用中捕获所有模块声明 - 在我可以替换为的普通代码中(...)$ ...是否有一些等价物[d|...|]可以让我避免使用右括号和缩进?

或者模块A是否可以通过某种方式说导入A的任何模块B的顶级声明都由函数A导出的函数自动处理?

笔记:

  1. 我的 Real Program 中的 Template Haskell 比它更复杂id——它扫描声明以查找 start 的变量名prop_,并构建一个包含它们的测试套件。有没有其他纯粹的 Haskell 方法我可以这样做,而不直接修改源文件?
  2. 我正在使用 GHC v6.12.1。当我使用 GHC v7.0.3 时,b.hs 的错误是针对不同的位置报告的b.hs:3:1——但行为是相同的。
4

2 回答 2

4

如果测试套件用于 QuickCheck,我建议您改用新All模块:http: //hackage.haskell.org/packages/archive/QuickCheck/2.4.1.1/doc/html/Test-QuickCheck-All.html

它做同样的事情,只是它通过访问文件系统并解析拼接所在的文件来获取属性的名称(如果您正在使用其他一些测试框架,您仍然可以使用相同的方法)。

如果你真的想引用整个文件,你可以使用一个 quasi-quoter 代替(它不需要缩进)。您可以轻松地在 haskell-src-meta 上构建您的报价器,但我建议不要使用这种方法,因为它不支持某些 Haskell 功能,并且可能会给出糟糕的错误消息。


聚合测试套件是一个难题,可以扩展名称收集例程以某种方式遵循导入,但这是很多工作。这是一个解决方法:

您可以使用以下修改版本forAllProperties

import Test.QuickCheck
import Test.QuickCheck.All
import Language.Haskell.TH
import Data.Char
import Data.List
import Control.Monad

allProperties :: Q Exp -- :: [(String,Property)]
allProperties = do
  Loc { loc_filename = filename } <- location
  when (filename == "<interactive>") $ error "don't run this interactively"
  ls <- runIO (fmap lines (readFile filename))
  let prefixes = map (takeWhile (\c -> isAlphaNum c || c == '_') . dropWhile (\c -> isSpace c || c == '>')) ls
      idents = nubBy (\x y -> snd x == snd y) (filter (("prop_" `isPrefixOf`) . snd) (zip [1..] prefixes))
      quickCheckOne :: (Int, String) -> Q [Exp]
      quickCheckOne (l, x) = do
        exists <- return False `recover` (reify (mkName x) >> return True)
        if exists then sequence [ [| ($(stringE $ x ++ " on " ++ filename ++ ":" ++ show l),
                                     property $(mono (mkName x))) |] ]
         else return []
  [|$(fmap (ListE . concat) (mapM quickCheckOne idents)) |]

您还需要runQuickCheckAll未从 All 导出的函数:

runQuickCheckAll :: [(String, Property)] -> (Property -> IO Result) -> IO Bool
runQuickCheckAll ps qc =
  fmap and . forM ps $ \(xs, p) -> do
    putStrLn $ "=== " ++ xs ++ " ==="
    r <- qc p
    return $ case r of
      Success { } -> True
      Failure { } -> False
      NoExpectedFailure { } -> False

在您现在定义的每个测试模块中

propsN = $allProperties

其中N是某个数字或其他唯一标识符(或者您可以在下面的步骤中使用相同的名称并使用限定名称)。

在您定义的主测试套件中

props :: [(String,Property)]
props = concat [props1, props2 ... propsN]

如果你真的想避免为每个模块添加一个列表成员,你可以制作一个 TH 脚本来生成这个列表。

要运行所有测试,您只需说

runTests = runQuickCheckAll quickCheckResult props
于 2011-10-01T10:35:22.090 回答
3

[我的程序] 扫描以 prop_ 开头的变量名的声明,并构建一个包含它们的测试套件。有没有其他纯粹的 Haskell 方法我可以这样做,而不直接修改源文件?

就在这里!使用_language-haskell-extract

{-# LANGUAGE TemplateHaskell #-}

import Language.Haskell.Extract
import Test.QuickCheck

prop_foo xs = reverse (reverse xs) == (xs :: [Int])
prop_bar = 2 + 2 == 4

properties = $(functionExtractorMap "^prop_"
    [|\name prop -> putStrLn name >> quickCheck prop|])

main = sequence_ properties

运行这个,我们得到:

prop_foo
+++ OK, passed 100 tests.
prop_bar
+++ OK, passed 100 tests.

然而,在你重新发明轮子之前,我还建议你看一下package test-framework-th它几乎完全做到了这一点,而且还支持 HUnit 并且有一个很好的测试运行器(有颜色!)。

{-# LANGUAGE TemplateHaskell #-}

import Test.Framework.Providers.HUnit
import Test.Framework.Providers.QuickCheck2
import Test.Framework.TH
import Test.HUnit
import Test.QuickCheck

prop_bar = 1+1 == 2
case_foo = 2+2 @?= 4

main = $(defaultMainGenerator)

输出:

Main:
  bar: [OK, passed 100 tests]
  foo: [OK]

         Properties  Test Cases  Total      
 Passed  1           1           2          
 Failed  0           0           0          
 Total   1           1           2   

如果您想组合来自多个文件的测试,还有一个testGroupGenerator很有用。

于 2011-10-01T16:47:35.677 回答