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 = {}