module Test.Tasty.Auto (findTests, showTestDriver) where import Data.Function (on) import Data.List (find, isPrefixOf, isSuffixOf, nub, intersperse, groupBy, sortOn) import Data.Maybe (fromJust) import System.Directory (getDirectoryContents, doesDirectoryExist) import Data.Traversable (for) import System.FilePath ((), takeDirectory, pathSeparator, dropExtension) import Data.Monoid (Endo(..)) import Data.Foldable (fold) data Generator = Generator { genPrefix :: String , genImport :: ShowS , genInstance :: ShowS , genSetup :: Test -> ShowS } data Test = Test { testModule, testFunction :: String } str :: String -> ShowS str = (++) sp, nl :: ShowS sp = (' ':) nl = ('\n':) tr :: Char -> Char -> String -> String tr a b = map $ \c -> if c == a then b else c name, fn, var :: Test -> ShowS name = shows . tr '_' ' ' . tail . dropWhile (/= '_') . testFunction fn t = str (testModule t) . ('.':) . str (testFunction t) var t = str "setup_" . str (tr '.' '_' $ testModule t) . ('_':) . str (testFunction t) generators :: [Generator] generators = [ Generator { genPrefix = "prop_" , genImport = str "import qualified Test.Tasty.QuickCheck\n" , genInstance = id , genSetup = \t -> str "pure $ Test.Tasty.QuickCheck.testProperty " . name t . sp . fn t } , Generator { genPrefix = "scprop_" , genImport = str "import qualified Test.Tasty.SmallCheck\n" , genInstance = id , genSetup = \t -> str "pure $ Test.Tasty.SmallCheck.testProperty " . name t . sp . fn t } , Generator { genPrefix = "case_" , genImport = str "import qualified Test.Tasty.HUnit\n" , genInstance = str "class TestCase a where testCase :: String -> a -> IO Test.Tasty.TestTree\n" . str "instance TestCase (IO ()) where testCase n = pure . Test.Tasty.HUnit.testCase n\n" . str "instance TestCase (IO String) where testCase n = pure . Test.Tasty.HUnit.testCaseInfo n\n" . str "instance TestCase ((String -> IO ()) -> IO ()) where testCase n = pure . Test.Tasty.HUnit.testCaseSteps n\n" , genSetup = \t -> str "testCase " . name t . sp . fn t } , Generator { genPrefix = "spec_" , genImport = str "import qualified Test.Tasty.Hspec\n" , genInstance = id , genSetup = \t -> str "Test.Tasty.Hspec.testSpec " . name t . sp . fn t } , Generator { genPrefix = "test_" , genImport = id , genInstance = str "class TestGroup a where testGroup :: String -> a -> IO Test.Tasty.TestTree\n" . str "instance TestGroup Test.Tasty.TestTree where testGroup _ a = pure a\n" . str "instance TestGroup [Test.Tasty.TestTree] where testGroup n a = pure $ Test.Tasty.testGroup n a\n" . str "instance TestGroup (IO Test.Tasty.TestTree) where testGroup _ a = a\n" . str "instance TestGroup (IO [Test.Tasty.TestTree]) where testGroup n a = Test.Tasty.testGroup n <$> a\n" , genSetup = \t -> str "testGroup " . name t . sp . fn t } ] testFileSuffixes :: [String] testFileSuffixes = (++) <$> ["Spec", "Test"] <*> [".lhs", ".hs"] getGenerator :: Test -> Generator getGenerator t = fromJust $ find ((`isPrefixOf` testFunction t) . genPrefix) generators getGenerators :: [Test] -> [Generator] getGenerators = map head . groupBy ((==) `on` genPrefix) . sortOn genPrefix . map getGenerator showImports :: [Test] -> ShowS showImports = foldEndo . map (\m -> str "import qualified " . str m . nl) . nub . map testModule showSetup :: Test -> ShowS showSetup t = str " " . var t . str " <- " . genSetup (getGenerator t) t . nl foldEndo :: (Functor f, Foldable f) => f (a -> a) -> (a -> a) foldEndo = appEndo . fold . fmap Endo showTestDriver :: FilePath -> [Test] -> ShowS showTestDriver src ts = let gs = getGenerators ts in str "{-# LINE 1 " . shows src . str " #-}\n" . str "{-# LANGUAGE FlexibleInstances #-}\n" . str "module Main where\n" . str "import Prelude\n" . str "import qualified Test.Tasty\n" . foldEndo (map genImport gs) . showImports ts . foldEndo (map genInstance gs) . str "main :: IO ()\n" . str "main = do\n" . foldEndo (map showSetup ts) . str " Test.Tasty.defaultMain $ Test.Tasty.testGroup " . shows src . str "\n [ " . foldEndo (intersperse (str "\n , ") $ map var ts) . str " ]\n" filesBySuffix :: FilePath -> [String] -> IO [FilePath] filesBySuffix dir suffixes = do entries <- filter (\s -> head s /= '.') <$> getDirectoryContents dir found <- for entries $ \entry -> do let dir' = dir entry exists <- doesDirectoryExist dir' if exists then map (entry ) <$> filesBySuffix dir' suffixes else pure [] pure $ filter (\x -> any (`isSuffixOf` x) suffixes) entries ++ concat found findTests :: FilePath -> IO [Test] findTests src = do let dir = takeDirectory src files <- filesBySuffix dir testFileSuffixes concat <$> traverse (\f -> extractTests f <$> readFile (dir f)) files mkTest :: FilePath -> String -> Test mkTest = Test . tr pathSeparator '.' . dropExtension extractTests :: FilePath -> String -> [Test] extractTests file = map (mkTest file) . nub . filter (\n -> any ((`isPrefixOf` n) . genPrefix) generators) . map fst . concatMap lex . lines