packages feed

webdriver-precore-0.0.0.2: test/JSONParsingTest.hs

module JSONParsingTest where

import Data.Aeson (ToJSON (toJSON), Value (..), decode, encode)
import Data.Aeson.KeyMap qualified as KM

import Data.Bool (Bool, (&&), (||))
import Data.Enum (Bounded (minBound), Enum, maxBound)
import Data.Foldable (all, null)
import Data.Function (($), (.), id)
import Data.Functor ((<$>))
import Data.Map.Strict qualified as M
import Data.Maybe (Maybe (..), isNothing)
import Data.String (String)
import Data.Text (Text, unpack)
import Data.Text qualified as T
import GHC.Base (Applicative (..), Bool (..), Eq (..), Int, const, Functor (..))
import GHC.Data.Maybe (maybe)
import GHC.Float (Double)
import GHC.IO (FilePath)
import Test.Falsify.Generator as G
  ( Gen,
    bool,
    frequency,
    inRange,
    list,
  )
import Test.Falsify.Predicate (dot, expect, fn, (.$))
import Test.Falsify.Range as R (between, enum)
import Test.Tasty (TestTree)
import Test.Tasty.Falsify
  ( ExpectFailure (DontExpectFailure),
    Property,
    TestOptions (..),
    Verbose (Verbose),
    assert,
    gen,
    info,
    testPropertyWith,
  )
import Text.Show.Pretty (ppShow)
import WebDriverPreCore
  ( Capabilities (..),
    DeviceMetrics (..),
    LogLevel (..),
    MobileEmulation (..),
    PerfLoggingPrefs (..),
    Proxy (..),
    SocksProxy (..),
    Timeouts (..),
    VendorSpecific (..), LogSettings (MkLogSettings)
  )
import WebDriverPreCore.Internal.Utils (jsonToText)
import GHC.Real (Integral, Fractional (..), fromIntegral)
import Data.Bits (FiniteBits)
import GHC.Num (Num(..))

