packages feed

settei-yaml-0.2.0.0: test/Settei/YamlCharacterizationTest.hs

module Settei.YamlCharacterizationTest (tests) where

import Data.ByteString.Char8 qualified as ByteString8
import Data.Generics.Labels ()
import Data.List.NonEmpty qualified as NonEmpty
import Data.Ratio ((%))
import Data.Text (Text)
import Settei
import Settei.Yaml
import Test.Tasty (TestTree, localOption, mkTimeout, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))

tests :: TestTree
tests =
  testGroup
    "Settei.Yaml.Characterization"
    [ testCase "duplicate block mapping keys fail at the second key" $ do
        problem <- expectError "service:\n  port: 8000\n  port: 9000\n"
        yamlErrorCategory problem @?= YamlDuplicateKey
        yamlErrorLine problem @?= Just 3
        yamlErrorContext problem @?= "$.service.port",
      testCase "duplicate flow mapping keys fail at the second key" $ do
        problem <- expectError "service: {port: 8000, port: 9000}\n"
        yamlErrorCategory problem @?= YamlDuplicateKey
        yamlErrorLine problem @?= Just 1,
      testCase "multiple documents fail instead of dropping a document" $ do
        problem <- expectError "---\nservice: one\n---\nservice: two\n"
        yamlErrorCategory problem @?= YamlMultipleDocuments
        yamlErrorLine problem @?= Just 3,
      testCase "aliases and anchors fail explicitly" $ do
        anchorProblem <- expectError "shared: &shared\n  port: 8000\n"
        yamlErrorCategory anchorProblem @?= YamlUnsupportedFeature
        aliasProblem <- expectError "service: *shared\n"
        yamlErrorCategory aliasProblem @?= YamlUnsupportedFeature,
      testCase "merge keys fail explicitly" $ do
        problem <- expectError "service:\n  <<: {port: 8000}\n"
        yamlErrorCategory problem @?= YamlUnsupportedFeature
        yamlErrorContext problem @?= "$.service.<<",
      testCase "custom tags fail explicitly" $ do
        problem <- expectError "service: !custom value\n"
        yamlErrorCategory problem @?= YamlUnsupportedFeature,
      testCase "non-string mapping keys fail" $ do
        problem <- expectError "1: value\n"
        yamlErrorCategory problem @?= YamlNonStringKey,
      testCase "syntax failures retain one-based parser marks" $ do
        problem <- expectError "service: [one, two\n"
        yamlErrorCategory problem @?= YamlSyntaxError
        assertBool "syntax error omitted its line" (maybe False (> 0) (yamlErrorLine problem))
        assertBool "syntax error omitted its column" (maybe False (> 0) (yamlErrorColumn problem)),
      testCase "tagged !!bool with a YAML 1.1 spelling fails as an invalid scalar" $ do
        problem <- expectError "feature:\n  enabled: !!bool yes\n"
        yamlErrorCategory problem @?= YamlInvalidScalar
        yamlErrorContext problem @?= "$.feature.enabled"
        yamlErrorMessage problem @?= "invalid boolean scalar",
      testCase "tagged !!bool true still parses" $ do
        input <- expectSource "feature:\n  enabled: !!bool true\n"
        expectValue "feature.enabled" input (RawBool True),
      localOption (mkTimeout 10000000) $
        testGroup
          "bounded numeric scalar conversion"
          [ testCase "huge positive exponents are rejected quickly" $ do
              problem <- expectError "huge: 1e1000000000\n"
              expectExponentError "$.huge" problem,
            testCase "huge negative exponents are rejected quickly" $ do
              problem <- expectError "tiny: 1e-1000000000\n"
              expectExponentError "$.tiny" problem,
            testCase "float-tagged numbers apply the exponent bound" $ do
              problem <- expectError "huge: !!float 1e1000000000\n"
              expectExponentError "$.huge" problem,
            testCase "integer-tagged numbers apply the exponent bound" $ do
              problem <- expectError "huge: !!int 1e1000000000\n"
              expectExponentError "$.huge" problem,
            testCase "positive boundary exponents convert exactly" $ do
              input <- expectSource "edge: 1e4096\n"
              expectValue "edge" input (RawNumber (10 ^ (4096 :: Integer) % 1)),
            testCase "negative boundary exponents convert exactly" $ do
              input <- expectSource "edge: 1e-4096\n"
              expectValue "edge" input (RawNumber (1 % 10 ^ (4096 :: Integer))),
            testCase "exponents immediately above the bound are rejected" $ do
              problem <- expectError "over: 1e4097\n"
              expectExponentError "$.over" problem,
            testCase "ordinary decimals, hex, octal, and integers remain exact" $ do
              input <- expectSource "rate: 1.5e-3\nmask: 0x1A\nmode: 0o17\nport: 8080\n"
              expectValue "rate" input (RawNumber (3 % 2000))
              expectValue "mask" input (RawNumber 26)
              expectValue "mode" input (RawNumber 15)
              expectValue "port" input (RawNumber 8080)
          ]
    ]

sourceOptions :: YamlSourceOptions
sourceOptions = withYamlSourcePath "characterization.yaml" (yamlSourceOptions "characterization")

expectSource :: String -> IO Source
expectSource input =
  case decodeYamlSource sourceOptions (ByteString8.pack input) of
    Left errors -> fail (show errors)
    Right sourceValue -> pure sourceValue

expectError :: String -> IO YamlSourceError
expectError input =
  case decodeYamlSource sourceOptions (ByteString8.pack input) of
    Left errors -> pure (NonEmpty.head errors)
    Right _ -> fail "expected strict YAML characterization to fail"

expectValue :: Text -> Source -> RawValue -> IO ()
expectValue keyText input expected = case lookupSource (validKey keyText) input of
  Right (Just found) -> assertBool "unexpected YAML raw value" (candidateValue found == expected)
  _ -> fail "expected YAML candidate"

expectExponentError :: Text -> YamlSourceError -> IO ()
expectExponentError expectedContext problem = do
  yamlErrorCategory problem @?= YamlInvalidScalar
  yamlErrorLine problem @?= Just 1
  yamlErrorContext problem @?= expectedContext
  yamlErrorMessage problem @?= "numeric scalar exponent is out of the supported range"

validKey :: Text -> Key
validKey value = either (error . show) id (parseKey value)