packages feed

yi-0.10.1: src/tests/Generic/TestPureBufferManipulations.hs

-- | This module aims to provide a generic back-end for other keymaps to
-- use for pure buffer manipulations. Pure buffer manipulations are considered
-- to be operations which simply change the contents of the buffer and move the
-- cursor. For example, opening a second buffer is not considered a pure buffer
-- operation.

module Generic.TestPureBufferManipulations where

import Test.Tasty.HUnit
import Test.Tasty (TestTree, testGroup)

import Control.Monad (filterM, forM, void, unless)
import Control.Lens ((%=))

import Data.List (sort, isSuffixOf, intercalate, isPrefixOf)
import Data.Ord (comparing)

import System.Directory
import System.FilePath

import Text.Printf

import Yi.Buffer
import Yi.Config (Config)
import Yi.Editor
import Yi.Window
import Yi.Region

import Generic.TestUtils

data KeymapTest = KeymapTest {
                   ktName :: String
                 , ktOptionalSettings :: [OptionalSetting]
                 , ktInput :: String
                 , ktOutput :: String
                 , ktEventString :: String
                 , ktKeysEval :: KeyEval
               }

data OptionalSetting = WindowSize Int Int  -- ^ WindowSize Width Height 
                     deriving Eq

instance Show OptionalSetting where
    show (WindowSize w h) = unwords ["+WindowSize", (show w), (show h)]

instance Eq KeymapTest where
  KeymapTest n s i o e _ == KeymapTest n' s' i' o' e' _ =
    n == n' && s == s' && i == i' && o == o' && e == e'

instance Ord KeymapTest where
  compare = comparing ktName


data TestResult = TestPassed String
                | TestFailed String String

instance Show TestResult where
    show (TestPassed name) = "PASSED " ++ name
    show (TestFailed name msg) = "FAILED " ++ name ++ ":\n" ++ msg

unlines' :: [String] -> String
unlines' = intercalate "\n"

optionalSettingPrefix :: String
optionalSettingPrefix = "--+ " 

isOptionalSetting :: String -> Bool
isOptionalSetting = (optionalSettingPrefix `isPrefixOf`)

decodeOptionalSetting :: [String] -> OptionalSetting
decodeOptionalSetting ["WindowSize", w, h] = WindowSize (read w) (read h)
decodeOptionalSetting unknownSetting = 
    error $ "Invalid Setting: " ++ (intercalate " " unknownSetting) 

loadTestFromDirectory :: FilePath -- ^ Directory of the test
                      -> KeyEval -- ^ Function that can run
                                                -- ‘events’ commands
                      -> IO KeymapTest
