packages feed

test-karya (empty) → 0.0.1

raw patch · 24 files changed

+2688/−0 lines, 24 filesdep +Diffdep +QuickCheckdep +asyncsetup-changed

Dependencies added: Diff, QuickCheck, async, base, bytestring, containers, data-ordlist, deepseq, directory, filepath, ghc-prim, haskell-src, pcre-heavy, pcre-light, pretty, process, test-karya, text, unix

Files

+ LICENSE view
@@ -0,0 +1,31 @@+Copyright Evan Laforge 2011++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Evan Laforge nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.+
+ README.md view
@@ -0,0 +1,71 @@+Some time before 2008 I needed to write tests for Haskell, specifically for+what became <https://github.com/elaforge/karya>.  At the time, I think HUnit+existed, but it did stuff I didn't think I needed, such as manually arranging+tests into a hierarchy, and didn't have stuff I thought I did, such as+automatic test discovery, diffs, filename:linenumber for failed tests, etc.+So I wrote my own.++Over the years some features became obsolete (e.g. ghc now supports+filename:linenumber natively via call stacks), but I got used to this style+of tests, so I decided to try to extract it for use in cabal projects.++Here's what I like:++- Automatic test discovery.  Tests are any functions named `test_*` in a+module named `*_test.hs`.  In Karya, I use shake to collect the tests, but+in this cabal version, I borrowed the -pgmF technique from `hspec`.++- Automatic test hierarchy.  Tests are named by their module name, so you can+run subsets by pattern matching on the name.  You don't have to collect them+into suites, and when running them you don't have to dig through the files to+see what they named their suites.++- Low syntax overhead tests.  A test is any standalone function that calls+test functions (like `equal`):++    ```+    module M_test where+    import EL.Test.Global+    import qualified M++    test_something = do+        let f = M.something+        equal (f 10) 20+        equal (f 20) 30+    ```++    Tests don't abort on failure, so you can put a bunch together with local+    definitions.  There's no need to name each test case, the error message+    will tell you module and line number.  Since `test_something` is a normal+    `IO ()` function you can run it in ghci, which is how I mainly work.++- Doesn't mess with stdout.  A check, like the `equal` function, is any+function that prints to stdout.  A line starting with `__->` is a failure.+This was originally the simplest stupidest thing that could work, but over+the last 10 years there was never a reason to change it, and it turns out to+be convenient.  Since test output is integrated into the stdout stream, it's+in the right context with any debugging or logging.++- Colored formatted diffs.  When values don't match, they are pretty printed+and differing parts highlighted.++- Various other ad-hoc conveniences: `equalFmt` attaches a formatter so you+can get higher level diffs, `stringsLike` for matching strings with patterns,+`equalf` and `ApproxEq` typeclass for floating point, and `quickcheck` for+quickcheck properties with formatted diffs.++### how to use++See `example`, but the short version is:++- Install library and `test-karya-generate` binary with `cabal install`.++- Make a test module like `M_test` above.++- Make `RunTests.hs` with `{-# OPTIONS_GHC -F -pgmF test-karya-generate #-}`++- Make a `test-suite` like in `example`.++- `cabal test`.  You can rerun the tests with `dist/build/test/test`, run+only `M_test` with `dist/build/test/test M_test` or pass `--help` for more+options, including running tests in parallel.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ TODO view
@@ -0,0 +1,51 @@+* subprocess communication is getting stuck+  . But why?  I think this should be the same code as karya's version, which+    doesn't do that.+  . There's definitely something fishy going on in both cases though, since+    karya's version sometimes fails on travis.+  . dist/build/test/test --clear-dirs --output=dist/test-output .++How to run GenerateRunTests?+  - use physical CPUs instead of getNumCapabilities+    . The cpuinfo package is meant to do this but it's linux only:+      https://github.com/TravisWhitaker/cpuinfo/issues/1+  * There should be a standard set of "run from cabal" flags:+    --clear-dir --output=dist/test-output --check-output .+  / Makefile to update when out of date, cabal Setup hooks to run make.+  / Like Makefile, but a shell script that does the same thing.+  / Use Setup hooks directly.  Does cabal know how to only rebuild when+    necessary?+  * HSpec uses a -pgmF program to generate the module.  But I think it runs+    every single time you build tests.  Of course, maybe that's actually pretty+    fast.+    . pgmF args: OrigSourceName.hs input.hs output.hs++* remove the ioHuman and interactive stuff+/ argv0 is wrong, so parallelization probably doesn't work+  . Use Environment.getExecutablePath+* fix tmpBaseDir+  . Surely cabal has some way to pass in a tmp dir?+  . Haha, it's cabal of course not.  Use CPP then.+  . Well, it would have to come from the testee config, which means putting it+    in Testing.Config.  Too much bother, just hardcode to dist/build/tmp-test++Interpret the output+  . I need the equivalent of test/run_tests.+  . Can I embed that logic in RunTest itself?  The problem is I want to write+    to stdout, and then check that stdout.  It's hard to do that when cabal is+    redirecting stdout.+  . Does cabal have any more flexible way to run tests?+    Looks like no, just 'detailed', which is even less flexible, since it+    requires the tests to be some cabal data structure.+  . I would like a runner that runs the test binary, then runs something else+    with access to the log.+  . Can I redirect stdout to a file temporarily?  I could do+    withStdout (open "output") $ runTests ...+    check "output"+  . I can have tests write to a file via 'success' and 'failure'.  The problem+    is that then debugging isn't interleaved.  But I could write to both the+    output file and stdout.  Or write everything to sdout, only errors to+    stderr.+  . Or use the parallelization feature, run as a subprocess.+  . How this works is you call with --outputs.  Then RunTests can have a flag+    to check the outputs afterwards.
+ example/example.cabal view
@@ -0,0 +1,22 @@+name: example+version: 0.0.1+cabal-version: >= 1.9+build-type: Simple+synopsis: test testing tests+description: Test testing test-karya test framework.++library+    hs-source-dirs: src+    exposed-modules:+        A+    build-depends: base++test-suite test+    type: exitcode-stdio-1.0+    hs-source-dirs: src+    main-is: RunTests.hs+    other-modules:+        A, A_test+    build-depends:+        base+        , test-karya
+ example/src/A.hs view
@@ -0,0 +1,4 @@+module A where++f :: Int -> Int+f x = x * 2
+ example/src/A_test.hs view
@@ -0,0 +1,9 @@+module A_test where+import EL.Test.Global+import qualified A+++test_f = do+    let f = A.f+    equal (f 4) 8+    equal (f 4) 9
+ example/src/RunTests.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF test-karya-generate #-}
+ src/EL/Private/ExtractHs.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE OverloadedStrings #-}+-- | A library to collect definitions from haskell source and collect them+-- into an output source file.+module EL.Private.ExtractHs where+import qualified Data.Char as Char+import qualified Data.Map as Map+import qualified Data.Text as Text+import qualified Data.Text.IO as Text.IO++import qualified System.Directory as Directory+import qualified System.Environment+import qualified System.Exit+import qualified System.FilePath as FilePath+import qualified System.IO as IO++import Global+++type Error = Text+type Warning = Text++process :: [String] -> (Text -> a)+    -> (FilePath -> Map FilePath a -> ([Warning], Text))+    -> IO ()+process args extract generate = do+    progName <- System.Environment.getProgName+    (outFname, inputs) <- case args of+        outFname : inputs -> return (outFname, inputs)+        _ -> die $ "usage: " <> txt progName+            <> " output.hs input1.hs input2.hs ..."+    extracted <- extractFiles extract inputs+    case generate outFname extracted of+        (warnings, output) -> do+            mapM_ (Text.IO.hPutStrLn IO.stderr) warnings+            Directory.createDirectoryIfMissing True $+                FilePath.takeDirectory outFname+            Text.IO.writeFile outFname $ header progName <> output++header :: String -> Text+header program = "-- automatically generated by " <> txt program <> "\n"++extractFiles :: (Text -> a) -> [FilePath] -> IO (Map FilePath a)+extractFiles extract =+    fmap Map.fromList . mapM (\fn -> (,) fn . extract <$> Text.IO.readFile fn)++-- * extract++typeDeclarations :: Text -> [(Int, (Text, Text))]+typeDeclarations = mapMaybe parse . zip [1..] . Text.lines+    where+    parse (lineno, line)+        | line == "" || Char.isSpace (Text.head line) = Nothing+        | otherwise = case Text.words line of+            name : "::" : rest -> Just (lineno, (name, Text.unwords rest))+            _ -> Nothing++-- | This will be fooled by a {- or -} inside a string.  I don't strip --+-- comments because the extract functions look for left justified text.+stripComments :: Text -> Text+stripComments = mconcat . go (0 :: Int)+    where+    go nesting text+        | Text.null post = [text]+        | "{-" `Text.isPrefixOf` post = (if nesting > 0 then id else (pre:))+            (go (nesting+1) (Text.drop 2 post))+        | otherwise = (if nesting == 0 then (pre <> Text.take 2 post :) else id)+            (go (max 0 (nesting-1)) (Text.drop 2 post))+        where (pre, post) = breakOnFirst "{-" "-}" text++-- | Like 'Text.breakOn', but break on two things.+breakOnFirst :: Text -> Text -> Text -> (Text, Text)+breakOnFirst a b text+    | Text.length aPre <= Text.length bPre = (aPre, aPost)+    | otherwise = (bPre, bPost)+    where+    (aPre, aPost) = Text.breakOn a text+    (bPre, bPost) = Text.breakOn b text++-- * generate++moduleDeclaration :: FilePath -> Text+moduleDeclaration fname = "module " <> pathToModule fname <> " where"++makeImport :: FilePath -> Text+makeImport fname = "import qualified " <> pathToModule fname++pathToModule :: FilePath -> Text+pathToModule =+    Text.map dot . txt . FilePath.dropExtension . FilePath.normalise+    where dot c = if c == '/' then '.' else c++-- * util++die :: Text -> IO a+die msg = do+    Text.IO.hPutStrLn IO.stderr msg+    System.Exit.exitFailure
+ src/EL/Private/File.hs view
@@ -0,0 +1,54 @@+-- Copyright 2013 Evan Laforge+-- This program is distributed under the terms of the GNU General Public+-- License 3.0, see COPYING or http://www.gnu.org/licenses/gpl-3.0.txt++{-# LANGUAGE ScopedTypeVariables #-}+{- | Do things with files.+-}+module EL.Private.File where+import qualified Control.Exception as Exception+import Control.Monad (guard)+import Data.Text (Text)+import qualified Data.Text.IO as Text.IO+import qualified System.Directory as Directory+import System.FilePath ((</>))+import qualified System.IO as IO+import qualified System.IO.Error as IO.Error+++-- | Like 'Directory.getDirectoryContents' except don't return dotfiles and+-- it prepends the directory.+list :: FilePath -> IO [FilePath]+list dir = do+    fns <- Directory.getDirectoryContents dir+    return $ map (strip . (dir </>)) $ filter ((/=".") . take 1) fns+    where+    strip ('.' : '/' : path) = path+    strip path = path++writeLines :: FilePath -> [Text] -> IO ()+writeLines fname lines = IO.withFile fname IO.WriteMode $ \hdl ->+    mapM_ (Text.IO.hPutStrLn hdl) lines++-- * IO errors++-- | If @op@ raised ENOENT, return Nothing.+ignoreEnoent :: IO a -> IO (Maybe a)+ignoreEnoent = ignoreError IO.Error.isDoesNotExistError++ignoreEOF :: IO a -> IO (Maybe a)+ignoreEOF = ignoreError IO.Error.isEOFError++-- | Ignore all IO errors.  This is useful when you want to see if a file+-- exists, because some-file/x will not give ENOENT, but ENOTDIR, which is+-- probably isIllegalOperation.+ignoreIOError :: IO a -> IO (Maybe a)+ignoreIOError = ignoreError (\(_ :: IO.Error.IOError) -> True)++ignoreError :: Exception.Exception e => (e -> Bool) -> IO a -> IO (Maybe a)+ignoreError ignore action = Exception.handleJust (guard . ignore)+    (const (return Nothing)) (fmap Just action)++-- | 'Exception.try' specialized to IOError.+tryIO :: IO a -> IO (Either IO.Error.IOError a)+tryIO = Exception.try
+ src/EL/Private/Map.hs view
@@ -0,0 +1,10 @@+-- | Extra utils for "Data.Map".+module EL.Private.Map where+import qualified Data.Map as Map++import qualified EL.Private.Seq as Seq+++-- | Pair up elements from each map with equal keys.+pairs :: Ord k => Map.Map k v1 -> Map.Map k v2 -> [(k, Seq.Paired v1 v2)]+pairs map1 map2 = Seq.pair_sorted (Map.toAscList map1) (Map.toAscList map2)
+ src/EL/Private/PPrint.hs view
@@ -0,0 +1,92 @@+{- | This is based on gleb.alexeev\@gmail.com's ipprint package on hackage.++    I'm not just using it directly because I want to pass custom formatting+    flags because my terminal is 80 chars wide, not the 137-whatever default.+-}+module EL.Private.PPrint (+    pprint, pshow+    , format, format_str+) where+import qualified Data.Char as Char+import qualified Data.Maybe as Maybe+import qualified Language.Haskell.Parser as Parser+import qualified Language.Haskell.Pretty as Pretty+import Language.Haskell.Syntax++import qualified Text.PrettyPrint as PrettyPrint+++-- * showable++pprint :: Show a => a -> IO ()+pprint = putStr . pshow++-- | Pretty show.+pshow :: Show a => a -> String+pshow = format . show++-- * String++-- | Pretty up a string containing a parseable haskell value.+format :: String -> String+format = parse format_parsed++-- | Pretty up haskell value, unless it's a string, in which case return it+-- directly.+--+-- Previously I needed this in the REPL since it didn't have a way to say text+-- should be unformatted.  I don't need it any more, but it doesn't seem to be+-- hurting so I'll leave it here for now.+format_str :: String -> String+format_str = parse format_nonstr+    where+    format_nonstr m = Maybe.fromMaybe (format_parsed m) (is_str m)+    is_str (HsModule _ _ _ _ [HsPatBind _ _ (HsUnGuardedRhs rhs) _]) =+        case rhs of+            HsLit (HsString s) -> Just s+            _ -> Nothing+    is_str _ = Nothing+++-- * implementation++parse :: (HsModule -> String) -> String -> String+parse format s = case Parser.parseModule ("value = " ++ s) of+    Parser.ParseOk m -> format m+    -- The formatted version appends a newline, so the unformatted one should+    -- too.+    Parser.ParseFailed _ _  -> s ++ "\n"++format_parsed :: HsModule -> String+format_parsed = strip_boilerplate . pprint_mode++strip_boilerplate :: String -> String+strip_boilerplate = dedent . ("    "++) . strip_match "value="+    . dropWhile (/='\n') -- Strip module line and "value =".+    -- Prefix 4 spaces since this is how much will have been stripped from+    -- the first line, namely " = ", and make this line up vertically with the+    -- following lines.  If it fit on one line, it'll be "value = " which is+    -- not 4 characters but it doesn't matter because there's no following+    -- line.++strip_match :: String -> String -> String+strip_match pattern str = go pattern str+    where+    go "" s = strip s+    go _ "" = ""+    go (p:ps) s = case strip s of+        c : cs | p == c -> go ps cs+        _ -> str+    strip = dropWhile Char.isSpace++pprint_mode :: Pretty.Pretty a => a -> String+pprint_mode = Pretty.prettyPrintStyleMode pp_style Pretty.defaultMode+    where+    pp_style = PrettyPrint.style+        { PrettyPrint.ribbonsPerLine = 1, PrettyPrint.lineLength = 80 }++dedent :: String -> String+dedent s = unlines $ map (drop indent) slines+    where+    indent = minimum $ 80 : map (length . takeWhile Char.isSpace) slines+    slines = lines s
+ src/EL/Private/Process.hs view
@@ -0,0 +1,68 @@+-- Copyright 2013 Evan Laforge+-- This program is distributed under the terms of the GNU General Public+-- License 3.0, see COPYING or http://www.gnu.org/licenses/gpl-3.0.txt++{-# LANGUAGE CPP #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Utilities to deal with processes.+module EL.Private.Process where+import qualified Control.Concurrent as Concurrent+import qualified Control.Concurrent.Async as Async+import qualified Control.Concurrent.Chan as Chan+import Control.Monad (forever, void)++import qualified Data.String as String+import qualified Data.Text as Text+import qualified Data.Text.IO as Text.IO++import qualified EL.Private.File as File+import Global+import qualified System.Exit+import qualified System.IO as IO+import qualified System.Process as Process+++data TalkOut = Stdout !Text | Stderr !Text+    -- | This always terminates the conversation, and effectively marks the+    -- channel closed.+    | Exit !Int+    deriving (Eq, Show)+data TalkIn = Text !Text | EOF+    deriving (Eq, Show)++instance String.IsString TalkIn where+    fromString = Text . Text.pack++-- | Have a conversation with a subprocess.  This doesn't use ptys, so this+-- will only work if the subprocess explicitly doesn't use block buffering.+conversation :: FilePath -> [String] -> Maybe [(String, String)]+    -> Chan.Chan TalkIn -> IO (Chan.Chan TalkOut)+conversation cmd args env input = do+    output <- Chan.newChan+    Concurrent.forkIO $ Process.withCreateProcess proc $+        \(Just stdin) (Just stdout) (Just stderr) pid -> do+            IO.hSetBuffering stdout IO.LineBuffering+            IO.hSetBuffering stderr IO.LineBuffering+            outThread <- Async.async $ void $ File.ignoreEOF $ forever $+                Chan.writeChan output . Stdout =<< Text.IO.hGetLine stdout+            errThread <- Async.async $ void $ File.ignoreEOF $ forever $+                Chan.writeChan output . Stderr =<< Text.IO.hGetLine stderr+            Concurrent.forkIO $ forever $ Chan.readChan input >>= \case+                Text t -> Text.IO.hPutStrLn stdin t >> IO.hFlush stdin+                EOF -> IO.hClose stdin+            -- Ensure both stdout and stderr are flushed before exit.+            Async.waitBoth outThread errThread+            code <- Process.waitForProcess pid+            Chan.writeChan output $ Exit $ case code of+                System.Exit.ExitFailure code -> code+                System.Exit.ExitSuccess -> 0+    return output+    where+    proc = (Process.proc cmd args)+        { Process.std_in = Process.CreatePipe+        , Process.std_out = Process.CreatePipe+        , Process.std_err = Process.CreatePipe+        , Process.close_fds = True+        , Process.env = env+        }
+ src/EL/Private/Ranges.hs view
@@ -0,0 +1,10 @@+module EL.Private.Ranges where+++merge_sorted :: Ord n => [(n, n)] -> [(n, n)]+merge_sorted [] = []+merge_sorted [x] = [x]+merge_sorted ((s1, e1) : (s2, e2) : rest)+    | e1 >= e2 = merge_sorted ((s1, e1) : rest)+    | e1 >= s2 = merge_sorted ((s1, e2) : rest)+    | otherwise = (s1, e1) : merge_sorted ((s2, e2) : rest)
+ src/EL/Private/Regex.hs view
@@ -0,0 +1,94 @@+-- | More user friendly regex api for PCRE regexes.+module EL.Private.Regex (+    Regex+    -- * compile+    , Option(..)+    , compile, compileOptions, compileUnsafe, compileOptionsUnsafe++    -- * matching+    , matches, groups, groupRanges+    -- * substitute+    , substitute, substituteGroups++    -- * misc+    , escape+) where+import qualified Data.ByteString as ByteString+import qualified Data.Text as Text+import Data.Text (Text)+import qualified Data.Text.Encoding as Encoding++import GHC.Stack (HasCallStack)+import qualified Text.Regex.PCRE.Heavy as PCRE+import Text.Regex.PCRE.Heavy (Regex)+import qualified Text.Regex.PCRE.Light as PCRE+++-- * compile++fromText :: Text -> ByteString.ByteString+fromText = Encoding.encodeUtf8++data Option = CaseInsensitive | DotAll | Multiline+    deriving (Ord, Eq, Show)++compile :: String -> Either String Regex+compile = compileOptions []++compileOptions :: [Option] -> String -> Either String Regex+compileOptions options text =+    case PCRE.compileM (fromText (Text.pack text)) (convertOptions options) of+        Left msg -> Left $ "compiling regex " ++ show text ++ ": " ++ msg+        Right regex -> Right regex++convertOptions :: [Option] -> [PCRE.PCREOption]+convertOptions = (options++) . map convert+    where+    convert opt = case opt of+        CaseInsensitive -> PCRE.caseless+        DotAll -> PCRE.dotall+        Multiline -> PCRE.multiline+    options = [PCRE.utf8, PCRE.no_utf8_check]++-- | Will throw a runtime error if the regex has an error!+compileUnsafe :: HasCallStack => String -> Regex+compileUnsafe = compileOptionsUnsafe []++-- | Will throw a runtime error if the regex has an error!+compileOptionsUnsafe :: HasCallStack => [Option] -> String -> Regex+compileOptionsUnsafe options = either error id . compileOptions options++-- * match++matches :: Regex -> Text -> Bool+matches = flip (PCRE.=~)++-- | Return (complete_match, [group_match]).+groups :: Regex -> Text -> [(Text, [Text])]+groups = PCRE.scan++-- | Half-open ranges of where the regex matches.+groupRanges :: Regex -> Text -> [((Int, Int), [(Int, Int)])]+    -- ^ (entire, [group])+groupRanges = PCRE.scanRanges++-- * substitute++-- | TODO this is not the usual thing where it replaces \1 \2 etc., but+-- it replaces the entire match.+substitute :: Regex -> Text -> Text -> Text+substitute regex sub = PCRE.gsub regex sub++substituteGroups :: Regex -> (Text -> [Text] -> Text)+    -- ^ (complete_match -> groups -> replacement)+    -> Text -> Text+substituteGroups = PCRE.gsub++-- * misc++-- | Escape a string so the regex matches it literally.+escape :: String -> String+escape "" = ""+escape (c : cs)+    | c `elem` ("\\^$.[|()?*+{" :: [Char]) = '\\' : c : escape cs+    | otherwise = c : escape cs
+ src/EL/Private/Seq.hs view
@@ -0,0 +1,779 @@+module EL.Private.Seq where+import Prelude hiding (head, last, tail)+import qualified Control.Arrow as Arrow+import qualified Data.Char as Char+import qualified Data.Either as Either+import Data.Function+import qualified Data.List as List+import Data.List.NonEmpty (NonEmpty(..))+import qualified Data.List.Ordered as Ordered+import qualified Data.Map as Map+import qualified Data.Maybe as Maybe+import Data.Monoid ((<>))+import qualified Data.Ord as Ord+import qualified Data.Set as Set++import qualified EL.Private.Then as Then+++-- | This is just a list, but is documentation that a return value will never+-- be null, or an argument should never be null.  This is for cases where+-- 'NonEmpty' is too annoying to work with.+type NonNull a = [a]++-- * enumeration++enumerate :: [a] -> [(Int, a)]+enumerate = zip [0..]++-- | Enumerate an inclusive range.  Uses multiplication instead of successive+-- addition to avoid loss of precision.+--+-- Also it doesn't require an Enum instance.+range :: (Num a, Ord a) => a -> a -> a -> [a]+range start end step = go 0+    where+    go i+        | step >= 0 && val > end = []+        | step < 0 && val < end = []+        | otherwise = val : go (i+1)+        where val = start + (i*step)+{-# INLINEABLE range #-}+{-# SPECIALIZE range :: Int -> Int -> Int -> [Int] #-}++-- | Enumerate a half-open range.+range' :: (Num a, Ord a) => a -> a -> a -> [a]+range' start end step = go 0+    where+    go i+        | step >= 0 && val >= end = []+        | step < 0 && val <= end = []+        | otherwise = val : go (i+1)+        where val = start + (i*step)+{-# INLINEABLE range' #-}+{-# SPECIALIZE range' :: Int -> Int -> Int -> [Int] #-}++-- | Like 'range', but always includes the end, even if it doesn't line up on+-- a step.+range_end :: (Num a, Ord a) => a -> a -> a -> [a]+range_end start end step = go 0+    where+    go i+        | step >= 0 && val >= end = [end]+        | step < 0 && val <= end = [end]+        | otherwise = val : go (i+1)+        where val = start + (i*step)+{-# INLINEABLE range_end #-}+{-# SPECIALIZE range_end :: Int -> Int -> Int -> [Int] #-}++-- | Infinite range.+range_ :: Num a => a -> a -> [a]+range_ start step = go 0+    where go i = start + (i*step) : go (i+1)+{-# INLINEABLE range_ #-}+{-# SPECIALIZE range_ :: Int -> Int -> [Int] #-}++-- * transformation++key_on :: (a -> k) -> [a] -> [(k, a)]+key_on f xs = zip (map f xs) xs++key_on_snd :: (a -> k) -> [a] -> [(a, k)]+key_on_snd f xs = zip xs (map f xs)++key_on_just :: (a -> Maybe k) -> [a] -> [(k, a)]+key_on_just f xs = [(k, a) | (Just k, a) <- key_on f xs]++-- | Apply a function to the first and last elements.  Middle elements are+-- unchanged.  A null or singleton list is also unchanged.+first_last :: (a -> a) -> (a -> a) -> [a] -> [a]+first_last start end xs = case xs of+        [] -> []+        [x] -> [x]+        x : xs -> start x : go xs+    where+    go [] = []+    go [x] = [end x]+    go (x:xs) = x : go xs++-- | Filter on the fst values returning Just.+map_maybe_fst :: (a -> Maybe a2) -> [(a, b)] -> [(a2, b)]+map_maybe_fst f xs = [(a, b) | (Just a, b) <- map (Arrow.first f) xs]++-- | Filter on the snd values returning Just.+map_maybe_snd :: (b -> Maybe b2) -> [(a, b)] -> [(a, b2)]+map_maybe_snd f xs = [(a, b) | (a, Just b) <- map (Arrow.second f) xs]++map_head :: (a -> a) -> [a] -> [a]+map_head _ [] = []+map_head f (x:xs) = f x : xs++map_tail :: (a -> a) -> [a] -> [a]+map_tail f (x:xs) = x : map f xs+map_tail _ [] = []++map_init :: (a -> a) -> [a] -> [a]+map_init _ [] = []+map_init _ [x] = [x]+map_init f (x:xs) = f x : map_init f xs++map_last :: (a -> a) -> [a] -> [a]+map_last _ [] = []+map_last f [x] = [f x]+map_last f (x:xs) = x : map_last f xs+++-- * permutations++-- | The cartesian product of a list of lists.  E.g.+-- @[[1, 2], [3, 4]]@ -> @[[1, 3], [1, 4], [2, 3], [2, 4]]@.+cartesian :: [[a]] -> [[a]]+cartesian [] = []+cartesian [xs] = [[x] | x <- xs]+cartesian (xs:rest) = [x:ps | x <- xs, ps <- cartesian rest]++-- * indexing lists++-- | Get @xs !! n@, but return Nothing if the index is out of range.+{-# SPECIALIZE at :: [a] -> Int -> Maybe a #-}+at :: (Num i, Ord i) => [a] -> i -> Maybe a+at xs n+    | n < 0 = Nothing+    | otherwise = _at xs n+    where+    _at [] _ = Nothing+    _at (x:_) 0 = Just x+    _at (_:xs) n = at xs (n-1)++-- | Insert @x@ into @xs@ at index @i@.  If @i@ is out of range, insert at the+-- beginning or end of the list.+insert_at :: Int -> a -> [a] -> [a]+insert_at i x xs = let (pre, post) = splitAt i xs in pre ++ (x : post)++-- | Remove the element at the given index.  Do nothing if the index is out+-- of range.+remove_at :: Int -> [a] -> [a]+remove_at i xs = let (pre, post) = splitAt i xs in pre ++ drop 1 post++-- | Like 'remove_at' but return the removed element as well.+take_at :: Int -> [a] -> Maybe (a, [a])+take_at i xs = case post of+        v : vs -> Just (v, pre ++ vs)+        [] -> Nothing+    where (pre, post) = splitAt i xs++-- | Modify element at an index by applying a function to it.  If the index is+-- out of range, nothing happens.+modify_at :: Int -> (a -> a) -> [a] -> [a]+modify_at i f xs = case post of+    [] -> pre+    elt : rest -> pre ++ f elt : rest+    where (pre, post) = splitAt i xs++-- | Find an element, then change it.  Return Nothing if the element wasn't+-- found.+find_modify :: (a -> Bool) -> (a -> a) -> [a] -> Maybe [a]+find_modify match modify = go+    where+    go (x:xs)+        | match x = Just $ modify x : xs+        | otherwise = (x:) <$> go xs+    go [] = Nothing++-- | Similar to 'modify_at', but will insert an element for an out of range+-- positive index.  The list will be extended with 'deflt', and the modify+-- function passed a Nothing.+update_at :: a -> Int -> (Maybe a -> a) -> [a] -> [a]+update_at deflt i f xs+    | i < 0 = error $ "Seq.update_at: negative index " ++ show i+    | otherwise = go i xs+    where+    go 0 [] = [f Nothing]+    go 0 (x:xs) = f (Just x) : xs+    go i [] = deflt : go (i-1) []+    go i (x:xs) = x : go (i-1) xs++-- | Move an element from one index to another, or Nothing if the @from@+-- index was out of range.+move :: Int -> Int -> [a] -> Maybe [a]+move from to xs = do+    (x, dropped) <- take_at from xs+    return $ insert_at to x dropped+++-- * min / max++min_on :: Ord k => (a -> k) -> a -> a -> a+min_on key x y = if key x <= key y then x else y++max_on :: Ord k => (a -> k) -> a -> a -> a+max_on key x y = if key x >= key y then x else y++minimum_on :: Ord k => (a -> k) -> [a] -> Maybe a+minimum_on _ [] = Nothing+minimum_on key xs = Just (List.foldl1' f xs)+    where f low x = if key x < key low then x else low++maximum_on :: Ord k => (a -> k) -> [a] -> Maybe a+maximum_on _ [] = Nothing+maximum_on key xs = Just (List.foldl1' f xs)+    where f high x = if key x > key high then x else high++minimum :: Ord a => [a] -> Maybe a+minimum [] = Nothing+minimum xs = Just (List.minimum xs)++maximum :: Ord a => [a] -> Maybe a+maximum [] = Nothing+maximum xs = Just (List.maximum xs)++ne_minimum :: Ord a => NonEmpty a -> a+ne_minimum (x :| xs) = List.minimum (x : xs)++ne_maximum :: Ord a => NonEmpty a -> a+ne_maximum (x :| xs) = List.maximum (x : xs)++-- * ordered lists++insert_on :: Ord k => (a -> k) -> a -> [a] -> [a]+insert_on key = List.insertBy (\a b -> compare (key a) (key b))++-- | Stable sort on a cheap key function.+sort_on :: Ord k => (a -> k) -> [a] -> [a]+sort_on = Ordered.sortOn'++-- | Like 'sort_on', but sort highest-to-lowest.+reverse_sort_on :: Ord b => (a -> b) -> [a] -> [a]+reverse_sort_on f = List.sortBy $ \a b -> Ord.comparing f b a++-- | Merge sorted lists.  If two elements compare equal, the one from the left+-- list comes first.+merge :: Ord a => [a] -> [a] -> [a]+merge = merge_on id++merge_by :: (a -> a -> Ordering) -> [a] -> [a] -> [a]+merge_by = Ordered.mergeBy++merge_on :: Ord k => (a -> k) -> [a] -> [a] -> [a]+merge_on key = Ordered.mergeBy (Ord.comparing key)++merge_lists :: Ord k => (a -> k) -> [[a]] -> [a]+merge_lists key = foldr (merge_on key) []++-- | If the heads of the sublists are also sorted I can be lazy in the list of+-- sublists too.  This version is optimized for minimal overlap.+merge_asc_lists :: Ord k => (a -> k) -> [[a]] -> [a]+merge_asc_lists key = foldr go []+    where+    go [] ys = ys+    go (x:xs) ys = x : merge_on key xs ys++-- * grouping++-- ** adjacent++-- Adjacent groups only group adjacent elements, just like 'List.group'.++-- | This is just 'List.groupBy' except with a key function.+group_adjacent :: Eq key => (a -> key) -> [a] -> [NonNull a]+group_adjacent key = List.groupBy ((==) `on` key)++-- | Like 'group_adjacent', but include the key.+keyed_group_adjacent :: Eq key => (a -> key) -> [a] -> [(key, NonNull a)]+keyed_group_adjacent key =+    key_on (key . List.head) . List.groupBy ((==) `on` key)++-- ** sort++-- Sort groups sort the input by the group key as a side-effect of grouping.++-- | Group the unsorted list into @(key x, xs)@ where all @xs@ compare equal+-- after @key@ is applied to them.+keyed_group_sort :: Ord key => (a -> key) -> [a] -> [(key, NonNull a)]+    -- ^ Sorted by key. The NonNull group is in the same order as the input.+keyed_group_sort key = Map.toAscList . foldr go Map.empty+    where go x = Map.alter (Just . maybe [x] (x:)) (key x)++-- | Similar to 'keyed_group_sort', but key on the fst element, and strip the+-- key out of the groups.+group_fst :: Ord a => [(a, b)] -> [(a, NonNull b)]+group_fst xs = [(key, map snd group) | (key, group) <- keyed_group_sort fst xs]++-- | Like 'group_fst', but group on the snd element.+group_snd :: Ord b => [(a, b)] -> [(NonNull a, b)]+group_snd xs = [(map fst group, key) | (key, group) <- keyed_group_sort snd xs]++-- | Like 'groupBy', but the list doesn't need to be sorted, and use a key+-- function instead of equality.  The list is sorted by the key, and the groups+-- appear in their original order in the input list.+group_sort :: Ord key => (a -> key) -> [a] -> [NonNull a]+group_sort key = map snd . keyed_group_sort key++-- ** stable++-- Stable groups preserve the original input order, in both the heads of the+-- groups, and within the groups themselves.++-- | Group each element with all the other elements that compare equal to it.+-- The heads of the groups appear in their original order.+group_stable_with :: (a -> a -> Bool) -> [a] -> [NonEmpty a]+group_stable_with is_equal = go+    where+    go [] = []+    go (x:xs) = (x :| equal) : go inequal+        where (equal, inequal) = List.partition (is_equal x) xs++-- | 'group_stable_with' but with a key function.+group_stable :: Eq key => (a -> key) -> [a] -> [NonEmpty a]+group_stable key = group_stable_with (\x y -> key x == key y)++keyed_group_stable :: Eq key => (a -> key) -> [a] -> [(key, [a])]+keyed_group_stable key = map (\(g :| gs) -> (key g, g:gs)) . group_stable key++-- * zipping++-- | Pair each element with the following element.  The last element is paired+-- with Nothing.  Like @zip xs (drop 1 xs ++ f (last xs))@ but only traverses+-- @xs@ once.+zip_next :: [a] -> [(a, Maybe a)]+zip_next [] = []+zip_next [x] = [(x, Nothing)]+zip_next (x : xs@(y:_)) = (x, Just y) : zip_next xs++zip_nexts :: [a] -> [(a, [a])]+zip_nexts xs = zip xs (drop 1 (List.tails xs))++zip_prev :: [a] -> [(Maybe a, a)]+zip_prev xs = zip (Nothing : map Just xs) xs++-- | Like 'zip_next' but with both preceding and following elements.+zip_neighbors :: [a] -> [(Maybe a, a, Maybe a)]+zip_neighbors [] = []+zip_neighbors (x:xs) = (Nothing, x, head xs) : go x xs+    where+    go _ [] = []+    go prev [x] = [(Just prev, x, Nothing)]+    go prev (x : xs@(y:_)) = (Just prev, x, Just y) : go x xs++-- | This is like 'zip', but it returns the remainder of the longer argument+-- instead of discarding it.+zip_remainder :: [a] -> [b] -> ([(a, b)], Either [a] [b])+zip_remainder (x:xs) (y:ys) = Arrow.first ((x, y) :) (zip_remainder xs ys)+zip_remainder [] ys = ([], Right ys)+zip_remainder xs [] = ([], Left xs)++data Paired a b = First !a | Second !b | Both !a !b+    deriving (Show, Eq)++paired_second :: Paired a b -> Maybe b+paired_second (First _) = Nothing+paired_second (Second b) = Just b+paired_second (Both _ b) = Just b++paired_first :: Paired a b -> Maybe a+paired_first (First a) = Just a+paired_first (Second _) = Nothing+paired_first (Both a _) = Just a++partition_paired :: [Paired a b] -> ([a], [b])+partition_paired (pair : pairs) = case pair of+    Both a b -> (a : as, b : bs)+    First a -> (a : as, bs)+    Second b -> (as, b : bs)+    where (as, bs) = partition_paired pairs+partition_paired [] = ([], [])++-- | Like 'zip', but emit 'First's or 'Second's if the list lengths are+-- unequal.+zip_padded :: [a] -> [b] -> [Paired a b]+zip_padded [] [] = []+zip_padded [] bs = map Second bs+zip_padded as [] = map First as+zip_padded (a:as) (b:bs) = Both a b : zip_padded as bs++-- | Like 'zip', but the second list is padded with Nothings.+zip_padded_snd :: [a] -> [b] -> [(a, Maybe b)]+zip_padded_snd [] _ = []+zip_padded_snd (x:xs) (y:ys) = (x, Just y) : zip_padded_snd xs ys+zip_padded_snd (x:xs) [] = [(x, Nothing) | x <- x : xs]++-- | Return the reversed inits paired with the tails.  This is like a zipper+-- moving focus along the input list.+zipper :: [a] -> [a] -> [([a], [a])]+zipper prev [] = [(prev, [])]+zipper prev lst@(x:xs) = (prev, lst) : zipper (x:prev) xs++-- | Pair @a@ elements up with @b@ elements.  If they are equal according to+-- the function, they'll both be Both in the result.  If an @a@ is deleted+-- going from @a@ to @b@, it will be First, and Second for @b@.+--+-- Kind of like an edit distance, or a diff.+equal_pairs :: (a -> b -> Bool) -> [a] -> [b] -> [Paired a b]+equal_pairs _ [] ys = map Second ys+equal_pairs _ xs [] = map First xs+equal_pairs eq (x:xs) (y:ys)+    | x `eq` y = Both x y : equal_pairs eq xs ys+    | any (eq x) ys = Second y : equal_pairs eq (x:xs) ys+    | otherwise = First x : equal_pairs eq xs (y:ys)++-- | This is like 'equal_pairs', except that the index of each pair in the+-- /right/ list is included.  In other words, given @(i, Second y)@,+-- @i@ is the position of @y@ in the @b@ list.  Given @(i, First x)@,+-- @i@ is where @x@ was deleted from the @b@ list.+indexed_pairs :: (a -> b -> Bool) -> [a] -> [b] -> [(Int, Paired a b)]+indexed_pairs eq xs ys = zip (indexed pairs) pairs+    where+    pairs = equal_pairs eq xs ys+    indexed = scanl f 0+        where+        f i (First _) = i+        f i _ = i+1++indexed_pairs_on :: Eq k => (a -> k) -> [a] -> [a] -> [(Int, Paired a a)]+indexed_pairs_on key = indexed_pairs (\a b -> key a == key b)++-- | Pair up two lists of sorted pairs by their first element.+pair_sorted :: Ord k => [(k, a)] -> [(k, b)] -> [(k, Paired a b)]+pair_sorted xs [] = [(k, First v) | (k, v) <- xs]+pair_sorted [] ys = [(k, Second v) | (k, v) <- ys]+pair_sorted x@((k0, v0) : xs) y@((k1, v1) : ys)+    | k0 == k1 = (k0, Both v0 v1) : pair_sorted xs ys+    | k0 < k1 = (k0, First v0) : pair_sorted xs y+    | otherwise = (k1, Second v1) : pair_sorted x ys++-- | Like 'pair_sorted', but use a key function, and omit the extracted key.+pair_sorted_on :: Ord k => (a -> k) -> [a] -> [a] -> [Paired a a]+pair_sorted_on key xs ys =+    map snd $ pair_sorted (key_on key xs) (key_on key ys)++-- | Sort the lists on with the key functions, then pair them up.+pair_on :: Ord k => (a -> k) -> (b -> k) -> [a] -> [b] -> [Paired a b]+pair_on k1 k2 xs ys = map snd $+    pair_sorted (sort_on fst (key_on k1 xs)) (sort_on fst (key_on k2 ys))++-- | Like 'pair_on', but when the lists have the same type.+pair_on1 :: Ord k => (a -> k) -> [a] -> [a] -> [Paired a a]+pair_on1 k = pair_on k k++-- | Left if the val was in the left list but not the right, Right for the+-- converse.+diff :: (a -> b -> Bool) -> [a] -> [b] -> [Either a b]+diff eq xs ys = Maybe.mapMaybe f (equal_pairs eq xs ys)+    where+    f (First a) = Just (Left a)+    f (Second a) = Just (Right a)+    f _ = Nothing++-- * partition++-- | Like 'List.partition', but partition by two functions consecutively.+partition2 :: (a -> Bool) -> (a -> Bool) -> [a] -> ([a], [a], [a])+partition2 f1 f2 xs = (as, bs, xs3)+    where+    (as, xs2) = List.partition f1 xs+    (bs, xs3) = List.partition f2 xs2++partition_on :: (a -> Maybe b) -> [a] -> ([b], [a])+partition_on f = go+    where+    go [] = ([], [])+    go (x:xs) = case f x of+        Just b -> (b:bs, as)+        Nothing -> (bs, x:as)+        where (bs, as) = go xs++-- * sublists++-- | Split into groups of a certain size.+chunked :: Int -> [a] -> [[a]]+chunked n xs = case splitAt n xs of+    ([], []) -> []+    (pre, []) -> [pre]+    (pre, post) -> pre : chunked n post++-- | Take a list of rows to a list of columns.  This is like a zip except+-- for variable-length lists.  Similar to zip, the result is trimmed to the+-- length of the shortest row.+rotate :: [[a]] -> [[a]]+rotate [] = []+rotate xs = maybe [] (: rotate (map List.tail xs)) (mapM head xs)++-- | Similar to 'rotate', except that the result is the length of the longest+-- row and missing columns are Nothing.  Analogous to 'zip_padded'.+rotate2 :: [[a]] -> [[Maybe a]]+rotate2 xs+    | all Maybe.isNothing heads = []+    | otherwise = heads : rotate2 (map tl xs)+    where+    heads = map head xs+    tl [] = []+    tl (_:xs) = xs+++-- ** extracting sublists++-- | Total variants of unsafe list operations.+head, last :: [a] -> Maybe a+head [] = Nothing+head (x:_) = Just x+last [] = Nothing+last xs = Just (List.last xs)++tail :: [a] -> Maybe [a]+tail [] = Nothing+tail (_:xs) = Just xs++-- | Drop adjacent elts if they are equal after applying the key function.+-- The first elt is kept.+drop_dups :: Eq k => (a -> k) -> [a] -> [a]+drop_dups _ [] = []+drop_dups key (x:xs) = x : map snd (filter (not . equal) (zip (x:xs) xs))+    where equal (x, y) = key x == key y++-- | Filter out elts when the predicate is true for adjacent elts.  The first+-- elt is kept, and the later ones are dropped.  This is like 'drop_dups'+-- except it can compare two elements.  E.g. @drop_with (>=)@ will ensure the+-- sequence is increasing.+drop_with :: (a -> a -> Bool) -> [a] -> [a]+drop_with _ [] = []+drop_with _ [x] = [x]+drop_with f (x:y:xs)+    | f x y = drop_with f (x:xs)+    | otherwise = x : drop_with f (y:xs)++-- | Like 'drop_dups', but return the dropped values.+partition_dups :: Ord k => (a -> k) -> [a] -> ([a], [(a, NonNull a)])+    -- ^ ([unique], [(used_for_unique, [dups])])+partition_dups key xs =+    Either.partitionEithers $ concatMap extract (group_sort key xs)+    where+    extract [] = []+    extract [x] = [Left x]+    extract (x:xs) = [Left x, Right (x, xs)]++-- | Like 'drop_dups', but keep the last adjacent equal elt instead of the+-- first.+drop_initial_dups :: Eq k => (a -> k) -> [a] -> [a]+drop_initial_dups _ [] = []+drop_initial_dups _ [x] = [x]+drop_initial_dups key (x:xs@(next:_))+    | key x == key next = rest+    | otherwise = x:rest+    where rest = drop_initial_dups key xs++unique :: Ord a => [a] -> [a]+unique = unique_on id++-- | This is like 'drop_dups', except that it's not limited to just adjacent+-- elts.  The output list is in the same order as the input.+unique_on :: Ord k => (a -> k) -> [a] -> [a]+unique_on f xs = go Set.empty xs+    where+    go _set [] = []+    go set (x:xs)+        | k `Set.member` set = go set xs+        | otherwise = x : go (Set.insert k set) xs+        where k = f x++-- | Like 'unique', but sort the list, and should be more efficient.+unique_sort :: Ord a => [a] -> [a]+unique_sort = Set.toList . Set.fromList++rtake :: Int -> [a] -> [a]+rtake n = snd . foldr go (n, [])+    where+    go x (n, xs)+        | n <= 0 = (0, xs)+        | otherwise = (n - 1, x : xs)++rtake_while :: (a -> Bool) -> [a] -> [a]+rtake_while f = either id (const []) . foldr go (Right [])+    where+    -- Left means I'm done taking.+    go _ (Left xs) = Left xs+    go x (Right xs)+        | f x = Right (x:xs)+        | otherwise = Left xs++rdrop :: Int -> [a] -> [a]+rdrop n = either id (const []) . foldr f (Right n)+    where+    f x (Right n)+        | n <= 0 = Left [x]+        | otherwise = Right (n-1)+    f x (Left xs) = Left (x:xs)++rdrop_while :: (a -> Bool) -> [a] -> [a]+rdrop_while f = foldr (\x xs -> if null xs && f x then [] else x:xs) []++lstrip, rstrip, strip :: String -> String+lstrip = dropWhile Char.isSpace+rstrip = rdrop_while Char.isSpace+strip = rstrip . lstrip++-- | If the list doesn't have the given prefix, return the original list and+-- False.  Otherwise, strip it off and return True.+drop_prefix :: Eq a => [a] -> [a] -> ([a], Bool)+drop_prefix pref list = go pref list+    where+    go [] xs = (xs, True)+    go _ [] = (list, False)+    go (p:ps) (x:xs)+        | p == x = go ps xs+        | otherwise = (list, False)++drop_suffix :: Eq a => [a] -> [a] -> ([a], Bool)+drop_suffix suffix list+    | post == suffix = (pre, True)+    | otherwise = (list, False)+    where (pre, post) = splitAt (length list - length suffix) list++-- ** span and break++-- | Like 'break', but the called function has access to the entire tail.+break_tails :: ([a] -> Bool) -> [a] -> ([a], [a])+break_tails _ [] = ([], [])+break_tails f lst@(x:xs)+    | f lst = ([], lst)+    | otherwise = let (pre, post) = break_tails f xs in (x:pre, post)++-- | 'List.span' from the end of the list.+span_end :: (a -> Bool) -> [a] -> ([a], [a])+span_end f xs = (reverse post, reverse pre)+    where (pre, post) = span f (reverse xs)++-- | Like 'span', but it can transform the spanned sublist.+span_while :: (a -> Maybe b) -> [a] -> ([b], [a])+span_while f = go+    where+    go [] = ([], [])+    go (a:as) = case f a of+        Just b -> Arrow.first (b:) (go as)+        Nothing -> ([], a : as)++-- | 'span_while' from the end of the list.+span_end_while :: (a -> Maybe b) -> [a] -> ([a], [b])+span_end_while f xs = (reverse post, reverse pre)+    where (pre, post) = span_while f (reverse xs)++-- | List initial and final element, if any.+viewr :: [a] -> Maybe ([a], a)+viewr [] = Nothing+viewr (x:xs) = Just $ go x xs+    where+    go x [] = ([], x)+    go x (x':xs) = let (pre, post) = go x' xs in (x:pre, post)++ne_viewr :: NonEmpty a -> ([a], a)+ne_viewr (x :| xs) =+    Maybe.fromMaybe (error "ne_viewr: not reached") (viewr (x : xs))++-- ** split and join++-- | Split before places where the function matches.+--+-- > > split_before (==1) [1,2,1]+-- > [[], [1, 2], [1]]+split_before :: (a -> Bool) -> [a] -> [[a]]+split_before f = go+    where+    go [] = []+    go xs = pre : case post of+        x : xs -> cons1 x (go xs)+        [] -> []+        where (pre, post) = break f xs+    cons1 x [] = [[x]]+    cons1 x (g:gs) = (x:g) : gs++-- | Split after places where the function matches.+split_after :: (a -> Bool) -> [a] -> [[a]]+split_after f = go+    where+    go [] = []+    go xs = pre : go post+        where (pre, post) = Then.break1 f xs++-- | Split 'xs' on 'sep', dropping 'sep' from the result.+split :: Eq a => NonNull a -> [a] -> NonNull [a]+split [] _ = error "Util.Seq.split: empty separator"+split sep xs = go sep xs+    where+    go sep xs+        | null post = [pre]+        | otherwise = pre : split sep (drop (length sep) post)+        where (pre, post) = break_tails (sep `List.isPrefixOf`) xs++-- | Like 'split', but it returns [] if the input was null.+split_null :: Eq a => NonNull a -> [a] -> [[a]]+split_null _ [] = []+split_null sep xs = split sep xs++-- | Like 'split', but only split once.+split1 :: Eq a => NonNull a -> [a] -> ([a], [a])+split1 [] _ = error "Util.Seq.split1: empty seperator"+split1 sep xs = (pre, drop (length sep) post)+    where (pre, post) = break_tails (sep `List.isPrefixOf`) xs++-- | Interspense a separator and concat.+join :: Monoid a => a -> [a] -> a+join sep = mconcat . List.intersperse sep++-- | Binary join, but the separator is only used if both joinees are non-empty.+join2 :: (Monoid a, Eq a) => a -> a -> a -> a+join2 sep x y+    | y == mempty = x+    | x == mempty = y+    | otherwise = x <> sep <> y++-- | Split the list on the points where the given function returns true.+--+-- This is similar to 'groupBy', except this is defined to compare adjacent+-- elements.  'groupBy' actually compares to the first element of each group.+-- E.g. you can't group numeric runs with @groupBy (\a b -> b > a+1)@.+split_between :: (a -> a -> Bool) -> [a] -> [[a]]+split_between _ [] = []+split_between f xs = pre : split_between f post+    where (pre, post) = break_between f xs++break_between :: (a -> a -> Bool) -> [a] -> ([a], [a])+break_between f (x1 : xs@(x2:_))+    | f x1 x2 = ([x1], xs)+    | otherwise = let (pre, post) = break_between f xs in (x1 : pre, post)+break_between _ xs = (xs, [])+++-- * replace++-- | Replace sublist @from@ with @to@ in the given list.+replace :: Eq a => [a] -> [a] -> [a] -> [a]+replace from to = go+    where+    len = length from+    go [] = []+    go lst@(x:xs)+        | from `List.isPrefixOf` lst = to ++ go (drop len lst)+        | otherwise = x : go xs++-- | Replace occurrances of an element with zero or more other elements.+replace1 :: Eq a => a -> [a] -> [a] -> [a]+replace1 from to = concatMap (\v -> if v == from then to else [v])+++-- * search++count :: Foldable t => (a -> Bool) -> t a -> Int+count f = List.foldl' (\n c -> if f c then n + 1 else n) 0+++-- * monadic++-- | Like 'List.mapAccumL', but monadic.+mapAccumLM :: Monad m => (state -> x -> m (state, y)) -> state -> [x]+    -> m (state, [y])+mapAccumLM f state xs = go state xs+    where+    go state [] = return (state, [])+    go state (x:xs) = do+        (state, y) <- f state x+        (state, ys) <- go state xs+        return (state, y : ys)
+ src/EL/Private/Then.hs view
@@ -0,0 +1,68 @@+-- Copyright 2013 Evan Laforge+-- This program is distributed under the terms of the GNU General Public+-- License 3.0, see COPYING or http://www.gnu.org/licenses/gpl-3.0.txt++-- | List functions with continuations.  This allows you to chain them and+-- easily express things like 'take until f then take one more'.+module EL.Private.Then where+import Prelude hiding (break, span, take, takeWhile, mapM)+import qualified Data.List as List+++takeWhile :: (a -> Bool) -> ([a] -> [a]) -> [a] -> [a]+takeWhile _ cont [] = cont []+takeWhile f cont (x:xs)+    | f x = x : takeWhile f cont xs+    | otherwise = cont (x:xs)++-- | takeWhile plus one extra+takeWhile1 :: (a -> Bool) -> [a] -> [a]+takeWhile1 f = takeWhile f (List.take 1)++take :: Int -> ([a] -> [a]) -> [a] -> [a]+take _ cont [] = cont []+take n cont (x:xs)+    | n <= 0 = cont (x:xs)+    | otherwise = x : take (n-1) cont xs++filter :: (a -> Bool) -> (a -> Bool) -> ([a] -> [a]) -> [a] -> [a]+filter f done cont = go+    where+    go [] = []+    go (x:xs)+        | done x = cont (x:xs)+        | f x = x : go xs+        | otherwise = go xs++-- | Like 'List.mapAccumL', except that you can do something with the final+-- state and append that to the list.+mapAccumL :: (acc -> x -> (acc, y)) -> acc -> (acc -> [y]) -> [x] -> [y]+mapAccumL f acc cont = go acc+    where+    go acc [] = cont acc+    go acc (x:xs) = y : go acc2 xs+        where (acc2, y) = f acc x++break :: (a -> Bool) -> ([a] -> ([a], rest))+    -- ^ Given the list after the break, return (pre, post), where pre will+    -- be appended to the end of the first list.+    -> [a] -> ([a], rest)+break f cont (x:xs)+    | f x = cont (x:xs)+    | otherwise = let (pre, post) = break f cont xs in (x:pre, post)+break _ cont [] = cont []++span :: (a -> Bool) -> ([a] -> ([a], rest)) -> [a] -> ([a], rest)+span f = break (not . f)++-- | Break right after the function returns True.+break1 :: (a -> Bool) -> [a] -> ([a], [a])+break1 f = break f (splitAt 1)++mapM :: Monad m => (a -> m b) -> m [b] -> [a] -> m [b]+mapM f cont = go+    where+    go [] = cont+    go (a:as) = do+        b <- f a+        (b:) <$> go as
+ src/EL/Test/ApproxEq.hs view
@@ -0,0 +1,67 @@+-- Copyright 2013 Evan Laforge+-- This program is distributed under the terms of the GNU General Public+-- License 3.0, see COPYING or http://www.gnu.org/licenses/gpl-3.0.txt++{-# LANGUAGE MagicHash #-}+-- | ApproxEq class for comparing floating point numbers.+module EL.Test.ApproxEq (ApproxEq(eq), compare) where+import Prelude hiding (compare)+import qualified Data.Text as Text+import qualified GHC.Prim as Prim+import qualified GHC.Types as Types+++compare :: (ApproxEq a, Ord a) => Double -> a -> a -> Ordering+compare eta a b+    | eq eta a b = EQ+    | a > b = GT+    | otherwise = LT++class ApproxEq a where+    eq :: Double -> a -> a -> Bool++instance ApproxEq Float where+    eq eta x y = abs (x - y) <= d2f eta+instance ApproxEq Double where+    eq eta x y = abs (x - y) <= eta++-- * prelude types++instance ApproxEq Char where eq _ = (==)+instance ApproxEq Int where eq _ = (==)+instance ApproxEq Integer where eq _ = (==)+instance ApproxEq Bool where eq _ = (==)++instance ApproxEq a => ApproxEq [a] where+    eq eta = go+        where+        go [] [] = True+        go [] _ = False+        go _ [] = False+        go (x:xs) (y:ys) = eq eta x y && go xs ys++instance ApproxEq a => ApproxEq (Maybe a) where+    eq eta (Just x) (Just y) = eq eta x y+    eq _ _ _ = False++instance (ApproxEq a, ApproxEq b) => ApproxEq (Either a b) where+    eq eta (Right x) (Right y) = eq eta x y+    eq eta (Left x) (Left y) = eq eta x y+    eq _ _ _ = False++instance (ApproxEq a, ApproxEq b) => ApproxEq (a, b) where+    eq eta (a, b) (a', b') = eq eta a a' && eq eta b b'+instance (ApproxEq a, ApproxEq b, ApproxEq c) => ApproxEq (a, b, c) where+    eq eta (a, b, c) (a', b', c') = eq eta a a' && eq eta b b' && eq eta c c'+instance (ApproxEq a, ApproxEq b, ApproxEq c, ApproxEq d) =>+        ApproxEq (a, b, c, d) where+    eq eta (a, b, c, d) (a', b', c', d') =+        eq eta a a' && eq eta b b' && eq eta c c' && eq eta d d'++-- * hackage types++instance ApproxEq Text.Text where eq _ = (==)+++d2f :: Double -> Float+d2f (Types.D# d) = Types.F# (Prim.double2Float# d)
+ src/EL/Test/Global.hs view
@@ -0,0 +1,7 @@+-- | Exports from "EL.Test.Testing".  This is meant to be imported+-- unqualified.+module EL.Test.Global (module EL.Test.Testing) where+import EL.Test.Testing+       (ModuleMeta(ModuleMeta), check, checkVal, equal, equalFmt, rightEqual,+        notEqual, equalf, stringsLike, leftLike, match, throws, ioEqual,+        success, failure, expectRight, quickcheck, qcEqual, pprint)
+ src/EL/Test/RunTests.hs view
@@ -0,0 +1,351 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE MultiWayIf #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+-- | Run tests.  This is meant to be invoked via a main module generated by+-- "EL.Test.GenerateRunTests".+module EL.Test.RunTests where+import qualified Control.Concurrent as Concurrent+import qualified Control.Concurrent.Async as Async+import qualified Control.Concurrent.Chan as Chan+import qualified Control.Concurrent.MVar as MVar+import qualified Control.Exception as Exception+import qualified Control.Monad.Fix as Fix++import qualified Data.List as List+import qualified Data.Maybe as Maybe+import qualified Data.Set as Set+import qualified Data.Text as Text+import qualified Data.Text.IO as Text.IO+import qualified Data.Text.Lazy as Text.Lazy+import qualified Data.Text.Lazy.IO as Text.Lazy.IO++import qualified Numeric+import qualified System.CPUTime as CPUTime+import qualified System.Console.GetOpt as GetOpt+import qualified System.Directory as Directory+import qualified System.Environment as Environment+import qualified System.Environment+import qualified System.Exit+import System.FilePath ((</>))+import qualified System.IO as IO+import qualified System.Process as Process++import qualified Text.Read as Read++import qualified EL.Private.File as File+import qualified EL.Private.Process as EL.Process+import qualified EL.Private.Regex as Regex+import qualified EL.Private.Seq as Seq+import qualified EL.Test.Testing as Testing++import Global+++data Test = Test {+    -- | Name of the test function.+    testSymName :: Text+    -- | Run the test.+    , testRun :: IO ()+    -- | Test module filename.+    , testFilename :: FilePath+    -- | Line of the test function declaration.+    , testLine :: Int+    -- | Module-level metadata, declared as @meta@ in the test module toplevel.+    , testModuleMeta_ :: Maybe Testing.ModuleMeta+    }++testModuleMeta :: Test -> Testing.ModuleMeta+testModuleMeta = Maybe.fromMaybe Testing.moduleMeta . testModuleMeta_++testName :: Test -> Text+testName test = Text.intercalate "," tags <> "-" <> testSymName test+    where+    tags = if null tags_ then ["normal"] else tags_+    tags_ = Seq.unique_sort $ map (Text.toLower . showt) $+        Testing.tags (testModuleMeta test)++-- Prefix for lines with test metadata.+metaPrefix :: Text+metaPrefix = "===>"++data Flag =+    CheckOutput+    | ClearDirs+    | Jobs !Jobs+    | List+    | Output !FilePath+    | Subprocess+    deriving (Eq, Show)++data Jobs = Auto | NJobs !Int deriving (Eq, Show)++options :: [GetOpt.OptDescr Flag]+options =+    [ GetOpt.Option [] ["check-output"] (GetOpt.NoArg CheckOutput)+        "Check output for failures after running. Only valid with --output."+    , GetOpt.Option [] ["clear-dirs"] (GetOpt.NoArg ClearDirs)+        "Remove everything in the test tmp dir and --output.\+        \ This is probably just for cabal, which can't wrap tests in a shell\+        \ script."+    , GetOpt.Option [] ["jobs"] (GetOpt.ReqArg (Jobs . parseJobs) "1")+        "Number of parallel jobs, or 'auto' for Concurrent.getNumCapabilities."+    , GetOpt.Option [] ["list"] (GetOpt.NoArg List) "display but don't run"+    , GetOpt.Option [] ["output"] (GetOpt.ReqArg Output "path")+        "Path to a directory to put output logs, if not given output goes to\+        \ stdout."+    , GetOpt.Option [] ["subprocess"] (GetOpt.NoArg Subprocess)+        "Read test names on stdin.  This is meant to be run as a subprocess\+        \ by --jobs."+    ]+    where+    parseJobs s+        | s == "auto" = Auto+        | Just n <- Read.readMaybe s = NJobs n+        | otherwise = error $ "jobs should be auto or a number, was: " <> show s++-- | Called by the generated main function.+run :: [String] -> [Test] -> IO ()+run defaultArgs allTests = do+    IO.hSetBuffering IO.stdout IO.LineBuffering+    args <- System.Environment.getArgs+    args <- return $ if null args then defaultArgs else args+    (flags, args) <- case GetOpt.getOpt GetOpt.Permute options args of+        (opts, n, []) -> return (opts, n)+        (_, _, errors) -> quitWithUsage defaultArgs errors+    ok <- runTests allTests flags args+    if ok then System.Exit.exitSuccess else System.Exit.exitFailure++quitWithUsage :: [String] -> [String] -> IO a+quitWithUsage defaultArgs errors = do+    progName <- System.Environment.getProgName+    putStrLn $ "usage: " <> progName <> " [ flags ] regex regex ..."+    putStr $ GetOpt.usageInfo "Run tests that match any regex." options+    unless (null defaultArgs) $+        putStrLn $ "\ndefault args provided by generator:\n"+            <> unwords defaultArgs+    unless (null errors) $+        putStr $ "\nerrors:\n" <> unlines errors+    System.Exit.exitFailure++runTests :: [Test] -> [Flag] -> [String] -> IO Bool+runTests allTests flags regexes = do+    when (mbOutputDir == Nothing && CheckOutput `elem` flags) $+        quitWithUsage [] ["--check-output requires --output"]+    when (ClearDirs `elem` flags) $ do+        Directory.createDirectoryIfMissing True Testing.tmpBaseDir+        clearDirectory Testing.tmpBaseDir+        whenJust mbOutputDir clearDirectory+    if  | List `elem` flags -> do+            mapM_ Text.IO.putStrLn $ List.sort $ map testName $+                if null regexes then allTests else matches+            return True+        | Subprocess `elem` flags -> subprocess allTests >> return True+        | Just outputDir <- mbOutputDir -> do+            jobs <- getJobs $+                fromMaybe (NJobs 1) $ Seq.last [n | Jobs n <- flags]+            runOutput outputDir jobs matches (CheckOutput `elem` flags)+        | otherwise -> mapM_ runTest matches >> return True+    where+    mbOutputDir = Seq.last [d | Output d <- flags]+    matches = matchingTests regexes allTests++getJobs :: Jobs -> IO Int+getJobs (NJobs n) = return n+getJobs Auto = Concurrent.getNumCapabilities++runOutput :: FilePath -> Int -> [Test] -> Bool -> IO Bool+runOutput outputDir jobs tests check = do+    Directory.createDirectoryIfMissing True outputDir+    let outputs = [outputDir </> "out" <> show n <> ".stdout" | n <- [1..jobs]]+    runParallel outputs tests+    if check+        then checkOutputs outputs+        else return True+    -- TODO run hpc?++-- | Isolate the test by running it in a subprocess.  I'm not sure if this is+-- necessary, but I believe at the time GUI-using tests would crash each other+-- without it.  Presumably they left some GUI state around that process exit+-- will clean up.+runInSubprocess :: Test -> IO ()+runInSubprocess test = do+    argv0 <- System.Environment.getExecutablePath+    putStrLn $ "subprocess: " ++ show argv0 ++ " " ++ show [testName test]+    val <- Process.rawSystem argv0 [untxt (testName test)]+    case val of+        System.Exit.ExitFailure code -> Testing.withTestName (testName test) $+            void $ Testing.failure $+                "test returned " <> showt code <> ": " <> testName test+        _ -> return ()++-- * parallel jobs++-- | Run tests in parallel, redirecting stdout and stderr to each output.+runParallel :: [FilePath] -> [Test] -> IO ()+runParallel _ [] = return ()+runParallel outputs tests = do+    let byModule = Seq.keyed_group_adjacent testFilename tests+    queue <- newQueue [(txt name, tests) | (name, tests) <- byModule]+    Async.forConcurrently_ (map fst (zip outputs byModule)) $+        \output -> jobThread output queue++-- | Pull tests off the queue and feed them to a single subprocess.+jobThread :: FilePath -> Queue (Text, [Test]) -> IO ()+jobThread output queue =+    Exception.bracket (IO.openFile output IO.AppendMode) IO.hClose $ \hdl -> do+        to <- Chan.newChan+        env <- Environment.getEnvironment+        argv0 <- System.Environment.getExecutablePath+        -- Give each subprocess its own .tix, or they will stomp on each other+        -- and crash.+        from <- EL.Process.conversation argv0 ["--subprocess"]+            (Just (("HPCTIXFILE", output <> ".tix") : env)) to+        whileJust (takeQueue queue) $ \(name, tests) -> do+            put $ untxt name+            Chan.writeChan to $ EL.Process.Text $+                Text.unwords (map testName tests) <> "\n"+            Fix.fix $ \loop -> Chan.readChan from >>= \case+                EL.Process.Stdout line+                    | line == testsCompleteLine -> return ()+                    | otherwise -> Text.IO.hPutStrLn hdl line >> loop+                EL.Process.Stderr line -> Text.IO.hPutStrLn hdl line >> loop+                EL.Process.Exit n -> put $ "completed early: " <> show n+        Chan.writeChan to EL.Process.EOF+        final <- Chan.readChan from+        case final of+            EL.Process.Exit n+                | n == 0 -> return ()+                | otherwise -> put $ "completed " <> show n+            _ -> put $ "expected Exit, but got " <> show final+    where+    put = putStr . ((output <> ": ")<>) . (<>"\n")++subprocess :: [Test] -> IO ()+subprocess allTests = void $ File.ignoreEOF $ forever $ do+    testNames <- Set.fromList . Text.words <$> Text.IO.getLine+    -- For some reason, I get an extra "" from getLine when the parent process+    -- closes the pipe.  From the documentation I think it should throw EOF.+    unless (Set.null testNames) $ do+        let tests = filter ((`Set.member` testNames) . testName) allTests+        mapM_ runTest tests+            `Exception.finally` Text.IO.hPutStrLn IO.stdout testsCompleteLine+            -- I previously wrote this on stderr, but it turns out it then+            -- gets nondeterministically interleaved with stdout.++-- | Signal to the caller that the current batch of tests are done.+testsCompleteLine :: Text+testsCompleteLine = "•complete•"++-- * run tests++-- | Match all tests whose names match any regex, or if a test is an exact+-- match, just that test.+matchingTests :: [String] -> [Test] -> [Test]+matchingTests regexes tests = concatMap match regexes+    where+    match reg = case List.find ((== txt reg) . testName) tests of+        Just test -> [test]+        Nothing -> filter (Regex.matches (Regex.compileUnsafe reg) . testName)+            tests++runTest :: Test -> IO ()+runTest test = Testing.withTestName name $ isolate $ do+    Text.IO.putStrLn $ Text.unwords [metaPrefix, "run-test", testName test]+    start <- CPUTime.getCPUTime+    Testing.initialize (testModuleMeta test) $+        catch (testSymName test) (testRun test)+    end <- CPUTime.getCPUTime+    -- CPUTime is in picoseconds.+    let secs = fromIntegral (end - start) / 10^12+    -- Grep for timing to make a histogram.+    Text.IO.putStrLn $ Text.unwords [metaPrefix, "timing", testName test,+        txt $ Numeric.showFFloat (Just 3) secs ""]+    return ()+    where name = last (Text.split (=='.') (testName test))++-- | Try to save and restore any process level state in case the test messes+-- with it.  Currently this just restores CWD, but probably there is more than+-- that.  For actual isolation probably a subprocess is necessary.+isolate :: IO a -> IO a+isolate = Directory.withCurrentDirectory "."++catch :: Text -> IO a -> IO ()+catch name action = do+    result <- Exception.try action+    case result of+        Left (exc :: Exception.SomeException) -> do+            void $ Testing.failure $ name <> " threw exception: " <> showt exc+            -- Die on async exception, otherwise it will try to continue+            -- after ^C or out of memory.+            case Exception.fromException exc of+                Just (exc :: Exception.AsyncException) -> Exception.throwIO exc+                Nothing -> return ()+        Right _ -> return ()++-- * queue++-- | This is a simple channel which is written to once, and read from until+-- empty.+newtype Queue a = Queue (MVar.MVar [a])++newQueue :: [a] -> IO (Queue a)+newQueue = fmap Queue . MVar.newMVar++takeQueue :: Queue a -> IO (Maybe a)+takeQueue (Queue mvar) = MVar.modifyMVar mvar $ \as -> return $ case as of+    [] -> ([], Nothing)+    a : as -> (as, Just a)++whileJust :: Monad m => m (Maybe a) -> (a -> m ()) -> m ()+whileJust get action = Fix.fix $ \loop -> get >>= \case+    Nothing -> return ()+    Just a -> action a >> loop++-- * check output++-- | Empty the directory, but don't remove it entirely, in case it's /tmp or+-- something.+clearDirectory :: FilePath -> IO ()+clearDirectory dir = mapM_ rm =<< File.list dir+    where+    -- Let's not go all the way to Directory.removePathForcibly.+    rm fn = Directory.doesDirectoryExist fn >>= \isDir -> if isDir+        then Directory.removeDirectoryRecursive fn+        else Directory.removeFile fn++checkOutputs :: [FilePath] -> IO Bool+checkOutputs outputs = do+    (failureContext, failures, checks, tests) <-+        extractStats <$> concatMapM readFileEmpty outputs+    unless (null failureContext) $ putStrLn "\n*** FAILURES:"+    mapM_ Text.IO.putStrLn failureContext+    Text.IO.putStrLn $ showt failures <> " failed / "+        <> showt checks <> " checks / " <> showt tests <> " tests"+    return $ failures == 0++readFileEmpty :: FilePath -> IO Text.Lazy.Text+readFileEmpty = fmap (fromMaybe "") . File.ignoreEnoent . Text.Lazy.IO.readFile++extractStats :: Text.Lazy.Text -> ([Text], Int, Int, Int)+extractStats = collect . drop 1 . Seq.split_before isTest . Text.Lazy.lines+    where+    collect tests = (failures, length failures, length extracted, length tests)+        where+        failures = Maybe.catMaybes extracted+        extracted = concatMap extractFailures tests+    isTest = ((Text.Lazy.fromStrict (metaPrefix <> " run-test"))+        `Text.Lazy.isPrefixOf`)++-- | Collect lines before each failure for context.+extractFailures :: [Text.Lazy.Text] -> [Maybe Text]+extractFailures =+    mapMaybe fmt . Seq.split_after (\x -> isFailure x || isSuccess x)+    where+    fmt lines+        | maybe False isFailure (Seq.last lines) =+            Just $ Just $ Text.Lazy.toStrict (Text.Lazy.unlines lines)+        | maybe False isSuccess (Seq.last lines) = Just Nothing+        | otherwise = Nothing+    isFailure = ("__-> " `Text.Lazy.isPrefixOf`)+    isSuccess = ("++-> " `Text.Lazy.isPrefixOf`)
+ src/EL/Test/TestKaryaGenerate.hs view
@@ -0,0 +1,178 @@+{-# LANGUAGE OverloadedStrings #-}+{- | Collect tests from the given modules and generate a haskell module that+    calls the tests.  Test functions are any function starting with @test_@ or+    @profile_@ and immediately followed by @=@ (implying the function has no+    arguments).  This module doesn't distinguish between tests and profiles,+    but they should presumably be compiled separately since they require+    different flags.++    If a module has a function called @initialize@, it will be called as+    @IO ()@ prior to the tests.+-}+module EL.Test.TestKaryaGenerate (main) where+import qualified Data.Char as Char+import qualified Data.List as List+import qualified Data.Map as Map+import qualified Data.Text as Text+import qualified Data.Text.IO as Text.IO++import qualified System.Directory as Directory+import qualified System.Environment+import qualified System.FilePath as FilePath+import qualified System.IO as IO++import qualified EL.Private.ExtractHs as ExtractHs+import qualified EL.Private.Regex as Regex+import Global+++-- pgmF args: OrigSourceName.hs input.hs output.hs+main :: IO ()+main = do+    args <- System.Environment.getArgs+    (baseDir, outputFile, defaultArgs) <- case args of+        origFile : _inputFile : outputFile : defaultArgs -> return+            ( FilePath.takeDirectory origFile+            , outputFile+            , if null defaultArgs then defaultDefaultArgs else defaultArgs+            )+        _ -> error "expected: origFile inputFile outputFile"+    inputFiles <- find (Char.isUpper . head) ("_test.hs" `List.isSuffixOf`)+        baseDir+    extracted <- ExtractHs.extractFiles extract inputFiles+    let (warnings, output) = generate defaultArgs $+            Map.mapKeys (stripPrefix baseDir) extracted+    mapM_ (Text.IO.hPutStrLn IO.stderr) warnings+    progName <- System.Environment.getProgName+    Text.IO.writeFile outputFile $ header progName <> output++defaultDefaultArgs :: [String]+defaultDefaultArgs =+    [ "--jobs=auto"+    , "--clear-dir"+    , "--output=dist/test-output"+    , "--check-output"+    , "."+    ]++header :: String -> Text+header program = Text.unlines+    [ "-- automatically generated by " <> txt program+    , "{-# LANGUAGE OverloadedStrings #-}"+    ]++-- | Recursively find files below a directory.+find :: (FilePath -> Bool) -> (FilePath -> Bool) -> FilePath -> IO [FilePath]+find wantDir wantFile = fmap concat . go+    where+    go dir = do+        (dirs, fns) <- partitionM Directory.doesDirectoryExist =<< list dir+        rest <- mapM go (filter (wantDir . FilePath.takeFileName) dirs)+        return $ filter wantFile fns : concat rest++list :: FilePath -> IO [FilePath]+list dir = map (dir FilePath.</>) <$> Directory.listDirectory dir++partitionM :: Monad m => (a -> m Bool) -> [a] -> m ([a], [a])+partitionM fp = go+    where+    go (x:xs) = fp x >>= \b -> case b of+        True -> (\(as, bs) -> (x:as, bs)) <$> go xs+        False -> (\(as, bs) -> (as, x:bs)) <$> go xs+    go [] = return ([], [])++stripPrefix :: FilePath -> FilePath -> FilePath+stripPrefix prefix fname = dropWhile (=='/') $+    if prefix `List.isPrefixOf` fname+        then drop (length prefix) fname else fname+++-- * generate++type Warning = Text++generate :: [String] -> Map FilePath ([Test], HasMeta) -> ([Warning], Text)+generate defaultArgs extracted = (,) warnings $+    testTemplate+        (Text.unlines $ map ExtractHs.makeImport (Map.keys fnameTests))+        (Text.intercalate "\n    , " $ makeTests fnameTests)+        defaultArgs+    where+    (empty, fnameTests) = Map.partition (null . fst) extracted+    warnings = map (("Warning: no (test|profile)_* defs in " <>) . txt)+        (Map.keys empty)++testTemplate :: Text -> Text -> [String] -> Text+testTemplate imports allTests defaultArgs =+    "import qualified EL.Test.RunTests as RunTests\n\+    \import EL.Test.RunTests (Test(..))\n\+    \\n"+    <> imports <> "\n\+    \\n\+    \tests :: [Test]\n\+    \tests = \n\+    \    [ " <> allTests <> "\n\+    \    ]\n\+    \\n\+    \main :: IO ()\n\+    \main = RunTests.run " <> showt defaultArgs <> " tests\n"++data Test  = Test {+    testLineNumber :: !LineNumber+    , testName :: !Text+    } deriving (Show)+type LineNumber = Int+++-- * extract++type HasMeta = Maybe Text++-- | Extract test functions and possible metadata from the file.+extract :: Text -> ([Test], HasMeta)+extract content = (extractTests content, hasMeta content)++hasMeta :: Text -> HasMeta+hasMeta =+    (\b -> if b then Just "meta" else Nothing) . Regex.matches reg+    where reg = Regex.compileOptionsUnsafe [Regex.Multiline] "^meta\\b"++extractTests :: Text -> [Test]+extractTests = go . zip [1..] . Text.lines+    where+    go ((i, line) : lines)+        | Just def <- hasTestFunction line = Test+            { testLineNumber = i+            , testName = def+            } : go rest+        | otherwise = go lines+        where+        rest = dropWhile (isIndented . snd) lines+        -- TODO does this get fooled by empty lines?+        isIndented t = " " `Text.isPrefixOf` t || t == "\n"+    go [] = []++hasTestFunction :: Text -> Maybe Text+hasTestFunction line+    | Regex.matches reg line = Just $ head (Text.words line)+    | otherwise = Nothing+    where+    reg = Regex.compileUnsafe "^(?:test|profile)_[a-zA-Z0-9_]+ \\="++-- * make++makeTests :: Map.Map FilePath ([Test], HasMeta) -> [Text]+makeTests fnameTests =+    [ makeTestLine fname test meta+    | (fname, (tests, meta)) <- Map.toList fnameTests, test <- tests+    ]++makeTestLine :: FilePath -> Test -> HasMeta -> Text+makeTestLine fname test meta = Text.unwords+    [ "Test", showt name, "(" <> name <> " >> return ())"+    , showt fname, showt (testLineNumber test)+    , case meta of+        Nothing -> "Nothing"+        Just fn -> "(Just " <> ExtractHs.pathToModule fname <> "." <> fn <> ")"+    ]+    where name = ExtractHs.pathToModule fname <> "." <> testName test
+ src/EL/Test/Testing.hs view
@@ -0,0 +1,498 @@+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE NamedFieldPuns, DisambiguateRecordFields #-}+-- | Basic testing utilities.+module EL.Test.Testing (+    Config(..), modifyTestConfig, withTestName+    -- * metadata+    , ModuleMeta(..), moduleMeta, Tag(..)+    -- * assertions+    , check, checkVal+    , equal, equalFmt, rightEqual, notEqual, equalf, stringsLike+    , leftLike, match+    , Pattern+    -- ** exception assertions+    , throws++    -- ** io assertions+    , ioEqual++    -- ** low level+    , success, failure++    -- * extracting+    , expectRight++    -- * QuickCheck+    , quickcheck+    , qcEqual++    -- * pretty printing+    , pprint++    -- * filesystem+    , uniqueTmpDir, inTmpDir, tmpBaseDir++    -- * util+    , force+) where+import qualified Control.DeepSeq as DeepSeq+import qualified Control.Exception as Exception+import Control.Monad (unless)++import qualified Data.Algorithm.Diff as Diff+import qualified Data.IORef as IORef+import qualified Data.IntMap as IntMap+import qualified Data.Map as Map+import Data.Monoid ((<>))+import qualified Data.Set as Set+import qualified Data.Text as Text+import Data.Text (Text)+import qualified Data.Text.IO as Text.IO++import qualified GHC.Stack as Stack+import GHC.Stack (HasCallStack)+import qualified System.Directory as Directory+import System.FilePath ((</>))+import qualified System.IO.Unsafe as Unsafe+import qualified System.Posix.IO as IO+import qualified System.Posix.Temp as Temp+import qualified System.Posix.Terminal as Terminal++import qualified Test.QuickCheck as QuickCheck++import qualified EL.Private.Map as EL.Map+import qualified EL.Private.PPrint as PPrint+import qualified EL.Private.Ranges as Ranges+import qualified EL.Private.Regex as Regex+import qualified EL.Private.Seq as Seq+import qualified EL.Test.ApproxEq as ApproxEq+++{-# NOINLINE testConfig #-}+testConfig :: IORef.IORef Config+testConfig = Unsafe.unsafePerformIO $ IORef.newIORef $ Config+    { configTestName = "no-test"+    }++modifyTestConfig :: (Config -> Config) -> IO ()+modifyTestConfig = IORef.modifyIORef testConfig++-- | Set 'configTestName'.  This is a grody hack, but I need it because GHC+-- call stack is off by one, so you get the caller line number, but the+-- callee's function name: https://ghc.haskell.org/trac/ghc/ticket/11686+withTestName :: Text -> IO a -> IO a+withTestName name action = do+    modifyTestConfig (\config -> config { configTestName = name })+    action++data Config = Config {+    -- | Keep the test name so I can report it in 'success' in 'failure'.+    configTestName :: !Text+    } deriving (Show)++check :: HasCallStack => Text -> Bool -> IO Bool+check msg False = failure ("failed: " <> msg)+check msg True = success msg++-- | Check against a function.  Use like:+--+-- > checkVal (f x) $ \case -> ...+checkVal :: Show a => HasCallStack => a -> (a -> Bool) -> IO Bool+checkVal val f+    | f val = success $ "ok: " <> pshowt val+    | otherwise = failure $ "failed:" <> pshowt val++-- * metadata++data ModuleMeta = ModuleMeta {+    -- | Wrap each test with IO level setup and teardown.  Sync exceptions are+    -- caught from the test function, so this should only see async exceptions.+    initialize :: IO () -> IO ()+    , tags :: [Tag]+    }++moduleMeta :: ModuleMeta+moduleMeta = ModuleMeta+    { initialize = id+    , tags = []+    }++data Tag = Large -- ^ Especially expensive to run.+    deriving (Eq, Show)++-- * equal and diff++equal :: (HasCallStack, Show a, Eq a) => a -> a -> IO Bool+equal a b+    | a == b = success $ cmp True+    | otherwise = failure $ cmp False+    where cmp = prettyCompare "==" "/=" True a b++equalFmt :: (HasCallStack, Eq a, Show a) => (a -> Text) -> a -> a -> IO Bool+equalFmt fmt a b = do+    ok <- equal a b+    let (pa, pb) = (fmt a, fmt b)+    unless (ok || Text.null pa && Text.null pb) $+        Text.IO.putStrLn $ showDiff pa pb+    return ok+    where+    showDiff prettyA prettyB = fmtLines "/="+        (Text.lines $ highlightLines color diffA prettyA)+        (Text.lines $ highlightLines color diffB prettyB)+        where+        color = failureColor+        (diffA, diffB) = diffRanges prettyA prettyB++notEqual :: (HasCallStack, Show a, Eq a) => a -> a -> IO Bool+notEqual a b+    | a == b = failure $ cmp True+    | otherwise = success $ cmp False+    where cmp = prettyCompare "==" "/=" False a b++rightEqual :: (HasCallStack, Show err, Show a, Eq a) => Either err a -> a+    -> IO Bool+rightEqual (Right a) b = equal a b+rightEqual (Left err) _ = failure $ "Left: " <> pshowt err++-- | Show the values nicely, whether they are equal or not.+prettyCompare :: Show a =>+    Text -- ^ equal operator+    -> Text -- ^ inequal operator+    -> Bool -- ^ If True then equal is expected so inequal will be highlighted+    -- red.  Otherwise, inequal is expected and highlighted green.+    -> a -> a -> Bool -- ^ True if as are equal+    -> Text+prettyCompare equal inequal expectEqual a b isEqual+    | isEqual = equal <> " " <> ellipse (showt a)+    | otherwise = fmtLines inequal+        (Text.lines $ highlightLines color diffA prettyA)+        (Text.lines $ highlightLines color diffB prettyB)+    where+    color = if expectEqual then failureColor else successColor+    (diffA, diffB) = diffRanges prettyA prettyB+    prettyA = Text.strip $ pshowt a+    prettyB = Text.strip $ pshowt b+    -- Equal values are usually not interesting, so abbreviate if they're too+    -- long.+    ellipse s+        | len > maxlen = Text.take maxlen s <> "... {" <> showt len <> "}"+        | otherwise = s+        where len = Text.length s+    maxlen = 200++-- | Apply color ranges as produced by 'diffRanges'.+highlightLines :: ColorCode -> IntMap.IntMap [CharRange] -> Text -> Text+highlightLines color nums = Text.unlines . zipWith hi [0..] . Text.lines+    where+    hi i line = case IntMap.lookup i nums of+        Just ranges -> highlightRanges color ranges line+        Nothing -> line++highlightRanges :: ColorCode -> [CharRange] -> Text -> Text+highlightRanges color ranges = mconcat . map hi . splitRanges ranges+    where hi (outside, inside) = outside <> highlight color inside++splitRanges :: [(Int, Int)] -> Text -> [(Text, Text)] -- ^ (out, in) pairs+splitRanges ranges = go 0 ranges+    where+    go _ [] text+        | Text.null text = []+        | otherwise = [(text, mempty)]+    go prev ((s, e) : ranges) text = (pre, within) : go e ranges post+        where+        (pre, rest) = Text.splitAt (s-prev) text+        (within, post) = Text.splitAt (e - s) rest++type CharRange = (Int, Int)++diffRanges :: Text -> Text+    -> (IntMap.IntMap [CharRange], IntMap.IntMap [CharRange])+diffRanges first second =+    toMap $ Seq.partition_paired $ map diffLine $+        EL.Map.pairs firstByLine secondByLine+    where+    toMap (as, bs) = (IntMap.fromList as, IntMap.fromList bs)+    diffLine (num, d) = case d of+        Seq.Both line1 line2 -> Seq.Both (num, d1) (num, d2)+            where (d1, d2) = charDiff line1 line2+        Seq.First line1 -> Seq.First (num, [(0, Text.length line1)])+        Seq.Second line2 -> Seq.Second (num, [(0, Text.length line2)])+    firstByLine = Map.fromList+        [(n, text) | Diff.First (Numbered n text) <- diffs]+    secondByLine = Map.fromList+        [(n, text) | Diff.Second (Numbered n text) <- diffs]+    diffs = numberedDiff (==) (Text.lines first) (Text.lines second)++charDiff :: Text -> Text -> ([CharRange], [CharRange])+charDiff first second+    | tooDifferent firstCs || tooDifferent secondCs =+        ([(0, Text.length first)], [(0, Text.length second)])+    | otherwise = (firstCs, secondCs)+    where+    firstCs = toRanges [n | Diff.First (Numbered n _) <- diffs]+    secondCs = toRanges [n | Diff.Second (Numbered n _) <- diffs]+    diffs = numberedDiff (==) (Text.unpack first) (Text.unpack second)+    -- If there are too many diff ranges let's just mark the whole thing+    -- different.  Perhaps I should ignore spaces that are the same, but let's+    -- see how this work first.+    tooDifferent ranges = length ranges > 2++toRanges :: [Int] -> [(Int, Int)]+toRanges xs = Ranges.merge_sorted [(n, n+1) | n <- xs]++numberedDiff :: (a -> a -> Bool) -> [a] -> [a] -> [Diff.Diff (Numbered a)]+numberedDiff equal a b =+    Diff.getDiffBy (\a b -> _numberedVal a `equal` _numberedVal b)+        (number a) (number b)+    where number = zipWith Numbered [0..]++data Numbered a = Numbered {+    _numbered :: !Int+    , _numberedVal :: !a+    } deriving (Show)++-- * approximately equal++equalf :: (HasCallStack, Show a, ApproxEq.ApproxEq a) => Double -> a -> a+    -> IO Bool+equalf eta a b+    | ApproxEq.eq eta a b = success $ pretty True+    | otherwise = failure $ pretty False+    where pretty = prettyCompare "~~" "/~" True a b++-- * other assertions++class Show a => TextLike a where toText :: a -> Text+instance TextLike String where toText = Text.pack+instance TextLike Text where toText = id++-- | Strings in the first list match patterns in the second list, using+-- 'patternMatches'.+stringsLike :: forall txt. (HasCallStack, TextLike txt) => [txt] -> [Pattern]+    -> IO Bool+stringsLike gotten_ expected+    | all isBoth diffs = success $ fmtLines "=~" gotten expected+    | otherwise = failure $ fmtLines "/~"+        (map (fmtLine (Set.fromList [_numbered a | Diff.Second a <- diffs]))+            (zip [0..] gotten))+        (map (fmtLine (Set.fromList [_numbered a | Diff.First a <- diffs]))+            (zip [0..] expected))+    where+    fmtLine failures (n, line)+        | Set.member n failures = highlight failureColor line+        | otherwise = line+    gotten = map toText gotten_+    diffs = numberedDiff patternMatches expected gotten+    isBoth (Diff.Both {}) = True+    isBoth _ = False++-- | Format multiple lines with an operator between them, on a single line if+-- they fit.+fmtLines :: Text -> [Text] -> [Text] -> Text+fmtLines operator [x] [y] | Text.length x + Text.length y <= 70 =+    x <> " " <> operator <> " " <> y+fmtLines operator [] [y] = "<empty> " <> operator <> " " <> y+fmtLines operator [x] [] = x <> " " <> operator <> " <empty>"+fmtLines operator xs ys = ("\n"<>) $ Text.stripEnd $+    Text.unlines $ xs <> ["    " <> operator] <> ys++-- | It's common for Left to be an error msg, or be something that can be+-- converted to one.+leftLike :: (HasCallStack, Show a, TextLike txt) => Either txt a -> Pattern+    -> IO Bool+leftLike gotten expected = case gotten of+    Left msg+        | patternMatches expected msg -> success $+            "Left " <> toText msg <> " =~ Left " <> toText expected+        | otherwise ->+            failure $ "Left " <> toText msg <> " !~ Left " <> toText expected+    Right a ->+        failure $ "Right (" <> showt a <> ") !~ Left " <> toText expected++match :: (HasCallStack, TextLike txt) => txt -> Pattern -> IO Bool+match gotten pattern =+    (if matches then success else failure) $+        fmtLines (if matches then "=~" else "!~")+            (Text.lines (toText gotten)) (Text.lines pattern)+    where+    matches = patternMatches pattern gotten++-- | Pattern as matched by 'patternMatches'.+type Pattern = Text++-- | This is a simplified pattern that only has the @*@ operator, which is+-- equivalent to regex's @.*?@.  This reduces the amount of quoting you have+-- to write.  You can escape @*@ with a backslash.+patternMatches :: TextLike txt => Pattern -> txt -> Bool+patternMatches pattern = not . null . Regex.groups (patternToRegex pattern)+    . toText++patternToRegex :: HasCallStack => Text -> Regex.Regex+patternToRegex =+    Regex.compileOptionsUnsafe [Regex.DotAll] . mkstar . Regex.escape+        . Text.unpack+    where+    mkstar "" = ""+    mkstar ('\\' : '\\' : '\\' : '*' : cs) = '\\' : '*' : mkstar cs+    mkstar ('\\' : '*' : cs) = '.' : '*' : '?' : mkstar cs+    mkstar (c : cs) = c : mkstar cs++-- | The given pure value should throw an exception that matches the predicate.+throws :: (HasCallStack, Show a) => a -> Pattern -> IO Bool+throws val excPattern =+    (Exception.evaluate val >> failure ("didn't throw: " <> showt val))+    `Exception.catch` \(exc :: Exception.SomeException) ->+        if patternMatches excPattern (showt exc)+            then success ("caught exc: " <> showt exc)+            else failure $ "exception <" <> showt exc <> "> didn't match "+                <> excPattern++ioEqual :: (HasCallStack, Eq a, Show a) => IO a -> a -> IO Bool+ioEqual ioVal expected = do+    val <- ioVal+    equal val expected++expectRight :: (HasCallStack, Show a) => Either a b -> b+expectRight (Left v) = error (pshow v)+expectRight (Right v) = v++-- * QuickCheck++-- | Run a quickcheck property.+quickcheck :: (HasCallStack, QuickCheck.Testable prop) => prop -> IO Bool+quickcheck prop = do+    (ok, msg) <- fmtQuickCheckResult <$>+        QuickCheck.quickCheckWithResult args prop+    (if ok then success else failure) msg+    where+    args = QuickCheck.stdArgs { QuickCheck.chatty = False }++fmtQuickCheckResult :: QuickCheck.Result -> (Bool, Text)+fmtQuickCheckResult result = fmap Text.strip $ case result of+    QuickCheck.Success { output } -> (True, Text.pack output)+    QuickCheck.GaveUp { output } -> (False, Text.pack output)+    QuickCheck.Failure { output } -> (False, Text.pack output)+    QuickCheck.NoExpectedFailure { output } -> (False, Text.pack output)+    QuickCheck.InsufficientCoverage { output } -> (False, Text.pack output)++-- | 'equal' for quickcheck.+qcEqual :: (Show a, Eq a) => a -> a -> QuickCheck.Property+qcEqual a b = QuickCheck.counterexample+    (Text.unpack $ prettyCompare "==" "/=" True a b False)+    (a == b)++-- * util++-- These used to write to stderr, but the rest of the diagnostic output goes to+-- stdout, and it's best these appear in context.++-- | Print a msg with a special tag indicating a passing test.+success :: HasCallStack => Text -> IO Bool+success msg = do+    printTestLine Stack.callStack successColor "++-> " msg+    return True++-- | Print a msg with a special tag indicating a failing test.+failure :: HasCallStack => Text -> IO Bool+failure msg = do+    printTestLine Stack.callStack failureColor "__-> " msg+    return False++printTestLine :: Stack.CallStack -> ColorCode -> Text -> Text -> IO ()+printTestLine stack color prefix msg = do+    -- Make sure the output doesn't get mixed with trace debug msgs.+    force msg+    isatty <- Terminal.queryTerminal IO.stdOutput+    testName <- configTestName <$> IORef.readIORef testConfig+    -- If there is highlighting in the msg, then it's probably a diff so+    -- most of it may be unhighlighted.  So highlight the prefix to make+    -- the line visible.+    let fullPrefix = (if isHighlighted msg then highlight color else id)+            (prefix <> showStack testName stack)+    let fullMsg = fullPrefix <> " " <> msg+        highlighted+            -- I only want colors in tty output.+            | not isatty = stripColors fullMsg+            -- Don't put on a color if it already has some.+            | isHighlighted fullMsg = fullMsg+            | otherwise = highlight color fullMsg+    Text.IO.putStrLn highlighted+    where+    isHighlighted = (vt100Prefix `Text.isInfixOf`)++showStack :: Text -> Stack.CallStack -> Text+showStack testName =+    maybe "<empty-stack>" showFrame . Seq.last . Stack.getCallStack+    where+    showFrame (_, srcloc) =+        Text.pack (Stack.srcLocFile srcloc) <> ":"+        <> showt (Stack.srcLocStartLine srcloc)+        <> if Text.null testName then "" else " [" <> testName <> "]"++highlight :: ColorCode -> Text -> Text+highlight (ColorCode code) text+    | Text.null text = text+    | otherwise = code <> text <> vt100Normal++-- | Remove vt100 color codes.+stripColors :: Text -> Text+stripColors = mconcat . Seq.map_tail (Text.drop 1 . Text.dropWhile (/='m'))+    . Text.splitOn vt100Prefix++newtype ColorCode = ColorCode Text deriving (Show)++vt100Prefix :: Text+vt100Prefix = "\ESC["++vt100Normal :: Text+vt100Normal = "\ESC[m\ESC[m"++-- | These codes should probably come from termcap, but I can't be bothered.+failureColor :: ColorCode+failureColor = ColorCode "\ESC[31m" -- red++successColor :: ColorCode+successColor = ColorCode "\ESC[32m" -- green++-- * pretty++pprint :: Show a => a -> IO ()+pprint = putStr . pshow++showt :: Show a => a -> Text+showt = Text.pack . show++pshowt :: Show a => a -> Text+pshowt = Text.pack . pshow++-- | Strict pshow, so I don't get debug traces interleaved with printing.+pshow :: Show a => a -> String+pshow val = s `DeepSeq.deepseq` s+    where s = PPrint.pshow val++-- * filesystem++-- | Get a tmp dir, which will be unique for each test run.+uniqueTmpDir :: String -> IO FilePath+uniqueTmpDir prefix = do+    Directory.createDirectoryIfMissing True tmpBaseDir+    Temp.mkdtemp $ tmpBaseDir </> prefix ++ "-"++-- | Run the computation with cwd in a new tmp dir.+inTmpDir :: String -> IO a -> IO a+inTmpDir prefix action = do+    dir <- uniqueTmpDir prefix+    Directory.withCurrentDirectory dir action++-- | All tmp files used by tests should go in this directory.+--+-- TODO instead of being hardcoded this should be configured per-project.+tmpBaseDir :: FilePath+tmpBaseDir = "dist/test-tmp"++-- * util++force :: DeepSeq.NFData a => a -> IO ()+force x = Exception.evaluate (DeepSeq.rnf x)
+ src/Global.hs view
@@ -0,0 +1,27 @@+module Global (+    when, forever, unless, void, Map, mapMaybe, fromMaybe+    , Text, txt, untxt, showt+    , whenJust, concatMapM+) where+import qualified Control.Monad as Monad+import Control.Monad (when, forever, unless, void)+import Data.Map (Map)+import Data.Maybe (mapMaybe, fromMaybe)+import qualified Data.Text as Text+import Data.Text (Text)+++txt :: String -> Text+txt = Text.pack++untxt :: Text -> String+untxt = Text.unpack++showt :: Show a => a -> Text+showt = txt . show++whenJust :: Monad m => Maybe a -> (a -> m ()) -> m ()+whenJust ma f = maybe (return ()) f ma++concatMapM :: (Monad m, Monoid b) => (a -> m b) -> [a] -> m b+concatMapM f = Monad.liftM mconcat . mapM f
+ test-karya.cabal view
@@ -0,0 +1,97 @@+name: test-karya+version: 0.0.1+cabal-version: >=1.10+build-type: Simple+synopsis: Testing framework.+description:+    This is Karya's test framework, extracted to be usable standalone.++category: Haskell, Test+license: BSD3+license-file: LICENSE+author: Evan Laforge+maintainer: Evan Laforge <qdunkan@gmail.com>+stability: stable+tested-with: GHC>=8.0.2+extra-source-files:+    LICENSE+    TODO+    README.md+    example/example.cabal+    example/src/*.hs++homepage: https://github.com/elaforge/test-karya+source-repository head+    type: git+    location: git://github.com/elaforge/test-karya.git++library+    hs-source-dirs: src+    default-language: Haskell2010+    ghc-options:+        -Wall -Wno-name-shadowing -Wno-unused-do-bind -Wno-type-defaults+    build-depends:+        Diff+        , QuickCheck+        , base >= 4.9 && < 4.12+        , bytestring+        , containers+        , data-ordlist+        , deepseq+        , directory+        , filepath+        , ghc-prim+        , pcre-heavy >=0.2+        , pcre-light >=0.4+        , text+        , unix++        -- RunTests+        , async+        , process++        -- for pprint+        , pretty, haskell-src+    exposed-modules:+        EL.Test.ApproxEq+        EL.Test.Global+        EL.Test.Testing+        EL.Test.RunTests+    -- Previously I had these in a local internal library, but cabal+    -- would install only the internal library, and not the public one.+    -- Now I just copy paste the dependencies between the library and the+    -- executable as is traditional with cabal, and it seems to work.+    other-modules:+        EL.Private.ExtractHs+        EL.Private.Map+        EL.Private.PPrint+        EL.Private.Ranges+        EL.Private.Regex+        EL.Private.Seq+        EL.Private.Then+        EL.Private.File+        EL.Private.Process+        Global++executable test-karya-generate+    hs-source-dirs: src+    default-language: Haskell2010+    ghc-options:+        -Wall -Wno-name-shadowing -Wno-unused-do-bind -Wno-type-defaults+    main-is: EL/Test/TestKaryaGenerate.hs+    ghc-options: -main-is EL.Test.TestKaryaGenerate+    build-depends:+        test-karya+        , base+        , bytestring+        , containers+        , directory+        , filepath+        , text+        , pcre-heavy >=0.2+        , pcre-light >=0.4++    other-modules:+        EL.Private.ExtractHs+        EL.Private.Regex+        Global