dhall-haskell/dhall/tests/Dhall/Test/Util.hs
Frederik Ramcke fd8683216d Fix tests without with-http flag (#1159)
* Allow customization of remote import resolution

Makes the `Status` type more general; previously support for
`Network.HTTP.Client` was hardcoded. In short:

```
data Status = Status
    { _stack :: NonEmpty Chained
    [...]
--  , _manager :: Maybe Dynamic
--  --   importing the same expression twice with different values
++  , _remote :: URL -> StateT Status IO Data.Text.Text
++  -- ^ The remote resolver, fetches the content at the given URL.

    [...]
    }

```

* Simplify and expose `toHeaders`

`toHeaders` will be needed for mock http testing

* Fix compilation without `with-http` flag

* Fix compilation with `with-http` flag

* Fix tests without `with-http` flag

Implements a mock http client that handles requests to:
- `https://raw.githubusercontent.com/dhall-lang/dhall-lang/master/`
- `https://test.dhall-lang.org/Bool/package.dhall`
- `https://httpbin.org/user-agent`

This allows tests involving remote imports to succeed even when compiled
without the `with-http` flag.

* Build `dhall` with HTTP support compiled out in CI

... to prevent regressions from occurring in the future
2019-07-27 02:59:25 +00:00

209 lines
7.4 KiB
Haskell

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE CPP #-}
module Dhall.Test.Util
( code
, codeWith
, equivalent
, load
, loadWith
, normalize'
, normalizeWith'
, assertNormalizesTo
, assertNormalizesToWith
, assertNormalized
, assertTypeChecks
, assertDoesntTypeCheck
, discover
, toDhallPath
) where
import Control.Monad.Trans.State.Strict (StateT)
import Data.Bifunctor (first)
import Data.Text (Text)
import Dhall.Context (Context)
import Dhall.Core (Expr, Normalizer, ReifiedNormalizer(..), Import)
import Dhall.Import (Status)
import Dhall.Parser (Src)
import Dhall.TypeCheck (X)
import Prelude hiding (FilePath)
import Test.Tasty.HUnit
import Test.Tasty (TestTree)
import Turtle (FilePath, Pattern, Shell, fp)
import qualified Control.Exception
import qualified Control.Foldl as Foldl
import qualified Data.Functor
import qualified Data.Text as Text
import qualified Dhall.Context
import qualified Dhall.Core
import qualified Dhall.Import
import qualified Dhall.Parser
import qualified Dhall.TypeCheck
import qualified Control.Monad.Trans.State.Strict as State
import qualified Test.Tasty as Tasty
import qualified Turtle
#ifndef WITH_HTTP
import Control.Monad.IO.Class (MonadIO(..))
import Dhall.Core (URL(..))
import Lens.Family.State.Strict (zoom)
import qualified Data.Foldable
import qualified Data.Text.Encoding
import qualified Data.Text.IO
#endif
normalize' :: Expr Src X -> Text
normalize' = Dhall.Core.pretty . Dhall.Core.normalize
normalizeWith' :: Normalizer X -> Expr Src X -> Text
normalizeWith' ctx t =
Dhall.Core.pretty (Dhall.Core.normalizeWith (Just (ReifiedNormalizer ctx)) t)
code :: Text -> IO (Expr Src X)
code = codeWith Dhall.Context.empty
codeWith :: Context (Expr Src X) -> Text -> IO (Expr Src X)
codeWith ctx expr = do
expr0 <- case Dhall.Parser.exprFromText mempty expr of
Left parseError -> Control.Exception.throwIO parseError
Right expr0 -> return expr0
expr1 <- load expr0
case Dhall.TypeCheck.typeWith ctx expr1 of
Left typeError -> Control.Exception.throwIO typeError
Right _ -> return ()
return expr1
load :: Expr Src Import -> IO (Expr Src X)
load expression =
State.evalStateT (loadWith expression) (Dhall.Import.emptyStatus ".")
#ifdef WITH_HTTP
loadWith :: Expr Src Import -> StateT Status IO (Expr Src X)
loadWith = Dhall.Import.loadWith
#else
loadWith :: Expr Src Import -> StateT Status IO (Expr Src X)
loadWith expr = do
let mockRemote' url = do
liftIO . putStrLn $ "\nTesting without real HTTP support --"
++ " using mock HTTP client to resolve remote import."
mockRemote url
zoom Dhall.Import.remote (State.put mockRemote')
Dhall.Import.loadWith expr
mockRemote :: Dhall.Core.URL -> StateT Status IO Data.Text.Text
-- Matches anything pointing to
-- `https://raw.githubusercontent.com/dhall-lang/dhall-lang/master/`
mockRemote (URL { authority = "raw.githubusercontent.com"
, path = Dhall.Core.File (Dhall.Core.Directory components) file })
| take 3 (reverse components) == ["dhall-lang", "dhall-lang", "master"] = do
let dropEnd n ls = take (length ls - n) ls
let localDir = dropEnd 3 components ++ ["dhall-lang"]
localPath <- Dhall.Import.localToPath Dhall.Core.Here (Dhall.Core.File (Dhall.Core.Directory localDir) file)
liftIO $ Data.Text.IO.readFile localPath
-- Matches anything pointing to
-- `https://test.dhall-lang.org/Bool/package.dhall`; checks that a `test` header
-- is present and redirects to the local copy of the prelude.
mockRemote (URL { authority = "test.dhall-lang.org"
, path = Dhall.Core.File (Dhall.Core.Directory components) file
, headers = Just headersExpr }) =
case Data.Foldable.find ((== "test") . fst) hs of
Nothing -> fail $ "(mock http) Tried to load an import from "
++"\"test.dhall-lang.org\""
++ "without setting the \"test\" header field."
Just (_, _) -> do
let localDir = components ++ ["Prelude", "dhall-lang"]
localPath <- Dhall.Import.localToPath Dhall.Core.Here (Dhall.Core.File (Dhall.Core.Directory localDir) file)
liftIO $ Data.Text.IO.readFile localPath
where
hs = Dhall.Import.toHeaders headersExpr
-- Emulates `https://httpbin.org/user-agent`
mockRemote (URL { authority = "httpbin.org"
, path = Dhall.Core.File (Dhall.Core.Directory []) "user-agent"
, headers = Just headersExpr }) =
case Data.Foldable.find ((== "user-agent") . fst) hs of
Nothing -> fail $ "(mock http) Tried to read the user agent via "
++ "\"httpbin.com/user-agent\" without supplying one "
++ "in the header!"
Just (_, userAgent) -> do
let agentText = Data.Text.Encoding.decodeUtf8 userAgent
return ("{\n \"user-agent\": \"" <> agentText <> "\"\n}\n")
where
hs = Dhall.Import.toHeaders headersExpr
mockRemote url = do
let urlString = Text.unpack (Dhall.Core.pretty url)
fail ("(mock http) Url does not match any of the hard-coded rules: "
<> urlString)
#endif
equivalent :: Text -> Text -> IO ()
equivalent text0 text1 = do
expr0 <- fmap Dhall.Core.normalize (code text0) :: IO (Expr X X)
expr1 <- fmap Dhall.Core.normalize (code text1) :: IO (Expr X X)
assertEqual "Expressions are not equivalent" expr0 expr1
assertNormalizesTo :: Expr Src X -> Text -> IO ()
assertNormalizesTo e expected = do
assertBool msg (not $ Dhall.Core.isNormalized e)
normalize' e @?= expected
where msg = "Given expression is already in normal form"
assertNormalizesToWith :: Normalizer X -> Expr Src X -> Text -> IO ()
assertNormalizesToWith ctx e expected = do
assertBool msg (not $ Dhall.Core.isNormalizedWith ctx (first (const ()) e))
normalizeWith' ctx e @?= expected
where msg = "Given expression is already in normal form"
assertNormalized :: Expr Src X -> IO ()
assertNormalized e = do
assertBool msg1 (Dhall.Core.isNormalized e)
assertEqual msg2 (normalize' e) (Dhall.Core.pretty e)
where msg1 = "Expression was not in normal form"
msg2 = "Normalization is not supposed to change the expression"
assertTypeChecks :: Text -> IO ()
assertTypeChecks text = Data.Functor.void (code text)
assertDoesntTypeCheck :: Text -> IO ()
assertDoesntTypeCheck text = do
expr0 <- case Dhall.Parser.exprFromText mempty text of
Left parseError -> Control.Exception.throwIO parseError
Right e -> return e
expr1 <- load expr0
case Dhall.TypeCheck.typeOf expr1 of
Left _ -> return ()
Right type_ -> fail ("Bad type for " <> Text.unpack text <> "\n " <> show type_)
{-| Automatically run a test on all files in a directory tree that end in
@A.dhall@
-}
discover :: Pattern Text -> (Text -> TestTree) -> Shell FilePath -> IO TestTree
discover pattern buildTest paths = do
let shell = do
path_ <- paths
let pathText = Turtle.format fp path_
prefix : _ <- return (Turtle.match pattern pathText)
return (buildTest prefix)
tests <- Turtle.fold shell Foldl.list
return (Tasty.testGroup "discover" tests)
{-| Path names on Windows are not valid Dhall paths due to using backslashes
instead of forwardslashes to separate path components. This utility fixes
them if necessary
-}
toDhallPath :: Text -> Text
toDhallPath = Text.replace "\\" "/"