{-# LANGUAGE OverloadedStrings #-} module Main where import Control.Monad (forM) import Data.List (intercalate, sort) import Data.Maybe (fromMaybe) import qualified Data.Set as Set (fromList, member) import GHC (mkModuleName, moduleNameString) import GHC.Paths (ghc) import Smuggler2.Options (ExportAction (..), ImportAction (..), Options (..)) import System.Directory (doesDirectoryExist, getDirectoryContents) import System.Environment (lookupEnv) import System.FilePath (takeBaseName, takeExtension, (-<.>), ()) import System.Process.Typed ( ProcessConfig, proc, runProcess_, setEnvInherit, setWorkingDirInherit, ) import Test.Tasty (TestTree, defaultMain, testGroup) import Test.Tasty.Golden (goldenVsFileDiff, writeBinaryFile) -- | Where the tests are, relative to the project level cabal file testDir :: FilePath testDir = "test" "tests" -- | Combinations of import and export action options to be tested optionsList :: [Options] optionsList = [ mkOptions PreserveInstanceImports NoExportProcessing [] ["Prelude"], mkOptions MinimiseImports ReplaceExports [] [], mkOptions MinimiseImports NoExportProcessing ["Prelude", "Data.Version"] [], mkOptions NoImportProcessing AddExplicitExports [] [], mkOptions NoImportProcessing NoExportProcessing [] [], mkOptions NoImportProcessing ReplaceExports [] [] ] where mkOptions :: ImportAction -> ExportAction -> [String] -> [String] -> Options mkOptions ia ea lo mo = Options ia ea (Just $ mkExt ia ea lo mo) (mkModuleName <$> lo) (mkModuleName <$> mo) -- | Make an extention for an output file mkExt :: ImportAction -> ExportAction -> [String] -> [String] -> String mkExt ia ea lo mo = show ia ++ show ea ++ filter (/= '.') (concat lo ++ concat mo) -- ++ "-" ++ takeFileName ghc -- | Generate test for a list of 'Options' each of which specify what action to -- take on imports and exports testOptions :: [Options] -> IO TestTree testOptions opts = testGroup "All" <$> sequenceA (goldenTests <$> opts) -- | Generate tests for a set of 'Options' (that specifies what to do to -- imports and exports) goldenTests :: Options -> IO TestTree goldenTests opts = do testFiles <- findByExtension' [".hs"] testDir return $ testGroup testName [ goldenVsFileDiff (takeBaseName testFile) -- test name ( \ref new -> -- how to display diffs -- The -G. is needed because cabal sdist changes the golden file -- permissions and so all the tests fail. ["git", "diff", "-G.", "--ignore-cr-at-eol", "--no-index", ref, new] ) (testFile -<.> testName ++ "-golden") -- golden file outputFilename ( do -- Write a default output file for those tests where smuggler2 -- (deliberately) does not generate a new one writeBinaryFile outputFilename "Source file was not touched\r\n" compile testFile opts ) | testFile <- sort testFiles, let outputFilename = testFile -<.> testName ] where testName = fromMaybe "NoNewExtension" (newExtension opts) -- | A version of 'Test.Tasty.Golden.findByExtension' that does not look into -- subdirectories. This allows tests to be run on a module that imports other -- modules without risking the race condition where smuggler is being run -- on the imported module directly, and also when the importing module is being -- tested. ('Tasty' runs test in parallel and the same output files are -- produced in both direct and imported cases.) Putting the imported module in -- a subdirectory and not testing it direcly avoids this race condition. findByExtension' :: -- | extensions [FilePath] -> -- | directory FilePath -> -- | paths IO [FilePath] findByExtension' extsList = go where exts = Set.fromList extsList go :: FilePath -> IO [FilePath] go dir = do allEntries <- getDirectoryContents dir let entries = filter (not . (`elem` [".", ".."])) allEntries fmap concat $ forM entries $ \e -> do let path = dir ++ "/" ++ e isDir <- doesDirectoryExist path return $ if isDir then [] -- don't recurse else [path | takeExtension path `Set.member` exts] -- | Just run all the tests main :: IO () main = defaultMain =<< testOptions optionsList -- | Use `cabal exec` to run the compilation, so that the smuggler plugin is -- picked up from the local database. GHC alone would use the global one. compile :: FilePath -> Options -> IO () compile testcase opts = do cabalPath <- lookupEnv "CABAL" -- find, eg, @/opt/ghc/bin/cabal@ or @cabal -vnormal+nowrap@ let cabalCmd = words $ fromMaybe "cabal" cabalPath -- default to @cabal@ if @CABAL@ is not set let cabalConfig = setWorkingDirInherit . setEnvInherit $ proc (head cabalCmd) (tail cabalCmd ++ cabalArgs) :: ProcessConfig () () () runProcess_ cabalConfig where cabalArgs :: [String] cabalArgs = -- - not sure why it is necessary to mention the smuggler2 package explicitly, -- but it appears to be hidden otherwise. -- - This also puts the .imports files that smuggler2 generates somewhere they -- can easily be found [ "--with-compiler=" ++ ghc, "exec", ghc, "--", "-package smuggler2", "-v0", "-dumpdir=" ++ testDir, "-fno-code", "-i" ++ testDir, "-fplugin=Smuggler2.Plugin" ] ++ map ("-fplugin-opt=Smuggler2.Plugin:" ++) ( let ia = importAction opts ea = exportAction opts ne = newExtension opts lo = intercalate "," (moduleNameString <$> leaveOpenImports opts) mo = intercalate "," (moduleNameString <$> makeOpenImports opts) in -- The extension should have been set by 'optionsList' -- The rest of the list are the other arguments for the test ( fromMaybe "missng" ne : [show ia, show ea] ++ ["LeaveOpenImports:" ++ lo | not (null lo)] ++ ["MakeOpenImports:" ++ mo | not (null mo)] ) ) ++ [testcase]