{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ViewPatterns #-}

-----------------------------------------------------------------------------

-- |
-- Module      :  Distribution.Simple.Test
-- Copyright   :  Thomas Tuegel 2010
-- License     :  BSD3
--
-- Maintainer  :  cabal-devel@haskell.org
-- Portability :  portable
--
-- This is the entry point into testing a built package. It performs the
-- \"@.\/setup test@\" action. It runs test suites designated in the package
-- description and reports on the results.
module Distribution.Simple.Test
  ( test
  ) where

import Distribution.Compat.Prelude
import Prelude ()

import qualified Distribution.PackageDescription as PD
import Distribution.Pretty
import Distribution.Simple.Build (addInternalBuildTools)
import Distribution.Simple.Compiler
import Distribution.Simple.Hpc
import Distribution.Simple.InstallDirs
import qualified Distribution.Simple.LocalBuildInfo as LBI
import Distribution.Simple.Setup.Test
import qualified Distribution.Simple.Test.ExeV10 as ExeV10
import qualified Distribution.Simple.Test.LibV09 as LibV09
import Distribution.Simple.Test.Log
import Distribution.Simple.UserHooks
import Distribution.Simple.Utils
import Distribution.TestSuite
import qualified Distribution.Types.LocalBuildInfo as LBI
import Distribution.Types.UnqualComponentName
import Distribution.Utils.Path

import Distribution.Simple.Configure (getInstalledPackagesById)
import Distribution.Simple.Errors
import Distribution.Simple.Register (internalPackageDBPath)
import Distribution.Simple.Setup.Common
import Distribution.Simple.Setup.Config
import Distribution.Types.ExposedModule
import Distribution.Types.InstalledPackageInfo (InstalledPackageInfo (libraryDirs), exposedModules)
import Distribution.Types.LocalBuildInfo (LocalBuildInfo (..))
import System.Directory
  ( createDirectoryIfMissing
  , doesFileExist
  , getDirectoryContents
  , removeFile
  )

-- | Perform the \"@.\/setup test@\" action.
test
  :: Args
  -- ^ positional command-line arguments
  -> PD.PackageDescription
  -- ^ information from the .cabal file
  -> LBI.LocalBuildInfo
  -- ^ information from the configure step
  -> TestFlags
  -- ^ flags sent to test
  -> IO ()
test args pkg_descr lbi0 flags = do
  curDir <- LBI.absoluteWorkingDirLBI lbi0
  let common = testCommonFlags flags
      verbosity = fromFlag $ setupVerbosity common
      distPref = fromFlag $ setupDistPref common
      i = LBI.interpretSymbolicPathLBI lbi -- See Note [Symbolic paths] in Distribution.Utils.Path
      machineTemplate = fromFlag $ testMachineLog flags
      testLogDir = distPref </> makeRelativePathEx "test"
      testNames = args
      pkgTests = PD.testSuites pkg_descr
      enabledTests = LBI.enabledTestLBIs pkg_descr lbi
      -- We must add the internalPkgDB to the package database stack to lookup
      -- the path to HPC dirs of libraries local to this package
      internalPkgDb = internalPackageDBPath lbi0 distPref
      lbi = lbi0{withPackageDB = withPackageDB lbi0 ++ [SpecificPackageDB internalPkgDb]}

      doTest
        :: HPCMarkupInfo
        -> ( (PD.TestSuite, LBI.ComponentLocalBuildInfo)
           , Maybe TestSuiteLog
           )
        -> IO TestSuiteLog
      doTest hpcMarkupInfo ((suite, clbi), _) = do
        let lbiForTest =
              lbi
                { withPrograms =
                    -- Include any build-tool-depends on build tools internal to the current package.
                    addInternalBuildTools
                      curDir
                      pkg_descr
                      lbi
                      (PD.testBuildInfo suite)
                      (withPrograms lbi)
                }
        case PD.testInterface suite of
          PD.TestSuiteExeV10 _ _ ->
            ExeV10.runTest pkg_descr lbiForTest clbi hpcMarkupInfo flags suite
          PD.TestSuiteLibV09 _ _ ->
            LibV09.runTest pkg_descr lbiForTest clbi hpcMarkupInfo flags suite
          _ ->
            return
              TestSuiteLog
                { testSuiteName = PD.testName suite
                , testLogs =
                    TestLog
                      { testName = unUnqualComponentName $ PD.testName suite
                      , testOptionsReturned = []
                      , testResult =
                          Error $
                            "No support for running test suite type: "
                              ++ show (pretty $ PD.testType suite)
                      }
                , logFile = ""
                }

  unless (PD.hasTests pkg_descr) $ do
    notice verbosity "Package has no test suites."
    exitSuccess

  when (PD.hasTests pkg_descr && null enabledTests) $
    dieWithException verbosity NoTestSuitesEnabled

  testsToRun <- case testNames of
    [] -> return $ zip enabledTests $ repeat Nothing
    names -> for names $ \tName ->
      let testMap = zip enabledNames enabledTests
          enabledNames = map (PD.testName . fst) enabledTests
          allNames = map PD.testName pkgTests
          tCompName = mkUnqualComponentName tName
       in case lookup tCompName testMap of
            Just t -> return (t, Nothing)
            _
              | tCompName `elem` allNames ->
                  dieWithException verbosity $ TestNameDisabled tName
              | otherwise -> dieWithException verbosity $ NoSuchTest tName

  createDirectoryIfMissing True $ i testLogDir

  -- Delete ordinary files from test log directory.
  getDirectoryContents (i testLogDir)
    >>= filterM doesFileExist . map (i testLogDir </>)
    >>= traverse_ removeFile

  -- We configured the unit-ids of libraries we should cover in our coverage
  -- report at configure time into the local build info. At build time, we built
  -- the hpc artifacts into the extraCompilationArtifacts directory, which, at
  -- install time, is copied into the ghc-pkg database files.
  -- Now, we get the path to the HPC artifacts and exposed modules of each
  -- library by querying the package database keyed by unit-id:
  let coverageFor =
        nub $
          fromFlagOrDefault [] (configCoverageFor (configFlags lbi))
            <> extraCoverageFor lbi
  ipkginfos <- getInstalledPackagesById verbosity lbi MissingCoveredInstalledLibrary coverageFor
  let ( concat -> pathsToLibsArtifacts
        , concat -> libsModulesToInclude
        ) =
          unzip $
            map
              ( \ip ->
                  ( map ((</> coerceSymbolicPath extraCompilationArtifacts) . makeSymbolicPath) $ libraryDirs ip
                  , map exposedName $ exposedModules ip
                  )
              )
              ipkginfos
      hpcMarkupInfo = HPCMarkupInfo{pathsToLibsArtifacts, libsModulesToInclude}

  let totalSuites = length testsToRun
  notice verbosity $ "Running " ++ show totalSuites ++ " test suites..."
  suites <- traverse (doTest hpcMarkupInfo) testsToRun
  let packageLog = (localPackageLog pkg_descr lbi){testSuites = suites}
      packageLogFile =
        i testLogDir
          </> packageLogPath machineTemplate pkg_descr lbi
  allOk <- summarizePackage verbosity packageLog
  writeFile packageLogFile $ show packageLog

  when (LBI.testCoverage lbi) $
    markupPackage verbosity hpcMarkupInfo lbi distPref pkg_descr $
      map (fst . fst) testsToRun

  unless allOk exitFailure

packageLogPath
  :: PathTemplate
  -> PD.PackageDescription
  -> LBI.LocalBuildInfo
  -> FilePath
packageLogPath template pkg_descr lbi =
  fromPathTemplate $ substPathTemplate env template
  where
    env =
      initialPathTemplateEnv
        (PD.package pkg_descr)
        (LBI.localUnitId lbi)
        (compilerInfo $ LBI.compiler lbi)
        (LBI.hostPlatform lbi)
