yamlet-1.0.0.0: tests/Yamlet/Test/Encode/Properties.hs
-- | The properties of the encoder on random values and syntax trees.
module Yamlet.Test.Encode.Properties
( propertyTests
) where
import Data.List qualified as L
import Data.Scientific qualified as Sci
import Data.Text qualified as T
import Test.Tasty
import Test.Tasty.QuickCheck
import Yamlet
import Yamlet.Syntax qualified as S
propertyTests :: TestTree
propertyTests =
testGroup
"properties"
[ -- The renderers differ only in rare cases, e.g. for a key that needs an
-- explicit entry. 10000 cases take about 0.2 s.
localOption (QuickCheckTests 10000) $ testProperty "fast renderer" prop_fastRenderer
, testProperty "fast renderer of several documents" prop_fastRendererAll
, localOption (QuickCheckTests 10000) $
testProperty "fast renderer of syntax trees" prop_fastRendererNodes
, testProperty "round trip" prop_roundTrip
, testProperty "syntax round trip" prop_syntaxRoundTrip
]
-- | The faster renderer of the encoder gives the same output as the renderer
-- of syntax trees.
prop_fastRenderer :: Doc -> Property
prop_fastRenderer (Doc n) =
encodeText n === S.renderSyntax S.defaultRenderOptions [S.document (toYaml n)]
-- | The same for several documents.
prop_fastRendererAll :: [Doc] -> Property
prop_fastRendererAll docs =
encodeAllText ns
=== S.renderSyntax S.defaultRenderOptions (map (S.document . toYaml) ns)
where
ns :: [Value]
ns = [n | Doc n <- docs]
-- | The same for syntax trees that the faster renderer takes, with what the
-- trees of values do not have: tags on keys and on collections, scalar keys
-- in each style and keys of more than 1024 characters.
prop_fastRendererNodes :: SimpleNode -> Property
prop_fastRendererNodes (SimpleNode n) =
encodeText n === S.renderSyntax S.defaultRenderOptions [S.document n]
-- | A tree without comments, anchors, aliases and flow collections, with
-- scalars on one line in the styles of the encoder.
newtype SimpleNode = SimpleNode S.Node
deriving stock (Show)
instance Arbitrary SimpleNode where
arbitrary = SimpleNode <$> sized genNode
where
genNode :: Int -> Gen S.Node
genNode size
| size <= 1 = genScalar
| otherwise =
frequency
[ (3, genScalar)
, (1, collection S.sequenceNode (genNode (size `div` 3)))
,
( 1
, collection
S.mappingNode
((,) <$> genNode (size `div` 3) <*> genNode (size `div` 3))
)
, (1, elements [S.sequenceNode [], S.mappingNode []] >>= withTag)
]
collection :: ([a] -> S.Node) -> Gen a -> Gen S.Node
collection node item = do
k <- choose (1, 3)
xs <- vectorOf k item
withTag (node xs)
genScalar :: Gen S.Node
genScalar = do
style <- elements [S.Plain, S.SingleQuoted, S.DoubleQuoted, S.Literal]
t <- genText
-- The faster renderer does not take an empty plain scalar.
withTag (S.scalarNode style (if style == S.Plain && T.null t then "x" else t))
withTag :: S.Node -> Gen S.Node
withTag n = do
tag <-
frequency
[ (4, pure S.NoTag)
, (1, S.Tag <$> elements ["!t", "xy", "tag:yaml.org,2002:str", "", "a#b"])
]
pure n {S.props = S.Props Nothing tag}
genText :: Gen T.Text
genText =
oneof
[ elements
[ ""
, " "
, " a"
, "\ta"
, "a\n"
, "\n"
, " lead\nx"
, "-"
, "a: b"
, "#"
, "yes"
, "x'y"
, "a\x2028b"
, "a\x01"
, "---"
, "a\n\n"
]
, T.pack <$> listOf (elements "ab :#\n\t'\"-")
, pure (T.replicate 1030 "k")
]
-- | Encoding a value and decoding the result gives the same value.
prop_roundTrip :: Doc -> Property
prop_roundTrip (Doc n) = readsBack (encodeText n) n
-- | Rendering the syntax tree of a value and decoding the result gives the
-- same value.
prop_syntaxRoundTrip :: Doc -> Property
prop_syntaxRoundTrip (Doc n) = readsBack output n
where
output :: T.Text
output = S.renderSyntax S.defaultRenderOptions [S.document (toYaml n)]
readsBack :: T.Text -> Value -> Property
readsBack output n = case decodeAllText @Value output of
Right [n'] -> counterexample (T.unpack output) $ n' === n
r -> counterexample (T.unpack output ++ "\n" ++ show r) False
newtype Doc = Doc Value
deriving stock (Show)
instance Arbitrary Doc where
arbitrary = Doc <$> sized genValue
genValue :: Int -> Gen Value
genValue size
| size <= 1 = genScalar
| otherwise =
frequency
[ (3, genScalar)
, (1, Sequence <$> genList)
, (1, Mapping <$> genEntries)
, (1, tagged <$> genScalar)
, (1, Tagged <$> genTag <*> (Sequence <$> genList))
]
where
genList :: Gen [Value]
genList = do
k <- choose (0, 4)
vectorOf k (genValue (size `div` 3))
genEntries :: Gen [(Value, Value)]
genEntries = do
k <- choose (0, 4)
keys <- L.nub <$> vectorOf k genKey
mapM (\key -> (key,) <$> genValue (size `div` 3)) keys
genKey :: Gen Value
genKey = frequency [(4, genScalar), (1, elements [Sequence [], Mapping []])]
-- A tag that is not a valid URI needs a %TAG directive.
genTag :: Gen T.Text
genTag = elements ["!custom", "xy"]
tagged :: Value -> Value
tagged v = case v of
String _ -> Tagged "!custom" v
_ -> v
genScalar :: Gen Value
genScalar =
oneof
[ pure Null
, Bool <$> arbitrary
, Int <$> arbitrary
, Float . Finite <$> (Sci.scientific <$> arbitrary <*> chooseInt (-30, 30))
, Float <$> elements [NegativeZero, Infinity, NegativeInfinity, NaN]
, String <$> genText
]
genText :: Gen T.Text
genText =
oneof
[ elements tricky
, T.pack <$> listOf genChar
, T.intercalate "\n" <$> listOf (T.pack <$> listOf genChar)
]
where
tricky :: [T.Text]
tricky =
[ ""
, " "
, "-"
, "- a"
, "? a"
, ": a"
, "a: b"
, "a:b"
, "#"
, "a #b"
, "true"
, "null"
, "1"
, "0x1F"
, "0o7"
, ".5"
, "~"
, "---"
, "..."
, "@x"
, "`x"
, "foo\n"
, "\nfoo"
, " lead"
, "trail "
, "a\n\nb\n\n"
, "\t"
, "é"
, "\x85"
, "\x2028"
, "\xFEFF"
, "\n"
, "\n\n"
, " \n"
, "a\n "
, "|"
, ">"
, "%x"
, "&a"
, "*a"
, "!a"
, "{}"
, "[]"
, "a, b"
, "key:"
, "'quoted'"
, "\"dq\""
, "\r\n"
, "\\"
, "a\tb"
]
genChar :: Gen Char
genChar =
frequency
[ (10, elements "abc xyz-:#,[]{}'\"!&*?|>%@`\\")
, (2, elements "\t\r\x85\xA0\x2028\xFEFF\x01\x7F")
, (1, arbitrary)
]