packages feed

guardian-0.5.0.0: test/Development/Guardian/AppSpec.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TupleSections #-}

module Development.Guardian.AppSpec (test_defaultMainWith) where

import Control.Exception
import Control.Monad (void)
import qualified Data.ByteString.Builder as BB
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Encoding as LT
import Data.Version (showVersion)
import Development.Guardian.App
import Development.Guardian.Flags (cabalEnabled, stackEnabled)
import Development.Guardian.Graph.Adapter.Detection
import Development.Guardian.Test.Flags
import GHC.IO.Exception (ExitCode (..))
import Path
import Path.IO
import Paths_guardian (version)
import RIO (logOptionsMemory, mkSimpleApp, readIORef, runRIO, runSimpleApp, withLogFunc)
import qualified RIO.ByteString.Lazy as LBS
import RIO.Process (proc, runProcess_)
import Test.Tasty
import Test.Tasty.ExpectedFailure (ignoreTestBecause)
import Test.Tasty.HUnit

fakeBuildInfo :: BuildInfo
fakeBuildInfo = BuildInfo {versionString = showVersion version, gitInfo = Nothing}

test_defaultMainWith :: TestTree
test_defaultMainWith =
  testGroup "defaultMainWith" $
    concreteAdapterTests : stackCases ++ cabalCases ++ autoCases ++ customCases

customCases :: [TestTree]
customCases =
  [ testGroup
      "custom adapter"
      [ testConditional "cabal-plan" testCabalPlan
          $ testCase "works with cabal-plan dot"
          $ withCurrentDir
            ([reldir|data|] </> [reldir|only-custom-cabal-plan|])
          $ do
            runSimpleApp $ proc "cabal" ["v2-update"] runProcess_
            runSimpleApp $ proc "cabal" ["v2-build", "--dry-run", "all"] runProcess_
            successfully_ $
              mainWith ["custom"]
      , testConditional "graphmod" testGraphmod $
          testCase "works with graphmod" $
            successfully_ $
              mainWith ["custom", "-c", "dependency-domains-graphmod.yaml"]
      , testCase "works with stack dot"
          $ withCurrentDir
            ([reldir|data|] </> [reldir|only-stack|])
          $ successfully_
          $ mainWith ["custom", "-c", "dependency-domains-stack-dot.yaml"]
      ]
  ]

stackCases :: [TestTree]
stackCases =
  [ skipIfStackDisabled $
      testGroup
        "stack-specific options"
        [ testCase "Respects --stack-yaml"
            $ withCurrentDir
              ([reldir|data|] </> [reldir|test-only-dependency|])
            $ successfully_
            $ mainWith ["cabal", "-c", "dependency-domains-custom-stack.yaml"]
        ]
  ]

cabalCases :: [TestTree]
cabalCases =
  [ skipIfCabalDisabled $
      testGroup
        "cabal-specific options"
        [ testCase "Respects projectFile"
            $ withCurrentDir
              ([reldir|data|] </> [reldir|test-only-dependency|])
            $ successfully_
            $ mainWith ["cabal", "-c", "dependency-domains-custom-cabal.yaml"]
        , testCase "Respects update: true"
            $ withCurrentDir
              ([reldir|data|] </> [reldir|test-only-dependency|])
            $ successfully_
            $ mainWith ["cabal", "-c", "dependency-domains-cabal-update-true.yaml"]
        , testCase "Respects update: (index-state)"
            $ withCurrentDir
              ([reldir|data|] </> [reldir|test-only-dependency|])
            $ successfully_
            $ mainWith ["cabal", "-c", "dependency-domains-cabal-update-index.yaml"]
        ]
  ]

skipIfCabalDisabled :: TestTree -> TestTree
skipIfCabalDisabled = testConditional "cabal" cabalEnabled

skipIfStackDisabled :: TestTree -> TestTree
skipIfStackDisabled = testConditional "stack" stackEnabled

testConditional :: String -> Bool -> TestTree -> TestTree
testConditional label testIt =
  if testIt
    then id
    else ignoreTestBecause $ "Test disabled for " <> label

