testbench (empty) → 0.1.0.0
raw patch · 7 files changed
+736/−0 lines, 7 filesdep +HUnitdep +basedep +boxessetup-changed
Dependencies added: HUnit, base, boxes, bytestring, containers, criterion, deepseq, statistics, testbench, transformers
Files
- LICENSE +20/−0
- Setup.hs +2/−0
- examples/Simple.hs +74/−0
- src/Criterion/Tree.hs +153/−0
- src/TestBench.hs +388/−0
- src/TestBench/LabelTree.hs +26/−0
- testbench.cabal +73/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2015 Ivan Miljenovic++Permission is hereby granted, free of charge, to any person obtaining+a copy of this software and associated documentation files (the+"Software"), to deal in the Software without restriction, including+without limitation the rights to use, copy, modify, merge, publish,+distribute, sublicense, and/or sell copies of the Software, and to+permit persons to whom the Software is furnished to do so, subject to+the following conditions:++The above copyright notice and this permission notice shall be included+in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ examples/Simple.hs view
@@ -0,0 +1,74 @@+{-# LANGUAGE ConstraintKinds, FlexibleInstances, MultiParamTypeClasses,+ TypeFamilies, UndecidableInstances #-}++{- |+ Module : Main+ Description : Sample TestBench usage+ Copyright : (c) Ivan Lazar Miljenovic+ License : MIT+ Maintainer : Ivan.Miljenovic@gmail.com++++ -}+module Main (main) where++import TestBench++import qualified Data.ByteString as SB+import qualified Data.ByteString.Lazy as LB+import Data.Monoid ((<>))+import Data.Proxy (Proxy (..))+import qualified Data.Sequence as Seq+import Data.Word (Word8)+import Test.HUnit.Base ((@=?), (@?))++--------------------------------------------------------------------------------++main :: IO ()+main = testBench $ do+ collection "Let's kick this off" $ do+ testOnly 1 length "length of (,)" ('a', ())++ testOnly 0 sum "sum of []" ([] :: [Int])++ -- testOnly False null "Should fail" []++ compareFunc "List length"+ (\n -> length (replicate n ()) == n)+ (testWith (@? "Not as long as specified") <> benchNormalForm)+ (mapM_ (\n -> comp ("len == " ++ show n) n) [1..5])++ compareFuncConstraint (Proxy :: Proxy Sequential)+ "Length of different linear types"+ len+ (baseline "Lists" sampleList <> benchNormalForm)+ $ do comp "sequence" (Seq.fromList sampleList)+ comp "strict bytestring" (SB.pack sampleList)+ comp "lazy bytestring" (LB.pack sampleList)++sampleList :: [Word8]+sampleList = [1..10]++class Sequential xs where+ len :: xs -> Int++instance Sequential [Word8] where+ len = length++instance Sequential (Seq.Seq Word8) where+ len = length++instance Sequential SB.ByteString where+ len = SB.length++instance Sequential LB.ByteString where+ len = fromIntegral . LB.length++--------------------------------------------------------------------------------++testOnly :: (Show b, Eq b) => b -> (a -> b) -> String -> a -> TestBench+testOnly = mkTestBench (\_ _ -> Nothing) . (Just .: (@=?))++(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d+(f .: g) x y = f (g x y)
+ src/Criterion/Tree.hs view
@@ -0,0 +1,153 @@+{-# LANGUAGE BangPatterns, OverloadedStrings #-}++{- |+ Module : Criterion.Tree+ Description : Tree-based representation for Criterion+ Copyright : (c) Ivan Lazar Miljenovic+ License : MIT+ Maintainer : Ivan.Miljenovic@gmail.com++An extremely simple rose tree-based representation of criterion+benchmarks.++ -}+module Criterion.Tree+ ( -- * Types+ BenchTree+ , BenchForest+ -- * Conversion+ , flattenBenchTree+ , flattenBenchForest+ -- * Running benchmarks+ , benchmarkForest+ ) where++import TestBench.LabelTree++import Criterion.Analysis (OutlierVariance (ovFraction),+ SampleAnalysis (..))+import Criterion.Internal (runAndAnalyseOne)+import Criterion.Measurement (initializeTime, secs)+import Criterion.Monad (withConfig)+import Criterion.Types (Benchmark, Benchmarkable, Config (..),+ DataRecord (..), Report (..),+ Verbosity (..), bench, bgroup)+import Statistics.Resampling.Bootstrap (Estimate (..))++import Data.List (transpose)+import Text.PrettyPrint.Boxes++--------------------------------------------------------------------------------++-- | A more explicit tree-like structure for benchmarks than using+-- Criterion's 'Benchmark' type.+type BenchTree = LabelTree (String, Benchmarkable)++type BenchForest = [BenchTree]++flattenBenchTree :: BenchTree -> Benchmark+flattenBenchTree = toCustomTree (uncurry bench) bgroup++-- | Remove the explicit tree-like structure into the implicit one+-- used by Criterion.+--+-- Useful for embedding the results into an existing benchmark+-- suite.+flattenBenchForest :: BenchForest -> [Benchmark]+flattenBenchForest = map flattenBenchTree++-- | Run the specified benchmarks, printing the results (once they're+-- all complete) to stdout in a tabular format for easier+-- comparisons.+benchmarkForest :: Config -> BenchForest -> IO ()+benchmarkForest cfg bf = do initializeTime+ rs <- toRows cfg bf+ printBox (rowsToBox rs)++--------------------------------------------------------------------------------++data Row = Row { rowLabel :: !String+ , rowDepth :: !Int+ , rowResult :: !(Maybe Results)+ }+ deriving (Eq, Show, Read)++toRows :: Config -> BenchForest -> IO [Row]+toRows cfg = f2r 0+ where+ f2r :: Int -> BenchForest -> IO [Row]+ f2r !d = fmap concat . mapM (t2r d)++ t2r :: Int -> BenchTree -> IO [Row]+ t2r !d bt = case bt of+ Leaf (lbl,b) -> (:[]) <$> makeRow cfg lbl d b+ Branch lbl ts -> (Row lbl d Nothing :)+ <$> f2r (d+1) ts++makeRow :: Config -> String -> Int -> Benchmarkable -> IO Row+makeRow cfg lbl d b = Row lbl d <$> getResults cfg lbl b++data Results = Results { resMean :: !Estimate+ , resStdDev :: !Estimate+ , resOutVar :: !OutlierVariance+ }+ deriving (Eq, Show, Read)++getResults :: Config -> String -> Benchmarkable -> IO (Maybe Results)+getResults cfg lbl b = do dr <- withConfig cfg' (runAndAnalyseOne i lbl b)+ return $ case dr of+ Measurement{} -> Nothing+ Analysed rpt -> Just $+ let sa = reportAnalysis rpt+ in Results { resMean = anMean sa+ , resStdDev = anStdDev sa+ , resOutVar = anOutlierVar sa+ }++ where+ cfg' = cfg { verbosity = Quiet }++ i = 0 -- We're ignoring this value anyway, so it should be OK to+ -- just set it.++--------------------------------------------------------------------------------++rowsToBox :: [Row] -> Box+rowsToBox = hsep columnGap center1+ . withHead (vcat left) (vcat right)+ . transpose+ . ((empty11:resHeaders):) -- Add header row+ . map rowToBoxes++rowToBoxes :: Row -> [Box]+rowToBoxes r = moveRight (indentPerLevel * rowDepth r) (text (rowLabel r))+ : maybe blankRes resToBoxes (rowResult r)+ where+ blankRes = map (const empty11) resHeaders++empty11 :: Box+empty11 = emptyBox 1 1 -- Can't use nullBox, as /some/ size is needed.++indentPerLevel :: Int+indentPerLevel = 2++columnGap :: Int+columnGap = 2++resHeaders :: [Box]+resHeaders = ["Mean", "MeanLB", "MeanUB", "Stddev", "StddevLB", "StddevUB", "OutlierVariance"]++resToBoxes :: Results -> [Box]+resToBoxes r = e2b (resMean r) (e2b (resStdDev r) [ov])+ where+ e2b e bs = toB estPoint : toB estLowerBound : toB estUpperBound : bs+ where+ toB f = text (secs (f e))++ ov = text (show (round (ovFraction (resOutVar r) * 100) :: Int)) <> "%"++--------------------------------------------------------------------------------++withHead :: (a -> b) -> (a -> b) -> [a] -> [b]+withHead _ _ [] = []+withHead fh fr (h:r) = fh h : map fr r
+ src/TestBench.hs view
@@ -0,0 +1,388 @@+{-# LANGUAGE CPP, ConstraintKinds, FlexibleInstances,+ GeneralizedNewtypeDeriving, MultiParamTypeClasses, RankNTypes,+ ScopedTypeVariables, UndecidableInstances #-}++#if __GLASGOW_HASKELL__ >= 800+{-# LANGUAGE UndecidableSuperClasses #-}+#endif++{- |+ Module : TestBench+ Description : Create tests and benchmarks together+ Copyright : (c) Ivan Lazar Miljenovic+ License : MIT+ Maintainer : Ivan.Miljenovic@gmail.com++Make it easier to compare benchmarks and to test that benchmarks are+indeed valid.++At the top level you will probably run the 'testBench' function, and+create comparisons using 'compareFunc' or 'compareFuncConstraint'.++For example:++> main :: IO ()+> main = testBench $ do+> -- Compare how long it takes to make a list of the specified length.+> compareFunc "List length"+> (\n -> length (replicate n ()) == n)+> (testWith (@? "Not as long as specified") `mappend` benchNormalForm)+> (mapM_ (\n -> comp ("len == " ++ show n) n) [1..5])+>+> -- Polymorphic comparisons.+> --+> -- Currently it isn't possible to use a Proxy as the argument to+> -- the function (this will probably require Injective Type Families+> -- in GHC 8.0), so we're using 'undefined' to specify the type.+> compareFuncConstraint (Proxy :: Proxy (CUnion Eq Num))+> "Number type equality"+> (join (==) . (0`asTypeOf`))+> (baseline "Integer" (undefined :: Integer) `mappend` benchNormalForm)+> $ do comp "Int" (undefined :: Int)+> comp "Double" (undefined :: Double)++When run, the output will look something like:++> Cases: 7 Tried: 7 Errors: 0 Failures: 0+> Mean MeanLB MeanUB Stddev StddevLB StddevUB OutlierVariance+> List length+> len == 1 323.8 ns 318.6 ns 335.9 ns 23.86 ns 5.834 ns 40.90 ns 83%+> len == 2 352.8 ns 349.1 ns 358.1 ns 15.05 ns 11.76 ns 19.62 ns 61%+> len == 3 372.4 ns 358.4 ns 393.8 ns 62.50 ns 39.83 ns 90.85 ns 96%+> len == 4 396.3 ns 378.4 ns 419.2 ns 67.83 ns 46.71 ns 94.74 ns 96%+> len == 5 426.0 ns 407.0 ns 459.5 ns 82.23 ns 53.37 ns 110.2 ns 97%+> Number type equality+> Integer 75.43 ns 74.48 ns 76.71 ns 3.615 ns 2.748 ns 5.524 ns 69%+> Int 74.39 ns 73.48 ns 76.24 ns 3.964 ns 2.500 ns 7.235 ns 74%+> Double 78.05 ns 75.84 ns 82.50 ns 9.790 ns 6.133 ns 16.99 ns 94%++ -}+module TestBench+ ( -- * Specification and running+ TestBench+ , testBench+ -- ** Running manually+ , getTestBenches+ , BenchTree+ , BenchForest+ , flattenBenchForest+ , benchmarkForest+ -- ** Lower-level types+ , TestBenchM+ , OpTree+ , Operation+ , LabelTree(..)++ -- * Grouping+ , collection++ -- * Direct benchmarks\/tests+ , nfEq+ , whnfEq+ , mkTestBench++ -- * Comparisons+ , compareFunc+ , compareFuncConstraint++ -- ** Specifying constraints+ , CUnion++ -- ** Comparison parameters+ , CompParams+ -- *** Control benchmarking+ , benchNormalForm+ , withBenchMode+ , noBenchmarks++ -- *** Control testing+ , baseline+ , testWith+ , noTests++ -- ** Specify comparisons+ , Comparison+ , comp+ , compBench+ , compTest++ -- ** Lower-level types+ , ComparisonM+ , SameAs+ ) where++import Criterion.Tree+import TestBench.LabelTree++import Criterion (Benchmarkable, nf, whnf)+import Criterion.Main.Options (defaultConfig)+import Test.HUnit.Base (Assertion, Counts (..), Test (..), (@=?), (~:))+import Test.HUnit.Text (runTestTT)++import Control.Applicative (liftA2)+import Control.Arrow ((&&&))+import Control.DeepSeq (NFData (..))+import Control.Monad (when)+import Control.Monad.IO.Class (MonadIO (liftIO))+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Reader (ReaderT, ask, runReaderT)+import Control.Monad.Trans.Writer.Lazy (WriterT, execWriterT, tell)+import Data.Maybe (mapMaybe)+import Data.Monoid (Endo (..))+import Data.Proxy (Proxy (..))++-- -----------------------------------------------------------------------------++-- | An individual operation potentially consisting of a benchmark+-- and/or test.+data Operation = Op { opName :: String+ , opBench :: Maybe Benchmarkable+ , opTest :: Maybe Assertion+ }++-- | A tree of operations.+type OpTree = LabelTree Operation++opTreeTo :: (Operation -> Maybe a) -> OpTree -> Maybe (LabelTree (String, a))+opTreeTo f = go+ where+ go tr = case tr of+ Leaf op -> Leaf <$> liftA2 (fmap . (,)) opName f op+ Branch lb trs -> case mapMaybe go trs of+ [] -> Nothing+ trs' -> Just (Branch lb trs')++opForestTo :: (Operation -> Maybe a) -> (String -> a -> b) -> (String -> [b] -> b)+ -> [OpTree] -> [b]+opForestTo f lf br = mapMaybe (fmap (toCustomTree (uncurry lf) br) . opTreeTo f)++toBenchmarks :: [OpTree] -> BenchForest+toBenchmarks = mapMaybe (opTreeTo opBench)++toTests :: [OpTree] -> Test+toTests = TestList . opForestTo opTest (~:) (~:)++-- -----------------------------------------------------------------------------++-- TODO: does this /really/ need to be in IO?+newtype TestBenchM r = TestBenchM { getOpTrees :: WriterT [OpTree] IO r}+ deriving (Functor, Applicative, Monad, MonadIO)++-- | An environment for combining testing and benchmarking.+type TestBench = TestBenchM ()++makeOpTree :: String -> TestBench -> IO OpTree+makeOpTree nm = fmap (Branch nm) . execWriterT . getOpTrees++-- | Label a sub-part of a @TestBench@.+collection :: String -> TestBench -> TestBench+collection nm ops = liftIO (makeOpTree nm ops) >>= singleTree++treeList :: [OpTree] -> TestBench+treeList = TestBenchM . tell++singleTree :: OpTree -> TestBench+singleTree = treeList . (:[])++runTestBench :: TestBench -> IO [OpTree]+runTestBench = execWriterT . getOpTrees++-- | Obtain the resulting tests and benchmarks from the specified+-- @TestBench@.+getTestBenches :: TestBench -> IO (Test, BenchForest)+getTestBenches = fmap (toTests &&& toBenchmarks) . runTestBench++-- | Run the specified benchmarks if and only if all tests pass, using+-- a comparison-based format for benchmarking output.+--+-- Please note that this is currently very simplistic: no+-- parameters, configuration, etc. Also, benchmark results will not+-- be shown until all benchmarks are complete.+--+-- For more control, use 'getTestBenches'.+testBench :: TestBench -> IO ()+testBench tb = do (tst,bf) <- getTestBenches tb+ tcnts <- runTestTT tst+ when (errors tcnts == 0 && failures tcnts == 0)+ (benchmarkForest defaultConfig bf) -- TODO: make this configurable++-- -----------------------------------------------------------------------------++-- | Create a single benchmark evaluated to normal form, where the+-- results should equal the value specified.+nfEq :: (NFData b, Show b, Eq b) => b -> (a -> b) -> String -> a -> TestBench+nfEq = mkTestBench (Just .: nf) . (Just .: (@=?))++-- | Create a single benchmark evaluated to weak head normal form,+-- where the results should equal the value specified.+whnfEq :: (Show b, Eq b) => b -> (a -> b) -> String -> a -> TestBench+whnfEq = mkTestBench (Just .: whnf) . (Just .: (@=?))++-- | A way of writing custom testing/benchmarking statements. You+-- will probably want to use one of the pre-defined versions+-- instead.+mkTestBench :: ((a -> b) -> a -> Maybe Benchmarkable) -> (b -> Maybe Assertion)+ -> (a -> b) -> String -> a -> TestBench+mkTestBench toB checkRes fn nm arg = singleTree+ . Leaf+ $ Op { opName = nm+ , opBench = toB fn arg+ , opTest = checkRes (fn arg)+ }++--------------------------------------------------------------------------------++-- | Compare how various input values (of the same type) behave for a+-- specific function.+--+-- By default:+--+-- * Results are only evaluated to /Weak Head Normal Form/. To+-- fully evaluate results, use 'benchNormalForm'.+--+-- * No tests are performed by default; use either 'baseline' or+-- 'testWith' to specify one.+compareFunc :: forall a b. String -> (a -> b) -> CompParams (SameAs a) b+ -> Comparison (SameAs a) b -> TestBench+compareFunc = compareFuncConstraint (Proxy :: Proxy (SameAs a))++-- | An alias for readability.+type SameAs a = (~) a++-- | As with 'compareFunc' but allow for polymorphic inputs by+-- specifying the constraint to be used.+compareFuncConstraint :: forall ca b. Proxy ca -> String -> (forall a. (ca a) => a -> b)+ -> CompParams ca b -> Comparison ca b -> TestBench+compareFuncConstraint _ lbl f params cmpM = do ops <- liftIO (runComparison ci cmpM)+ let opTr = map Leaf (withOps' ops)+ singleTree (Branch lbl opTr)+ where+ ci0 :: CompInfo ca b+ ci0 = CI { func = f+ , toBench = Just .: whnf+ , toTest = const Nothing+ }++ (withOps , Endo mkCI) = unCP params+ ci = mkCI ci0++ withOps' = appEndo (withOps ci)++-- TODO: work out how to fix it if multiple test setting functions are called; might need a Last in here.+-- | Monoidally build up the parameters used to control a 'Comparison'+-- environment.+--+-- This will typically be a combination of 'benchNormalForm' with+-- either 'baseline' or 'testWith'.+newtype CompParams ca b = CP { unCP :: ( CompInfo ca b -> Endo [Operation]+ , Endo (CompInfo ca b)+ )+ }+ deriving (Monoid)++-- | Evaluate all benchmarks to normal form.+benchNormalForm :: (NFData b) => CompParams ca b+benchNormalForm = withBenchMode nf++-- | Allow specifying how benchmarks should be evaluated. This may+-- allow usage of methods such as @nfIO@, but this has not been+-- tested as yet.+withBenchMode :: (forall a. (ca a) => (a -> b) -> a -> Benchmarkable) -> CompParams ca b+withBenchMode toB = CP (mempty, Endo (\ci -> ci { toBench = Just .: toB }))++-- | Don't run any benchmarks. I'm not sure why you'd want to do this+-- as there's surely easier\/better testing environments available,+-- but this way it's possible.+noBenchmarks :: CompParams ca b+noBenchmarks = CP (mempty, Endo (\ci -> ci { toBench = \_ _ -> Nothing }))++-- | Don't run any tests. This isn't recommended, but could be useful+-- if all you want to do is run comparisons (potentially because no+-- meaningful tests are possible).+noTests :: CompParams ca b+noTests = CP (mempty, Endo (\ci -> ci { toTest = const Nothing }))++-- | Specify a sample baseline value to benchmark and test against+-- (such that the result of applying the function to this @a@ is+-- what everything should match).+--+-- You shouldn't specify this more than once, nor mix it with+-- 'noTests' or 'testWith'.+baseline :: (ca a, Eq b, Show b) => String -> a -> CompParams ca b+baseline nm arg = CP (addOp, Endo setTest)+ where+ opFrom ci = Op { opName = nm+ , opBench = toBench ci (func ci) arg+ , opTest = Nothing+ }++ addOp ci = Endo (opFrom ci:)++ setTest ci = ci { toTest = Just . (func ci arg @=?) }++-- | Specify a predicate that all results should satisfy.+--+-- Note that the last statement between 'testWith', 'baseline' and+-- 'noTests' \"wins\" in specifying which testing (if any) to do.+testWith :: (b -> Assertion) -> CompParams ca b+testWith f = CP (mempty, Endo (\ci -> ci { toTest = Just . f }))++data CompInfo ca b = CI { func :: (forall a. (ca a) => a -> b)+ , toBench :: (forall a. (ca a) => (a -> b) -> a -> Maybe Benchmarkable)+ , toTest :: (b -> Maybe Assertion)+ }++type Comper ca b = ReaderT (CompInfo ca b) (WriterT [Operation] IO)++newtype ComparisonM ca b r = ComparisonM { runComparisonM :: Comper ca b r }+ deriving (Functor, Applicative, Monad, MonadIO)++-- | A specialised monad used solely for running comparisons.+--+-- No lifting is permitted; the only operations permitted are+-- 'comp', 'compBench' and 'compTest'.+type Comparison ca b = ComparisonM ca b ()++runComparison :: CompInfo ca b -> Comparison ca b -> IO [Operation]+runComparison cmpr cmpM = execWriterT . runReaderT (runComparisonM cmpM) $ cmpr++-- | Benchmark and test (if specified) this value against the+-- specified function.+comp :: (ca a) => String -> a -> Comparison ca b+comp = compWith id++-- | Only benchmark (but do not test) this value against the specified+-- function.+compBench :: (ca a) => String -> a -> Comparison ca b+compBench = compWith (\op -> op { opTest = Nothing })++-- | Only test (but do not benchmark) this value against the specified+-- function.+compTest :: (ca a) => String -> a -> Comparison ca b+compTest = compWith (\op -> op { opBench = Nothing })++compWith :: (ca a) => (Operation -> Operation) -> String -> a -> Comparison ca b+compWith f nm arg = ComparisonM $ do ci <- ask+ lift $ tell [f (compOp nm arg ci)]++compOp :: (ca a) => String -> a -> CompInfo ca b -> Operation+compOp nm arg ci = Op { opName = nm+ , opBench = toBench ci (func ci) arg+ , opTest = toTest ci $ func ci arg+ }++-- | The union of two @(* -> 'Constraint')@ values.+--+-- Whilst @type EqNum a = ('Eq' a, 'Num' a)@ is a valid+-- specification of a 'Constraint' when using the @ConstraintKinds@+-- extension, it cannot be used with 'compareFuncConstraint' as type+-- aliases cannot be partially applied.+--+-- As such, you can use @type EqNum = CUnion Eq Num@ instead.+class (c1 a, c2 a) => CUnion c1 c2 a+instance (c1 a, c2 a) => CUnion c1 c2 a++--------------------------------------------------------------------------------++(.:) :: (c -> d) -> (a -> b -> c) -> a -> b -> d+(f .: g) x y = f (g x y)
+ src/TestBench/LabelTree.hs view
@@ -0,0 +1,26 @@+{-# LANGUAGE DeriveFunctor #-}+{- |+ Module : TestBench.LabelTree+ Description : Labelled rose-tree structure+ Copyright : (c) Ivan Lazar Miljenovic+ License : MIT+ Maintainer : Ivan.Miljenovic@gmail.com++++ -}+module TestBench.LabelTree where++--------------------------------------------------------------------------------++-- | A simple labelled rose-tree data structure.+data LabelTree a = Leaf a+ | Branch String [LabelTree a]+ deriving (Eq, Ord, Show, Read, Functor)++toCustomTree :: (a -> b) -> (String -> [b] -> b) -> LabelTree a -> b+toCustomTree lf br = go+ where+ go tr = case tr of+ Leaf a -> lf a+ Branch str trs -> br str (map go trs)
+ testbench.cabal view
@@ -0,0 +1,73 @@+name: testbench+version: 0.1.0.0+synopsis: Create tests and benchmarks together+description: {+Test your benchmarks! Benchmark your tests!+.+It's too easy to accidentally try and benchmark apples and oranges+together. Wouldn't it be nice if you could somehow guarantee that+your benchmarks satisfy some simple tests (e.g. a group of comparisons+all return the same value)?+.+Furthermore, trying to compare multiple inputs/functions against each+other requires a lot of boilerplate, making it even easier to+accidentally compare the wrong things (e.g. using `whnf` instead of+`nf`).+.+/testbench/ aims to help solve these problems and more by making it+easier to write unit tests and benchmarks together by stating up-front+what requirements are needed and then using simple functions to state+the next parameter to be tested/benchmarked.+}+license: MIT+license-file: LICENSE+author: Ivan Lazar Miljenovic+maintainer: Ivan.Miljenovic@gmail.com+-- copyright:+category: Testing+build-type: Simple+-- extra-source-files:+cabal-version: >=1.10++tested-with: GHC == 7.10.2, GHC == 8.0.1, GHC == 8.1.*++source-repository head+ type: git+ location: https://github.com/ivan-m/testbench.git++flag examples+ description: Build example executable+ default: False++library+ exposed-modules: TestBench+ other-modules: Criterion.Tree+ , TestBench.LabelTree+ build-depends: base >=4.7 && <4.11+ , boxes == 0.1.*+ , criterion == 1.1.*+ , deepseq+ , HUnit+ , statistics+ , transformers+ hs-source-dirs: src+ default-language: Haskell2010++executable examples+ if flag(examples)+ buildable: True+ else+ buildable: False++ default-language: Haskell2010++ hs-source-dirs: examples++ main-is: Simple.hs++ build-depends: testbench+ , base+ , bytestring+ , containers+ , criterion+ , HUnit