{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- | A preprocessor that finds and combines specs. module Run where import Control.Monad import Control.Applicative import Data.List import Data.String import Data.Function import System.Environment import System.Exit import System.IO import System.Directory import System.FilePath hiding (combine) instance IsString ShowS where fromString = showString run :: [String] -> IO () run args_ = case args_ of src : _ : dst : args -> do nested <- case args of [] -> return False ["--nested"] -> return True _ -> exit specs <- findSpecs src writeFile dst (mkSpecModule src nested specs) _ -> exit where exit = do name <- getProgName hPutStrLn stderr ("usage: " ++ name ++ " SRC CUR DST [--nested]") exitFailure mkSpecModule :: FilePath -> Bool -> [SpecNode] -> String mkSpecModule src nested nodes = ( "{-# LINE 1 " . shows src . " #-}" . showString "module Main where\n" . showString "import Test.Hspec.Discover\n" . importList nodes . showString "main :: IO ()\n" . showString "main = hspec $ " . format nodes ) "\n" where format | nested = formatSpecsNested | otherwise = formatSpecs -- | Generate imports for a list of specs. importList :: [SpecNode] -> ShowS importList = go "" where go :: ShowS -> [SpecNode] -> ShowS go current = foldr (.) "" . map (f current) f current (SpecNode name inhabited children) = this . go (current . showString name . ".") children where this | inhabited = "import qualified " . current . showString name . "Spec\n" | otherwise = id -- | Combine a list of strings with (>>). sequenceS :: [ShowS] -> ShowS sequenceS = foldr (.) "" . intersperse " >> " -- | Convert a list of specs to code. formatSpecs :: [SpecNode] -> ShowS formatSpecs = sequenceS . map formatSpec -- | Convert a spec to code. formatSpec :: SpecNode -> ShowS formatSpec = sequenceS . go "" where go :: String -> SpecNode -> [ShowS] go current (SpecNode name inhabited children) = addThis $ concatMap (go (current ++ name ++ ".")) children where addThis :: [ShowS] -> [ShowS] addThis | inhabited = ("describe " . shows (current ++ name) . " " . showString (current ++ name ++ "Spec.spec") :) | otherwise = id -- | Convert a list of specs to code. -- -- Hierarchical modules are mapped to nested specs. formatSpecsNested :: [SpecNode] -> ShowS formatSpecsNested = sequenceS . map formatSpecNested -- | Convert a spec to code. -- -- Hierarchical modules are mapped to nested specs. formatSpecNested :: SpecNode -> ShowS formatSpecNested = go "" where go current (SpecNode name inhabited children) = "describe " . shows name . " (" . specs . ")" where specs :: ShowS specs = (sequenceS . addThis . map (go (current . showString name . "."))) children addThis | inhabited = ((current . showString name . "Spec.spec") :) | otherwise = id data SpecNode = SpecNode String Bool [SpecNode] deriving (Eq, Show) specNodeName :: SpecNode -> String specNodeName (SpecNode name _ _) = name specNodeInhabited :: SpecNode -> Bool specNodeInhabited (SpecNode _ inhabited _) = inhabited specNodeChildren :: SpecNode -> [SpecNode] specNodeChildren (SpecNode _ _ children) = children getFilesAndDirectories :: FilePath -> IO ([FilePath], [FilePath]) getFilesAndDirectories dir = do c <- filter (`notElem` ["..", "."]) <$> getDirectoryContents dir dirs <- filterM (doesDirectoryExist . (dir )) c files <- filterM (doesFileExist . (dir )) c return (dirs, files) -- | Find specs relative to given source file. -- -- The source file itself is not considered. findSpecs :: FilePath -> IO [SpecNode] findSpecs src = do let (dir, file) = splitFileName src (dirs, files) <- getFilesAndDirectories dir go dir (dirs, filter (/= file) files) where go :: FilePath -> ([FilePath], [FilePath]) -> IO [SpecNode] go base (dirs, files) = do nestedSpecs <- forM dirs $ \d -> do let dir = base d SpecNode d False <$> (getFilesAndDirectories dir >>= go dir) (return . filterSpecs . combineSpecs) (specsFromFiles files ++ nestedSpecs) where specsFromFiles = map (\x -> SpecNode (stripSuffix x) True []) . filter (isSuffixOf suffix) where suffix = "Spec.hs" stripSuffix = reverse . drop (length suffix) . reverse -- remove empty leafs filterSpecs :: [SpecNode] -> [SpecNode] filterSpecs = filter (\x -> specNodeInhabited x || (not . null . specNodeChildren) x) -- sort specs, and merge nodes with the same name combineSpecs :: [SpecNode] -> [SpecNode] combineSpecs = foldr f [] . sortBy (compare `on` specNodeName) where f x@(SpecNode n1 _ _) (y@(SpecNode n2 _ _):acc) | n1 == n2 = x `combine` y : acc f x acc = x : acc x `combine` y = SpecNode name inhabited children where name = specNodeName x inhabited = specNodeInhabited x || specNodeInhabited y children = specNodeChildren x ++ specNodeChildren y