kdl-hs-1.2.1: test/KDL/DecoderSpec.hs
{-# LANGUAGE OverloadedStrings #-}
module KDL.DecoderSpec (spec) where
import Control.Monad (unless, when)
import Data.Char (isAlpha)
import Data.Text (Text)
import Data.Text qualified as Text
import KDL qualified
import KDL.TestUtils.Error (decodeErrorMsg)
import KDL.Types (Node)
import Skeletest
import Skeletest.Predicate qualified as P
import System.FilePath ((</>))
decodeErrorMsgSnapshot :: Maybe FilePath -> Predicate IO (Either KDL.DecodeError a)
decodeErrorMsgSnapshot mfile = P.left (KDL.renderDecodeError P.>>> sanitize P.>>> P.matchesSnapshot)
where
sanitize =
case mfile of
Nothing -> id
Just file -> Text.replace (Text.pack file) "test_config.kdl"
spec :: Spec
spec = do
spec_decodeWith
spec_decodeFileWith
spec_decodeDocWith
spec_errorMessages
spec_regressionTests
spec_decodeWith :: Spec
spec_decodeWith = do
describe "decodeWith" $ do
it "fails with helpful error if parsing fails" $ do
let config = "foo 123=123"
decoder = KDL.document $ KDL.node @Node "foo"
KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsgSnapshot Nothing
it "fails with user-defined error" $ do
let config = "foo -1"
decoder =
KDL.document . KDL.argAtWith "foo" $
KDL.withDecoder KDL.number $ \x -> do
when (x < 0) $ do
KDL.failM $ "Got negative number: " <> (Text.pack . show) x
pure x
KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsgSnapshot Nothing
it "shows context in deeply nested error" $ do
let config = "foo; foo { bar { baz; baz; baz; baz a=1; }; }"
decoder =
KDL.document
. (KDL.many . KDL.nodeWith "foo" . KDL.children)
. (KDL.many . KDL.nodeWith "bar" . KDL.children)
. (KDL.many . KDL.nodeWith "baz")
$ KDL.optional (KDL.prop @Text "a")
KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsgSnapshot Nothing
spec_decodeFileWith :: Spec
spec_decodeFileWith = do
describe "decodeFileWith" $ do
it "fails with helpful error if parsing fails" $ do
FixtureKdlFile file <- getFixture
writeFile file "foo 123=123"
let decoder = KDL.document $ KDL.node @Node "foo"
KDL.decodeFileWith decoder file `shouldSatisfy` P.returns (decodeErrorMsgSnapshot (Just file))
it "fails with user-defined error" $ do
FixtureKdlFile file <- getFixture
writeFile file "foo -1"
let decoder =
KDL.document . KDL.argAtWith "foo" $
KDL.withDecoder KDL.number $ \x -> do
when (x < 0) $ do
KDL.failM $ "Got negative number: " <> (Text.pack . show) x
pure x
KDL.decodeFileWith decoder file `shouldSatisfy` P.returns (decodeErrorMsgSnapshot (Just file))
it "shows context in deeply nested error" $ do
FixtureKdlFile file <- getFixture
writeFile file "foo; foo { bar { baz; baz; baz; baz a=1; }; }"
let decoder =
KDL.document
. (KDL.many . KDL.nodeWith "foo" . KDL.children)
. (KDL.many . KDL.nodeWith "bar" . KDL.children)
. (KDL.many . KDL.nodeWith "baz")
$ KDL.optional (KDL.prop @Text "a")
KDL.decodeFileWith decoder file `shouldSatisfy` P.returns (decodeErrorMsgSnapshot (Just file))
spec_decodeDocWith :: Spec
spec_decodeDocWith = do
describe "decodeDocWith" $ do
it "fails with user-defined error" $ do
let config = "foo -1"
decoder =
KDL.document . KDL.argAtWith "foo" $
KDL.withDecoder KDL.number $ \x -> do
when (x < 0) $ do
KDL.failM $ "Got negative number: " <> (Text.pack . show) x
pure x
Right doc <- pure $ KDL.parseWith KDL.def config
KDL.decodeDocWith decoder doc
`shouldSatisfy` decodeErrorMsgSnapshot Nothing
it "shows context in deeply nested error" $ do
let config = "foo; foo { bar { baz; baz; baz; baz a=1; }; }"
decoder =
KDL.document
. (KDL.many . KDL.nodeWith "foo" . KDL.children)
. (KDL.many . KDL.nodeWith "bar" . KDL.children)
. (KDL.many . KDL.nodeWith "baz")
$ KDL.optional (KDL.prop @Text "a")
Right doc <- pure $ KDL.parseWith KDL.def config
KDL.decodeDocWith decoder doc
`shouldSatisfy` decodeErrorMsgSnapshot Nothing
spec_errorMessages :: Spec
spec_errorMessages = do
describe "Error messages" $ do
it "only shows first line when context spans multiple lines" $ do
let config = "foo \\\n 1"
decoder =
KDL.document . KDL.nodeWith "foo" $ do
_ <- KDL.arg @Int
_ <- KDL.children $ KDL.argAt @Int "bar"
pure ()
KDL.decodeWith decoder config `shouldSatisfy` decodeErrorMsgSnapshot Nothing
newtype FixtureKdlFile = FixtureKdlFile FilePath
instance Fixture FixtureKdlFile where
fixtureAction = do
FixtureTmpDir tmpdir <- getFixture
pure . noCleanup $ FixtureKdlFile (tmpdir </> "kdl-hs-test.kdl")
{----- Regression tests -----}
spec_regressionTests :: Spec
spec_regressionTests = do
describe "Regression tests" $ do
it "fails with correct error when error occurs in another node after backtracking in a previous node" $ do
let config = "user a { foo { bar } }; user a1"
decoder =
KDL.document . KDL.many . KDL.nodeWith "user" $ do
_ <-
KDL.children . KDL.many . KDL.nodeWith "foo" $ do
KDL.children . KDL.nodeWith "bar" $ do
KDL.children $
sequence
[ KDL.optional $ KDL.node @KDL.Node "opt1"
, KDL.optional $ KDL.node @KDL.Node "opt2"
]
KDL.argWith $ do
s <- KDL.string
unless (Text.all isAlpha s) $ do
KDL.fail "Invalid username"
KDL.decodeWith decoder config
`shouldSatisfy` decodeErrorMsg
[ "<input>:1:30:"
, " • Invalid username"
, " │"
, "1 │ user a { foo { bar } }; user a1"
, " │ ^^"
]