packages feed

prosidy-1.6.0.2: test/Prosidy/Test/Source.hs

{-# LANGUAGE OverloadedStrings #-}
module Prosidy.Test.Source (tests) where

import           Test.Tasty
import           Test.Tasty.HUnit
import           Test.Tasty.QuickCheck
import           Data.Foldable                  ( toList
                                                , for_
                                                )
import           Data.IORef
import           System.IO.Unsafe
import           Control.Exception              ( evaluate )

import qualified Prosidy.Source                as PS
import qualified Data.Text                     as T

tests :: TestTree
tests = testGroup
    "source"
    [ testEmptyLineMap
    , testSimpleLineMap
    , testLineDelimiters
    , testLocation
    , testLocationLazy
    , propLineOffset
    ]

testEmptyLineMap :: TestTree
testEmptyLineMap = testCase "empty" $ do
    let source = PS.makeSource "<test>" ""
    assertBool "An empty source generates an empty line map."
        $ null (PS.lineOffsets $ PS.sourceLineMap source)

testSimpleLineMap :: TestTree
testSimpleLineMap = testCase "simple" $ do
    let sourceLines =
            [ "This source file contains a few lines."
            , "Some, line the next, are empty."
            , ""
            , "Multiple consecutive empty lines are fine as well."
            , ""
            , ""
            , "終わり"
            ]
        source = PS.makeSource "<test>" $ T.unlines sourceLines
    toList (PS.lineOffsets $ PS.sourceLineMap source)
        @?= fmap PS.Offset [39, 71, 72, 123, 124, 125, 129]
    for_ (zip [PS.Line 0 ..] sourceLines) $ \(lineNumber, line) ->
        PS.getSourceLine lineNumber source @?= Just (line <> "\n")

testLineDelimiters :: TestTree
testLineDelimiters = testCase "endings" $ do
    let source = PS.makeSource "<test>" "abc\ndef\rghi\r\njkl"
        lines  = PS.sourceLineMap source
    PS.getSourceLine (PS.Line 0) source @?= Just "abc\n"
    PS.lineToOffset (PS.Line 0) lines @?= Just (PS.Offset 0)
    PS.getSourceLine (PS.Line 1) source @?= Just "def\r"
    PS.lineToOffset (PS.Line 1) lines @?= Just (PS.Offset 4)
    PS.getSourceLine (PS.Line 2) source @?= Just "ghi\r\n"
    PS.lineToOffset (PS.Line 2) lines @?= Just (PS.Offset 8)
    PS.getSourceLine (PS.Line 3) source @?= Just "jkl"
    PS.lineToOffset (PS.Line 3) lines @?= Just (PS.Offset 13)
    PS.getSourceLine (PS.Line 4) source @?= Nothing

testLocation :: TestTree
testLocation = testCase "location" $ do
    let source = PS.makeSource "<test>" "ab\ncd\nef"
        checkLocation offset line column = do
            let loc = PS.getLocation (PS.Offset offset) source
            fmap PS.locationOffset loc @?= Just (PS.Offset offset)
            fmap PS.locationLine loc @?= Just (PS.Line line)
            fmap PS.locationColumn loc @?= Just (PS.Column column)
    checkLocation 0 0 0
    checkLocation 1 0 1
    checkLocation 2 0 2
    checkLocation 3 1 0
    checkLocation 4 1 1
    checkLocation 5 1 2
    checkLocation 6 2 0
    checkLocation 7 2 1
    PS.getLocation (PS.Offset 8) source @?= Nothing

testLocationLazy :: TestTree
testLocationLazy = testCase "lazy" $ do
    let source   = PS.makeSource "<test>" "abc\ndef"
        Just loc = PS.getLocation (PS.Offset 5) source
    (line, checkLine) <- checkEvaluated (PS.locationLine loc)
    (col , checkCol ) <- checkEvaluated (PS.locationColumn loc)
    assertBool "line is unevaluated" . not =<< checkLine
    assertBool "col is unevaluated" . not =<< checkCol
    _ <- evaluate line
    assertBool "line is evaluated" =<< checkLine
    assertBool "col is unevaluated" . not =<< checkCol
    _ <- evaluate col
    assertBool "line is evaluated" =<< checkLine
    assertBool "col is evaluated" =<< checkCol

propLineOffset :: TestTree
propLineOffset =
    testProperty "line-and-offset" $ forAll gen $ \(source, initialOffset) ->
        let lineMap     = PS.sourceLineMap source
            line        = PS.offsetToLine initialOffset lineMap
            Just offset = PS.lineToOffset line lineMap
            line'       = PS.offsetToLine offset lineMap
        in  (initialOffset >= offset) .&&. (line === line')
  where
    gen = do
        text          <- T.pack <$> genChar
        initialOffset <-
            elements
                [PS.Offset 0 .. toEnum
                    (if T.null text then 0 else T.length text - 1)]
        pure (PS.makeSource "<text>" text, initialOffset)
    genChar =
        listOf $ frequency
            [(10, elements ['a' .. 'z']), (4, pure ' '), (1, pure '\n')]

{-# NOINLINE checkEvaluated #-}
checkEvaluated :: a -> IO (a, IO Bool)
checkEvaluated val = do
    ref <- newIORef False
    pure (unsafePerformIO (writeIORef ref True) `seq` val, readIORef ref)