{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE FlexibleContexts #-} module Test.Cabal.DecodeShowBuildInfo where import Test.Cabal.Prelude import Test.Cabal.Plan import Distribution.Compat.Stack import Distribution.Text (display) import Distribution.Types.ComponentName import Distribution.Types.LibraryName import Distribution.Types.UnqualComponentName import Distribution.Package import Distribution.Pretty (prettyShow) import Control.Monad.Trans.Reader import Data.Aeson import GHC.Generics import System.Exit -- | Execute 'cabal build --enable-build-info'. -- -- Results can be read via 'withPlan', 'buildInfoFile' and 'decodeBuildInfoFile'. runShowBuildInfo :: [String] -> TestM () runShowBuildInfo args = cabal "build" ("--enable-build-info":args) -- | Read 'build-info.json' for a given package and component -- from disk and record the content. Helpful for defining test-cases -- where the build info matters. recordBuildInfo :: PackageName -> ComponentName -> TestM () recordBuildInfo pkgName cname = do Just plan <- fmap testPlan ask let fp = buildInfoFile plan pkgName cname recordMode RecordAll $ do recordHeader ["show-build-info", prettyShow pkgName, prettyShow cname] buildInfo <- liftIO $ readFile fp recordLog $ Result ExitSuccess "build --enable-build-info" buildInfo -- | Decode the given filepath into a 'BuildInfo'. -- -- If the filepath doesn't exist or its contents are not a valid 'BuildInfo' -- json file, then an error is raised. decodeBuildInfoFile :: FilePath -> TestM BuildInfo decodeBuildInfoFile fp = do shouldExist fp res <- liftIO $ eitherDecodeFileStrict fp case res of Left err -> fail $ "Could not parse show-build-info file: " ++ err Right buildInfos -> return buildInfos data BuildInfo = BuildInfo { cabalLibVersion :: String , compiler :: CompilerInfo , components :: [ComponentInfo] } deriving (Generic, Show) data CompilerInfo = CompilerInfo { flavour :: String , compilerId :: String , path :: String } deriving (Generic, Show) data ComponentInfo = ComponentInfo { componentType :: String , componentName :: String , componentUnitId :: String , componentCompilerArgs :: [String] , componentModules :: [String] , componentSrcFiles :: [FilePath] , componentHsSrcDirs :: [FilePath] , componentSrcDir :: FilePath } deriving (Generic, Show) instance ToJSON BuildInfo where toEncoding = genericToEncoding defaultOptions instance FromJSON BuildInfo where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelTo2 '-' } instance ToJSON CompilerInfo where toEncoding = genericToEncoding defaultOptions instance FromJSON CompilerInfo where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = camelTo2 '-' } instance ToJSON ComponentInfo where toEncoding = genericToEncoding defaultOptions instance FromJSON ComponentInfo where parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = drop 10 . camelTo2 '-' } -- ----------------------------------------------------------- -- Assertion Helpers to define succinct test cases -- ----------------------------------------------------------- data ComponentAssertion = ComponentAssertion { unitIdPred :: (String -> Bool) -- ^ Predicate to apply to a component's unit id. , compilerArgsPred :: ([String] -> Bool) -- ^ Predicate the compilation arguments must satisfy. , modules :: [String] -- ^ Which modules should a component contain. , sourceFiles :: [FilePath] -- ^ Which source files are part of a component. , sourceDirs :: [FilePath] -- ^ Expected source directories for a component. , compType :: String -- ^ Type of the component, usually one of 'bench', 'exe', 'test', 'lib', 'flib' } defCompAssertion :: ComponentAssertion defCompAssertion = ComponentAssertion { unitIdPred = not . null , compilerArgsPred = not . null , modules = [] , sourceFiles = [] , sourceDirs = [] , compType = "" } -- | Assert common build information, such as compiler location, compiler version -- and cabal library version. assertCommonBuildInfo :: WithCallStack (BuildInfo -> TestM ()) assertCommonBuildInfo buildInfo = do assertEqual "Cabal Version" (display cabalVersionLibrary) (cabalLibVersion buildInfo) assertEqual "Compiler flavour" "ghc" (flavour $ compiler buildInfo) assertBool "Compiler id" (and $ zipWith (==) "ghc" (compilerId $ compiler buildInfo)) assertBool "Compiler path non-empty" (not . null . path $ compiler buildInfo) -- | Pure assertion helper. Check whether the given 'ComponentInfo' satisfy -- the 'ComponentAssertion'. assertComponentPure :: WithCallStack (ComponentInfo -> ComponentAssertion -> TestM ()) assertComponentPure component ComponentAssertion{..} = do assertEqual "Component type" compType (componentType component) assertBool "Component Unit Id" (unitIdPred $ componentUnitId component) assertBool "Component compiler args" (compilerArgsPred $ componentCompilerArgs component) assertEqual "Component modules" modules (componentModules component) assertEqual "Component source files" sourceFiles (componentSrcFiles component) assertEqual "Component source directories" sourceDirs (componentHsSrcDirs component) -- | @'assertComponent' pkgName cname assertion@ -- -- Assert that a component identified by 'pkgName' and 'cname', generated -- a 'build-info.json' and its contents satisfy the assertions specified in 'assertion'. -- -- This assertion must be wrapped in 'withPlan'. assertComponent :: WithCallStack (PackageName -> ComponentName -> ComponentAssertion -> TestM ()) assertComponent pkgName cname assert = do Just plan <- fmap testPlan ask let fp = buildInfoFile plan pkgName cname buildInfo <- decodeBuildInfoFile fp assertCommonBuildInfo buildInfo let component = findComponentInfo buildInfo let assertWithCompType = assert { compType = compTypeStr cname } assertComponentPure component assertWithCompType where compTypeStr :: ComponentName -> String compTypeStr (CLibName _) = "lib" compTypeStr (CFLibName _) = "flib" compTypeStr (CExeName _) = "exe" compTypeStr (CTestName _) = "test" compTypeStr (CBenchName _) = "bench" findComponentInfo :: BuildInfo -> ComponentInfo findComponentInfo buildInfo = case filter (\c -> prettyShow cname == componentName c) (components buildInfo) of [x] -> x [] -> error $ "findComponentInfo: component " ++ prettyShow cname ++ " does not" ++ " exist in build info-file" _ -> error $ "findComponentInfo: found multiple copies of component " ++ prettyShow cname ++ " in build info plan" -- | Helper function to create an executable component name. exe :: String -> ComponentName exe = CExeName . mkUnqualComponentName -- | Helper function to create a named sub-library component name. lib :: String -> ComponentName lib = CLibName . LSubLibName . mkUnqualComponentName -- | Helper function to create an test component name. test :: String -> ComponentName test = CTestName . mkUnqualComponentName -- | Helper function to create an benchmark component name. bench :: String -> ComponentName bench = CBenchName . mkUnqualComponentName -- | Helper function to create a main library component name. mainLib :: ComponentName mainLib = CLibName LMainLibName