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)