koneko-0.0.2: src/Koneko/Test.hs
-- -- ; {{{1
--
-- File : Koneko/Test.hs
-- Maintainer : Felix C. Stegerman <flx@obfusk.net>
-- Date : 2022-02-12
--
-- Copyright : Copyright (C) 2022 Felix C. Stegerman
-- Version : v0.0.1
-- License : GPLv3+
--
-- -- ; }}}1
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Koneko.Test (
doctest, doctest', testFiles, testKoneko, testMarkdown, testKoneko_,
testMarkdown_, testKonekoFile, testMarkdownFile, testKonekoFile_,
testMarkdownFile_
) where
import GHC.IO.Handle (hDuplicate, hDuplicateTo)
import Control.Exception (bracket)
import Control.Monad (unless, when)
import Data.Char (isSpace)
import Data.Foldable (foldl', traverse_)
import Data.Text (Text)
import Prelude hiding (exp, fail)
import System.Console.CmdArgs.Verbosity (Verbosity(..), getVerbosity)
import System.Directory (getTemporaryDirectory, removeFile)
import System.Exit (exitFailure)
import System.FilePath (takeExtension)
import System.IO (Handle)
#if !MIN_VERSION_GLASGOW_HASKELL(8, 8, 1, 0)
import Data.Monoid((<>))
#endif
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified System.IO as IO
import qualified System.IO.Silently as S
import Koneko.Data (Context, Stack, emptyStack, initMain)
import Koneko.Eval (initContext)
import qualified Koneko.Repl as RE
data Example = Example {
fileName :: FilePath,
lineNo :: Int,
inputLine :: Text,
outputLines :: [Text]
} deriving Show
type ExampleGroup = [Example]
type Examples = [ExampleGroup]
doctest :: Verbosity -> [FilePath] -> IO Bool
doctest v fs = do
ctx <- initContext
_testFail <$> testFiles ctx v fs
doctest' :: [FilePath] -> IO ()
doctest' fs
= getVerbosity >>= flip doctest fs >>= flip unless exitFailure
testFiles :: Context -> Verbosity -> [FilePath] -> IO (Int, Int, Int)
testFiles ctx verb files = do
r@(t,o,f) <- s <$> traverse process files
when (verb /= Quiet) $ do
putStrLn "=== Summary ==="
putStrLn $ "Files: " ++ show (length files) ++ "."
printSummary t o f
return r
where
process fp = do
let (what, func) = typAndFunc fp
info = fp ++ " (" ++ what ++ ")"
when (verb /= Quiet) $
putStrLn $ "=== Testing " ++ info ++ " ==="
func ctx verb fp <* when (verb /= Quiet) (putStrLn "")
typAndFunc fp = if takeExtension fp == ".md"
then ("markdown", testMarkdownFile)
else ("koneko" , testKonekoFile )
s = foldl' (\(t,o,f) (t',o',f') -> (t+t',o+o',f+f')) (0,0,0)
testKoneko, testMarkdown
:: Context -> Verbosity -> FilePath -> [Text] -> IO (Int, Int, Int)
testKoneko ctx v fp = testExamples ctx v . parseKoneko fp
testMarkdown ctx v fp = testExamples ctx v . parseMarkdown fp
testKoneko_, testMarkdown_
:: Context -> Verbosity -> FilePath -> [Text] -> IO Bool
testKoneko_ = _test parseKoneko
testMarkdown_ = _test parseMarkdown
_test :: (FilePath -> [Text] -> Examples)
-> Context -> Verbosity -> FilePath -> [Text] -> IO Bool
_test f ctx v fp ls = _testFail <$> testExamples ctx v (f fp ls)
_testFail :: (Int, Int, Int) -> Bool
_testFail (_, _, fail) = fail == 0
testKonekoFile, testMarkdownFile
:: Context -> Verbosity -> FilePath -> IO (Int, Int, Int)
testKonekoFile = _testFile testKoneko
testMarkdownFile = _testFile testMarkdown
testKonekoFile_, testMarkdownFile_
:: Context -> Verbosity -> FilePath -> IO Bool
testKonekoFile_ = _testFile testKoneko_
testMarkdownFile_ = _testFile testMarkdown_
_testFile :: (Context -> Verbosity -> FilePath -> [Text] -> IO a)
-> Context -> Verbosity -> FilePath -> IO a
_testFile f ctx v fp = T.readFile fp >>= f ctx v fp . T.lines
-- parsing --
parseKoneko, parseMarkdown :: FilePath -> [Text] -> Examples
parseKoneko fp = examples fp . knkCommentBlocks [] . zip [1..]
parseMarkdown fp = examples fp . mdCodeBlocks [] . zip [1..]
examples :: FilePath -> [[(Int, Text)]] -> Examples
examples fp = filter (not . null) . map (exampleGroup fp [])
-- TODO
exampleGroup :: FilePath -> ExampleGroup -> [(Int, Text)] -> ExampleGroup
exampleGroup fileName es ls
= if null ls || null ls' then reverse es
else exampleGroup fileName (Example{..}:es) ls''
where
ls' = dropWhile (not . isPrompt' . snd) ls
(e, ls'') = span (isSameExample . snd) $ tail ls' -- safe!
e' = map (dropPrefix . snd) e
(c,outputLines) = span isCont e'
(lineNo, fl) = head ls' -- safe!
inputLine = T.concat $ map dropPrompt (dropPrefix fl:c)
isSameExample s = maybe False (\x -> not $ isPrompt x || T.null x)
$ T.stripPrefix prefix s
dropPrefix = T.drop $ T.length prefix
dropPrompt = T.drop $ T.length RE.promptText
prefix = T.takeWhile isSpace fl
isPrompt' = isPrompt . T.dropWhile isSpace
isPrompt = T.isPrefixOf RE.promptText
isCont = T.isPrefixOf "... " -- TODO
-- TODO
knkCommentBlocks :: [[(Int, Text)]] -> [(Int, Text)] -> [[(Int, Text)]]
knkCommentBlocks bs ls
= if null ls || null ls' then reverse bs
else knkCommentBlocks (b':bs) ls''
where
ls' = dropWhile (not . isComment) ls
(b, ls'') = span isSameComment ls'
b' = [ (n,T.drop (T.length prefix) x) | (n,x) <- b ]
isComment = T.isPrefixOf ";" . T.dropWhile isSpace . snd
isSameComment = T.isPrefixOf prefix . snd
prefix = T.takeWhile isSpace (snd $ head ls') <> ";" -- safe!
-- TODO
mdCodeBlocks :: [[(Int, Text)]] -> [(Int, Text)] -> [[(Int, Text)]]
mdCodeBlocks bs [] = reverse bs
mdCodeBlocks bs ls = mdCodeBlocks (b:bs) $ drop 1 ls''
where
ls' = dropWhile ((/= mdCodeStart) . snd) ls
(b, ls'') = break ((== mdCodeEnd) . snd) $ drop 1 ls'
mdCodeStart, mdCodeEnd :: Text
mdCodeStart = "```koneko"
mdCodeEnd = "```"
-- internal --
-- TODO
testExamples :: Context -> Verbosity -> Examples -> IO (Int, Int, Int)
testExamples ctx verb ex = do
r@(total, ok, fail) <- go 0 0 0 ex
when (verb == Loud) $ do
putStrLn "=== Summary ==="
printSummary total ok fail
return r
where
go total ok fail [] = return (total, ok, fail)
go total ok fail (g:gt) = do
(t, o, f) <- testExampleGroup ctx verb g
go (total+t) (ok+o) (fail+f) gt
-- TODO
testExampleGroup
:: Context -> Verbosity -> ExampleGroup -> IO (Int, Int, Int)
testExampleGroup ctx verb g = do
initMain ctx -- TODO
let st = emptyStack; total = length g
(ok, fail, _) <- loop 0 0 g ctx st
when (verb == Loud) $ do printTTPF total ok fail; putStrLn ""
return (total, ok, fail)
where
loop :: Int -> Int -> ExampleGroup
-> Context -> Stack -> IO (Int, Int, Stack)
loop ok fail [] _ s = return (ok, fail, s)
loop ok fail (e@Example{..}:et) c s = do
(out, err, s') <- provide (T.unpack inputLine)
$ capture $ repl c s
let olines = asLines out; elines = asLines err
if compareOutput outputLines olines elines then do
when (verb == Loud) $ printSucc e
loop (ok+1) fail et c s'
else do
printFail e olines elines
return (ok, fail+1, s')
repl = RE.repl' True ""
asLines x = let l = T.lines $ T.pack x in
if not (null l) && last l == "" then init l else l
-- TODO: "...", ...
compareOutput :: [Text] -> [Text] -> [Text] -> Bool
compareOutput exp got err
= if null err then exp' == got else null got &&
T.isPrefixOf RE.errorText (head err) && exp' == err -- safe!
where
exp' = [ if l == "<BLANKLINE>" then "" else l | l <- exp ]
printSummary :: Int -> Int -> Int -> IO ()
printSummary total ok fail = do
printTTPF total ok fail
putStrLn $ "Test " ++ if fail == 0 then "passed." else "failed."
printTTPF :: Int -> Int -> Int -> IO ()
printTTPF total ok fail =
putStrLn $ "Total: " ++ (show total) ++
", Tried: " ++ (show $ ok + fail) ++
", Passed: " ++ (show ok) ++
", Failed: " ++ (show fail) ++ "."
-- TODO
printSucc :: Example -> IO ()
printSucc Example{..} = do
p "Trying:" ; p $ indent inputLine
p "Expecting:"; traverse_ (p . indent) outputLines
p "ok"
where
p = T.putStrLn
-- TODO
printFail :: Example -> [Text] -> [Text] -> IO ()
printFail Example{..} out err = do
p $ T.pack $ "File " ++ fileName ++ ", line " ++ show lineNo
p "Failed example:" ; p $ indent inputLine
p "Expected:" ; traverse_ (p . indent) outputLines
p "Got:" ; traverse_ (p . indent) out
unless (null err) $ do
p "Errors:" ; traverse_ (p . indent) err
where
p = T.hPutStrLn IO.stderr
indent :: Text -> Text
indent = (" " <>)
-- stdio --
capture :: IO a -> IO (String, String, a)
capture act = do
(err, (out, x)) <- S.hCapture [IO.stderr] $ S.capture act
return (out, err, x)
provide :: String -> IO a -> IO a
provide = hProvide IO.stdin
hProvide :: Handle -> String -> IO a -> IO a
hProvide h str act = withTempFile "provide" f
where
f hTmp = do
IO.hPutStr hTmp str; IO.hFlush hTmp
IO.hSeek hTmp IO.AbsoluteSeek 0
withRedirect h hTmp act
withRedirect :: Handle -> Handle -> IO a -> IO a
withRedirect hOrig hRepl act = do
buf <- IO.hGetBuffering hOrig
bracket redirect (restore buf) $ const act
where
redirect = do
hDup <- hDuplicate hOrig
hDuplicateTo hRepl hOrig
return hDup
restore buf hDup = do
hDuplicateTo hDup hOrig
IO.hSetBuffering hOrig buf
IO.hClose hDup
withTempFile :: String -> (Handle -> IO a) -> IO a
withTempFile template f = do
tmpDir <- getTemporaryDirectory
bracket (IO.openTempFile tmpDir template) cleanup (f . snd)
where
cleanup (tmpFile, h) = IO.hClose h >> removeFile tmpFile -- !!!
-- vim: set tw=70 sw=2 sts=2 et fdm=marker :