module Main where import Language.Haskell.Exts import Language.C.Preprocessor.Remover import Language.C.Preprocessor.Remover.Internal.Types (CabalFilePath) import Test.Hspec import Control.Exception (bracket) import Control.Monad (void) import Data.List ((\\)) import System.Directory (getCurrentDirectory, setCurrentDirectory) import System.IO.Temp (withSystemTempDirectory) import System.Process (readCreateProcessWithExitCode, shell) {- Note [Structure of the test suite] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We are checking two properties for each libraries: - every line which begins with # in the original file is either unchanged (maybe it's part of a comment or drawing, like in pipes) or has been substituted in the new file with a blank line. The relevant function is hashesAreProcessed. - after preprocessing, the files are parsed without errors by haskell-src-exts (which can't parse cpp by itself). The relevant function is isParsedByHaskellSrcExts. For comparison, here are some tests which don't work naively: - checking that every line not starting with # is preserved (cpp may rightfully alter the lines, like it does in the lens library). - checking that the files have the same number of lines: this may fail because the file is not padded after the end of the source. These tests could be adapted, for example setting a limit on the edit distance for the first one, or manually padding the end of the file for the second one. For the moment I'll keep the simple tests I outlined earlier. -} -- Check some important libraries. I still have a problem in processing "text", -- due to the enabling of some flags in the cabal file. main :: IO () main = mapM_ testPackage testLibraries where testLibraries = [ "lens", "varying", "aeson", "pipes" , "conduit", "wreq", "haskell-src-exts" ] -- Given the name of the package, builds it in a temporary location and executes -- the tests. testPackage :: String -> IO () testPackage packageName = withStackInstalledPackage packageName $ \cabalPath -> hspec $ describe ("Testing " ++ packageName) $ do it "The lines beginning with # have been substituted with blank lines" $ and <$> onAllFiles cabalPath hashesAreProcessed `shouldReturn` True it "The package is be parseable with haskell-src-exts" $ and <$> onAllFiles cabalPath isParsedByHaskellSrcExts `shouldReturn` True -- Wrapper function (in the spirit of bracket) which, given a package to install -- and something to do with the cabal file path, initialize a temp directory, -- does the action and cleans up. withStackInstalledPackage :: String -> (FilePath -> IO ()) -> IO () withStackInstalledPackage packageName action = withSystemTempDirectory "preprocessorTest" $ \dir -> withCurrentDirectory dir $ do execCmd ("\nUnpacking " ++ packageName) ("stack unpack " ++ packageName) setCurrentDirectory =<< uniqueOutputOf "ls" execCmd ("Initializing " ++ packageName) ("stack init --solver --ignore-subdirs --silent") execCmd ("Building " ++ packageName) ("stack build --silent") action =<< uniqueOutputOf "ls *.cabal" -- This function belongs to a new version on System.Directory, which is not yet -- on hackage. When it's released, I should import it from that package. withCurrentDirectory :: FilePath -- ^ Directory to execute in -> IO a -- ^ Action to be executed -> IO a withCurrentDirectory dir action = bracket getCurrentDirectory setCurrentDirectory $ \ _ -> do setCurrentDirectory dir action -- Executes an action on every file listed as exported library file by the cabal -- file. onAllFiles :: CabalFilePath -> (FilePath -> IO a) -> IO [a] onAllFiles cabalFilePath action = do sourceFiles <- getLibExposedModulesPath cabalFilePath mapM action sourceFiles -- Convenience function to retrieve the result of a shell command, assuming it -- fits in a single line (otherwise it takes the first line). uniqueOutputOf :: String -> IO String uniqueOutputOf cmd = do (_, stdout, _) <- readCreateProcessWithExitCode (shell cmd) "" return . head . lines $ stdout -- Convenience function to execute the command in a bash-like fashion. The first -- argument is a message to be printed, the second one the actual command like -- it would be typed in shell. execCmd :: String -> String -> IO () execCmd message cmd = do putStrLn message void $ readCreateProcessWithExitCode (shell cmd) "" -------------------------------------------------------------------- -- Test: Checking that lines beginning with # have been processed -- -------------------------------------------------------------------- hashesAreProcessed :: FilePath -> IO Bool hashesAreProcessed fp = do originFile <- readFile fp parsedFile <- preprocessFile fp return . and $ zipWith compatible (lines originFile) (lines parsedFile) where compatible s1@('#':_) s2 = null s2 || s1 == s2 compatible _ _ = True -------------------------------------------------------------------- -- Test: check that the files are parseable with haskell-src-exts -- -------------------------------------------------------------------- isParsedByHaskellSrcExts :: FilePath -> IO Bool isParsedByHaskellSrcExts f = isParseResultOk . parseFileContentsWithComments parseMode <$> preprocessFile f where isParseResultOk :: ParseResult a -> Bool isParseResultOk (ParseOk _) = True isParseResultOk _ = False {- Note [Passing extensions to the haskell-src-exts parser] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The parser contained in haskell-src-exts may fail for the absence of the right extensions coming from the cabal file. A common trick, used for example by: https://github.com/ndmitchell/hlint/blob/e1c22030721999d4505eb14b19e6f8560a87507a/src/Util.hs is to import all possible reasonable extensions by default. This solution is temporary, until I find a better replacement. -} parseMode :: ParseMode parseMode = defaultParseMode { fixities = Nothing, extensions = defaultExtensions } where defaultExtensions :: [Extension] defaultExtensions = [e | e@EnableExtension{} <- knownExtensions] \\ map EnableExtension badExtensions badExtensions :: [KnownExtension] badExtensions = [ Arrows -- steals proc , TransformListComp -- steals the group keyword , XmlSyntax, RegularPatterns -- steals a-b , UnboxedTuples -- breaks (#) lens operator , QuasiQuotes -- breaks [x| ...], making whitespace free list comps break , DoRec, RecursiveDo -- breaks rec ]