genMaybe :: G.Gen a -> G.Gen (Maybe a)
genMaybe gen' =
  G.frequency
    [ (1, pure Nothing),
      (3, Just <$> gen')
    ]

genOneOf :: forall a. (Enum a, Bounded a) => G.Gen a
genOneOf = G.inRange $ R.enum (minBound @a, maxBound @a)

genMEnum :: forall a. (Enum a, Bounded a) => G.Gen (Maybe a)
genMEnum = genMaybe $ genOneOf @a

genTextValueMap :: G.Gen (M.Map Text Value)
genTextValueMap =
  M.fromList
    <$> G.list
      (R.between (0, 4))
      ((,) <$> genText <*> genValue)
  where
    genValue :: G.Gen Value
    genValue =
      G.frequency
        [ (1, pure $ toJSON False),
          (1, pure $ toJSON ""),
          (1, pure $ toJSON 0),
          (1, pure $ toJSON 1),
          (1, pure $ toJSON 2),
          (1, pure $ toJSON 3),
          (1, pure $ toJSON 4),
          (1, pure $ toJSON 5),
          (1, pure $ toJSON 6),
          (1, pure $ toJSON 7),
          (1, pure $ toJSON 8),
          (1, pure $ toJSON 9)
        ]

genDeviseMetrics :: Gen DeviceMetrics
genDeviseMetrics = do
  width <- genInt 0 10000
  height <- genInt 0 10000
  pixelRatio <- genDouble 0.0 10000.0
  touch <- genBool
  pure $ MkDeviceMetrics {..}

genMobileEmulation :: Gen MobileEmulation
genMobileEmulation = MkMobileEmulation <$> genMaybeText <*> genMaybe genDeviseMetrics <*> genMaybeText

genPerfLoggingPrefs :: G.Gen PerfLoggingPrefs
genPerfLoggingPrefs = do
  enableNetwork <- genMaybeBool
  enablePage <- genMaybeBool
  enableTimeline <- genMaybeBool
  traceCategories <- genMaybeText
  bufferUsageReportingInterval <- genMaybeInt
  pure $ MkPerfLoggingPrefs {..}

genVendorSpecific :: G.Gen VendorSpecific
genVendorSpecific =
  G.frequency
    [ ( 1,
        -- edge and chrome options are the same at this stage
        ChromeOptions
          <$> genMaybeTextList
            <*> genMaybeText
            <*> genMaybeTextList
            <*> genMaybe genTextValueMap
            <*> genMaybe genMobileEmulation
            <*> genMaybe genTextValueMap
            <*> genMaybeBool
            <*> genMaybe genText
            <*> genMaybeTextList
            <*> genMaybe genFilePath
            <*> genMaybe genPerfLoggingPrefs
            <*> genMaybeTextList
      ),
      ( 1,
        EdgeOptions
          <$> genMaybeTextList
            <*> genMaybeText
            <*> genMaybeTextList
            <*> genMaybe genTextValueMap
            <*> genMaybe genMobileEmulation
            <*> genMaybe genTextValueMap
            <*> genMaybeBool
            <*> genMaybe genText
            <*> genMaybeTextList
            <*> genMaybe genFilePath
            <*> genMaybe genPerfLoggingPrefs
            <*> genMaybeTextList
      ),
      (1, FirefoxOptions <$> genMaybeTextList <*> genMaybeText <*> genMaybeText <*> genMaybe genLogSettings),
      (1, SafariOptions <$> genMaybeBool <*> genMaybeBool)
    ]

genLogLevel :: Gen LogLevel
genLogLevel =
  G.frequency
    [ (1, pure Trace),
      (1, pure Debug),
      (1, pure Info),
      (1, pure Warning),
      (1, pure Error),
      (1, pure Fatal),
      (1, pure Off)
    ]

genLogSettings :: G.Gen LogSettings
genLogSettings = do
  level <- genLogLevel
  pure $ MkLogSettings level

genBool :: G.Gen Bool
genBool = bool False

genMaybeBool :: G.Gen (Maybe Bool)
genMaybeBool = genMaybe genBool

genMaybeTimeout :: G.Gen (Maybe Int)
genMaybeTimeout = genMaybeInt

genMaybeInt :: G.Gen (Maybe Int)
genMaybeInt = genMaybe $ genInt 0 10000

genTimeouts :: G.Gen Timeouts
genTimeouts =
  MkTimeouts
    <$> genMaybeTimeout
      <*> genMaybeTimeout
      <*> genMaybeTimeout

genText :: G.Gen Text
genText =
  G.frequency
    [ (1, pure ""),
      (1, pure "Awd34rtf"),
      (1, pure "f"),
      (1, pure "sfdsds"),
      (1, pure "5"),
      (1, pure "ttttttttttttt"),
      (1, pure "a"),
      (1, pure "zzzzz"),
      (1, pure "rr"),
      (1, pure "aa"),
      (1, pure "pppp")
    ]

genFilePath :: G.Gen FilePath
genFilePath =
  G.frequency
    [ (1, pure "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe"),
      (1, pure "C:\\Program Files\\Mozilla Firefox\\firefox.exe"),
      (1, pure "C:\\Program Files\\Safari\\Safari.exe"),
      (1, pure "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe"),
      (1, pure "C:\\Program Files (x86)\\Internet Explorer\\iexplore.exe"),
      (1, pure "C:\\Program Files (x86)\\Opera Software\\Opera Stable\\opera.exe")
    ]

genTextList :: G.Gen [Text]
genTextList = list (R.between (0, 4)) genText

genMaybeTextList :: G.Gen (Maybe [Text])
genMaybeTextList = genMaybe genTextList

genMaybeText :: G.Gen (Maybe Text)
genMaybeText = genMaybe genText

genManualProxy :: G.Gen Proxy
genManualProxy =
  Manual
    <$> genMaybeText
      <*> genMaybeText
      <*> genMaybeText
      <*> genMaybe genSocksProxy
      <*> (genMaybe genTextList)


genFromRange :: (Integral a,  FiniteBits a) => a -> a -> Gen a
genFromRange lb ub = G.inRange $ R.between (lb, ub)

genInt :: Int -> Int -> G.Gen Int
genInt = genFromRange @Int

genDouble :: Double -> Double -> G.Gen Double
genDouble lb ub = do
  -- Generate an integer in [0..1000000].
  bits <- G.inRange (R.between (0, 1000000 :: Int))
  -- Convert bits to a fraction in [0..1], then interpolate between lb and ub.
  let fraction = fromIntegral bits / 1000000
  pure $ lb + fraction * (ub - lb)

genSocksProxy :: G.Gen SocksProxy
genSocksProxy = MkSocksProxy <$> genText <*> genInt 1 7

genProxy :: G.Gen Proxy
genProxy =
  G.frequency
    [ (1, pure Direct),
      (1, genManualProxy),
      (1, pure AutoDetect),
      (1, pure System),
      (1, Pac <$> genText)
    ]

-- Generate random Capabilities
genCapabilities :: G.Gen Capabilities
genCapabilities = do
  browserName <- genMEnum
  browserVersion <- genMaybe genText
  platformName <- genMEnum
  strictFileInteractability <- genMaybe genBool
  unhandledPromptBehavior <- genMEnum
  acceptInsecureCerts <- genMaybe genBool
  pageLoadStrategy <- genMEnum
  proxy <- genMaybe genProxy
  timeouts <- genMaybe genTimeouts
  vendorSpecific <- genMaybe genVendorSpecific
  pure MkCapabilities {..}

options :: TestOptions
options =
  TestOptions
    { expectFailure = DontExpectFailure,
      overrideVerbose = if wantLogging then Just Verbose else Nothing,
      overrideMaxShrinks = Nothing,
      overrideNumTests = Just 1000,
      overrideMaxRatio = Nothing
    }


subEmt :: forall a. (a -> Bool) -> Maybe a -> Maybe a
subEmt f = maybe Nothing (\x -> if f x then Nothing else Just x)

subEmptyTxt :: Maybe Text -> Maybe Text
subEmptyTxt = subEmt T.null

subEmptyList :: Maybe String -> Maybe String
subEmptyList = subEmt null 

subEmptyTxtLst :: Maybe [Text] -> Maybe [Text]
subEmptyTxtLst = subEmt emptyTextList


emptyVal :: Value -> Bool
emptyVal = \case
  Bool _ -> False
  String t -> T.null t
  Number _ -> False
  Array arr -> null arr
  Object o -> KM.null o
  Null -> True

subEmtyValueMap :: Maybe (M.Map Text Value) -> Maybe (M.Map Text Value)
subEmtyValueMap = fmap M.fromList . subEmt emptyValList . fmap M.toList
 where
  emptyValList :: [(Text, Value)] -> Bool
  emptyValList ml = null ml || all (\(t, v) -> T.null t || emptyVal v) ml

-- | Substitutes empty fields with Nothing
subEmptyVendor :: Maybe VendorSpecific -> Maybe VendorSpecific
subEmptyVendor = subEmt allPropsNothing . fmap subEmptFields
 where 
  allPropsNothing :: VendorSpecific -> Bool
  allPropsNothing = \case 
    ChromeOptions p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 -> allTrue [n p1, n p2, n p3, n p4, n p5, n p6, n p7, n p8, n p9, n p10, n p11, n p12]
    EdgeOptions p1 p2 p3 p4 p5 p6 p7 p8 p9 p10 p11 p12 -> allTrue [n p1, n p2, n p3, n p4, n p5, n p6, n p7, n p8, n p9, n p10, n p11, n p12]
    FirefoxOptions p1 p2 p3 p4 -> allTrue [n p1, n p2, n p3, n p4]
    SafariOptions p1 p2-> allTrue [n p1, n p2]
    
    where 
      n = isNothing
      allTrue :: [Bool] -> Bool
      allTrue = all id

  subEmptFields :: VendorSpecific -> VendorSpecific
  subEmptFields = \case
    ChromeOptions {..} ->
      ChromeOptions
        { chromeArgs = subEmptyTxtLst chromeArgs,
          chromeBinary = subEmptyTxt chromeBinary,
          chromeExtensions = subEmptyTxtLst chromeExtensions,
          chromeLocalState = subEmtyValueMap chromeLocalState,
          chromePrefs = subEmtyValueMap chromePrefs,
          chromeDetach,
          chromeDebuggerAddress = subEmptyTxt chromeDebuggerAddress,
          chromeExcludeSwitches = subEmptyTxtLst chromeExcludeSwitches,
          chromeMobileEmulation = subEmptyMobileEmulation chromeMobileEmulation,
          chromeMinidumpPath = subEmptyList chromeMinidumpPath,
          chromePerfLoggingPrefs = subEmptyPerfLoggingPrefs chromePerfLoggingPrefs,
          chromeWindowTypes = subEmptyTxtLst chromeWindowTypes
        }
    EdgeOptions {..} ->
      EdgeOptions
        { edgeArgs = subEmptyTxtLst edgeArgs,
          edgeBinary = subEmptyTxt edgeBinary,
          edgeExtensions = subEmptyTxtLst edgeExtensions,
          edgeLocalState = subEmtyValueMap edgeLocalState,
          edgePrefs = subEmtyValueMap edgePrefs,
          edgeDetach,
          edgeDebuggerAddress = subEmptyTxt edgeDebuggerAddress,
          edgeExcludeSwitches = subEmptyTxtLst edgeExcludeSwitches,
          edgeMobileEmulation = subEmptyMobileEmulation edgeMobileEmulation,
          edgeMinidumpPath = subEmptyList edgeMinidumpPath,
          edgePerfLoggingPrefs = subEmptyPerfLoggingPrefs edgePerfLoggingPrefs,
          edgeWindowTypes = subEmptyTxtLst edgeWindowTypes
        }
    FirefoxOptions {..} ->
      FirefoxOptions
        { firefoxArgs = subEmptyTxtLst firefoxArgs,
          firefoxBinary = subEmptyTxt firefoxBinary,
          firefoxProfile = subEmptyTxt firefoxProfile,
          firefoxLog
        }
    s@SafariOptions {} -> s
    where
      subEmptyMobileEmulation :: Maybe MobileEmulation -> Maybe MobileEmulation
      subEmptyMobileEmulation = subEmt emptyMobileEmulation

      emptyMobileEmulation :: MobileEmulation -> Bool
      emptyMobileEmulation = \case
        MkMobileEmulation {deviceName, deviceMetrics, userAgent} ->
          emptyTxt deviceName && isNothing deviceMetrics && emptyTxt userAgent

      subEmptyPerfLoggingPrefs :: Maybe PerfLoggingPrefs -> Maybe PerfLoggingPrefs
      subEmptyPerfLoggingPrefs = subEmt emptyPerfLoggingPrefs

      emptyPerfLoggingPrefs :: PerfLoggingPrefs -> Bool
      emptyPerfLoggingPrefs = \case
        MkPerfLoggingPrefs {enableNetwork, enablePage, enableTimeline, traceCategories, bufferUsageReportingInterval} ->
          emptyTxt traceCategories && isNothing bufferUsageReportingInterval && isNothing enableNetwork && isNothing enablePage && isNothing enableTimeline


emptyTxt :: Maybe Text -> Bool
emptyTxt = maybe True T.null

emptyList :: Maybe [a] -> Bool
emptyList = maybe True null

emptyTextList :: [Text] -> Bool
emptyTextList ml = null ml || (all T.null) ml


isNothingProxy :: Proxy -> Bool
isNothingProxy = \case
  Direct -> False
  Manual ftpProxy httpProxy sslProxy noProxy socksProxy-> 
    all isNothing [ftpProxy, httpProxy, sslProxy] && isNothing noProxy && isNothing socksProxy
  AutoDetect -> False
  Pac url -> T.null url
  System -> False

subEmptyProxy :: Maybe Proxy -> Maybe Proxy
subEmptyProxy p = subEmt isNothingProxy $ subEmptProps <$> p
  where
    subEmptProps :: Proxy -> Proxy
    subEmptProps p' = case p' of
      Manual {..} ->
        Manual
          { ftpProxy = subEmptyTxt ftpProxy,
            httpProxy = subEmptyTxt httpProxy,
            sslProxy = subEmptyTxt sslProxy,
            noProxy = subEmptyTxtLst noProxy,
            socksProxy
          }
      Direct -> p'
      AutoDetect -> p'
      Pac {} -> p'
      System -> p'


subEmptyTimeouts :: Maybe Timeouts -> Maybe Timeouts
subEmptyTimeouts = subEmt isNothingTimeouts 
  where
    isNothingTimeouts :: Timeouts -> Bool
    isNothingTimeouts = \case
      MkTimeouts {..} ->
        all isNothing [implicit, pageLoad, script]

emptyFieldsToNothing :: Capabilities -> Capabilities
emptyFieldsToNothing caps@MkCapabilities {..} = caps {
  browserVersion = subEmptyTxt browserVersion,
  vendorSpecific = subEmptyVendor vendorSpecific,
  platformName,
  acceptInsecureCerts,
  pageLoadStrategy,
  proxy = subEmptyProxy proxy,
  timeouts = subEmptyTimeouts timeouts,
  strictFileInteractability,
  unhandledPromptBehavior
 }

subEmptyCapabilities :: Capabilities -> Capabilities
subEmptyCapabilities caps@MkCapabilities {..} = caps {
  browserVersion = subEmptyTxt browserVersion,
  strictFileInteractability = strictFileInteractability,
  acceptInsecureCerts = acceptInsecureCerts,
  proxy = subEmptyProxy proxy,
  timeouts = subEmptyTimeouts timeouts,
  vendorSpecific = subEmptyVendor vendorSpecific
 }


jsonEq :: Capabilities -> Maybe Capabilities -> Bool
jsonEq expected =
  maybe
    False
    (\actual -> subEmptyCapabilities expected == subEmptyCapabilities actual)

wantLogging :: Bool
wantLogging = False

log :: String -> Property ()
log = if wantLogging then info else const $ pure ()

test_round_trip :: TestTree
test_round_trip = testPropertyWith options "Roundtrip Capabilities Parsing" $ do
  c <- gen genCapabilities
  let encoded = encode c
      showEncode = jsonToText <$> decode @Value encoded
      decoded = decode @Capabilities encoded
      asExpected = jsonEq c

  log ""
  log "Initial Capabilities:"
  log $ ppShow c
  log ""
  log "Encoded Capabilities:"
  log $ maybe "Nothing" unpack showEncode
  log ""
  log "Decoded Capabilities:"
  log $ maybe "Nothing" ppShow decoded
  assert $ expect True `dot` fn ("matches encoded", asExpected) .$ ("decoded", decoded)