2{-# LANGUAGE PackageImports #-}
3{-# LANGUAGE ScopedTypeVariables #-}
4{-# LANGUAGE OverloadedStrings #-}
5{-# LANGUAGE TemplateHaskell #-}
7module Data.YamlSpec (main, spec) where
9import qualified Text.Libyaml as Y
10import qualified Data.ByteString.Char8 as B8
12import Test.HUnit hiding (Test, path)
14import qualified Data.Conduit as C
15import qualified Control.Monad.Trans.Resource as C
16import qualified Data.Conduit.List as CL
19import Control.Exception (try, SomeException)
21import Data.Either.Compat
22import System.Directory (createDirectory, createDirectoryIfMissing)
23import Test.Mockery.Directory
25import qualified Data.Yaml as D
26import qualified Data.Yaml.Pretty as Pretty
27import Data.Yaml (object, array, (.=))
29import qualified Data.HashMap.Strict as M
30import qualified Data.Text as T
32import Data.Text (Text)
33import Data.Vector (Vector)
34import qualified Data.Vector as V
35import Data.HashMap.Strict (HashMap)
36import qualified Data.HashMap.Strict as HM
37import System.IO (hClose)
38import System.IO.Temp (withSystemTempFile)
40data TestJSON = TestJSON
43 , anArray :: Vector Text
44 , hash :: HashMap Text Text
48deriveJSON defaultOptions ''TestJSON
55 describe "streaming" $ do
56 it "count scalars with anchor" caseCountScalarsWithAnchor
57 it "count sequences with anchor" caseCountSequencesWithAnchor
58 it "count mappings with anchor" caseCountMappingsWithAnchor
59 it "count aliases" caseCountAliases
60 it "count scalars" caseCountScalars
61 it "largest string" caseLargestString
62 it "encode/decode" caseEncodeDecode
63 it "encode/decode file" caseEncodeDecodeFile
64 it "interleaved encode/decode" caseInterleave
65 it "decode invalid document (without segfault)" caseDecodeInvalidDocument
66 describe "Data.Yaml" $ do
67 it "encode/decode" caseEncodeDecodeData
68 it "encode/decode file" caseEncodeDecodeFileData
69 it "encode/decode files with non-ASCII names" caseEncodeDecodeNonAsciiFileData
70 it "encode/decode strings" caseEncodeDecodeStrings
71 it "decode invalid file" caseDecodeInvalid
72 it "processes datatypes" caseDataTypes
73 describe "Data.Yaml.Pretty" $ do
74 it "encode/decode" caseEncodeDecodeDataPretty
75 it "encode/decode strings" caseEncodeDecodeStringsPretty
76 it "processes datatypes" caseDataTypesPretty
77 describe "Data.Yaml aliases" $ do
78 it "simple scalar alias" caseSimpleScalarAlias
79 it "simple sequence alias" caseSimpleSequenceAlias
80 it "simple mapping alias" caseSimpleMappingAlias
81 it "mapping alias before anchor" caseMappingAliasBeforeAnchor
82 it "mapping alias inside anchor" caseMappingAliasInsideAnchor
83 it "scalar alias overriding" caseScalarAliasOverriding
84 describe "Data.Yaml merge keys" $ do
85 it "test uniqueness of keys" caseAllKeysShouldBeUnique
86 it "test mapping merge" caseSimpleMappingMerge
87 it "test sequence of mappings merging" caseMergeSequence
88 describe "numbers" $ do
89 it "parses as string when quoted" caseQuotedNumber
90 it "parses as number when unquoted" caseUnquotedNumber
91 it "parses as number !!str is present" caseAttribNumber
92 it "integers have no decimals" caseIntegerDecimals
93 describe "booleans" $ do
94 it "parses when all lowercase" caseLowercaseBool
95 it "parses when all uppercase" caseUppercaseBool
96 it "parses when titlecase" caseTitlecaseBool
97 describe "empty input" $ do
98 it "parses as Null" caseEmptyInput
99 it "parses as Null from file" caseEmptyInputFile
100 describe "comment only input" $ do
101 it "parses as Null" caseCommentOnlyInput
102 it "parses as Null from file" caseCommentOnlyInputFile
103 describe "alias stripping" $ do
104 it "works" caseStripAlias
105 describe "nulls" $ do
112 describe "pretty output" $ do
113 it "simple nulls" $ D.encode (object ["foo" .= D.Null]) `shouldBe` "foo: null\n"
114 it "simple numbers" $ D.encode (object ["foo" .= (4 :: Int)]) `shouldBe` "foo: 4\n"
115 it "True" $ D.encode (object ["foo" .= True]) `shouldBe` "foo: true\n"
116 it "False" $ D.encode (object ["foo" .= False]) `shouldBe` "foo: false\n"
117 it "simple string" $ D.encode (object ["foo" .= ("bar" :: T.Text)]) `shouldBe` "foo: bar\n"
119 describe "special keys" $ do
120 let tester key = it (T.unpack key) $
121 let value = object [key .= True]
122 in D.decode (D.encode value) `shouldBe` Just value
123 mapM_ tester specialStrings
125 describe "special values" $ do
126 let tester value = it (T.unpack value) $
127 let value' = object ["foo" .= value]
128 in D.decode (D.encode value') `shouldBe` Just value'
129 mapM_ tester specialStrings
131 describe "decodeFileEither" $ do
132 it "loads YAML through JSON into Haskell data" $ do
133 tj <- either (error . show) id `fmap` D.decodeFileEither "test/json.yaml"
134 tj `shouldBe` TestJSON
137 , anArray = V.fromList ["a", "b"]
138 , hash = HM.fromList [("key1", "value1"), ("key2", "value2")]
139 , extrastring = "1234-foo"
142 context "when file does not exist" $ do
143 it "returns Left" $ do
144 (D.decodeFileEither "./does_not_exist.yaml" :: IO (Either D.ParseException D.Value)) >>= (`shouldSatisfy` isLeft)
147 describe "round-tripping of special scalars" $ do
148 let special = words "y Y On ON false 12345 12345.0 12345a 12e3"
149 forM_ special $ \w -> it w $
150 let v = object ["word" .= w]
151 in D.decode (D.encode v) `shouldBe` Just v
152 it "no tags" $ D.encode (object ["word" .= ("true" :: String)]) `shouldBe` "word: 'true'\n"
154 it "aliases in keys #49" caseIssue49
156 it "serialization of +123 #64" $ do
157 D.decode (D.encode ("+123" :: String)) `shouldBe` Just ("+123" :: String)
159 it "preserves Scientific precision" casePreservesScientificPrecision
161 it "truncates files" caseTruncatesFiles
164specialStrings :: [T.Text]
173counter :: Monad m => (Y.Event -> Bool) -> C.Sink Y.Event m Int
175 CL.fold (\cnt e -> (if pred' e then 1 else 0) + cnt) 0
181caseHelper yamlString pred' expRes = do
182 res <- C.runResourceT $ Y.decode (B8.pack yamlString) C.$$ counter pred'
185caseCountScalarsWithAnchor :: Assertion
186caseCountScalarsWithAnchor =
187 caseHelper yamlString isScalarA 1
189 yamlString = "foo:\n - &anchor bin1\n - bin2\n - bin3"
190 isScalarA (Y.EventScalar _ _ _ (Just _)) = True
193caseCountSequencesWithAnchor :: Assertion
194caseCountSequencesWithAnchor =
195 caseHelper yamlString isSequenceStartA 1
197 yamlString = "foo: &anchor\n - bin1\n - bin2\n - bin3"
198 isSequenceStartA (Y.EventSequenceStart (Just _)) = True
199 isSequenceStartA _ = False
201caseCountMappingsWithAnchor :: Assertion
202caseCountMappingsWithAnchor =
203 caseHelper yamlString isMappingA 1
205 yamlString = "foo: &anchor\n key1: bin1\n key2: bin2\n key3: bin3"
206 isMappingA (Y.EventMappingStart (Just _)) = True
209caseCountAliases :: Assertion
211 caseHelper yamlString isAlias 1
213 yamlString = "foo: &anchor\n key1: bin1\n key2: bin2\n key3: bin3\nboo: *anchor"
214 isAlias Y.EventAlias{} = True
217caseCountScalars :: Assertion
219 res <- C.runResourceT $ Y.decode yamlBS C.$$ CL.fold adder accum
222 yamlString = "foo:\n baz: [bin1, bin2, bin3]\nbaz: bazval"
223 yamlBS = B8.pack yamlString
224 adder (s, l, m) (Y.EventScalar{}) = (s + 1, l, m)
225 adder (s, l, m) (Y.EventSequenceStart{}) = (s, l + 1, m)
226 adder (s, l, m) (Y.EventMappingStart{}) = (s, l, m + 1)
228 accum = (0, 0, 0) :: (Int, Int, Int)
230caseLargestString :: Assertion
231caseLargestString = do
232 res <- C.runResourceT $ Y.decodeFile filePath C.$$ CL.fold adder accum
233 res @?= (length expected, expected)
235 expected = "this one is just a little bit bigger than the others"
236 filePath = "test/largest-string.yaml"
237 adder (i, s) (Y.EventScalar bs _ _ _) =
238 let s' = B8.unpack bs
240 in if i' > i then (i', s') else (i, s)
242 accum = (0, "no strings found")
244newtype MyEvent = MyEvent Y.Event deriving Show
245instance Eq MyEvent where
246 (MyEvent (Y.EventScalar s t _ _)) == (MyEvent (Y.EventScalar s' t' _ _)) =
248 MyEvent e1 == MyEvent e2 = e1 == e2
250caseEncodeDecode :: Assertion
252 eList <- C.runResourceT $ Y.decode yamlBS C.$$ CL.consume
253 bs <- C.runResourceT $ CL.sourceList eList C.$$ Y.encode
254 eList2 <- C.runResourceT $ Y.decode bs C.$$ CL.consume
255 map MyEvent eList @=? map MyEvent eList2
257 yamlString = "foo: bar\nbaz:\n - bin1\n - bin2\n"
258 yamlBS = B8.pack yamlString
260caseEncodeDecodeFile :: Assertion
261caseEncodeDecodeFile = withFile "" $ \tmpPath -> do
262 eList <- C.runResourceT $ Y.decodeFile filePath C.$$ CL.consume
263 C.runResourceT $ CL.sourceList eList C.$$ Y.encodeFile tmpPath
264 eList2 <- C.runResourceT $ Y.decodeFile filePath C.$$ CL.consume
265 map MyEvent eList @=? map MyEvent eList2
267 filePath = "test/largest-string.yaml"
269caseInterleave :: Assertion
270caseInterleave = withFile "" $ \tmpPath -> withFile "" $ \tmpPath2 -> do
271 () <- C.runResourceT $ Y.decodeFile filePath C.$$ Y.encodeFile tmpPath
272 () <- C.runResourceT $ Y.decodeFile tmpPath C.$$ Y.encodeFile tmpPath2
273 f1 <- readFile tmpPath
274 f2 <- readFile tmpPath2
277 filePath = "test/largest-string.yaml"
279caseDecodeInvalidDocument :: Assertion
280caseDecodeInvalidDocument = do
281 x <- try $ C.runResourceT $ Y.decode yamlBS C.$$ CL.sinkNull
283 Left (_ :: SomeException) -> return ()
285 putStrLn $ "bad return value: " ++ show y
286 assertFailure "expected parsing exception, but got no errors"
288 yamlString = " - foo\n - baz\nbuz"
289 yamlBS = B8.pack yamlString
291mkScalar :: String -> D.Value
292mkScalar = mkStrScalar
294mkStrScalar :: String -> D.Value
295mkStrScalar = D.String . T.pack
297mappingKey :: D.Value-> String -> D.Value
298mappingKey (D.Object m) k = (fromJust . M.lookup (T.pack k) $ m)
299mappingKey _ _ = error "expected Object"
301decodeYaml :: String -> Maybe D.Value
302decodeYaml s = D.decode $ B8.pack s
308 [ ("bar1", D.String "bar2")
309 , ("bar3", D.String "")
314caseEncodeDecodeData :: Assertion
315caseEncodeDecodeData = do
316 let out = D.decode $ D.encode sample
319caseEncodeDecodeDataPretty :: Assertion
320caseEncodeDecodeDataPretty = do
321 let out = D.decode $ Pretty.encodePretty Pretty.defConfig sample
324caseEncodeDecodeFileData :: Assertion
325caseEncodeDecodeFileData = withFile "" $ \fp -> do
326 D.encodeFile fp sample
327 out <- D.decodeFile fp
330caseEncodeDecodeNonAsciiFileData :: Assertion
331caseEncodeDecodeNonAsciiFileData = do
332 let mySample = (object ["foo" .= True])
334 createDirectory "accenté"
335 D.encodeFile "accenté/bar.yaml" mySample
336 out1 <- D.decodeFile "accenté/bar.yaml"
337 out1 @?= Just mySample
339 createDirectoryIfMissing True "test/resources/accenté/"
341 readFile "test/resources/accent/foo.yaml" >>=
342 writeFile "test/resources/accenté/foo.yaml"
343 out2 <- D.decodeFile "test/resources/accenté/foo.yaml"
344 out2 @?= Just mySample
346caseEncodeDecodeStrings :: Assertion
347caseEncodeDecodeStrings = do
348 let out = D.decode $ D.encode sample
351caseEncodeDecodeStringsPretty :: Assertion
352caseEncodeDecodeStringsPretty = do
353 let out = D.decode $ Pretty.encodePretty Pretty.defConfig sample
356caseDecodeInvalid :: Assertion
357caseDecodeInvalid = do
358 let invalid = B8.pack "\tthis is 'not' valid :-)"
359 Nothing @=? (D.decode invalid :: Maybe D.Value)
361caseSimpleScalarAlias :: Assertion
362caseSimpleScalarAlias = do
363 let maybeRes = decodeYaml "- &anch foo\n- baz\n- *anch"
364 isJust maybeRes @? "decoder should return Just YamlObject but returned Nothing"
365 let res = fromJust maybeRes
366 res @?= array [(mkScalar "foo"), (mkScalar "baz"), (mkScalar "foo")]
368caseSimpleSequenceAlias :: Assertion
369caseSimpleSequenceAlias = do
370 let maybeRes = decodeYaml "seq: &anch\n - foo\n - baz\nseq2: *anch"
371 isJust maybeRes @? "decoder should return Just YamlObject but returned Nothing"
372 let res = fromJust maybeRes
373 res @?= object [("seq", array [(mkScalar "foo"), (mkScalar "baz")]), ("seq2", array [(mkScalar "foo"), (mkScalar "baz")])]
375caseSimpleMappingAlias :: Assertion
376caseSimpleMappingAlias = do
377 let maybeRes = decodeYaml "map: &anch\n key1: foo\n key2: baz\nmap2: *anch"
378 isJust maybeRes @? "decoder should return Just YamlObject but returned Nothing"
379 let res = fromJust maybeRes
380 res @?= object [(T.pack "map", object [("key1", mkScalar "foo"), ("key2", (mkScalar "baz"))]), (T.pack "map2", object [("key1", (mkScalar "foo")), ("key2", mkScalar "baz")])]
382caseMappingAliasBeforeAnchor :: Assertion
383caseMappingAliasBeforeAnchor = do
384 let res = decodeYaml "map: *anch\nmap2: &anch\n key1: foo\n key2: baz"
385 isNothing res @? "decode should return Nothing due to unknown alias"
387caseMappingAliasInsideAnchor :: Assertion
388caseMappingAliasInsideAnchor = do
389 let res = decodeYaml "map: &anch\n key1: foo\n key2: *anch"
390 isNothing res @? "decode should return Nothing due to unknown alias"
392caseScalarAliasOverriding :: Assertion
393caseScalarAliasOverriding = do
394 let maybeRes = decodeYaml "- &anch foo\n- baz\n- *anch\n- &anch boo\n- buz\n- *anch"
395 isJust maybeRes @? "decoder should return Just YamlObject but returned Nothing"
396 let res = fromJust maybeRes
397 res @?= array [(mkScalar "foo"), (mkScalar "baz"), (mkScalar "foo"), (mkScalar "boo"), (mkScalar "buz"), (mkScalar "boo")]
399caseAllKeysShouldBeUnique :: Assertion
400caseAllKeysShouldBeUnique = do
401 let maybeRes = decodeYaml "foo1: foo\nfoo2: baz\nfoo1: buz"
402 isJust maybeRes @? "decoder should return Just YamlObject but returned Nothing"
403 let res = fromJust maybeRes
404 mappingKey res "foo1" @?= (mkScalar "buz")
406caseSimpleMappingMerge :: Assertion
407caseSimpleMappingMerge = do
408 let maybeRes = decodeYaml "foo1: foo\nfoo2: baz\n<<:\n foo1: buz\n foo3: fuz"
409 isJust maybeRes @? "decoder should return Just YamlObject but returned Nothing"
410 let res = fromJust maybeRes
411 mappingKey res "foo1" @?= (mkScalar "foo")
412 mappingKey res "foo3" @?= (mkScalar "fuz")
414caseMergeSequence :: Assertion
415caseMergeSequence = do
416 let maybeRes = decodeYaml "m1: &m1\n k1: !!str 1\n k2: !!str 2\nm2: &m2\n k1: !!str 3\n k3: !!str 4\nfoo1: foo\n<<: [ *m1, *m2 ]"
417 isJust maybeRes @? "decoder should return Just YamlObject but returned Nothing"
418 let res = fromJust maybeRes
419 mappingKey res "foo1" @?= (mkScalar "foo")
420 mappingKey res "k1" @?= (D.String "1")
421 mappingKey res "k2" @?= (D.String "2")
422 mappingKey res "k3" @?= (D.String "4")
424caseDataTypes :: Assertion
426 D.decode (D.encode val) @?= Just val
429 [ ("string", D.String "foo")
430 , ("int", D.Number 5)
431 , ("float", D.Number 4.3)
432 , ("true", D.Bool True)
433 , ("false", D.Bool False)
437caseDataTypesPretty :: Assertion
439 D.decode (Pretty.encodePretty Pretty.defConfig val) @?= Just val
442 [ ("string", D.String "foo")
443 , ("int", D.Number 5)
444 , ("float", D.Number 4.3)
445 , ("true", D.Bool True)
446 , ("false", D.Bool False)
449caseQuotedNumber, caseUnquotedNumber, caseAttribNumber, caseIntegerDecimals :: Assertion
450caseQuotedNumber = D.decode "foo: \"1234\"" @?= Just (object [("foo", D.String "1234")])
451caseUnquotedNumber = D.decode "foo: 1234" @?= Just (object [("foo", D.Number 1234)])
452caseAttribNumber = D.decode "foo: !!str 1234" @?= Just (object [("foo", D.String "1234")])
453caseIntegerDecimals = D.encode (1 :: Int) @?= "1\n...\n"
456obj = Just (object [("foo", D.Bool False), ("bar", D.Bool True), ("baz", D.Bool True)])
458caseLowercaseBool, caseUppercaseBool, caseTitlecaseBool :: Assertion
459caseLowercaseBool = D.decode "foo: off\nbar: y\nbaz: true" @?= obj
460caseUppercaseBool = D.decode "foo: FALSE\nbar: Y\nbaz: ON" @?= obj
461caseTitlecaseBool = D.decode "foo: No\nbar: Yes\nbaz: True" @?= obj
463caseEmptyInput :: Assertion
464caseEmptyInput = D.decodeEither B8.empty @?= Right D.Null
466caseEmptyInputFile :: Assertion
467caseEmptyInputFile = do
468 out <- D.decodeFileEither "test/resources/empty.yaml"
469 either (Left . D.prettyPrintParseException) Right out @?= Right D.Null
471caseCommentOnlyInput :: Assertion
472caseCommentOnlyInput = D.decodeEither "# comment\n" @?= Right D.Null
474caseCommentOnlyInputFile :: Assertion
475caseCommentOnlyInputFile = do
476 out <- D.decodeFileEither "test/resources/empty2.yaml"
477 either (Left . D.prettyPrintParseException) Right out @?= Right D.Null
479checkNull :: T.Text -> Spec
481 it ("null recognized: " ++ show x) assertion
483 assertion = Just (object [("foo", D.Null)]) @=? D.decode (B8.pack $ "foo: " ++ T.unpack x)
485caseStripAlias :: Assertion
487 D.decode src @?= Just (object
488 [ "Default" .= object
489 [ "foo" .= (1 :: Int)
490 , "bar" .= (2 :: Int)
493 [ "foo" .= (1 :: Int)
494 , "bar" .= (2 :: Int)
495 , "key" .= (3 :: Int)
499 src = "Default: &def\n foo: 1\n bar: 2\nObj:\n <<: *def\n key: 3\n"
501caseIssue49 :: Assertion
503 D.decodeEither src @?= Right (object
504 [ "a" .= object [ "value" .= (1.0 :: Double) ]
505 , "b" .= object [ "value" .= (1.2 :: Double) ]
508 src = "---\na:\n &id5 value: 1.0\nb:\n *id5: 1.2"
510-- | We cannot guarantee this before aeson started using 'Scientific'.
511casePreservesScientificPrecision :: Assertion
512casePreservesScientificPrecision = do
513 D.decodeEither "x: 1e-100000" @?= Right (object
514 [ "x" .= D.Number (read "1e-100000") ])
515 -- Note that this ought to work also without 'Scientific', given
516 -- that @read (show "9.78159610558926e-5") == 9.78159610558926e-5@.
517 -- However, it didn't work (and still doesn't work with aeson < 0.7)
520 -- * We use 'Data.Text.Read.double', which is not as accurate as it
522 -- * Even if we used 'Data.Text.Read.rational' we would not get good
523 -- results, because of <https://github.com/bos/text/issues/34>.
524 D.decodeEither "x: 9.78159610558926e-5" @?= Right (object
525 [ "x" .= D.Number (read "9.78159610558926e-5") ])
527caseTruncatesFiles :: Assertion
528caseTruncatesFiles = withSystemTempFile "truncate.yaml" $ \fp h -> do
529 replicateM_ 500 $ B8.hPut h "HELLO WORLD!!!!!\n"
531 let val = object ["hello" .= ("world" :: String)]
533 res <- D.decodeFileEither fp
534 either (Left . show) Right res `shouldBe` Right val
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list