packages feed

hydra-0.15.0: src/test/haskell/Hydra/TestUtils.hs

module Hydra.TestUtils (
  module Hydra.TestUtils,
  module Hydra.Sources.Libraries,
  module Hydra.Test.TestGraph,
  module Hydra.Test.TestTypes,
  module Hydra.Test.TestTerms,
) where

import Hydra.Kernel
import Hydra.Generation (showError)
import Hydra.ArbitraryCore()
import Hydra.Dsl.Bootstrap
import Hydra.Dsl.Terms
import qualified Hydra.EvalPrimitives as EvalPrims
import qualified Hydra.Sources.Kernel.Types.Coders as TypeCoders
import qualified Hydra.Sources.Kernel.Types.Context as TypeContext
import qualified Hydra.Sources.Kernel.Types.Core as TypeCore
import qualified Hydra.Sources.Kernel.Types.Errors as TypeError
import qualified Hydra.Sources.Kernel.Types.Util as TypeUtil
import qualified Hydra.Sources.Kernel.Terms.Annotations as TermAnnotations
import qualified Hydra.Sources.Kernel.Terms.Constants as TermConstants
import qualified Hydra.Sources.Kernel.Terms.Extract.Core as TermExtractCore
import qualified Hydra.Sources.Kernel.Terms.Lexical as TermLexical
import qualified Hydra.Sources.Kernel.Terms.Dependencies as TermDependencies
import qualified Hydra.Sources.Kernel.Terms.Rewriting as TermRewriting
import qualified Hydra.Sources.Kernel.Terms.Scoping as TermScoping
import qualified Hydra.Sources.Kernel.Terms.Strip as TermStrip
import qualified Hydra.Sources.Kernel.Terms.Variables as TermVariables
import qualified Hydra.Sources.Kernel.Terms.Show.Core as TermShowCore
import qualified Hydra.Sources.Decode.Core as TermDecodeCore
import qualified Hydra.Sources.Encode.Core as TermEncodeCore
import Hydra.Sources.Kernel.Types.Core
import Hydra.Sources.Libraries
import Hydra.Test.TestGraph hiding (testGraph, testContext)
import Hydra.Test.TestTypes
import Hydra.Test.TestTerms
import qualified Hydra.Dsl.Terms as Terms
import qualified Hydra.Dsl.Types as Types
import qualified Hydra.Encode.Core as EncodeCore
import qualified Hydra.Show.Core as ShowCore

import qualified Test.Hspec as H
import qualified Test.HUnit.Lang as HL
import qualified Data.List as L
import qualified Data.Map as M
import qualified Data.Maybe as Y
import qualified Data.ByteString.Lazy as BS


decodeSchemaTypes :: Graph -> M.Map Name TypeScheme
decodeSchemaTypes sg = case schemaGraphToTypingEnvironment sg of
  Left _ -> M.empty
  Right result -> result

testGraph :: Graph
testGraph = elementsToGraph hydraCoreGraph (decodeSchemaTypes testSchemaGraph) (kernelTermBindings ++ dataBindings)
  where
    -- Include only essential kernel term definitions for interpreter tests.
    -- The evaluator needs hydra.annotations (and its dependencies).
    kernelTermBindings = L.concat $ fmap moduleBindings
      [ TermAnnotations.module_
      , TermConstants.module_
      , TermDecodeCore.module_
      , TermDependencies.module_
      , TermEncodeCore.module_
      , TermExtractCore.module_
      , TermLexical.module_
      , TermRewriting.module_
      , TermScoping.module_
      , TermShowCore.module_
      , TermStrip.module_
      , TermVariables.module_
      ]
    dataBindings = (\(name, term) -> Binding name term Nothing) <$> M.toList testTerms

testSchemaGraph :: Graph
testSchemaGraph = elementsToGraph hydraCoreGraph (decodeSchemaTypes hydraCoreGraph)
    -- Only the kernel type modules that define types referenced by the test suite schema graph:
    -- CoderDirection (hydra.coders), Coder (hydra.util), and Type/Name/ForallType (hydra.core).
    (kernelElements ++ testElements)
  where
    kernelElements = L.concat $ fmap moduleBindings
      [ TypeCoders.module_
      , TypeContext.module_
      , TypeCore.module_
      , TypeError.module_
      , TypeUtil.module_
      ]
    testElements = fmap
      (\(n, t) -> Binding n (EncodeCore.type_ t) $ Just $ Types.mono $ TypeVariable _Type) $ M.toList testTypes

testContext :: Context
testContext = Context [] [] M.empty

check :: String -> H.SpecWith a -> H.SpecWith a
check desc = H.describe $ "Check type inference for " <> desc

checkSerdeRoundTrip :: (Context -> Graph -> Type -> Either Error (Coder Term BS.ByteString))
  -> TypeApplicationTerm -> H.Expectation
checkSerdeRoundTrip mkSerde (TypeApplicationTerm term typ) = do
    case mkSerde testContext testGraph typ of
      Left e -> HL.assertFailure (showError e)
      Right serde -> do
        case coderEncode serde testContext term >>= coderDecode serde testContext of
          Left e -> HL.assertFailure (showError e)
          Right roundTripped -> deannotateTerm roundTripped `H.shouldBe` deannotateTerm term

checkSerialization :: (Context -> Graph -> Type -> Either Error (Coder Term String))
  -> TypeApplicationTerm -> String -> H.Expectation