autoCases :: [TestTree]
autoCases =
  [ testGroup
      "Auto detection"
      [ skipIfCabalDisabled
          $ testCase "Accepts config with cabal section only"
          $ withCurrentDir
            ([reldir|data|] </> [reldir|test-only-dependency|])
          $ successfully
            (LT.isInfixOf "with backend Cabal" . LT.decodeUtf8)
          $ mainWith ["auto", "-c", "dependency-domains-custom-cabal.yaml"]
      , skipIfStackDisabled
          $ testCase "Accepts config with stack section only"
          $ withCurrentDir
            ([reldir|data|] </> [reldir|test-only-dependency|])
          $ successfully
            (LT.isInfixOf "with backend Stack" . LT.decodeUtf8)
          $ mainWith ["auto", "-c", "dependency-domains-custom-stack.yaml"]
      , skipIfCabalDisabled
          $ testCase "Accepts unambiguous directory (cabal)"
          $ withCurrentDir
            ([reldir|data|] </> [reldir|only-cabal|])
          $ successfully
            (LT.isInfixOf "with backend Cabal" . LT.decodeUtf8)
          $ mainWith ["auto"]
      , skipIfStackDisabled
          $ testCase "Accepts unambiguous directory (stack)"
          $ withCurrentDir
            ([reldir|data|] </> [reldir|only-stack|])
          $ successfully
            (LT.isInfixOf "with backend Stack" . LT.decodeUtf8)
          $ mainWith ["auto"]
      , testCaseSteps "Rejects ambiguous inputs" \step ->
          withCurrentDir
            ([reldir|data|] </> [reldir|test-only-dependency|])
            $ do
              step "Abmiguous Directory & Config without any custom section"
              mainWith ["auto"] `shouldThrow` (== NoCustomConfigSpecified)
              step "Abmiguous Directory & Config wit both custom sections"
              mainWith ["auto", "-c", "dependency-domains-ambiguous.yaml"]
                `shouldThrow` (== MultipleAdapterConfigFound)
      ]
  ]

concreteAdapterTests :: TestTree
concreteAdapterTests =
  testGroup
    "Concrete adapter behaviours, independent of adapters"
    [ skipIfDisabled $
      testGroup
        backend
        [ testCase "invalidates test-only-dependency with default config"
            $ withCurrentDir
              ([reldir|data|] </> [reldir|test-only-dependency|])
            $ mainWith [backend] `shouldThrow` (== ExitFailure 1)
        , testCaseSteps "invalidates test-only-dependency with default config (explicit path argument)" \step -> do
            step "Absolute dir"
            dir <- canonicalizePath ([reldir|data|] </> [reldir|test-only-dependency|])
            mainWith [backend, fromAbsDir dir] `shouldThrow` (== ExitFailure 1)
            step "Relative dir"
            let rdir = [reldir|data|] </> [reldir|test-only-dependency|]
            mainWith [backend, fromRelDir rdir] `shouldThrow` (== ExitFailure 1)
        , testCaseSteps "accepts non-standard config yaml" $ \step ->
            withCurrentDir ([reldir|data|] </> [reldir|test-only-dependency|]) $ do
              step "Accepts  when tests and benchmarks disabled"
              successfully_ $
                mainWith [backend, "-c", "dependency-domains-no-tests-benchmarks.yaml"]
              step "Accepts input with exception rule"
              successfully (LT.isInfixOf "exceptional rules are used" . LT.decodeUtf8) $
                mainWith [backend, "-c", "dependency-domains-except-A2-B1.yaml"]
        ]
    | (backend, skipIfDisabled) <-
        [ ("cabal", skipIfCabalDisabled)
        , ("stack", skipIfStackDisabled)
        ]
    ]

successfully_ :: HasCallStack => IO (LBS.ByteString, Maybe SomeException) -> IO ()
successfully_ = void . successfully (const True)

successfully :: HasCallStack => (LBS.ByteString -> Bool) -> IO (LBS.ByteString, Maybe SomeException) -> IO ()
successfully isOk act = do
  (a, exc) <- act
  case exc of
    Just err ->
      assertFailure $
        "Exception: "
          <> displayException err
          <> "\nwith log: \n"
          <> LT.unpack (LT.decodeUtf8 a)
    Nothing
      | isOk a -> pure ()
      | otherwise ->
          assertFailure $
            "Exits Successfully, but the output is invalid: \n"
              <> LT.unpack (LT.decodeUtf8 a)

shouldThrow :: (HasCallStack, Exception e) => IO (a, Maybe e) -> (e -> Bool) -> Assertion
shouldThrow act p =
  act >>= \case
    (_, Nothing) ->
      assertFailure
        "Expected to throw excetpion, but exists successfully"
    (_, Just err)
      | p err -> pure ()
      | otherwise ->
          assertFailure $
            "Exception has been thrown, but does not satisfy the requirement: "
              <> show err

mainWith :: Exception exc => [String] -> IO (LBS.ByteString, Maybe exc)
mainWith args = do
  (logs, opts) <- logOptionsMemory
  eith <- try $ withLogFunc opts $ \logFunc -> do
    app <- mkSimpleApp logFunc Nothing
    runRIO app $
      defaultMainWith fakeBuildInfo args
  (,either Just (const Nothing) eith) . BB.toLazyByteString
    <$> readIORef logs