packages feed

ideas-1.9.1: src/Ideas/Utils/BlackBoxTests.hs

-----------------------------------------------------------------------------
-- Copyright 2019, Ideas project team. This file is distributed under the
-- terms of the Apache License 2.0. For more information, see the files
-- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution.
-----------------------------------------------------------------------------
-- |
-- Maintainer  :  bastiaan.heeren@ou.nl
-- Stability   :  provisional
-- Portability :  portable (depends on ghc)
--
-----------------------------------------------------------------------------

module Ideas.Utils.BlackBoxTests 
   (blackBoxTests, TestRunner, Mode(..)) where

import Control.Monad
import Data.Char
import Data.IORef
import Data.List
import Ideas.Utils.TestSuite
import System.Directory
import System.IO
import qualified Data.Algorithm.Diff as Diff

type TestRunner = String -> IO String

data Mode = Interactive | Report | Replace

-- Returns the number of tests performed
blackBoxTests :: TestRunner -> Mode -> [String] -> String -> IO TestSuite
blackBoxTests runner mode exts path = do
   ref <- newIORef mode
   blackBoxTestsInMode ref runner exts path

blackBoxTestsInMode :: IORef Mode -> TestRunner -> [String] -> String -> IO TestSuite
blackBoxTestsInMode ref runner exts = rec
 where
    rec path = do
      -- analyse content
      xs0 <- getDirectoryContents path
      let (files, xs1) = partition (`elemExts` exts) xs0
          xs2          = map (path </>) (filter ((/= ".") . take 1) xs1)
      -- recursively visit subdirectories
      subs <- filterM doesDirectoryExist xs2
      rest <- mapM rec subs
      return $ suite ("Directory " ++ simplerDirectory path) $
         [ doBlackBoxTest ref runner (path </> x)
         | x <- files
         ] ++ rest

doBlackBoxTest :: IORef Mode -> TestRunner -> FilePath -> TestSuite
doBlackBoxTest ref runner path =
   assertMessageIO (stripDirectoryPart path) $
      -- Comparing output with expected output
      withFile path ReadMode $ \h1 -> do
         hSetBinaryMode h1 True
         txt <- hGetContents h1
         out <- runner txt
         withFile expPath ReadMode $ \h2 -> do
            hSetBinaryMode h2 True
            expt <- hGetContents h2
            -- Force evaluation of the result, to make sure that
            -- all file handles are closed afterwards.
            let list1 = prepare expt
                list2 = prepare out
                msg   = unlines (path : diffs list1 list2)
            if list1 == list2 then return mempty else do
               force msg -- force evaluation of message before closing files
               return $ addPostHook (postHook out) $ message msg
 where
   expPath = baseOf path ++ ".exp"
   baseOf  = reverse . drop 1 . dropWhile (/= '.') . reverse

   postHook out status = do
      mode <- readIORef ref
      case mode of 
         _ | isOk status -> return ()
         Report -> return ()
         Replace -> replaceExpected
         Interactive  -> do
            putStrLn "Replace expected file: (y)es, (s)kip, (a)lways, or (n)ever?"
            putStr "? "
            answer <- getLine
            case map toLower answer of
               "y" -> replaceExpected
               "s" -> return () 
               "a" -> replaceExpected >> writeIORef ref Replace 
               "n" -> writeIORef ref Report
               _   -> postHook out status
    where
      replaceExpected = withFile expPath WriteMode $ \h ->
         hSetBinaryMode h True >> hPutStr h out

elemExts :: FilePath -> [String] -> Bool
elemExts s = any (\xs -> ('.':xs)  `isSuffixOf` s)

force :: String -> IO ()
force s | sum (map ord s) >= 0 = return ()
        | otherwise = error "force"

prepare :: String -> [String]
prepare = filter (not . null) . lines . filter (/= '\r') . noVersion
 where
   noVersion s | "version\": \"" `isPrefixOf` s =
      "version\": \"X" ++ dropWhile (/='"') (drop 11 s)
   noVersion s | "version=\"" `isPrefixOf` s =
      "version=\"X" ++ dropWhile (/='"') (drop 9 s)
   noVersion (x:xs) = x:noVersion xs
   noVersion [] = []

diffs :: [String] -> [String] -> [String]
diffs xs ys = concatMap f $ Diff.getDiff xs ys
 where
   f (Diff.First a)  = ["- " ++ a]
   f (Diff.Second a) = ["+ " ++ a]
   f _ = []

simplerDirectory :: String -> String
simplerDirectory s
   | "../"   `isPrefixOf` s = simplerDirectory (drop 3 s)
   | "test/" `isPrefixOf` s = simplerDirectory (drop 5 s)
   | otherwise = s

stripDirectoryPart :: String -> String
stripDirectoryPart = reverse . takeWhile (/= '/') . reverse

(</>) :: FilePath -> FilePath -> FilePath
x </> y = x ++ "/" ++ y