graft-0.1.0.0: test/BuilderTests.hs
{-# LANGUAGE OverloadedStrings #-}
module BuilderTests (builderProperties) where
import Control.Monad (foldM)
import Data.Char (ord)
import qualified Data.List as List
import qualified Data.Text as Text
import Graft
import qualified Graft.Build as Build
import qualified Graft.Builder as Builder
import qualified Graft.Green as Green
import Test.QuickCheck
import TestModel (halfMax, hugeDepth, overflows)
-- An independent source-ordered tree, with no caches or builder positions.
data Tree = Branch Int [Tree] | Leaf Int String
deriving (Eq, Show)
instance Arbitrary Tree where
arbitrary = sized genTree
shrink (Leaf kind text) = [Leaf kind s | s <- shrink text]
shrink (Branch kind items) = items ++ [Branch kind xs | xs <- shrinkList shrink items]
genTree :: Int -> Gen Tree
genTree size
| size <= 0 = Leaf <$> chooseInt (0, 3) <*> genText
| otherwise = frequency
[ (2, genTree 0)
, (3, do
count <- chooseInt (0, min 4 size)
Branch <$> chooseInt (0, 3) <*> vectorOf count (genTree (size `div` (count + 1))))
]
where
genText = chooseInt (0, 12) >>= \n -> vectorOf n
(elements "ab09 \t\r\n\0éλ中😀\x0301\x10FFFF")
genForest :: Gen [Tree]
genForest = chooseInt (0, 5) >>= \n -> vectorOf n (resize 12 arbitrary)
source :: Tree -> String
source (Leaf _ text) = text
source (Branch _ items) = concatMap source items
byteLength :: String -> TextLength
byteLength = sum . map width
where
width c
| ord c <= 0x7F = 1
| ord c <= 0x7FF = 2
| ord c <= 0xFFFF = 3
| otherwise = 4
compile :: Tree -> GreenElement Int
compile (Leaf kind text) = Token (Green.token kind (Text.pack text))
compile (Branch kind items) = Node (Green.node kind (map compile items))
decode :: GreenElement Int -> Tree
decode (Token token) = Leaf (Green.tokenKind token) (Text.unpack (Green.text token))
decode (Node node) = Branch (Green.nodeKind node) (map decode (Green.children node))
cacheChecks :: GreenElement Int -> Property
cacheChecks element = conjoin $
(Green.length element === byteLength (source (decode element))) : case element of
Token _ -> []
Node node -> map cacheChecks (Green.children node)
matches :: Tree -> Either BuilderError (GreenNode Int) -> Property
matches expected result = case result of
Left err -> counterexample (show err) False
Right node -> conjoin
[ decode (Node node) === expected
, Text.unpack (Green.renderNode node) === source expected
, cacheChecks (Node node)
]
-- Stream start/token/finish events from the oracle rather than using Green.node.
emit :: Tree -> Builder Int -> Either BuilderError (Builder Int)
emit (Leaf kind text) builder = Right (Builder.token kind (Text.pack text) builder)
emit (Branch kind items) builder =
foldM (flip emit) (Builder.startNode kind builder) items >>= Builder.finishNode
emitAll :: [Tree] -> Builder Int -> Either BuilderError (Builder Int)
emitAll items builder = foldM (flip emit) builder items
-- The monadic layer streams the same events with bracketed nodes.
build :: Tree -> Build Int ()
build (Leaf kind text) = Build.token kind (Text.pack text)
build (Branch kind items) = Build.node kind (mapM_ build items)
propMonadicConstruction :: Tree -> Property
propMonadicConstruction tree = conjoin
[ matches (Branch 9 [tree]) (Build.exec (Build.node 9 (build tree)))
, fmap fst (Build.run (Build.node 9 (build tree >> pure 'x'))) === Right 'x'
]
propMonadicWrapping :: Property
propMonadicWrapping = forAll ((,,) <$> genForest <*> genForest <*> genForest) $ \(prefix, middle, suffix) ->
matches (Branch 9 (prefix ++ [Branch 8 (middle ++ suffix)])) $ Build.exec $ Build.node 9 $ do
mapM_ build prefix
marker <- Build.checkpoint
mapM_ build middle
Build.nodeAt marker 8 (mapM_ build suffix)
propMonadicBacktracking :: Property
propMonadicBacktracking = forAll ((,) <$> genForest <*> genForest) $ \(abandoned, kept) ->
matches (Branch 9 kept) $ Build.exec $ Build.node 9 $ do
saved <- Build.save
mapM_ build abandoned
Build.restore saved
mapM_ build kept
propConstruction :: Tree -> Property
propConstruction tree = matches (Branch 9 [tree]) $ do
builder <- emit tree (Builder.startNode 9 Builder.empty)
Builder.finishNode builder >>= Builder.finish
propWrapping :: Property
propWrapping = forAll ((,,) <$> genForest <*> genForest <*> genForest) $ \(prefix, middle, suffix) ->
classify (null middle) "empty initial suffix" $
classify (not (null suffix)) "emission after wrapping starts" $
matches (Branch 9 (prefix ++ [Branch 8 (middle ++ suffix)])) $ do
before <- emitAll prefix (Builder.startNode 9 Builder.empty)
let marker = Builder.checkpoint before
pending <- emitAll middle before
wrapped <- Builder.startNodeAt marker 8 pending
extended <- emitAll suffix wrapped
Builder.finishNode extended >>= Builder.finishNode >>= Builder.finish
propRepeatedWrapping :: Property
propRepeatedWrapping = forAll genForest $ \items ->
let seed = Leaf 1 "λ"
expected = Branch 9 [List.foldl' (\left right -> Branch 8 [left, right]) seed items]
initial = Builder.startNode 9 Builder.empty
marker = Builder.checkpoint initial
wrap builder item = do
opened <- Builder.startNodeAt marker 8 builder
emit item opened >>= Builder.finishNode
in matches expected $ do
first <- emit seed initial
folded <- foldM wrap first items
Builder.finishNode folded >>= Builder.finish
propBranching :: Property
propBranching = forAll ((,,) <$> genForest <*> genForest <*> genForest) $ \(prefix, left, right) ->
let saved = List.foldl' (flip (Builder.element . compile)) (Builder.startNode 9 Builder.empty) prefix
complete items = emitAll items saved >>= Builder.finishNode >>= Builder.finish
in conjoin
[ matches (Branch 9 (prefix ++ left)) (complete left)
, matches (Branch 9 (prefix ++ right)) (complete right)
, matches (Branch 9 prefix) (Builder.finishNode saved >>= Builder.finish)
]
propCheckpointBounds :: Property
propCheckpointBounds = forAll (chooseInt (2, 30)) $ \count ->
let initial = Builder.startNode 9 Builder.empty
pending = List.foldl' (\builder _ -> Builder.token 1 "x" builder) initial [1 .. count]
marker = Builder.checkpoint pending
prefix = List.foldl' (\builder _ -> Builder.token 1 "x" builder)
(Builder.empty :: Builder Int) [1 .. count]
in case Builder.finishNode pending of
Left err -> counterexample (show err) False
Right collapsed -> conjoin
[ Builder.startNodeAt marker 8 collapsed === Left (CheckpointOutOfBounds count 1)
, Builder.startNodeAt (Builder.checkpoint Builder.empty) 8 (Builder.startNode 9 prefix)
=== Left (CheckpointBeforeCurrentNode 0 count)
, matches (Branch 9 (replicate count (Leaf 1 "x"))) (Builder.finish collapsed)
]
propBalance :: Property
propBalance = forAll (chooseInt (1, 30)) $ \depth ->
let opened = List.foldl' (\builder kind -> Builder.startNode kind builder) Builder.empty [1 .. depth]
-- At each step an open node must be reported, even if the forest already
-- contains one completed node. Finishing cannot implicitly close it.
check remaining builder = conjoin
[ Builder.finish builder === Left (UnclosedNodes remaining)
, case Builder.finishNode builder of
Left err -> counterexample (show err) False
Right closed
| remaining == 1 -> matches (foldr (\kind inner -> Branch kind [inner])
(Branch depth []) [1 .. depth - 1]) (Builder.finish closed)
| otherwise -> check (remaining - 1) closed
]
in check depth opened
regressions :: Property
regressions = once $ conjoin
[ Builder.finishNode (Builder.empty :: Builder Int) === Left NoOpenNode
, Builder.finish (Builder.empty :: Builder Int) === Left EmptyBuilder
, Builder.finish (Builder.token 1 "" blank) === Left TokenRoot
, Builder.finish (Builder.token 1 "a" (Builder.token 1 "b" blank)) === Left (MultipleRoots 2)
, Builder.finish (Builder.startNode 9 (Builder.token 1 "a" blank)) === Left (UnclosedNodes 1)
, matches (Branch 9 []) (Builder.finishNode (Builder.startNode 9 Builder.empty) >>= Builder.finish)
, matches (Branch 9 [Leaf 1 "a", Leaf 1 "b"]) (do
let pending = Builder.token 1 "b" (Builder.token 1 "a" Builder.empty)
opened <- Builder.startNodeAt (Builder.checkpoint Builder.empty) 9 pending
Builder.finishNode opened >>= Builder.finish)
, matches (Branch 9 [Branch 8 [Leaf 1 "a"]]) (do
let initial = Builder.startNode 9 Builder.empty
marker = Builder.checkpoint initial
outer <- Builder.startNodeAt marker 8 (Builder.token 1 "a" initial)
Builder.finishNode outer >>= Builder.finishNode >>= Builder.finish)
-- The end position may be reused to create successive empty nodes.
, matches (Branch 9 [Branch 8 [], Branch 7 []]) (do
let initial = Builder.startNode 9 Builder.empty
emptyNode <- Builder.startNodeAt (Builder.checkpoint initial) 8 initial >>= Builder.finishNode
next <- Builder.startNodeAt (Builder.checkpoint emptyNode) 7 emptyNode >>= Builder.finishNode
Builder.finishNode next >>= Builder.finish)
-- Markers are numeric positions, with no provenance or history validation.
, matches (Branch 9 [Leaf 1 "a", Branch 8 [Leaf 1 "b"]]) (do
let foreignMarker = Builder.checkpoint (Builder.token (1 :: Int) "unrelated" Builder.empty)
initial = Builder.startNode 9 Builder.empty
pending = Builder.token 1 "b" (Builder.token 1 "a" initial)
opened <- Builder.startNodeAt foreignMarker 8 pending
Builder.finishNode opened >>= Builder.finishNode >>= Builder.finish)
, matches (Branch 7 [Branch 9 [Leaf 1 "a", Leaf 1 "b"], Leaf 1 "c", Branch 8 []]) (do
let initial = Builder.startNode 9 Builder.empty
pending = Builder.token 1 "b" (Builder.token 1 "a" initial)
oldMarker = Builder.checkpoint pending
collapsed <- Builder.finishNode pending
let revived = Builder.token 1 "c" collapsed
opened <- Builder.startNodeAt oldMarker 8 revived
-- The collapsed root and new token precede the revived position.
emptyNode <- Builder.finishNode opened
Builder.startNodeAt (Builder.checkpoint Builder.empty) 7 emptyNode >>= Builder.finishNode >>= Builder.finish)
-- Show prints calls that rebuild an equal builder, with open nodes placed
-- at their start positions even when they were opened at a checkpoint.
, either (\err -> counterexample (show err) False) (\opened -> show opened === concat
[ "Builder.element (Token (Green.token 1 \"b\")) "
, "(Builder.element (Token (Green.token 1 \"a\")) (Builder.startNode 8 (Builder.startNode 9 Builder.empty)))"
]) (do
let initial = Builder.startNode 9 blank
Builder.startNodeAt (Builder.checkpoint initial) 8 (Builder.token 1 "b" (Builder.token 1 "a" initial)))
, show (Builder.empty :: Builder Int) === "Builder.empty"
-- Reused subtrees whose lengths overflow fail when the node closes.
, overflows (Builder.finishNode (Builder.element (halfMax (1 :: Int)) (Builder.element (halfMax (1 :: Int)) (Builder.startNode 9 Builder.empty))))
, conjoin
[ Green.nodeLength hugeRoot === 2 ^ hugeDepth
, Green.nodeKind hugeRoot === 9
, length (Green.children hugeRoot) === 1
]
]
where
blank :: Builder Int
blank = Builder.empty
-- Reuse cached huge lengths without rendering or comparing the shared DAG.
huge :: GreenElement Int
huge = iterate (\element -> Node (Green.node 8 [element, element]))
(Token (Green.token 1 "x")) !! hugeDepth
hugeRoot = case Builder.finishNode (Builder.element huge (Builder.startNode 9 Builder.empty)) >>= Builder.finish of
Right node -> node
Left err -> error (show err)
monadicErrors :: Property
monadicErrors = once $ conjoin
[ Build.exec (Build.finishNode :: Build Int ()) === Left NoOpenNode
, Build.exec (Build.startNode (9 :: Int)) === Left (UnclosedNodes 1)
, Build.exec (pure () :: Build Int ()) === Left EmptyBuilder
, Build.exec (Build.token (1 :: Int) "a") === Left TokenRoot
, Build.exec (Build.node (9 :: Int) (Build.token 1 "a") >> Build.node 9 (pure ())) === Left (MultipleRoots 2)
-- A checkpoint taken inside a node lies past the end once it closes.
, Build.exec (Build.node (9 :: Int) (Build.token 1 "a"
>> Build.node 8 (Build.token 1 "b" >> Build.token 1 "c" >> Build.checkpoint)
>>= \marker -> Build.nodeAt marker 7 (pure ())))
=== Left (CheckpointOutOfBounds 3 2)
]
builderProperties :: [(String, Property)]
builderProperties =
[ ("builder streamed construction, rendering and caches", property propConstruction)
, ("builder checkpoint wrapping with later emission", propWrapping)
, ("builder repeated checkpoint wrapping", propRepeatedWrapping)
, ("builder persistent branches and green element reuse", propBranching)
, ("builder checkpoint bounds and recovery", propCheckpointBounds)
, ("builder balanced construction at every close", propBalance)
, ("builder empty, root, checkpoint lifetime and shared-length regressions", regressions)
, ("monadic builder construction and results", property propMonadicConstruction)
, ("monadic builder checkpoint wrapping", propMonadicWrapping)
, ("monadic builder saved states", propMonadicBacktracking)
, ("monadic builder errors", monadicErrors)
]