checkSerialization mkSerdeStr (TypeApplicationTerm term typ) expected = do
    case mkSerdeStr testContext testGraph typ of
      Left e -> HL.assertFailure (showError e)
      Right serde -> shouldSucceedWith
        (mapError $ fmap normalize $ coderEncode serde testContext term)
        (normalize expected)
  where
    normalize = unlines . L.filter (not . L.null) . lines

eval :: Term -> Either String Term
eval term = case reduceTerm testContext testGraph True term of
    Left err -> Left (showError err)
    Right result -> Right result

-- | A test graph where eval-mode primitives replace native ones where available.
-- Eval primitives operate at the term level (building application terms) rather than
-- using native Haskell implementations, testing that the eval approach is correct.
evalTestGraph :: Graph
evalTestGraph = elementsToGraph evalCoreGraph (decodeSchemaTypes testSchemaGraph) (kernelTermBindings ++ dataBindings)
  where
    kernelTermBindings = L.concat $ fmap moduleBindings
      [ TermAnnotations.module_
      , TermConstants.module_
      , TermDecodeCore.module_
      , TermDependencies.module_
      , TermEncodeCore.module_
      , TermExtractCore.module_
      , TermLexical.module_
      , TermRewriting.module_
      , TermScoping.module_
      , TermShowCore.module_
      , TermStrip.module_
      , TermVariables.module_
      ]
    dataBindings = (\(name, term) -> Binding name term Nothing) <$> M.toList testTerms

-- | A core graph with eval primitives merged over the standard ones.
-- Standard primitives are kept for functions that don't have eval implementations.
-- Eval primitives override the standard ones where they exist.
evalCoreGraph :: Graph
evalCoreGraph = hydraCoreGraph {
  graphPrimitives = M.union evalPrimMap (graphPrimitives hydraCoreGraph)}
  where
    evalPrimMap = M.fromList $ fmap (\p -> (primitiveName p, p))
      (L.concat (libraryPrimitives <$> EvalPrims.evalLibraries))

evalEval :: Term -> Either String Term
evalEval term = case reduceTerm testContext evalTestGraph True term of
    Left err -> Left (showError err)
    Right result -> Right result

expectEtaExpansionResult :: String -> Term -> Term -> H.SpecWith ()
expectEtaExpansionResult desc input output = H.it "eta expansion" $ do
  case etaExpandTypedTerm testContext testGraph input of
    Left err -> HL.assertFailure (showError err)
    Right result -> result `H.shouldBe` output

expectFailure :: (a -> String) -> String -> Either String a -> H.Expectation
expectFailure print desc f = case f of
    Left _ -> return ()
    Right v -> HL.assertFailure $ "Failure case succeeded with " ++ print v

expectInferenceFailure :: String -> Term -> H.Expectation
expectInferenceFailure desc term = case inferTypeOf testContext testGraph term of
    Left _ -> return ()
    Right ((_, ts), _) -> HL.assertFailure $ "Expected inference failure but got: " ++ ShowCore.typeScheme ts

expectInferenceResult :: String -> Term -> TypeScheme -> H.SpecWith ()
expectInferenceResult desc term expected = do
  case inferTypeOf testContext testGraph term of
    Left err -> H.runIO (HL.assertFailure (showError err)) >> return ()
    Right ((iterm, its), _cx') -> do
      H.it "inferred type" $
        H.shouldBe (ShowCore.typeScheme its) (ShowCore.typeScheme expected)
      H.it "inferred term" $
        H.shouldBe (ShowCore.term $ removeTypesFromTerm iterm) (ShowCore.term $ removeTypesFromTerm term)

expectSuccess :: (Eq a, Show a) => String -> Either String a -> a -> H.Expectation
expectSuccess desc result x = case result of
    Left err -> HL.assertFailure err
    Right y -> y `H.shouldBe` x

expectTypeCheckingResult :: String -> Term -> Term -> Type -> H.SpecWith ()
expectTypeCheckingResult desc input outputTerm outputType = do
  case inferTypeOf testContext testGraph input of
    Left err -> H.runIO (HL.assertFailure (showError err)) >> return ()
    Right ((iterm, ts), cx1) -> do
      let itype = typeSchemeToFType ts
      case typeOf cx1 testGraph [] iterm of
        Left err2 -> H.runIO (HL.assertFailure (showError err2)) >> return ()
        Right (rtype, _cx2) -> do
          H.it "inferred term" $
            H.shouldBe (ShowCore.term iterm) (ShowCore.term outputTerm)
          H.it "inferred type" $
            H.shouldBe (ShowCore.type_ itype) (ShowCore.type_ outputType)
          H.it "reconstructed type" $
            H.shouldBe (ShowCore.type_ rtype) (ShowCore.type_ outputType)

makeMap :: [(String, Int)] -> Term
makeMap keyvals = Terms.map $ M.fromList $ ((\(k, v) -> (Terms.string k, Terms.int32 v)) <$> keyvals)

shouldFail :: Either String a -> H.Expectation
shouldFail f = H.shouldBe True (either (const True) (const False) f)

shouldSucceed :: Either String a -> H.Expectation
shouldSucceed f = case f of
    Left err -> HL.assertFailure err
    Right _ -> True `H.shouldBe` True

shouldSucceedWith :: (Eq a, Show a) => Either String a -> a -> H.Expectation
shouldSucceedWith f x = case f of
    Left err -> HL.assertFailure err
    Right y -> y `H.shouldBe` x

-- | Map an Error to a plain String error (legacy name)
mapInContextError :: Either Error a -> Either String a
mapInContextError = mapError

mapError :: Either Error a -> Either String a
mapError (Left e) = Left (showError e)
mapError (Right a) = Right a

strip :: Term -> Term
strip = deannotateTerm