packages feed

mariadb-haskell-0.1.0.0: validation/hs/Validation/Common.hs

{-# LANGUAGE ScopedTypeVariables #-}
module Validation.Common where

import Control.Exception hiding (assert)
import Control.Monad
import qualified Data.ByteString.Char8 as BC
import Data.List (isInfixOf)
import Data.String (fromString)
import qualified Data.Text as T
import Data.Typeable
import Database.MySQL.Base
import qualified Database.MySQL.TLS as TLS
import System.Environment
import qualified System.IO.Streams as S
import System.Timeout

data Case = Case { caseId :: String, caseArea :: String, caseRun :: IO () }
data Unavailable = Unavailable String deriving (Show, Typeable)
instance Exception Unavailable
data Unsupported = Unsupported String deriving (Show, Typeable)
instance Exception Unsupported

assert :: Bool -> String -> IO ()
assert ok msg = unless ok (throwIO (userError msg))

equal :: (Eq a, Show a) => String -> a -> a -> IO ()
equal label expected actual = assert (expected == actual)
    (label ++ ": expected " ++ take 2000 (show expected) ++ ", got " ++ take 2000 (show actual))

env :: String -> String -> IO String
env name fallback = maybe fallback id <$> lookupEnv name

config :: IO ConnectInfo
config = do
    host <- env "MB_HOST" "127.0.0.1"
    port <- read <$> env "MB_PORT" "3306"
    db <- BC.pack <$> env "MB_DATABASE" "mariadb_haskell_validation"
    user <- BC.pack <$> env "MB_USER" "mariadb_haskell_test"
    pass <- BC.pack <$> env "MB_PASSWORD" ""
    pure defaultConnectInfoMB4 { ciHost=host, ciPort=port, ciDatabase=db, ciUser=user, ciPassword=pass }

socketPath :: IO FilePath
socketPath = env "MB_SOCKET" "/tmp/mysql.sock"

tlsConnect :: ConnectInfo -> String -> IO MySQLConn
tlsConnect ci name = do
    ca <- getEnv "MB_TLS_CA"
    params <- TLS.makeClientParams (TLS.CustomCAStore ca)
    TLS.connect ci (params, name)

quietClose :: MySQLConn -> IO ()
quietClose c = void (timeout 1000000 (close c)) `catch` \(_ :: SomeException) -> pure ()

withConnect :: IO MySQLConn -> (MySQLConn -> IO a) -> IO a
withConnect open = bracket open quietClose

withConn :: (MySQLConn -> IO a) -> IO a
withConn action = config >>= \ci -> withConnect (connect ci) action

withTLS :: (MySQLConn -> IO a) -> IO a
withTLS action = do
    ci <- config
    name <- env "MB_TLS_NAME" "localhost"
    withConnect (tlsConnect ci name) action

sql :: String -> Query
sql = fromString

exec :: MySQLConn -> String -> IO ()
exec c = void . execute_ c . sql

rows :: MySQLConn -> String -> IO [[MySQLValue]]
rows c q = query_ c (sql q) >>= S.toList . snd

preparedRows :: MySQLConn -> String -> [MySQLValue] -> IO [[MySQLValue]]
preparedRows c q ps = bracket (prepareStmt c (sql q)) (closeStmt c) $ \st ->
    queryStmt c st ps >>= S.toList . snd

number :: MySQLValue -> Integer
number v = case v of
    MySQLInt8U x -> fromIntegral x
    MySQLInt8 x -> fromIntegral x
    MySQLInt16U x -> fromIntegral x
    MySQLInt16 x -> fromIntegral x
    MySQLInt32U x -> fromIntegral x
    MySQLInt32 x -> fromIntegral x
    MySQLInt64U x -> fromIntegral x
    MySQLInt64 x -> fromIntegral x
    MySQLDecimal x -> truncate x
    MySQLFloat x -> truncate x
    MySQLDouble x -> truncate x
    _ -> error ("not a numeric value: " ++ show v)

scalar :: MySQLConn -> String -> IO MySQLValue
scalar c q = do
    result <- rows c q
    case result of
        [[x]] -> pure x
        _ -> throwIO (userError ("expected one cell: " ++ take 500 (show result)))

scalarInt :: MySQLConn -> String -> IO Integer
scalarInt c q = number <$> scalar c q

textOf :: MySQLValue -> T.Text
textOf (MySQLText t) = t
textOf (MySQLBytes b) = T.pack (BC.unpack b)
textOf x = error ("not text: " ++ show x)

assertQuery :: String -> String -> [[Integer]] -> Case
assertQuery ident queryText expected = Case ident "sql" $ withConn $ \c -> do
    actual <- map (map number) <$> rows c queryText
    equal queryText expected actual

expectERR :: [Int] -> IO a -> IO ()
expectERR codes action = do
    result <- try action
    case result of
        Left (ERRException e) -> assert (null codes || fromIntegral (errCode e) `elem` codes)
            ("unexpected server error " ++ show e)
        Right _ -> throwIO (userError "expected a server ERR packet")

expectRejection :: IO a -> IO ()
expectRejection action = do
    result <- try action
    case result of
        Left (_ :: SomeException) -> pure ()
        Right _ -> throwIO (userError "operation unexpectedly succeeded")

healthy :: MySQLConn -> IO ()
healthy c = scalarInt c "SELECT 937" >>= equal "following query" 937

preflight :: IO ()
preflight = withConn $ \c -> do
    version <- T.unpack . textOf <$> scalar c "SELECT VERSION()"
    assert ("MariaDB" `isInfixOf` version) ("expected MariaDB, got " ++ version)
    ci <- config
    assert (ciDatabase ci == "mariadb_haskell_validation") "refusing to run outside the reserved validation database"
    maxPacket <- scalarInt c "SELECT @@max_allowed_packet"
    assert (maxPacket >= 300*1024*1024) "set max_allowed_packet >= 300 MiB for the inherited suite"
    equal "row binlog required" "ROW" . textOf =<< scalar c "SELECT @@binlog_format"
    equal "binary logging required" 1 =<< scalarInt c "SELECT @@log_bin"
    equal "server session timezone" "+00:00" . textOf =<< scalar c "SELECT @@time_zone"
    putStrLn ("MariaDB preflight passed: " ++ version)