nbparts-0.1.0.0: tests/Tests/Util/MarkdownSpec.hs
module Tests.Util.MarkdownSpec where
import Commonmark (SourceRange (SourceRange))
import Data.Function ((&))
import Data.List.NonEmpty qualified as NonEmptyList
import Data.Text (Text)
import Data.Text qualified as Text
import Hedgehog (Gen, evalMaybe, forAll, success, (===))
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Nbparts.Util.Markdown (blockSourceRangeToIndices, sourceRangeToIndices)
import Test.Hspec (Spec, context, describe, it, shouldBe)
import Test.Hspec.Hedgehog (hedgehog)
import Text.Parsec (Column, Line, SourcePos)
import Text.Parsec.Pos (newPos)
mkSrcRange :: Line -> Column -> Line -> Column -> SourceRange
mkSrcRange startLine startCol endLine endCol = SourceRange [(mkPos startLine startCol, mkPos endLine endCol)]
mkPos :: Line -> Column -> SourcePos
mkPos = newPos ""
genTextLines :: Gen [Text]
genTextLines =
Gen.list
(Range.linear 1 5)
(Gen.text (Range.linear 1 50) Gen.alphaNum) -- TODO: Figure out how to generate unicode text, but without newlines.
genNonOverlappingSourceRanges ::
[Text.Text] ->
Gen SourceRange
genNonOverlappingSourceRanges textLines = do
targetN <- Gen.int (Range.linear 2 (length textLines))
pairs <- go targetN 1
pure $ SourceRange pairs
where
maxLine = length textLines
lastLineLength = Text.length (textLines !! (maxLine - 1))
go :: Int -> Int -> Gen [(SourcePos, SourcePos)]
go 0 _ = pure []
go k minLine
| minLine > maxLine = pure []
| otherwise = do
sLine <- Gen.int (Range.linear minLine maxLine)
let sTextLine = textLines !! (sLine - 1)
sCol <- Gen.int (Range.linear 1 (Text.length sTextLine))
eLine <- Gen.int (Range.linear sLine maxLine)
let eTextLine = textLines !! (eLine - 1)
let minEndCol = if eLine == sLine then sCol + 1 else 1
eCol <- Gen.int (Range.linear minEndCol (Text.length eTextLine))
if eLine == maxLine && eCol == lastLineLength
then pure [(mkPos sLine sCol, mkPos eLine eCol)] -- Stop early
else do
rest <- go (k - 1) eLine
pure ((mkPos sLine sCol, mkPos eLine eCol) : rest)
spec :: Spec
spec = do
describe "sourceRangeToIndices" $ do
context "when given an empty list" $ do
it "returns Nothing" $ hedgehog $ do
sLine <- forAll $ Gen.int (Range.linear minBound maxBound)
sCol <- forAll $ Gen.int (Range.linear minBound maxBound)
eLine <- forAll $ Gen.int (Range.linear minBound maxBound)
eCol <- forAll $ Gen.int (Range.linear minBound maxBound)
sourceRangeToIndices [] (mkSrcRange sLine sCol eLine eCol) === Nothing
context "when given single-line text and a valid single-line span" $ do
it "returns `startCol - 1` and `endCol - 1`" $ do
textLine <- forAll $ Gen.text (Range.linear 1 50) Gen.unicode
sCol <- forAll $ Gen.int (Range.linear 1 (Text.length textLine))
eCol <- forAll $ Gen.int (Range.linear sCol (Text.length textLine + 1))
sourceRangeToIndices [textLine] (mkSrcRange 1 sCol 1 eCol) === Just (sCol - 1, eCol - 1)
context "when given multiple lines with valid line and col" $ do
it "returns the correct indices for a simple case" $ do
let mdLines = ["foo", "barbaz"]
sourceRangeToIndices mdLines (mkSrcRange 1 2 2 6) `shouldBe` Just (1, 9)
it "returns indices whose text span is equal to the one retrieved using the SourceRange" $ hedgehog $ do
textLines <- forAll genTextLines
sLine <- forAll $ Gen.int (Range.linear 1 (length textLines))
let sTextLine = textLines !! (sLine - 1)
sCol <- forAll $ Gen.int (Range.linear 1 (Text.length sTextLine))
eLine <- forAll $ Gen.int (Range.linear sLine (length textLines))
let eTextLine = textLines !! (eLine - 1)
eCol <- forAll $ Gen.int (Range.linear (if eLine == sLine then sCol else 1) (Text.length eTextLine + 1))
let textSpan =
if sLine == eLine
then
sTextLine
& Text.drop (sCol - 1)
& Text.take (eCol - sCol)
else
let spanStart = Text.drop (sCol - 1) sTextLine
spanMiddle = [txt | (line, txt) <- zip [1 ..] textLines, line > sLine, line < eLine]
spanEnd = Text.take (eCol - 1) eTextLine
in Text.intercalate "\n" ([spanStart] ++ spanMiddle ++ [spanEnd])
let text = Text.intercalate "\n" textLines
(sIdx, eIdx) <- evalMaybe $ sourceRangeToIndices textLines (mkSrcRange sLine sCol eLine eCol)
let textSpan' =
text
& Text.drop sIdx
& Text.take (eIdx - sIdx)
textSpan === textSpan'
context "when given non-positive indices" $ do
it "returns Nothing for non-positive start indices" $ hedgehog $ do
textLines <- forAll genTextLines
sLine <- forAll $ Gen.int (Range.linear minBound 0)
sCol <- forAll $ Gen.int (Range.linear minBound 0)
eLine <- forAll $ Gen.int (Range.linear 1 (length textLines))
let eTextLine = textLines !! (eLine - 1)
eCol <- forAll $ Gen.int (Range.linear 1 (Text.length eTextLine))
sourceRangeToIndices textLines (mkSrcRange sLine sCol eLine eCol) === Nothing
it "returns Nothing for non-positive end indices" $ hedgehog $ do
textLines <- forAll genTextLines
sLine <- forAll $ Gen.int (Range.linear 1 (length textLines))
let sTextLine = textLines !! (sLine - 1)
sCol <- forAll $ Gen.int (Range.linear 1 (Text.length sTextLine))
eLine <- forAll $ Gen.int (Range.linear minBound 0)
eCol <- forAll $ Gen.int (Range.linear minBound 0)
sourceRangeToIndices textLines (mkSrcRange sLine sCol eLine eCol) === Nothing
context "when given out-of-bounds start indices" $ do
it "returns Nothing" $ hedgehog $ do
textLines <- forAll genTextLines
sLine <- forAll $ Gen.int (Range.linear (length textLines + 1) maxBound)
sCol <- forAll $ Gen.int (Range.linear 1 maxBound)
eLine <- forAll $ Gen.int (Range.linear 1 (length textLines))
let eTextLine = textLines !! (eLine - 1)
eCol <- forAll $ Gen.int (Range.linear 1 (Text.length eTextLine))
sourceRangeToIndices textLines (mkSrcRange sLine sCol eLine eCol) === Nothing
context "when given out-of-bounds end indices" $ do
it "returns Nothing" $ hedgehog $ do
textLines <- forAll genTextLines
sLine <- forAll $ Gen.int (Range.linear 1 (length textLines))
let sTextLine = textLines !! (sLine - 1)
sCol <- forAll $ Gen.int (Range.linear 1 (Text.length sTextLine))
eLine <- forAll $ Gen.int (Range.linear (length textLines + 1) maxBound)
eCol <- forAll $ Gen.int (Range.linear 1 maxBound)
sourceRangeToIndices textLines (mkSrcRange sLine sCol eLine eCol) === Nothing
context "when given start indices that are after end indices" $ do
it "returns Nothing" $ hedgehog $ do
textLines <- forAll genTextLines
eLine <- forAll $ Gen.int (Range.linear 1 (length textLines))
let eTextLine = textLines !! (eLine - 1)
eCol <- forAll $ Gen.int (Range.linear 1 (Text.length eTextLine))
sLine <- forAll $ Gen.int (Range.linear eLine (length textLines))
let sTextLine = textLines !! (sLine - 1)
sCol <- forAll $ Gen.int (Range.linear (if sLine == eLine then eCol + 1 else 1) (Text.length sTextLine))
if sLine == eLine && sCol == eCol
then
-- It's still possible to generate a valid set of start and end indices with the generators above,
-- so we skip the check in that case.
success
else
sourceRangeToIndices textLines (mkSrcRange sLine sCol eLine eCol) === Nothing
context "when given multiple pairs of source positions" $ do
it "behaves as if given a pair spanning from the first start pos to the last end pos" $ hedgehog $ do
textLines <- forAll genTextLines
srcRange <- forAll $ genNonOverlappingSourceRanges textLines
let (SourceRange pairs) = srcRange
let pairs' = NonEmptyList.fromList pairs
let srcRangeUnified = SourceRange [(fst $ NonEmptyList.head pairs', snd $ NonEmptyList.last pairs')]
sourceRangeToIndices textLines srcRange === sourceRangeToIndices textLines srcRangeUnified
describe "blockSourceRangeToIndices" $ do
context "when given line and column valid for sourceRangeToIndices" $
it "behaves like sourceRangeToIndices" $
hedgehog $ do
textLines <- forAll genTextLines
sLine <- forAll $ Gen.int (Range.linear 1 (length textLines))
let sTextLine = textLines !! (sLine - 1)
sCol <- forAll $ Gen.int (Range.linear 1 (Text.length sTextLine))
eLine <- forAll $ Gen.int (Range.linear sLine (length textLines))
let eTextLine = textLines !! (eLine - 1)
eCol <- forAll $ Gen.int (Range.linear (if eLine == sLine then sCol else 1) (Text.length eTextLine + 1))
blockSourceRangeToIndices textLines (mkSrcRange sLine sCol eLine eCol)
=== sourceRangeToIndices textLines (mkSrcRange sLine sCol eLine eCol)
context "when given (lastLine + 1, 1) as the end index" $
it "returns the length of the text as the end index" $
hedgehog $ do
textLines <- forAll genTextLines
let text = Text.intercalate "\n" textLines
sLine <- forAll $ Gen.int (Range.linear 1 (length textLines))
let sTextLine = textLines !! (sLine - 1)
sCol <- forAll $ Gen.int (Range.linear 1 (Text.length sTextLine))
(snd <$> blockSourceRangeToIndices textLines (mkSrcRange sLine sCol (length textLines + 1) 1))
=== Just (Text.length text)