packages feed

mmark-ext-0.3.0.0: tests/Text/MMark/Extension/TestUtils.hs

{-# LANGUAGE OverloadedStrings #-}

module Text.MMark.Extension.TestUtils
  ( withExt,
    withTrans,
    transErrors,
    transErrorsM,
    checkErrors,
    summarize,
    withTempDir,
  )
where

import Control.Exception (bracket)
import Data.Char (isDigit)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Lucid qualified as L
import System.Directory
  ( createDirectory,
    getTemporaryDirectory,
    removeDirectoryRecursive,
    removeFile,
  )
import System.IO (hClose, openTempFile)
import Test.Hspec
import Text.MMark qualified as MMark
import Text.MMark.Trans (Bni, Trans, TransT)
import Text.Megaparsec (errorBundlePretty)

-- | Feed input into MMark parser, apply a render extension, render the
-- parsed document and demand that it matches the given example.
withExt ::
  -- | Render extension to use
  MMark.RenderExtension ->
  -- | Input for the parser
  Text ->
  -- | Expected output of the render
  Text ->
  Expectation
withExt ext input expected = do
  Right doc <- pure (MMark.parse "" input)
  render mempty doc `shouldBe` expected
  where
    render e = TL.toStrict . L.renderText . MMark.render (e <> ext)

-- | Like 'withExt', but applies a transformation instead.
withTrans ::
  -- | Transformation to apply
  (Bni -> Trans Bni) ->
  -- | Input for the parser
  Text ->
  -- | Expected output of the render
  Text ->
  Expectation
withTrans f input expected = do
  Right doc <- pure (MMark.parse "" input)
  case MMark.runTrans f doc of
    Left errs -> expectationFailure (errorBundlePretty errs)
    Right doc' ->
      (TL.toStrict . L.renderText . MMark.render mempty) doc'
        `shouldBe` expected

-- | Apply a transformation that is expected to report problems and return
-- one @line:col: message@ string per problem.
transErrors ::
  -- | Transformation to apply
  (Bni -> Trans Bni) ->
  -- | Input for the parser
  Text ->
  IO [Text]
transErrors f input = do
  Right doc <- pure (MMark.parse "" input)
  pure $ case MMark.runTrans f doc of
    Right _ -> []
    Left errs -> summarize (errorBundlePretty errs)

-- | Like 'transErrors', but for a transformation that needs 'IO'.
transErrorsM ::
  -- | Transformation to apply
  (Bni -> TransT IO Bni) ->
  -- | Input for the parser
  Text ->
  IO [Text]
transErrorsM f input = do
  Right doc <- pure (MMark.parse "" input)
  r <- MMark.runTransM f doc
  pure $ case r of
    Right _ -> []
    Left errs -> summarize (errorBundlePretty errs)

-- | Reduce a rendered error bundle to one @line:col: message@ string per
-- error, dropping the source excerpt megaparsec prints in between.
summarize :: String -> [Text]
summarize = go Nothing . fmap T.strip . T.lines . T.pack
  where
    go _ [] = []
    go cur (l : ls)
      | T.null l = go cur ls
      | isPos l = go (Just l) ls
      | "|" `T.isInfixOf` l = go cur ls
      | otherwise = case cur of
          Just p -> (p <> " " <> l) : go Nothing ls
          Nothing -> go Nothing ls
    isPos t = ":" `T.isSuffixOf` t && T.all (\c -> isDigit c || c == ':') t

-- | Run a check that is expected to report problems and return one
-- @line:col: message@ string per problem.
checkErrors ::
  -- | Check to run
  Trans a ->
  -- | Input for the parser
  Text ->
  IO [Text]
checkErrors c input = do
  Right doc <- pure (MMark.parse "" input)
  pure $ case MMark.runCheck c doc of
    Right _ -> []
    Left errs -> summarize (errorBundlePretty errs)

-- | Run an action in a fresh empty directory, which is removed afterwards.
withTempDir :: (FilePath -> IO a) -> IO a
withTempDir = bracket acquire removeDirectoryRecursive
  where
    -- 'openTempFile' is the only way base offers to get a name nothing else
    -- has taken, so take one and swap the file for a directory.
    acquire = do
      tmp <- getTemporaryDirectory
      (path, h) <- openTempFile tmp "mmark-ext-test"
      hClose h
      removeFile path
      createDirectory path
      return path