graphql-engine/server/tests-py/PortToHaskell.py
Tom Harding 4885a3fd9a Import J, not Aeson, A, JSON, Yaml...
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8948
GitOrigin-RevId: d70c4a50b94ffe7d42a1fb1017051d351f236acc
2023-04-26 17:30:24 +00:00

290 lines
8.1 KiB
Python

import os
import ruamel.yaml as yaml
import textwrap
from abc import ABC, abstractmethod
# https://yaml.readthedocs.io/en/latest/example.html#output-of-dump-as-a-string
def dump_to_string(v):
import io
s = io.StringIO()
yaml.YAML().dump(v, stream=s)
return s.getvalue()
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, headers):
if headers is None:
return f"""\
GraphqlEngine.postGraphql
testEnvironment
[graphql|
{textwrap.indent(query, ' ')}
|]"""
else:
return f"""\
GraphqlEngine.postGraphqlWithHeaders
testEnvironment
{textwrap.indent(render_headers(headers), ' ')}
[graphql|
{textwrap.indent(query, ' ')}
|]"""
def render_v2query(expected_status, query, headers):
if headers is None:
return f"""\
GraphqlEngine.postV2Query
{expected_status}
testEnvironment
[interpolateYaml|
{textwrap.indent(dump_to_string(query), ' ')}
|]"""
else:
return f"""
GraphqlEngine.postV2QueryWithHeaders
{expected_status}
{textwrap.indent(render_headers(headers), ' ')}
testEnvironment
[interpolateYaml|
{textwrap.indent(dump_to_string(query), ' ')}
|]"""
def render_v1query(expected_status, query, headers):
if headers is None:
return f"""\
GraphqlEngine.postV1Query
{expected_status}
testEnvironment
[interpolateYaml|
{textwrap.indent(dump_to_string(query), ' ')}
|]"""
else:
return f"""\
GraphqlEngine.postV1QueryWithHeaders
{expected_status}
{textwrap.indent(render_headers(headers), ' ')}
testEnvironment
[interpolateYaml|
{textwrap.indent(dump_to_string(query), ' ')}
|]"""
def render_v1metadata(expected_status, query, headers):
if headers is None:
return f"""\
GraphqlEngine.postMetadataWithStatus
{expected_status}
testEnvironment
[interpolateYaml|
{textwrap.indent(dump_to_string(query), ' ')}
|]"""
else:
return f"""\
GraphqlEngine.postMetadataWithHeaders
testEnvironment
{textwrap.indent(render_headers(headers), ' ')}
[interpolateYaml|
{textwrap.indent(dump_to_string(query), ' ')}
|]"""
def render_headers(headers):
res = "(\n"
for key, val in headers.items():
res += f' ("{key}", "{val}"):\n'
res += "[])"
return res
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} :: J.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/query":
res = f"GraphqlEngine.postV1Query 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 Exception(f"Unknown query url: {self.url}, in file {self.original_file}")
return res
class Spec(ABC):
@abstractmethod
def render(self):
pass
class PostSpec(Spec):
"""
Specs that post something to HGE and make assertions against status code and
response.
"""
def __init__(self, desc, file, url, headers, query, expected_status, expected_response):
self.desc = desc
self.file = file
self.url = url
self.headers = headers
self.query = query
self.expected_status = expected_status
self.expected_response = expected_response
def render(self):
actual = ""
if self.url == "/v1/query":
actual = render_v1query(self.expected_status, self.query, self.headers)
elif self.url == "/v2/query":
actual = render_v2query(self.expected_status, self.query, self.headers)
elif self.url == "/v1/graphql":
actual = render_gql(self.query, self.headers)
elif self.url == "/v1/metadata":
actual = render_v1metadata(self.expected_status, self.query, self.headers)
else:
raise Exception(f"Unknown query url: {self.url}, in file {self.file}")
head =f"""-- from: {self.file}
it "{self.desc}" \\testEnvironment -> do"""
expected = ""
assertion = ""
if self.expected_response is not None:
expected = f"""
let expected :: J.Value
expected =
[interpolateYaml|
{textwrap.indent(dump_to_string(self.expected_response), ' ')}
|]
"""
assertion = "actual `shouldBe` expected"
else:
assertion = "void actual"
return f"""{head}
{expected}
let actual :: IO J.Value
actual =
{textwrap.indent(actual, ' ')}
{assertion}"""
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("{- HLINT ignore -}\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 J
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 (GlobalTestEnvironment, 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 GlobalTestEnvironment
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 J.Value -> J.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 = {}