Skip to content

Commit

Permalink
GDW Agent Test Suite [GDW-79]
Browse files Browse the repository at this point in the history
PR-URL: hasura/graphql-engine-mono#4175
GitOrigin-RevId: d37d7d131597af6b9cca6bd773c8dbbce8719ca5
  • Loading branch information
daniel-chambers authored and hasura-bot committed Apr 10, 2022
1 parent e9436c5 commit 38c41b2
Show file tree
Hide file tree
Showing 18 changed files with 3,641 additions and 8 deletions.
9 changes: 7 additions & 2 deletions cabal.project.freeze
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Cabal ==3.2.1.0,
Cabal -bundled-binary-generic,
any.Diff ==0.4.1,
any.HTTP ==4000.3.16,
HTTP -conduit10 -mtl1 +network-uri -warn-as-error -warp-tests,
any.HUnit ==1.6.2.0,
Expand Down Expand Up @@ -43,7 +44,7 @@ constraints: any.Cabal ==3.2.1.0,
any.authenticate-oauth ==1.7,
any.auto-update ==0.1.6,
any.autodocodec ==0.0.1.0,
any.autodocodec-openapi3 ==0.1.0.0,
any.autodocodec-openapi3 ==0.2.0.0,
any.barbies ==2.0.3.1,
any.base ==4.14.3.0,
any.base-compat ==0.11.2,
Expand Down Expand Up @@ -181,11 +182,13 @@ constraints: any.Cabal ==3.2.1.0,
any.hpc ==0.6.1.0,
any.hsc2hs ==0.68.8,
hsc2hs -in-ghc-tree,
any.hscolour ==1.24.4,
any.hspec ==2.9.4,
any.hspec-core ==2.9.4,
any.hspec-discover ==2.9.4,
any.hspec-expectations ==0.8.2,
any.hspec-expectations-lifted ==0.10.0,
any.hspec-expectations-pretty-diff ==0.7.2.6,
any.hspec-hedgehog ==0.0.1.2,
any.hspec-wai ==0.11.0,
any.hspec-wai-json ==0.11.0,
Expand Down Expand Up @@ -264,6 +267,7 @@ constraints: any.Cabal ==3.2.1.0,
any.network-info ==0.2.1,
any.network-ip ==0.3.0.3,
any.network-uri ==2.6.4.1,
any.nicify-lib ==1.0.1,
any.odbc ==0.2.6,
any.old-locale ==1.0.0.7,
any.old-time ==1.1.0.3,
Expand Down Expand Up @@ -410,6 +414,7 @@ constraints: any.Cabal ==3.2.1.0,
any.type-hint ==0.1,
any.typed-process ==0.2.8.0,
any.unbounded-delays ==0.1.1.1,
any.unicode-show ==0.1.1.0,
any.unix ==2.7.2.2,
any.unix-compat ==0.5.4,
unix-compat -old-time,
Expand Down Expand Up @@ -464,4 +469,4 @@ constraints: any.Cabal ==3.2.1.0,
yaml +no-examples +no-exe,
any.zlib ==0.6.2.3,
zlib -bundled-c-zlib -non-blocking-ffi -pkg-config
index-state: hackage.haskell.org 2022-02-16T22:54:12Z
index-state: hackage.haskell.org 2022-04-06T04:57:40Z
45 changes: 45 additions & 0 deletions server/graphql-engine.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -1103,3 +1103,48 @@ test-suite tests-hspec
Test.WhereSpec
Test.RunSQLSpec
Test.InsertCheckPermissionSpec

test-suite tests-gdw-api
import: common-all, common-exe
type: exitcode-stdio-1.0
build-depends:
, aeson
, autodocodec
, autodocodec-openapi3
, base
, bytestring
, deepseq
, file-embed
, gdw-api
, hashable
, hspec
, hspec-core
, hspec-expectations-pretty-diff
, http-client
, lens
, lens-aeson
, mtl
, network-uri
, openapi3
, optparse-applicative
, scientific
, servant
, servant-client
, servant-client-core
, servant-openapi3
, text
, unordered-containers
, vector
hs-source-dirs: tests-gdw-api
-- Turning off optimizations is intentional; tests aren't
-- performance sensitive and waiting for compilation is a problem.
ghc-options: -Wall -O0 -threaded
main-is: Main.hs
other-modules:
Command
, Paths_graphql_engine
, Test.Data
, Test.QuerySpec
, Test.QuerySpec.BasicSpec
, Test.QuerySpec.RelationshipsSpec
, Test.SchemaSpec
7 changes: 7 additions & 0 deletions server/src-gdw-api/Hasura/Backends/DataWrapper/API.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ module Hasura.Backends.DataWrapper.API
SchemaApi,
QueryApi,
openApiSchema,
Routes (..),
apiClient,
)
where

