hydra-0.15.0: src/main/haskell/Hydra/Sources/Haskell/Testing.hs
-- | Haskell test code generation in Hydra DSL.
-- This module generates HSpec-based test files for Haskell from universal test cases.
module Hydra.Sources.Haskell.Testing where
-- Standard imports for term-level sources outside of the kernel
import Hydra.Kernel
import Hydra.Sources.Libraries
import Hydra.Dsl.Meta.Lib.Strings as Strings
import Hydra.Dsl.Meta.Phantoms as Phantoms
import qualified Hydra.Dsl.Meta.Context as Ctx
import qualified Hydra.Dsl.Meta.Core as Core
import qualified Hydra.Dsl.Meta.Lib.Eithers as Eithers
import qualified Hydra.Dsl.Meta.Lib.Equality as Equality
import qualified Hydra.Dsl.Meta.Lib.Lists as Lists
import qualified Hydra.Dsl.Meta.Lib.Literals as Literals
import qualified Hydra.Dsl.Meta.Lib.Logic as Logic
import qualified Hydra.Dsl.Meta.Lib.Maps as Maps
import qualified Hydra.Dsl.Meta.Lib.Math as Math
import qualified Hydra.Dsl.Meta.Lib.Maybes as Maybes
import qualified Hydra.Dsl.Meta.Lib.Pairs as Pairs
import qualified Hydra.Dsl.Meta.Lib.Sets as Sets
import qualified Hydra.Dsl.Packaging as Packaging
import qualified Hydra.Dsl.Util as Util
import qualified Hydra.Sources.Kernel.Terms.Constants as Constants
import qualified Hydra.Sources.Kernel.Terms.Formatting as Formatting
import qualified Hydra.Sources.Kernel.Terms.Lexical as Lexical
import qualified Hydra.Sources.Kernel.Terms.Names as Names
import qualified Hydra.Sources.Kernel.Terms.Dependencies as Dependencies
import qualified Hydra.Sources.Kernel.Terms.Rewriting as Rewriting
import qualified Hydra.Sources.Kernel.Terms.Strip as Strip
import qualified Hydra.Sources.Kernel.Terms.Predicates as Predicates
import qualified Hydra.Sources.Kernel.Terms.Show.Errors as ShowError
import qualified Hydra.Sources.Kernel.Types.All as KernelTypes
import Prelude hiding ((++))
import qualified Data.Map as M
import qualified Data.Set as S
-- Additional imports
import qualified Hydra.Haskell.Syntax as H
import qualified Hydra.Sources.Haskell.Syntax as HaskellSyntax
import qualified Hydra.Sources.Haskell.Utils as HaskellUtilsSource
define :: String -> TTerm a -> TTermDefinition a
define = definitionInModule module_
ns :: Namespace
ns = Namespace "hydra.haskell.testing"
module_ :: Module
module_ = Module {
moduleNamespace = ns,
moduleDefinitions = definitions,
moduleTermDependencies = [HaskellUtilsSource.ns, Formatting.ns, Names.ns,
Constants.ns, Dependencies.ns, Predicates.ns, Rewriting.ns, ShowError.ns, Lexical.ns, Strip.ns],
moduleTypeDependencies = (HaskellSyntax.ns:KernelTypes.kernelTypesNamespaces),
moduleDescription = Just "Haskell test code generation for HSpec-based generation tests"}
where
definitions = [
toDefinition addNamespacesToNamespaces,
toDefinition buildNamespacesForTestGroup,
toDefinition buildTestModule,
toDefinition collectNames,
toDefinition collectTestCases,
toDefinition extractEncodedTermVariableNames,
toDefinition extractTestTerms,
toDefinition findHaskellImports,
toDefinition generateHaskellTestFile,
toDefinition generateTestCase,
toDefinition generateTestFile,
toDefinition generateTestGroupHierarchy,
toDefinition namespaceToModuleName]
-- | Add namespaces from a set of names to existing namespaces
addNamespacesToNamespaces :: TTermDefinition (Namespaces H.ModuleName -> S.Set Name -> Namespaces H.ModuleName)
addNamespacesToNamespaces = define "addNamespacesToNamespaces" $
doc "Add namespaces from a set of names to existing namespaces" $
lambda "ns0" $ lambda "names" $ lets [
"newNamespaces">: Sets.fromList (Maybes.cat (Lists.map Names.namespaceOf (Sets.toList (var "names")))),
"toModuleName">: lambda "namespace" $
wrap H._ModuleName (Formatting.capitalize @@ (Maybes.fromMaybe (unwrap _Namespace @@ var "namespace") (Lists.maybeLast (Strings.splitOn (string ".") (unwrap _Namespace @@ var "namespace"))))),
"newMappings">: Maps.fromList (Lists.map (lambda "ns_" $ pair (var "ns_") (var "toModuleName" @@ var "ns_")) (Sets.toList (var "newNamespaces")))] $
record _Namespaces [
_Namespaces_focus>>: project _Namespaces _Namespaces_focus @@ var "ns0",
_Namespaces_mapping>>: Maps.union (project _Namespaces _Namespaces_mapping @@ var "ns0") (var "newMappings")]
-- | Build namespaces for a test group including encoded term references
buildNamespacesForTestGroup :: TTermDefinition (Module -> TestGroup -> Graph -> Either String (Namespaces H.ModuleName))
buildNamespacesForTestGroup = define "buildNamespacesForTestGroup" $
doc "Build namespaces for a test group including encoded term references" $
lambda "mod" $ lambda "tgroup" $ lambda "graph_" $ lets [
"testCases_">: collectTestCases @@ var "tgroup",
"testTerms">: Lists.concat (Lists.map extractTestTerms (var "testCases_")),
"testBindings">: Lists.map
(lambda "term" $
record _Binding [
_Binding_name>>: wrap _Name (string "_test_"),
_Binding_term>>: var "term",
_Binding_typeScheme>>: nothing])
(var "testTerms"),
"tempModule">: record _Module [
_Module_description>>: project _Module _Module_description @@ var "mod",
_Module_namespace>>: Packaging.moduleNamespace (var "mod"),
_Module_termDependencies>>: project _Module _Module_termDependencies @@ var "mod",
_Module_typeDependencies>>: project _Module _Module_typeDependencies @@ var "mod",
_Module_definitions>>: Lists.map ("b" ~> Packaging.definitionTerm (Packaging.termDefinition
(Core.bindingName $ var "b") (Core.bindingTerm $ var "b")
(Core.bindingTypeScheme $ var "b")))
(var "testBindings")]] $
Eithers.bind
(Eithers.bimap
(lambda "e" $ ShowError.error_ @@ var "e")
(lambda "a" $ var "a")
(HaskellUtilsSource.namespacesForModule @@ var "tempModule" @@ asTerm Lexical.emptyContext @@ var "graph_"))
(lambda "baseNamespaces" $ lets [
"encodedNames">: Sets.unions (Lists.map (lambda "t" $ extractEncodedTermVariableNames @@ var "graph_" @@ var "t") (var "testTerms"))] $
right (addNamespacesToNamespaces @@ var "baseNamespaces" @@ var "encodedNames"))
-- | Build the complete test module for Haskell HSpec
buildTestModule :: TTermDefinition (Module -> TestGroup -> String -> Namespaces H.ModuleName -> String)
buildTestModule = define "buildTestModule" $
doc "Build the complete test module for Haskell HSpec" $
lambda "testModule" $ lambda "testGroup" $ lambda "testBody" $ lambda "namespaces" $ lets [
"ns_">: Packaging.moduleNamespace (var "testModule"),
"specNs">: wrap _Namespace (Strings.cat2 (unwrap _Namespace @@ var "ns_") (string "Spec")),
"moduleNameString">: namespaceToModuleName @@ var "specNs",
"groupName_">: project _TestGroup _TestGroup_name @@ var "testGroup",
"domainImports">: findHaskellImports @@ var "namespaces" @@ Sets.empty,
"standardImports">: list [
string "import Hydra.Kernel",
string "import qualified Test.Hspec as H",
string "import qualified Data.List as L",
string "import qualified Data.Map as M",
string "import qualified Data.Set as S",
string "import qualified Data.Maybe as Y"],
"allImports">: Lists.concat2 (var "standardImports") (var "domainImports"),
"header">: Strings.intercalate (string "\n") (Lists.concat (list [
list [
Strings.cat2 (string "-- ") (asTerm Constants.warningAutoGeneratedFile),
string ""],
list [
string "",
Strings.cat (list [string "module ", var "moduleNameString", string " where"]),
string ""],
var "allImports",
list [
string "",
string "spec :: H.Spec",
Strings.cat (list [string "spec = H.describe ", Literals.showString (var "groupName_"), string " $ do"])]]))] $
Strings.cat (list [var "header", string "\n", var "testBody", string "\n"])
-- | Collect variable names from encoded terms within a single term node
collectNames :: TTermDefinition (Graph -> S.Set Name -> Term -> S.Set Name)
collectNames = define "collectNames" $
doc "Collect variable names from encoded terms within a single term node" $
lambda "graf" $ lambda "names" $ lambda "t" $
Logic.ifElse (Predicates.isEncodedTerm @@ (Strip.deannotateTerm @@ var "t"))
(Eithers.either_
(lambda "_" $ var "names")
(lambda "decodedTerm" $
Sets.union (var "names") (Dependencies.termDependencyNames @@ true @@ true @@ true @@ var "decodedTerm"))
(Eithers.bimap
(lambda "_e" $ var "_e")
(lambda "_a" $ var "_a")
(decoderFor _Term @@ var "graf" @@ var "t")))
(var "names")
-- | Collect all test cases from a test group (recursively)
collectTestCases :: TTermDefinition (TestGroup -> [TestCaseWithMetadata])
collectTestCases = define "collectTestCases" $
doc "Collect all test cases from a test group recursively" $
lambda "tg" $
Lists.concat2
(project _TestGroup _TestGroup_cases @@ var "tg")
(Lists.concat (Lists.map collectTestCases (project _TestGroup _TestGroup_subgroups @@ var "tg")))
-- | Extract all variable names from term-encoded terms in a given term
extractEncodedTermVariableNames :: TTermDefinition (Graph -> Term -> S.Set Name)
extractEncodedTermVariableNames = define "extractEncodedTermVariableNames" $
doc "Extract all variable names from term-encoded terms in a given term" $
lambda "graf" $ lambda "term" $
Rewriting.foldOverTerm @@ inject _TraversalOrder _TraversalOrder_pre unit @@ (collectNames @@ var "graf") @@ Sets.empty @@ var "term"
-- | Extract terms from a test case
extractTestTerms :: TTermDefinition (TestCaseWithMetadata -> [Term])
extractTestTerms = define "extractTestTerms" $
doc "Extract input and output terms from a test case" $
lambda "tcm" $
list ([] :: [TTerm Term])
-- | Find necessary imports for Haskell based on referenced names
findHaskellImports :: TTermDefinition (Namespaces H.ModuleName -> S.Set Name -> [String])
findHaskellImports = define "findHaskellImports" $
doc "Find necessary imports for Haskell based on referenced names" $
lambda "namespaces" $ lambda "names_" $ lets [
"mapping_">: project _Namespaces _Namespaces_mapping @@ var "namespaces",
"filtered">: Maps.filterWithKey
(lambda "ns_" $ lambda "_v" $
Logic.not (Equality.equal
(Maybes.fromMaybe (string "") (Lists.maybeHead (Strings.splitOn (string "hydra.test.") (unwrap _Namespace @@ var "ns_"))))
(string "")))
(var "mapping_")] $
Lists.map
(lambda "entry" $ Strings.cat (list [
string "import qualified ",
Strings.intercalate (string ".") (Lists.map Formatting.capitalize (Strings.splitOn (string ".") (unwrap _Namespace @@ Pairs.first (var "entry")))),
string " as ",
unwrap H._ModuleName @@ Pairs.second (var "entry")]))
(Maps.toList (var "filtered"))
-- | Generate a Haskell test file for a test group, with type inference and namespace building
generateHaskellTestFile :: TTermDefinition (Module -> TestGroup -> Graph -> Either String (String, String))
generateHaskellTestFile = define "generateHaskellTestFile" $
doc "Generate a Haskell test file for a test group, with type inference and namespace building" $
lambda "testModule" $ lambda "testGroup" $ lambda "g" $
Eithers.bind
(buildNamespacesForTestGroup @@ var "testModule" @@ var "testGroup" @@ var "g")
(lambda "namespaces" $
generateTestFile @@ var "testModule" @@ var "testGroup" @@ var "namespaces")
-- | Generate a single HSpec test case from a universal test case
generateTestCase :: TTermDefinition (Int -> TestCaseWithMetadata -> Either String [String])
generateTestCase = define "generateTestCase" $
doc "Generate a single HSpec test case from a universal test case" $
lambda "depth" $ lambda "tcm" $ lets [
"name_">: project _TestCaseWithMetadata _TestCaseWithMetadata_name @@ var "tcm",
"tcase">: project _TestCaseWithMetadata _TestCaseWithMetadata_case @@ var "tcm",
"universal">: match _TestCase Nothing ["universal">: lambda "u" $ var "u"] @@ var "tcase",
"actual_">: project _UniversalTestCase _UniversalTestCase_actual @@ var "universal",
"expected_">: project _UniversalTestCase _UniversalTestCase_expected @@ var "universal"] $
right (list [
Strings.cat (list [string "H.it ", Literals.showString (var "name_"), string " $ H.shouldBe"]),
Strings.cat (list [string " (", var "actual_", string ")"]),
Strings.cat (list [string " (", var "expected_", string ")"])])
-- | Generate a complete Haskell test file
generateTestFile :: TTermDefinition (Module -> TestGroup -> Namespaces H.ModuleName -> Either String (String, String))
generateTestFile = define "generateTestFile" $
doc "Generate a complete Haskell test file" $
lambda "testModule" $ lambda "testGroup" $ lambda "namespaces" $
Eithers.map
(lambda "testBody" $ lets [
"testModuleContent">: buildTestModule @@ var "testModule" @@ var "testGroup" @@ var "testBody" @@ var "namespaces",
"ns_">: Packaging.moduleNamespace (var "testModule"),
"specNs">: wrap _Namespace (Strings.cat2 (unwrap _Namespace @@ var "ns_") (string "Spec")),
"filePath">: Names.namespaceToFilePath @@ Util.caseConventionPascal @@ (wrap _FileExtension (string "hs")) @@ var "specNs"] $
pair (var "filePath") (var "testModuleContent"))
(generateTestGroupHierarchy @@ int32 1 @@ var "testGroup")
-- | Generate test hierarchy preserving the structure with H.describe blocks for subgroups
generateTestGroupHierarchy :: TTermDefinition (Int -> TestGroup -> Either String String)
generateTestGroupHierarchy = define "generateTestGroupHierarchy" $
doc "Generate test hierarchy preserving the structure with H.describe blocks for subgroups" $
lambda "depth" $ lambda "testGroup" $ lets [
"cases_">: project _TestGroup _TestGroup_cases @@ var "testGroup",
"subgroups">: project _TestGroup _TestGroup_subgroups @@ var "testGroup",
"indent">: Strings.fromList (Lists.replicate (Math.mul (var "depth") (int32 2)) (int32 32))] $ -- space char
Eithers.bind
(Eithers.mapList
(lambda "tc" $ generateTestCase @@ var "depth" @@ var "tc")
(var "cases_"))
(lambda "testCaseLinesRaw" $ lets [
"testCaseLines">: Lists.map
(lambda "lines_" $ Lists.map (lambda "line" $ Strings.cat2 (var "indent") (var "line")) (var "lines_"))
(var "testCaseLinesRaw"),
"testCasesStr">: Strings.intercalate (string "\n") (Lists.concat (var "testCaseLines"))] $
Eithers.map
(lambda "subgroupsStr" $
Strings.cat (list [
var "testCasesStr",
Logic.ifElse (Logic.or (Equality.equal (var "testCasesStr") (string "")) (Equality.equal (var "subgroupsStr") (string "")))
(string "")
(string "\n"),
var "subgroupsStr"]))
(Eithers.map
(lambda "blocks" $ Strings.intercalate (string "\n") (var "blocks"))
(Eithers.mapList
(lambda "subgroup" $ lets [
"groupName_">: project _TestGroup _TestGroup_name @@ var "subgroup"] $
Eithers.map
(lambda "content" $
Strings.cat (list [
var "indent",
string "H.describe ",
Literals.showString (var "groupName_"),
string " $ do\n",
var "content"]))
(generateTestGroupHierarchy @@ Math.add (var "depth") (int32 1) @@ var "subgroup"))
(var "subgroups"))))
-- | Convert namespace to Haskell module name
namespaceToModuleName :: TTermDefinition (Namespace -> String)
namespaceToModuleName = define "namespaceToModuleName" $
doc "Convert namespace to Haskell module name" $
lambda "ns_" $
Strings.intercalate (string ".") (Lists.map Formatting.capitalize (Strings.splitOn (string ".") (unwrap _Namespace @@ var "ns_")))