packages feed

lima-0.2.1.2: test/Conversions/Main.hs

{-# OPTIONS_GHC -fplugin Debug.Breakpoint #-}

import Converter (Config, Format (..), Token (..), Tokens, User, def, exampleNonTexTokens, exampleTexTokens, normalizeTokens, selectFromTokens, selectToTokens, showFormatExtension, showFormatName, texHaskellCodeEnd, texHaskellCodeStart, toConfigInternal)
import Converter.Internal (stripEmpties)
import Data.List.NonEmpty (NonEmpty (..))
import Data.String.Interpolate (i)
import Data.Text qualified as T
import Data.Text.IO qualified as T
import Hedgehog (Gen, MonadGen, MonadTest, Property, property, tripping)
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Lens.Micro ((^.))
import System.Directory (createDirectoryIfMissing)
import System.IO (IOMode (WriteMode), withFile)
import Test.Tasty (TestTree, defaultMain, testGroup, withResource)
import Test.Tasty.HUnit (assertEqual, testCase)
import Test.Tasty.Hedgehog (testProperty)
import Text.Pretty.Simple (CheckColorTty (..), OutputOptions (..), StringOutputStyle (Literal), defaultOutputOptionsDarkBg, pHPrintOpt)

testDir :: String
testDir = "testdata"

main :: IO ()
main =
  defaultMain $
    testGroup
      "Top"
      [ testWrite Md
      , testWrite TeX
      , testTripping TeX
      , testTripping Md
      ]

-- How many tokens to generate for tripping tests
_N_TOKENS :: Int
_N_TOKENS = 1000

-- TODO test initial haskell code indentation is the same as after parsing
-- so, generate lines with indentation relative to the previous indent token

-- TODO test no newline as the first line in fromTokens

-- TODO no newlines in the generated lines

--

selectDialectName :: Format -> String
selectDialectName = \case
  TeX -> showFormatName TeX
  _ -> "Non-" <> showFormatName TeX

testDialectWrite :: FilePath -> Tokens -> Format -> TestTree
testDialectWrite dir tokens format =
  testCase [i|Write #{showFormatName format} code|] do
    let text = selectFromTokens def format tokens
    T.writeFile [i|#{dir}/test.#{showFormatExtension format}|] text
    assertEqual "Roundtrip" tokens (selectToTokens def format text)

writeTokens :: FilePath -> Tokens -> TestTree
writeTokens dir tokens = testCase "Write tokens to a file" $
  withFile [i|#{dir}/tokens.hs|] WriteMode $
    \h ->
      pHPrintOpt
        CheckColorTty
        ( defaultOutputOptionsDarkBg
            { outputOptionsStringStyle = Literal
            }
        )
        h
        tokens

testFormatsWrite :: FilePath -> [Format] -> Tokens -> [TestTree]
testFormatsWrite dir formats tokens =
  (testDialectWrite dir tokens <$> formats) <> [writeTokens dir tokens]

selectTokens :: Format -> Tokens
selectTokens = \case
  TeX -> exampleTexTokens
  _ -> exampleNonTexTokens

selectFormats :: Format -> [Format]
selectFormats = \case
  TeX -> [Hs, Md, Lhs, TeX]
  _ -> [Hs, Lhs, Md]

testWrite :: Format -> TestTree
testWrite format =
  let dir = [i|#{testDir}/#{showFormatExtension format}|]
   in withResource (createDirectoryIfMissing True dir) pure $
        const $
          testGroup [i|Using #{selectDialectName format} tokens|] $
            (testFormatsWrite dir (selectFormats format) (selectTokens format))

alphabet :: MonadGen m => m Char
alphabet = Gen.alphaNum

genNonEmpty :: Gen (NonEmpty T.Text)
genNonEmpty = Gen.nonEmpty (Range.constant 1 5) genLine

genLine :: Gen T.Text
genLine = do
  indent <- Gen.text (Range.constant 0 5) (pure ' ')
  content <- Gen.text (Range.constant 1 5) alphabet
  pure $ indent <> content

genLines :: Gen [T.Text]
genLines = Gen.list (Range.constant 1 5) genLine

-- Tokens

genIndent :: Gen Token
genIndent = do
  n <- Gen.int (Range.constant 0 10)
  pure Indent{..}

genDedent :: Gen Token
genDedent = pure Dedent

genComment :: Gen Token
genComment = do
  someLines <- genNonEmpty
  pure $ Comment{someLines}

genText :: Gen Token
genText = do
  someLines <- genNonEmpty
  pure Text{..}

genCommentSingleLine :: Gen Token
genCommentSingleLine = do
  someLine <- genLine
  pure CommentSingleLine{..}

genNonDisabled :: (?genCode :: GenCode) => Gen Tokens
genNonDisabled =
  Gen.choice $
    [?genCode] <> (((: []) <$>) <$> [genComment, genCommentSingleLine, genText, genIndent, genDedent])

genDisabled :: Env => Gen Token
genDisabled = do
  tokens <- genNonDisabled
  let manyLines = reverse $ T.lines $ selectFromTokens ?config ?format tokens
  pure $ Disabled{manyLines}

genTokensSublist :: Env => Gen Tokens
genTokensSublist =
  Gen.choice $ [genNonDisabled, (: []) <$> genDisabled]

genTokens :: Env => Gen Tokens
genTokens = do
  subLists <- Gen.list (Range.singleton _N_TOKENS) genTokensSublist
  pure $ normalizeTokens $ concat subLists

-- Code tokens from tex files can only be recognized in a specific sequence
-- That's why, they're generated in a list
texGenCode :: (?config :: Config User) => Gen [Token]
texGenCode = do
  let config = toConfigInternal ?config
  manyLines <- genLines
  let manyLines' = if null (stripEmpties manyLines) then ["a"] else manyLines
  pure $
    [ Text{someLines = config ^. texHaskellCodeStart :| []}
    , HaskellCode{manyLines = manyLines'}
    , Text{someLines = config ^. texHaskellCodeEnd :| []}
    ]

-- Code tokens from tex files can only be recognized in a specific sequence
-- That's why, they're generated in a list
nonTexGenCode :: Gen [Token]
nonTexGenCode = do
  manyLines <- genLines
  let manyLines' = if null (stripEmpties manyLines) then ["a"] else manyLines
  pure [HaskellCode{manyLines = manyLines'}]

-- Tripping
trippingTokens :: (MonadTest m, ?config :: Config User, ?format :: Format) => Tokens -> m ()
trippingTokens tokens =
  tripping tokens (selectFromTokens ?config ?format) (Just . selectToTokens ?config ?format)

testTrippingTokens :: Env => Property
testTrippingTokens = property do
  tokens <- Gen.sample $ genTokens
  -- breakpointM
  trippingTokens tokens

type GenCode = Gen [Token]

type Env = (?config :: Config User, ?format :: Format, ?genCode :: GenCode)

selectGenCode :: Format -> ((?config :: Config User) => GenCode)
selectGenCode = \case
  TeX -> texGenCode
  _ -> nonTexGenCode

testTripping :: Format -> TestTree
testTripping format =
  let ?config = def; ?format = format
   in let ?genCode = selectGenCode format
       in testProperty [i|Roundtrip #{selectDialectName format} tokens for all formats|] $ testTrippingTokens