graphql-engine/server/tests-py/PortToHaskell.py
Philip Lykke Carlsen 886302fd7e server/tests: Introuce pytest-to-haskell porting script
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7054
GitOrigin-RevId: 2c4597465da28d26993b347693813ce7d42962d3
2022-12-02 15:59:19 +00:00

223 lines
6.2 KiB
Python

import os
import textwrap
from abc import ABC, abstractmethod
class Fixtures():
setups = {} # Map backend [Setup]
specs = {} # Map desc [Spec]
def add_setup(self, backend, setup):
if not backend in self.setups.keys():
self.setups[backend] = []
self.setups[backend].append(setup)
def add_spec(self, desc, spec):
if not desc in self.specs.keys():
self.specs[desc] = []
self.specs[desc].append(spec)
def render_gql(query):
return f"""GraphqlEngine.postGraphql
testEnvironment
[graphql|
{textwrap.indent(query, ' ')}
|]"""
def render_v2query(expected_status, query):
return f"""GraphqlEngine.postV2Query
{expected_status}
testEnvironment
[interpolateYaml|
{textwrap.indent(query, ' ')}
|]"""
class Setup():
def __init__(self, setup_name, original_file, url, value):
self.setup_name = setup_name
self.original_file = original_file
self.url = url
self.value = value
def render_define(self, backend):
return f"""-- original file: {self.original_file}
{self.setup_name}_{backend} :: Yaml.Value
{self.setup_name}_{backend} =
[interpolateYaml|
{textwrap.indent(self.value, 4*' ')}
|]
"""
def render_run(self, backend):
res = ""
if self.url == "/v2/query":
res = f"GraphqlEngine.postV2Query 200 testEnvironment {self.setup_name}_{backend}"
elif self.url == "/v1/graphql":
res = f"GraphqlEngine.postGraphql testEnvironment {self.setup_name}_{backend}"
elif self.url == "/v1/metadata":
res = f"""GraphqlEngine.postMetadata_ testEnvironment {self.setup_name}_{backend}"""
else:
raise f"Unknown query url: {self.url}"
return res
class Spec(ABC):
@abstractmethod
def render(self):
pass
class CovertSetupSpec(Spec):
"""
Specs that do not make any assertions, i.e. are only run for their side
effects.
"""
def __init__(self, desc, file, url, query):
self.desc = desc
self.file = file
self.url = url
self.query = query
def render(self):
res = ""
if self.url == "/v2/query":
res = render_v2query(200, self.query)
elif self.url == "/v1/graphql":
res = render_gql(self.query)
else:
raise f"Unknown query url: {self.url}"
res = "void $ " + res
return f"""-- from: {self.file}
it "{self.desc}" \\testEnvironment -> do
{textwrap.indent(res, ' ')}"""
class PostSpec(Spec):
"""
Specs that post something to HGE and make assertions against status code and
response.
"""
def __init__(self, desc, file, url, query, expected_status, expected_response):
self.desc = desc
self.file = file
self.url = url
self.query = query
self.expected_status = expected_status
self.expected_response = expected_response
def render(self):
actual = ""
if self.url == "/v2/query":
actual = render_v2query(self.expected_status, self.query)
elif self.url == "/v1/graphql":
actual = render_gql(self.query)
else:
raise f"Unknown query url: {self.url}"
return f"""-- from: {self.file}
it "{self.desc}" \\testEnvironment -> do
let expected :: Yaml.Value
expected =
[interpolateYaml|
{textwrap.indent(self.expected_response, ' ')}
|]
actual :: IO Yaml.Value
actual =
{textwrap.indent(actual, ' ')}
actual `shouldBe` expected"""
tests_to_port = {} # Map classname Fixtures
def with_test(name):
if not name in tests_to_port.keys():
tests_to_port[name] = Fixtures()
return tests_to_port[name]
def write_tests_to_port():
global tests_to_port
os.makedirs("../lib/api-tests/src/Test/PortedFromPytest", exist_ok=True)
for name, fixtures in tests_to_port.items():
backends = fixtures.setups.keys()
# Rather than directly make a hspec-discover'ed spec we include ported specs manually
# for now.
# This enables us to bulk-update the ported specs if we make changes to this porting script.
#hspecified_name = name.removeprefix("Test") + "Spec"
hspecified_name = name
with open(f"../lib/api-tests/src/Test/PortedFromPytest/{hspecified_name}.hs", 'w') as ported_hs:
ported_hs.write("{-# OPTIONS_GHC -Wno-deprecations #-}\n")
ported_hs.write(f"""\
-- | GENERATED BY 'server/tests-py/PortToHaskell.py'.
-- Please avoid editing this file manually.
module Test.PortedFromPytest.{hspecified_name} (spec) where
import Data.Aeson qualified as Yaml
import Data.List.NonEmpty qualified as NE
""" + str.join('\n', [
f"import Harness.Backend.{backend} qualified as {backend}"
for backend in backends
]) +
"""
import Harness.GraphqlEngine qualified as GraphqlEngine
import Harness.PytestPortedCompat (compatSetup)
import Harness.Quoter.Graphql (graphql)
import Harness.Quoter.Yaml
import Harness.Test.Fixture qualified as Fixture
import Harness.TestEnvironment (TestEnvironment (..))
import Harness.Yaml (shouldReturnYaml)
import Hasura.Prelude
import Test.Hspec
"""
)
for backend, setup_values in fixtures.setups.items():
for setup in setup_values:
ported_hs.write(setup.render_define(backend) + '\n')
ported_hs.write(
f"""
fixture_{backend} :: Fixture.Fixture ()
fixture_{backend} =
(Fixture.fixture $ Fixture.Backend Fixture.{backend})""" +
"""
{ Fixture.setupTeardown = \\(testEnvironment, _) ->
[ Fixture.SetupAction
{ Fixture.setupAction = do""" + f"""
compatSetup testEnvironment Fixture.{backend}
""" + textwrap.indent(str.join('\n', map(lambda setup: setup.render_run(backend), setup_values)), 16*' ') + """,
Fixture.teardownAction = \\_ -> return ()
}
]
}
""")
ported_hs.write("""
spec :: SpecWith TestEnvironment
spec = Fixture.runSingleSetup (NE.fromList [""" + str.join(', ', ["fixture_" + backend for backend in backends]) + """]) tests
tests :: Fixture.Options -> SpecWith TestEnvironment
tests opts = do
let shouldBe :: IO Yaml.Value -> Yaml.Value -> IO ()
shouldBe = shouldReturnYaml opts
""")
for desc, specs in fixtures.specs.items():
ported_hs.write(f"""
describe "{desc}" do
""")
for spec in specs:
ported_hs.write('\n' + textwrap.indent(spec.render(), 4*' ') + '\n')
tests_to_port = {}