sandwich-0.3.1.0: src/Test/Sandwich/TH.hs
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE TemplateHaskell #-}
module Test.Sandwich.TH (
getSpecFromFolder
, defaultGetSpecFromFolderOptions
, GetSpecFromFolderOptions
, getSpecCombiner
, getSpecIndividualSpecHooks
, getSpecWarnOnParseError
, ShouldWarnOnParseError(..)
, buildModuleMap
) where
import Control.Monad
import Data.Char
import Data.Function
import qualified Data.List as L
import qualified Data.Map as M
import Data.Maybe
import Data.String.Interpolate
import qualified Data.Text as T
import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Safe
import System.Directory
import System.FilePath as F
import Test.Sandwich.TH.ModuleMap
import Test.Sandwich.Types.Spec hiding (location)
constId :: b -> a -> a
constId = const id
data GetSpecFromFolderOptions = GetSpecFromFolderOptions {
getSpecCombiner :: Name
, getSpecIndividualSpecHooks :: Name
, getSpecWarnOnParseError :: ShouldWarnOnParseError
}
{-# DEPRECATED getSpecWarnOnParseError "Not used anymore." #-}
defaultGetSpecFromFolderOptions :: GetSpecFromFolderOptions
defaultGetSpecFromFolderOptions = GetSpecFromFolderOptions {
getSpecCombiner = 'describe
, getSpecIndividualSpecHooks = 'constId
, getSpecWarnOnParseError = WarnOnParseError
}
{-# DEPRECATED ShouldWarnOnParseError "Not used anymore." #-}
data ShouldWarnOnParseError = WarnOnParseError | NoWarnOnParseError
deriving (Eq)
getSpecFromFolder :: GetSpecFromFolderOptions -> Q Exp
getSpecFromFolder getSpecFromFolderOptions = do
dir <- runIO getCurrentDirectory
filename <- loc_filename <$> location
let folder = dropExtension (dir </> filename)
Module _ (ModName moduleName) <- thisModule
let modulePrefix' = moduleName
& T.pack
& T.splitOn "."
& initMay
& fromMaybe []
& T.intercalate "."
& T.unpack
let modulePrefix = if modulePrefix' == "" then "" else modulePrefix' <> "."
moduleMap <- runIO $ buildModuleMap folder modulePrefix
let reverseModuleMap = M.fromList [(y, x) | (x, y) <- M.toList moduleMap]
getSpecFromFolder' folder reverseModuleMap (moduleName <> ".") getSpecFromFolderOptions
getSpecFromFolder' :: F.FilePath -> ReverseModuleMap -> String -> GetSpecFromFolderOptions -> Q Exp
getSpecFromFolder' folder reverseModuleMap modulePrefix gsfo@(GetSpecFromFolderOptions {..}) = do
items <- qRunIO $ L.sort <$> listDirectory folder
specs <- (catMaybes <$>) $ forM items $ \item -> do
isDirectory <- qRunIO $ doesDirectoryExist (folder </> item)
if | isDirectory -> do
qRunIO (doesFileExist (folder </> item <.> "hs")) >>= \case
False -> Just <$> getSpecFromFolder' (folder </> item) reverseModuleMap (modulePrefix <> item <> ".") gsfo
True -> return Nothing -- Do nothing, allow the .hs file to be picked up separately
| takeExtension item == ".hs" -> do
let fullyQualifiedModule = modulePrefix <> takeBaseName item
case M.lookup fullyQualifiedModule reverseModuleMap of
Nothing -> do
reportError [i|Couldn't find module #{fullyQualifiedModule} in #{reverseModuleMap}|]
return Nothing
Just importedName -> do
maybeMainFunction <- getMaybeMainFunction importedName
alterNodeOptionsFn <- [e|(\x -> x { nodeOptionsModuleInfo = Just ($(conE 'NodeModuleInfo) fullyQualifiedModule $(return maybeMainFunction)) })|]
Just <$> [e|$(varE 'alterTopLevelNodeOptions) $(return alterNodeOptionsFn)
$ $(varE getSpecIndividualSpecHooks) $(stringE item) $(varE $ mkName $ importedName <> ".tests")|]
| otherwise -> return Nothing
let currentModule = modulePrefix
& T.pack
& T.stripSuffix "."
& fromMaybe ""
& T.unpack
maybeMainFunction <- case M.lookup currentModule reverseModuleMap of
Nothing -> [e|Nothing|]
Just importedName -> getMaybeMainFunction importedName
alterNodeOptionsFn <- [e|(\x -> x { nodeOptionsModuleInfo = Just ($(conE 'NodeModuleInfo) currentModule $(return maybeMainFunction)) })|]
[e|$(varE 'alterTopLevelNodeOptions) $(return alterNodeOptionsFn)
$ $(varE getSpecCombiner) $(stringE $ mangleFolderName folder) (L.foldl (>>) (pure ()) $(listE $ fmap return specs))|]
-- * Util
-- | Detect if a main function is present in the given module by trying to reify it,
-- using 'recover' to handle exceptions.
-- We could also look at the actual reified value to see if it looks like a function,
-- but that's probably overkill.
getMaybeMainFunction :: String -> Q Exp
getMaybeMainFunction importedName = do
let mainName = mkName (importedName <> ".main")
recover [e|Nothing|]
(reify mainName >> [e|Just $(varE mainName)|])
mangleFolderName :: String -> String
mangleFolderName = T.unpack . wordify . T.pack . takeBaseName
-- | Convert a string like "TeamTests" to "Team tests"
wordify :: T.Text -> T.Text
wordify t = T.intercalate " " $ capitalizeFirst $ fmap (T.toLower . T.pack) parts
where parts = splitR (\c -> isUpper c || isDigit c) $ T.unpack t
capitalizeFirst :: [T.Text] -> [T.Text]
capitalizeFirst [] = []
capitalizeFirst (x:xs) = capitalize x : xs
capitalize :: T.Text -> T.Text
capitalize t | T.length t == 1 = T.toUpper t
capitalize t = toUpper (T.head t) `T.cons` (T.tail t)
splitR :: (Char -> Bool) -> String -> [String]
splitR _ [] = []
splitR p s =
let
go :: Char -> String -> [String]
go m s' = case L.break p s' of
(b', []) -> [ m:b' ]
(b', x:xs) -> ( m:b' ) : go x xs
in case L.break p s of
(b, []) -> [ b ]
([], h:t) -> go h t
(b, h:t) -> b : go h t