{- | Copyright: © 2020 James Alexander Feldman-Crough License: MPL-2.0 -} module Main (main) where import Data.IORef import System.IO.Unsafe import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck import Control.Exception (evaluate) import Data.Foldable (for_, toList) import Prelude hiding (lines) import qualified Data.Text as T import qualified Data.Text.Lazy as LT import qualified ProSource as PS main :: IO () main = defaultMain $ testGroup "test" [tests] tests :: TestTree tests = testGroup "source" [ testEmptyLineMap , testSimpleLineMap , testLineDelimiters , testLocation , testLocationLazy , propLineOffset ] testEmptyLineMap :: TestTree testEmptyLineMap = testCase "empty" $ do let source = PS.makeSource "" "" 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 "" $ LT.unlines (map LT.fromStrict 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 "" "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 "" "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 "" "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 "" (LT.fromStrict text), initialOffset) genChar = listOf $ frequency [(10, elements ['a' .. 'z']), (4, pure ' '), (1, pure '\n')] checkEvaluated :: a -> IO (a, IO Bool) checkEvaluated val = do ref <- newIORef False pure (unsafePerformIO (writeIORef ref True) `seq` val, readIORef ref)