packages feed

smuggler2-0.3.5.1: test/Test.hs

{-# 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.Paths (ghc)
import GHC (mkModuleName, moduleNameString)
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
          -- The -G. is needed because cabal sdist changes the golden file
          -- permissions and so all the tests fail.
          (\ref new -> ["git", "diff", "--no-index", "-G.", ref, new]) -- how to display diffs
          (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]