loadTestFromDirectory path ev = do
    [input, output, events] <- mapM (readFile' . (path </>)) ["input", "output", "events"]
    return $ KeymapTest (joinPath . drop 1 . splitPath $ path) [] input output events ev

isValidTestFile :: String -> Bool
isValidTestFile text =
    case (skipOptionals . lines $ text) of
        [] -> False
        ("-- Input": ls) ->
            case break (== "-- Output") ls of
                (_, []) -> False
                (_, "-- Output":ls') -> "-- Events" `elem` ls'
                _ -> False
        _ -> False
    where
        skipOptionals = dropWhile isOptionalSetting

-- | See Arguments to 'loadTestFromDirectory'
loadTestFromFile :: FilePath -> KeyEval -> IO KeymapTest
loadTestFromFile path ev = do
    text <- readFile' path
    unless (isValidTestFile text) $
        void $ printf "Test %s is invalid\n" path
    let (optionals, testContents) = span isOptionalSetting (lines text) 
        ls = tail testContents
        (input, rest) = break (== "-- Output") ls
        (output, rest2) = break (== "-- Events") $ tail rest
        eventText = tail rest2
    return $
      KeymapTest
        (joinPath . drop 1 . splitPath . dropExtension $ path)
        (map (decodeOptionalSetting . drop 1 . words) optionals)
        (unlines' input)
        (unlines' output)
        (unlines' eventText)
        ev

containsTest :: FilePath -> IO Bool
containsTest d = do
    files <- fmap (filter (`notElem` [".", ".."])) $ getDirectoryContents d
    return $ sort files == ["events", "input", "output"]

getRecursiveFiles :: FilePath -> IO [FilePath]
getRecursiveFiles topdir = do
    names <- getDirectoryContents topdir
    let properNames = filter (`notElem` [".", "..", ".git", ".svn"]) names
    paths <- forM properNames $ \name -> do
        let path = topdir </> name
        isDirectory <- doesDirectoryExist path
        if isDirectory
            then getRecursiveFiles path
            else return [path]
    return (concat paths)

getRecursiveDirectories :: FilePath -> IO [FilePath]
getRecursiveDirectories topdir = do
    names <- getDirectoryContents topdir
    let properNames = filter (`notElem` [".", "..", ".git", ".svn"]) names
    paths <- forM properNames $ \name -> do
        let path = topdir </> name
        isDirectory <- doesDirectoryExist path
        if isDirectory
            then fmap (path:) $ getRecursiveDirectories path
            else return []
    return (concat paths)

discoverTests :: FilePath -> KeyEval -> IO [KeymapTest]
discoverTests topdir ev = do
    dirs <- getRecursiveDirectories topdir
    testDirs <- filterM containsTest dirs
    testFiles <- fmap (filter (isSuffixOf ".test")) $ getRecursiveFiles topdir
    testsFromDirs <- mapM (`loadTestFromDirectory` ev) testDirs
    testsFromFiles <- mapM (`loadTestFromFile` ev) testFiles
    return $ testsFromDirs ++ testsFromFiles

optionalSettingAction :: OptionalSetting -> EditorM ()
optionalSettingAction (WindowSize w h) = 
    let region = mkSizeRegion (Point 0) (Size (w*h))
    in currentWindowA %= (\w -> w { height = h, winRegion = region })

mkTestCase :: Config -> KeymapTest -> TestTree
mkTestCase cf t = testCase (ktName t) $ do
    let setupActions = do
            let (cursorLine, '\n':text) = break (== '\n') (ktInput t)
            mapM_ optionalSettingAction $ ktOptionalSettings t
            insertText text
            setCursorPosition cursorLine

        preConditions _ _ = return ()

        testActions _ = ktKeysEval t $ ktEventString t

        assertions editor _ =
            let actualOut = cursorPos editor ++ "\n" ++
                            extractBufferString cf editor
            in  assertEqual (errorMsg actualOut) actualOut (ktOutput t)

    runTest setupActions preConditions testActions assertions cf

  where
    setCursorPosition cursorLine =
        let (x, y) = read cursorLine
        in withCurrentBuffer $ moveToLineColB x (y - 1)
    cursorPos = show . snd . runEditor cf (withCurrentBuffer $ do
                                            l <- curLn
                                            c <- curCol
                                            return (l, c + 1))
    errorMsg actualOut = unlines $ optionalSettings ++
                                 [ "Input:", ktInput t
                                 , "Expected:", ktOutput t
                                 , "Got:", actualOut
                                 , "Events:", ktEventString t
                                 , "---"]
    optionalSettings = map show $ ktOptionalSettings t

-- | Takes a directory with the tests, a name of the keymap
-- and an evaluation function for the keys contained in the tests.
-- For Vim, we might do something like:
--
-- @
-- getTests defaultVimConfig "src/tests/vimtests"
--   "Vim" (pureEval $ extractValue defVimConfig)
-- @
getTests :: Config -> FilePath -> String
         -> KeyEval -> IO TestTree
getTests c fp n ev = do
    tests <- discoverTests fp ev
    return $ testGroup (n ++ " keymap tests") $
      fmap (mkTestCase c) . sort $ tests

readFile' :: FilePath -> IO String
readFile' f = do
    s <- readFile f
    return $! length s `seq` s