Expand All @@ -13,6 +15,7 @@ import Data.OpenApi (OpenApi)
import Hasura.Backends.DataWrapper.API.V0.API as V0
import Servant.API
import Servant.API.Generic
import Servant.Client (Client, ClientM, client)
import Servant.OpenApi

--------------------------------------------------------------------------------
Expand Down Expand Up @@ -42,3 +45,7 @@ type Api = SchemaApi :<|> QueryApi
-- | Provide an OpenApi 3.0 schema for the API
openApiSchema :: OpenApi
openApiSchema = toOpenApi (Proxy :: Proxy Api)

apiClient :: Client ClientM (NamedRoutes Routes)
apiClient =
client (Proxy @(NamedRoutes Routes))
Original file line number Diff line number Diff line change
Expand Up @@ -62,15 +62,15 @@ instance HasCodec Query where
--------------------------------------------------------------------------------

data RelField = RelField
{ fieldMapping :: M.HashMap PrimaryKey ForeignKey,
{ columnMapping :: M.HashMap PrimaryKey ForeignKey,
query :: Query
}
deriving stock (Eq, Ord, Show, Generic, Data)

instance HasObjectCodec RelField where
objectCodec =
RelField
<$> requiredField "field_mapping" "Mapping from local fields to remote fields" .= fieldMapping
<$> requiredField "column_mapping" "Mapping from local fields to remote fields" .= columnMapping
<*> requiredField "query" "Relationship query" .= query

--------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ instance HasCodec TableInfo where
codec =
object "TableInfo" $
TableInfo
<$> requiredField "table_name" "The name of the table" .= dtiName
<$> requiredField "name" "The name of the table" .= dtiName
<*> requiredField "columns" "The columns of the table" .= dtiColumns
<*> optionalFieldOrNull "primary_key" "The primary key of the table" .= dtiPrimaryKey
<*> optionalFieldOrNull "description" "Description of the table" .= dtiDescription
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ spec = do
(RelationshipField $ RelField fieldMapping query)
[aesonQQ|
{ "type": "relationship",
"field_mapping": {"id": "my_foreign_id"},
"column_mapping": {"id": "my_foreign_id"},
"query": {"fields": {}, "from": "my_table_name"}
}
|]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ spec = do
testToFromJSONToSchema
(TableInfo (TableName "my_table_name") [] Nothing Nothing)
[aesonQQ|
{ "table_name": "my_table_name",
{ "name": "my_table_name",
"columns": []
}
|]
Expand All @@ -37,7 +37,7 @@ spec = do
(Just "my description")
)
[aesonQQ|
{ "table_name": "my_table_name",
{ "name": "my_table_name",
"columns": [{"name": "id", "type": "string", "nullable": false}],
"primary_key": "id",
"description": "my description"
Expand Down
197 changes: 197 additions & 0 deletions server/tests-gdw-api/Command.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
{-# LANGUAGE TemplateHaskell #-}

module Command
( Command (..),
TestOptions (..),
AgentCapabilities (..),
parseCommandLine,
)
where

import Control.Arrow (left)
import Control.Lens (contains, modifying, use, (^.), _2)
import Control.Lens.TH (makeLenses)
import Control.Monad (when)
import Control.Monad.State (State, runState)
import Data.HashSet (HashSet)
import Data.HashSet qualified as HashSet
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Version (showVersion)
import Hasura.Backends.DataWrapper.API qualified as API
import Options.Applicative
import Paths_graphql_engine qualified as PackageInfo
import Servant.Client (BaseUrl, parseBaseUrl)
import Prelude

data Command
= Test TestOptions
| ExportOpenAPISpec

data TestOptions = TestOptions
{ _toAgentBaseUrl :: BaseUrl,
_toAgentCapabilities :: AgentCapabilities,
_toParallelDegree :: Maybe Int,
_toMatch :: Maybe String,
_toSkip :: Maybe String
}

data AgentCapabilities
= AutoDetect
| Explicit API.Capabilities

data CapabilitiesState = CapabilitiesState
{ _csRemainingCapabilities :: HashSet Text,
_csCapabilitiesEnquired :: HashSet Text
}

$(makeLenses ''CapabilitiesState)

parseCommandLine :: IO Command
parseCommandLine =
execParser $
info
(helper <*> version <*> commandParser)
( fullDesc
<> header "Hasura GraphQL Data Wrapper Agent Test Utility"
)

version :: Parser (a -> a)
version =
infoOption
displayText
( long "version"
<> short 'v'
<> help "Prints the version of the application and quits"
<> hidden
)
where
displayText = "Version " <> showVersion PackageInfo.version

commandParser :: Parser Command
commandParser =
subparser
(testCommand <> exportOpenApiSpecCommand)
where
testCommand =
command
"test"
( info
(helper <*> testCommandParser)
(progDesc "Executes a suite of tests against an agent to ensure its correct function")
)
exportOpenApiSpecCommand =
command
"export-openapi-spec"
( info
(helper <*> pure ExportOpenAPISpec)
(progDesc "Exports the OpenAPI specification of the GDW API agents must implement")
)

testOptionsParser :: Parser TestOptions
testOptionsParser =
TestOptions
<$> option
baseUrl
( long "agent-base-url"
<> short 'u'
<> metavar "URL"
<> help "The base URL of the GDW agent to be tested"
)
<*> agentCapabilitiesParser
<*> optional
( option
positiveNonZeroInt
( long "jobs"
<> short 'j'
<> metavar "INT"
<> help "Run at most N parallelizable tests simultaneously (default: number of available processors)"
)
)
<*> optional
( option
auto
( long "match"
<> short 'm'
<> metavar "PATTERN"
<> help "Only run tests that match given PATTERN"
)
)
<*> optional
( option
auto
( long "skip"
<> short 's'
<> metavar "PATTERN"
<> help "Skip tests that match given PATTERN"
)
)

testCommandParser :: Parser Command
testCommandParser = Test <$> testOptionsParser

baseUrl :: ReadM BaseUrl
baseUrl = eitherReader $ left show . parseBaseUrl

positiveNonZeroInt :: ReadM Int
positiveNonZeroInt =
auto >>= \int ->
if int <= 0 then readerError "Must be a positive, non-zero integer" else pure int

agentCapabilitiesParser :: Parser AgentCapabilities
agentCapabilitiesParser =
option
agentCapabilities
( long "capabilities"
<> short 'c'
<> metavar "CAPABILITIES"
<> value AutoDetect
<> help (Text.unpack helpText)
)
where
helpText =
"The capabilities that the agent has, to determine what tests to run. By default, they will be autodetected. The valid capabilities are: " <> allCapabilitiesText
allCapabilitiesText =
"[autodetect | none | " <> Text.intercalate "," (HashSet.toList allPossibleCapabilities) <> "]"

agentCapabilities :: ReadM AgentCapabilities
agentCapabilities =
str >>= \text -> do
let capabilities = HashSet.fromList $ Text.strip <$> Text.split (== ',') text
if HashSet.member "autodetect" capabilities
then
if HashSet.size capabilities == 1
then pure AutoDetect
else readerError "You can either autodetect capabilities or specify them manually, not both"
else
if HashSet.member "none" capabilities
then
if HashSet.size capabilities == 1
then pure . Explicit . fst $ readCapabilities mempty
else readerError "You cannot specify other capabilities when specifying none"
else Explicit <$> readExplicitCapabilities capabilities
where
readExplicitCapabilities :: HashSet Text -> ReadM API.Capabilities
readExplicitCapabilities providedCapabilities =
let (capabilities, CapabilitiesState {..}) = readCapabilities providedCapabilities
in if _csRemainingCapabilities /= mempty
then readerError . Text.unpack $ "Unknown capabilities: " <> Text.intercalate "," (HashSet.toList _csRemainingCapabilities)
else pure capabilities

readCapabilities :: HashSet Text -> (API.Capabilities, CapabilitiesState)
readCapabilities providedCapabilities =
flip runState (CapabilitiesState providedCapabilities mempty) $
API.Capabilities
<$> readCapability "relationships"

readCapability :: Text -> State CapabilitiesState Bool
readCapability capability = do
modifying csCapabilitiesEnquired $ HashSet.insert capability
hasCapability <- use $ csRemainingCapabilities . contains capability
when hasCapability $
modifying csRemainingCapabilities $ HashSet.delete capability
pure hasCapability

allPossibleCapabilities :: HashSet Text
allPossibleCapabilities =
readCapabilities mempty ^. _2 . csCapabilitiesEnquired
Loading

0 comments on commit 38c41b2

Please sign in to comment.