From 2ddc63e66a1911e879cd8f14382134ad1847a46f Mon Sep 17 00:00:00 2001 From: Maximilian Tagher Date: Wed, 17 Jun 2020 17:31:00 -0400 Subject: [PATCH 1/7] When statusIs fails, print a preview of the body My team makes frequent use of `statusIs`, but in virtually all cases where `statusIs` fails, we need to add a call to `printBody` to do further debugging. Following in the footsteps of `requireJSONResponse`, this PR automatically prints a portion of the body when `statusIs` fails, assuming the body looks like a text-based response (e.g. not a JPEG). I've found that a status code alone is often very misleading and leads people on a wild good chase, because e.g. a 403 could be triggered for many different reasons. I'm opening this PR as a draft to confirm people like the idea of doing this. If so I'll do a closer review of the code (this is my first draft basically), and also write some tests + test the code works in all cases. --- yesod-test/Yesod/Test.hs | 67 +++++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/yesod-test/Yesod/Test.hs b/yesod-test/Yesod/Test.hs index fbe7b64f..16a1beca 100644 --- a/yesod-test/Yesod/Test.hs +++ b/yesod-test/Yesod/Test.hs @@ -278,6 +278,9 @@ import Data.ByteArray.Encoding (convertToBase, Base(..)) import Network.HTTP.Types.Header (hContentType) import Data.Aeson (FromJSON, eitherDecode') import Control.Monad (unless) +import qualified Data.Set as Set +import qualified Yesod.Core.Content as Content +import qualified Data.ByteString.Lazy as LBS {-# DEPRECATED byLabel "This function seems to have multiple bugs (ref: https://github.com/yesodweb/yesod/pull/1459). Use byLabelExact, byLabelContain, byLabelPrefix or byLabelSuffix instead" #-} {-# DEPRECATED fileByLabel "This function seems to have multiple bugs (ref: https://github.com/yesodweb/yesod/pull/1459). Use fileByLabelExact, fileByLabelContain, fileByLabelPrefix or fileByLabelSuffix instead" #-} @@ -569,17 +572,63 @@ assertEqualNoShow :: (HasCallStack, Eq a) => String -> a -> a -> YesodExample si assertEqualNoShow msg a b = liftIO $ HUnit.assertBool msg (a == b) -- | Assert the last response status is as expected. +-- If the status code doesn't match, a portion of the body is also printed to aid in debugging. -- -- ==== __Examples__ -- -- > get HomeR -- > statusIs 200 statusIs :: HasCallStack => Int -> YesodExample site () -statusIs number = withResponse $ \ SResponse { simpleStatus = s } -> - liftIO $ flip HUnit.assertBool (H.statusCode s == number) $ concat - [ "Expected status was ", show number - , " but received status was ", show $ H.statusCode s - ] +statusIs number = do + withResponse $ \(SResponse status headers body) -> do + let mContentType = lookup hContentType headers + isUTF8ContentType = maybe False contentTypeHeaderIsUtf8 mContentType + + liftIO $ flip HUnit.assertBool (H.statusCode status == number) $ concat + [ "Expected status was ", show number + , " but received status was ", show $ H.statusCode status + , if isUTF8ContentType + then ". For debugging, the body was: " <> (T.unpack $ getBodyTextPreview body) + else "" + ] + +-- | Helper function to determine if we can print a body as plain text, for debugging purposes +contentTypeHeaderIsUtf8 :: BS8.ByteString -> Bool +contentTypeHeaderIsUtf8 contentTypeBS = + -- Convert to Text, so we can use T.splitOn + let contentTypeText = T.toLower $ TE.decodeUtf8 contentTypeBS + isUTF8FromCharset = case T.splitOn "charset=" contentTypeText of + -- Either a specific designation as UTF-8, or ASCII (which is a subset of UTF-8) + [_, charSet] -> any (`T.isInfixOf` charSet) ["utf-8", "us-ascii"] + _ -> False + + isInferredUTF8FromContentType = BS8.takeWhile (/= ';') contentTypeBS `Set.member` assumedUTF8ContentTypes + + in isUTF8FromCharset || isInferredUTF8FromContentType + +-- | List of Content-Types that are assumed to be UTF-8 (e.g. JSON) +assumedUTF8ContentTypes :: Set.Set BS8.ByteString +assumedUTF8ContentTypes = Set.fromList $ map Content.simpleContentType + [ Content.typeHtml + , Content.typePlain + , Content.typeJson + , Content.typeXml + , Content.typeAtom + , Content.typeRss + , Content.typeSvg + , Content.typeJavascript + , Content.typeCss + ] + +-- | Helper function to get the first 1024 characters of the body, assuming it is UTF-8 +-- This function is used to preview the body in case of an assertion failure +getBodyTextPreview :: LBS.ByteString -> T.Text +getBodyTextPreview body = + let characterLimit = 1024 + textBody = TL.toStrict $ decodeUtf8 body + in if T.length textBody < characterLimit + then textBody + else T.take characterLimit textBody <> "... (use `printBody` to see complete response body)" -- | Assert the given header key/value pair was returned. -- @@ -774,13 +823,7 @@ requireJSONResponse = do isJSONContentType (failure $ T.pack $ "Expected `Content-Type: application/json` in the headers, got: " ++ show headers) case eitherDecode' body of - Left err -> do - let characterLimit = 1024 - textBody = TL.toStrict $ decodeUtf8 body - bodyPreview = if T.length textBody < characterLimit - then textBody - else T.take characterLimit textBody <> "... (use `printBody` to see complete response body)" - failure $ T.concat ["Failed to parse JSON response; error: ", T.pack err, "JSON: ", bodyPreview] + Left err -> failure $ T.concat ["Failed to parse JSON response; error: ", T.pack err, "JSON: ", getBodyTextPreview body] Right v -> return v -- | Outputs the last response body to stderr (So it doesn't get captured by HSpec). Useful for debugging. From 34927e3401d2fe5c3c2e723da8b863bb7832ab9d Mon Sep 17 00:00:00 2001 From: Maximilian Tagher Date: Sat, 20 Jun 2020 14:54:31 -0400 Subject: [PATCH 2/7] .. --- yesod-test/ChangeLog.md | 2 + yesod-test/Yesod/Test.hs | 44 +-------------------- yesod-test/Yesod/Test/Internal.hs | 65 +++++++++++++++++++++++++++++++ yesod-test/test/main.hs | 14 +++++++ yesod-test/yesod-test.cabal | 1 + 5 files changed, 84 insertions(+), 42 deletions(-) create mode 100644 yesod-test/Yesod/Test/Internal.hs diff --git a/yesod-test/ChangeLog.md b/yesod-test/ChangeLog.md index 91fe802e..7441295d 100644 --- a/yesod-test/ChangeLog.md +++ b/yesod-test/ChangeLog.md @@ -1,5 +1,7 @@ # ChangeLog for yesod-test +## 1.6.9.2 + ## 1.6.9.1 * Improve documentation [#1676](https://github.com/yesodweb/yesod/pull/1676) diff --git a/yesod-test/Yesod/Test.hs b/yesod-test/Yesod/Test.hs index 16a1beca..6c319234 100644 --- a/yesod-test/Yesod/Test.hs +++ b/yesod-test/Yesod/Test.hs @@ -249,7 +249,6 @@ import System.IO import Yesod.Core.Unsafe (runFakeHandler) import Yesod.Test.TransversingCSS import Yesod.Core -import Yesod.Core.Json (contentTypeHeaderIsJson) import qualified Data.Text.Lazy as TL import Data.Text.Lazy.Encoding (encodeUtf8, decodeUtf8, decodeUtf8With) import Text.XML.Cursor hiding (element) @@ -278,9 +277,8 @@ import Data.ByteArray.Encoding (convertToBase, Base(..)) import Network.HTTP.Types.Header (hContentType) import Data.Aeson (FromJSON, eitherDecode') import Control.Monad (unless) -import qualified Data.Set as Set -import qualified Yesod.Core.Content as Content -import qualified Data.ByteString.Lazy as LBS + +import Yesod.Test.Internal (getBodyTextPreview, contentTypeHeaderIsUtf8) {-# DEPRECATED byLabel "This function seems to have multiple bugs (ref: https://github.com/yesodweb/yesod/pull/1459). Use byLabelExact, byLabelContain, byLabelPrefix or byLabelSuffix instead" #-} {-# DEPRECATED fileByLabel "This function seems to have multiple bugs (ref: https://github.com/yesodweb/yesod/pull/1459). Use fileByLabelExact, fileByLabelContain, fileByLabelPrefix or fileByLabelSuffix instead" #-} @@ -592,44 +590,6 @@ statusIs number = do else "" ] --- | Helper function to determine if we can print a body as plain text, for debugging purposes -contentTypeHeaderIsUtf8 :: BS8.ByteString -> Bool -contentTypeHeaderIsUtf8 contentTypeBS = - -- Convert to Text, so we can use T.splitOn - let contentTypeText = T.toLower $ TE.decodeUtf8 contentTypeBS - isUTF8FromCharset = case T.splitOn "charset=" contentTypeText of - -- Either a specific designation as UTF-8, or ASCII (which is a subset of UTF-8) - [_, charSet] -> any (`T.isInfixOf` charSet) ["utf-8", "us-ascii"] - _ -> False - - isInferredUTF8FromContentType = BS8.takeWhile (/= ';') contentTypeBS `Set.member` assumedUTF8ContentTypes - - in isUTF8FromCharset || isInferredUTF8FromContentType - --- | List of Content-Types that are assumed to be UTF-8 (e.g. JSON) -assumedUTF8ContentTypes :: Set.Set BS8.ByteString -assumedUTF8ContentTypes = Set.fromList $ map Content.simpleContentType - [ Content.typeHtml - , Content.typePlain - , Content.typeJson - , Content.typeXml - , Content.typeAtom - , Content.typeRss - , Content.typeSvg - , Content.typeJavascript - , Content.typeCss - ] - --- | Helper function to get the first 1024 characters of the body, assuming it is UTF-8 --- This function is used to preview the body in case of an assertion failure -getBodyTextPreview :: LBS.ByteString -> T.Text -getBodyTextPreview body = - let characterLimit = 1024 - textBody = TL.toStrict $ decodeUtf8 body - in if T.length textBody < characterLimit - then textBody - else T.take characterLimit textBody <> "... (use `printBody` to see complete response body)" - -- | Assert the given header key/value pair was returned. -- -- ==== __Examples__ diff --git a/yesod-test/Yesod/Test/Internal.hs b/yesod-test/Yesod/Test/Internal.hs new file mode 100644 index 00000000..196ed4ef --- /dev/null +++ b/yesod-test/Yesod/Test/Internal.hs @@ -0,0 +1,65 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | This module exposes functions that are used internally by yesod-test. +-- The functions exposed here are **not** a stable API—they may be changed or removed without any major version bump. +-- +-- That said, you may find them useful if your application can accept API breakage. +module Yesod.Test.Internal + ( getBodyTextPreview + , contentTypeHeaderIsUtf8 + , assumedUTF8ContentTypes + ) where + + +import qualified Data.ByteString.Char8 as BS8 +import qualified Data.ByteString.Lazy as LBS +import qualified Data.Set as Set +import qualified Data.Text as T +import qualified Data.Text.Encoding as TE +import qualified Data.Text.Lazy as TL +import qualified Data.Text.Lazy.Encoding as DTLE +import qualified Yesod.Core.Content as Content + +-- | Helper function to get the first 1024 characters of the body, assuming it is UTF-8 +-- This function is used to preview the body in case of an assertion failure +-- +-- @since 1.6.9.2 +getBodyTextPreview :: LBS.ByteString -> T.Text +getBodyTextPreview body = + let characterLimit = 1024 + textBody = TL.toStrict $ DTLE.decodeUtf8 body + in if T.length textBody < characterLimit + then textBody + else T.take characterLimit textBody <> "... (use `printBody` to see complete response body)" + +-- | Helper function to determine if we can print a body as plain text, for debugging purposes +-- +-- @since 1.6.9.2 +contentTypeHeaderIsUtf8 :: BS8.ByteString -> Bool +contentTypeHeaderIsUtf8 contentTypeBS = + -- Convert to Text, so we can use T.splitOn + let contentTypeText = T.toLower $ TE.decodeUtf8 contentTypeBS + isUTF8FromCharset = case T.splitOn "charset=" contentTypeText of + -- Either a specific designation as UTF-8, or ASCII (which is a subset of UTF-8) + [_, charSet] -> any (`T.isInfixOf` charSet) ["utf-8", "us-ascii"] + _ -> False + + isInferredUTF8FromContentType = BS8.takeWhile (/= ';') contentTypeBS `Set.member` assumedUTF8ContentTypes + + in isUTF8FromCharset || isInferredUTF8FromContentType + +-- | List of Content-Types that are assumed to be UTF-8 (e.g. JSON) +-- +-- @since 1.6.9.2 +assumedUTF8ContentTypes :: Set.Set BS8.ByteString +assumedUTF8ContentTypes = Set.fromList $ map Content.simpleContentType + [ Content.typeHtml + , Content.typePlain + , Content.typeJson + , Content.typeXml + , Content.typeAtom + , Content.typeRss + , Content.typeSvg + , Content.typeJavascript + , Content.typeCss + ] \ No newline at end of file diff --git a/yesod-test/test/main.hs b/yesod-test/test/main.hs index 1e07ae8c..16acdf79 100644 --- a/yesod-test/test/main.hs +++ b/yesod-test/test/main.hs @@ -45,6 +45,7 @@ import Control.Monad.IO.Unlift (toIO) import qualified Web.Cookie as Cookie import Data.Maybe (isNothing) import qualified Data.Text as T +import Yesod.Test.Internal (contentTypeHeaderIsUtf8) parseQuery_ :: Text -> [[SelectorGroup]] parseQuery_ = either error id . parseQuery @@ -125,6 +126,19 @@ main = hspec $ do ] ] in HD.parseLBS html @?= doc + describe "identifying text-based bodies" $ do + it "matches content-types with an explicit UTF-8 charset" $ do + contentTypeHeaderIsUtf8 "application/custom; charset=UTF-8" @?= True + contentTypeHeaderIsUtf8 "application/custom; charset=utf-8" @?= True + it "matches content-types with an ASCII charset" $ do + contentTypeHeaderIsUtf8 "application/custom; charset=us-ascii" @?= True + it "matches content-types that we assume are UTF-8" $ do + contentTypeHeaderIsUtf8 "text/html" @?= True + contentTypeHeaderIsUtf8 "application/json" @?= True + it "doesn't match content-type headers that are binary data" $ do + contentTypeHeaderIsUtf8 "image/gif" @?= False + contentTypeHeaderIsUtf8 "application/pdf" @?= False + describe "basic usage" $ yesodSpec app $ do ydescribe "tests1" $ do yit "tests1a" $ do diff --git a/yesod-test/yesod-test.cabal b/yesod-test/yesod-test.cabal index f1868579..5fb5f504 100644 --- a/yesod-test/yesod-test.cabal +++ b/yesod-test/yesod-test.cabal @@ -45,6 +45,7 @@ library exposed-modules: Yesod.Test Yesod.Test.CssQuery Yesod.Test.TransversingCSS + Yesod.Test.Internal ghc-options: -Wall test-suite test From 8f00e762578ada2d52afe4645b90d831a3c83ba3 Mon Sep 17 00:00:00 2001 From: Maximilian Tagher Date: Sat, 20 Jun 2020 14:56:19 -0400 Subject: [PATCH 3/7] .. --- yesod-test/Yesod/Test/Internal.hs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yesod-test/Yesod/Test/Internal.hs b/yesod-test/Yesod/Test/Internal.hs index 196ed4ef..28e67585 100644 --- a/yesod-test/Yesod/Test/Internal.hs +++ b/yesod-test/Yesod/Test/Internal.hs @@ -1,7 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} -- | This module exposes functions that are used internally by yesod-test. --- The functions exposed here are **not** a stable API—they may be changed or removed without any major version bump. +-- The functions exposed here are _not_ a stable API—they may be changed or removed without any major version bump. -- -- That said, you may find them useful if your application can accept API breakage. module Yesod.Test.Internal @@ -20,8 +20,8 @@ import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as DTLE import qualified Yesod.Core.Content as Content --- | Helper function to get the first 1024 characters of the body, assuming it is UTF-8 --- This function is used to preview the body in case of an assertion failure +-- | Helper function to get the first 1024 characters of the body, assuming it is UTF-8. +-- This function is used to preview the body in case of an assertion failure. -- -- @since 1.6.9.2 getBodyTextPreview :: LBS.ByteString -> T.Text @@ -32,7 +32,7 @@ getBodyTextPreview body = then textBody else T.take characterLimit textBody <> "... (use `printBody` to see complete response body)" --- | Helper function to determine if we can print a body as plain text, for debugging purposes +-- | Helper function to determine if we can print a body as plain text, for debugging purposes. -- -- @since 1.6.9.2 contentTypeHeaderIsUtf8 :: BS8.ByteString -> Bool @@ -48,7 +48,7 @@ contentTypeHeaderIsUtf8 contentTypeBS = in isUTF8FromCharset || isInferredUTF8FromContentType --- | List of Content-Types that are assumed to be UTF-8 (e.g. JSON) +-- | List of Content-Types that are assumed to be UTF-8 (e.g. JSON). -- -- @since 1.6.9.2 assumedUTF8ContentTypes :: Set.Set BS8.ByteString From f50d23ce49506881729c8f020ba443834659e1ce Mon Sep 17 00:00:00 2001 From: Maximilian Tagher Date: Sat, 20 Jun 2020 15:01:26 -0400 Subject: [PATCH 4/7] .. --- yesod-test/ChangeLog.md | 3 +++ yesod-test/Yesod/Test/Internal.hs | 3 +-- yesod-test/yesod-test.cabal | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/yesod-test/ChangeLog.md b/yesod-test/ChangeLog.md index 7441295d..294d1298 100644 --- a/yesod-test/ChangeLog.md +++ b/yesod-test/ChangeLog.md @@ -2,6 +2,9 @@ ## 1.6.9.2 +* `statusIs` assertion failures now print a preview of the response body, if the response body is UTF-8 or ASCII. +* Adds an `Yesod.Test.Internal`, which exposes functions that yesod-test uses. These functions do _not_ constitute a stable API. + ## 1.6.9.1 * Improve documentation [#1676](https://github.com/yesodweb/yesod/pull/1676) diff --git a/yesod-test/Yesod/Test/Internal.hs b/yesod-test/Yesod/Test/Internal.hs index 28e67585..b035b5f0 100644 --- a/yesod-test/Yesod/Test/Internal.hs +++ b/yesod-test/Yesod/Test/Internal.hs @@ -10,7 +10,6 @@ module Yesod.Test.Internal , assumedUTF8ContentTypes ) where - import qualified Data.ByteString.Char8 as BS8 import qualified Data.ByteString.Lazy as LBS import qualified Data.Set as Set @@ -62,4 +61,4 @@ assumedUTF8ContentTypes = Set.fromList $ map Content.simpleContentType , Content.typeSvg , Content.typeJavascript , Content.typeCss - ] \ No newline at end of file + ] diff --git a/yesod-test/yesod-test.cabal b/yesod-test/yesod-test.cabal index 5fb5f504..8e32a269 100644 --- a/yesod-test/yesod-test.cabal +++ b/yesod-test/yesod-test.cabal @@ -1,5 +1,5 @@ name: yesod-test -version: 1.6.9.1 +version: 1.6.9.2 license: MIT license-file: LICENSE author: Nubis From 4ddff4284742b0bc9ee0105c0c4e4f518078e402 Mon Sep 17 00:00:00 2001 From: Maximilian Tagher Date: Sat, 20 Jun 2020 15:01:51 -0400 Subject: [PATCH 5/7] .. --- yesod-test/ChangeLog.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yesod-test/ChangeLog.md b/yesod-test/ChangeLog.md index 294d1298..d3469da8 100644 --- a/yesod-test/ChangeLog.md +++ b/yesod-test/ChangeLog.md @@ -2,7 +2,7 @@ ## 1.6.9.2 -* `statusIs` assertion failures now print a preview of the response body, if the response body is UTF-8 or ASCII. +* `statusIs` assertion failures now print a preview of the response body, if the response body is UTF-8 or ASCII. [#1680](https://github.com/yesodweb/yesod/pull/1680/files) * Adds an `Yesod.Test.Internal`, which exposes functions that yesod-test uses. These functions do _not_ constitute a stable API. ## 1.6.9.1 From 28e5b606b2c7c538330184fbcf6654bc33d4caee Mon Sep 17 00:00:00 2001 From: Maximilian Tagher Date: Sat, 20 Jun 2020 17:27:13 -0400 Subject: [PATCH 6/7] attempt to fix 8.2 --- yesod-test/Yesod/Test/Internal.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/yesod-test/Yesod/Test/Internal.hs b/yesod-test/Yesod/Test/Internal.hs index b035b5f0..c4057f52 100644 --- a/yesod-test/Yesod/Test/Internal.hs +++ b/yesod-test/Yesod/Test/Internal.hs @@ -18,6 +18,7 @@ import qualified Data.Text.Encoding as TE import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as DTLE import qualified Yesod.Core.Content as Content +import Data.Semigroup (Semigroup(..)) -- | Helper function to get the first 1024 characters of the body, assuming it is UTF-8. -- This function is used to preview the body in case of an assertion failure. From 1d67e3a359d6de540457117fe2cb3faf50122050 Mon Sep 17 00:00:00 2001 From: Maximilian Tagher Date: Sun, 21 Jun 2020 20:27:35 -0400 Subject: [PATCH 7/7] 1.6.10 --- yesod-test/ChangeLog.md | 2 +- yesod-test/Yesod/Test/Internal.hs | 6 +++--- yesod-test/yesod-test.cabal | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/yesod-test/ChangeLog.md b/yesod-test/ChangeLog.md index d3469da8..032be807 100644 --- a/yesod-test/ChangeLog.md +++ b/yesod-test/ChangeLog.md @@ -1,6 +1,6 @@ # ChangeLog for yesod-test -## 1.6.9.2 +## 1.6.10 * `statusIs` assertion failures now print a preview of the response body, if the response body is UTF-8 or ASCII. [#1680](https://github.com/yesodweb/yesod/pull/1680/files) * Adds an `Yesod.Test.Internal`, which exposes functions that yesod-test uses. These functions do _not_ constitute a stable API. diff --git a/yesod-test/Yesod/Test/Internal.hs b/yesod-test/Yesod/Test/Internal.hs index c4057f52..366f9cb1 100644 --- a/yesod-test/Yesod/Test/Internal.hs +++ b/yesod-test/Yesod/Test/Internal.hs @@ -23,7 +23,7 @@ import Data.Semigroup (Semigroup(..)) -- | Helper function to get the first 1024 characters of the body, assuming it is UTF-8. -- This function is used to preview the body in case of an assertion failure. -- --- @since 1.6.9.2 +-- @since 1.6.10 getBodyTextPreview :: LBS.ByteString -> T.Text getBodyTextPreview body = let characterLimit = 1024 @@ -34,7 +34,7 @@ getBodyTextPreview body = -- | Helper function to determine if we can print a body as plain text, for debugging purposes. -- --- @since 1.6.9.2 +-- @since 1.6.10 contentTypeHeaderIsUtf8 :: BS8.ByteString -> Bool contentTypeHeaderIsUtf8 contentTypeBS = -- Convert to Text, so we can use T.splitOn @@ -50,7 +50,7 @@ contentTypeHeaderIsUtf8 contentTypeBS = -- | List of Content-Types that are assumed to be UTF-8 (e.g. JSON). -- --- @since 1.6.9.2 +-- @since 1.6.10 assumedUTF8ContentTypes :: Set.Set BS8.ByteString assumedUTF8ContentTypes = Set.fromList $ map Content.simpleContentType [ Content.typeHtml diff --git a/yesod-test/yesod-test.cabal b/yesod-test/yesod-test.cabal index 8e32a269..5427594a 100644 --- a/yesod-test/yesod-test.cabal +++ b/yesod-test/yesod-test.cabal @@ -1,5 +1,5 @@ name: yesod-test -version: 1.6.9.2 +version: 1.6.10 license: MIT license-file: LICENSE author: Nubis