graft-0.1.0.0: test/CursorTests.hs
module CursorTests (cursorProperties) where
import qualified Data.Text as Text
import Graft
import qualified Graft.Cursor as Cursor
import qualified Graft.Range as Range
import Test.QuickCheck
import TestModel
-- One step of a program run on a cursor and on the model.
data Step
= Up
| Next
| Previous
| First
| Last
| Child Int
| Top
| Look
| Lookup Int
deriving (Show)
genStep :: Gen Step
genStep = frequency
[ (4, pure Next), (2, pure Previous), (3, pure Up), (3, pure First), (1, pure Last)
, (1, Child <$> chooseInt (0, 4)), (1, pure Top)
, (2, pure Look), (1, Lookup <$> chooseInt (0, 40))
]
shrinkStep :: Step -> [Step]
shrinkStep (Child index) = Child <$> shrink index
shrinkStep (Lookup offset) = Lookup <$> shrink offset
shrinkStep _ = []
modelAt :: Path -> Model -> Maybe Model
modelAt [] model = Just model
modelAt (index : rest) (Branch _ items)
| index >= 0, index < length items = modelAt rest (items !! index)
modelAt _ _ = Nothing
modelKind :: Model -> Kind
modelKind (Branch kind _) = kind
modelKind (Leaf kind _) = kind
modelChildren :: Model -> [Model]
modelChildren (Branch _ items) = items
modelChildren (Leaf _ _) = []
isLeaf :: Model -> Bool
isLeaf (Leaf _ _) = True
isLeaf (Branch _ _) = False
-- The path, range and kind of an occurrence, from the model's own text.
expected :: Model -> Path -> (Path, TextRange, Kind, Bool)
expected model path = case [(prefix, selected) | (other, prefix, selected, _) <- contexts model, other == path] of
(prefix, selected) : _ ->
(path, Range.fromLength (byteLength prefix) (byteLength (source selected)), modelKind selected, isLeaf selected)
[] -> error ("no occurrence at " ++ show path)
observe :: Cursor Kind -> (Path, TextRange, Kind, Bool)
observe cursor = (Cursor.path cursor, Cursor.range cursor, Cursor.kind cursor, Cursor.isToken cursor)
-- Where a step leads in the model, or Nothing where the cursor has nowhere
-- to go.
target :: Model -> Path -> Step -> Maybe Path
target model path step = case step of
Up -> case reverse path of
_ : above -> Just (reverse above)
[] -> Nothing
Next -> sibling 1
Previous -> sibling (-1)
First -> child 0
Last -> child (length focusChildren - 1)
Child index -> child index
Top -> Just []
_ -> Just path
where
focusChildren = maybe [] modelChildren (modelAt path model)
child index
| index >= 0, index < length focusChildren = Just (path ++ [index])
| otherwise = Nothing
sibling offset = case reverse path of
index : above
| Just above' <- modelAt (reverse above) model
, let next = index + offset
, next >= 0, next < length (modelChildren above') -> Just (reverse above ++ [next])
_ -> Nothing
move :: Step -> Cursor Kind -> Maybe (Cursor Kind)
move step cursor = case step of
Up -> Cursor.parent cursor
Next -> Cursor.nextSibling cursor
Previous -> Cursor.previousSibling cursor
First -> Cursor.firstChild cursor
Last -> Cursor.lastChild cursor
Child index -> either (const Nothing) Just (Cursor.childAt index cursor)
Top -> Just (Cursor.root cursor)
_ -> Just cursor
-- Run a program on a cursor and on the model. After every step the focus
-- and its ancestors agree with the model; reading steps also compare the
-- focus, its children and offset lookups from the root; at the end the
-- cursor is still in the tree it started in.
propProgram :: Model -> Property
propProgram model = forAllShrink (listOf genStep) (shrinkList shrinkStep) $ \steps ->
go (Cursor.fromGreen tree) [] steps
where
tree = compile model
go cursor path [] =
counterexample "tree kept" (Cursor.tree cursor === tree)
.&&. counterexample "path kept" (Cursor.path cursor === path)
go cursor path (step : rest) = case (move step cursor, target model path step) of
(Nothing, Nothing) -> go cursor path rest
(Just moved, Just path') ->
counterexample (show step) (check moved path' step) .&&. go moved path' rest
(moved, path') ->
counterexample (show step ++ ": cursor " ++ show (fmap Cursor.path moved) ++ ", model " ++ show path')
False
check cursor path step = conjoin $
[ observe cursor === expected model path
, map observe (Cursor.ancestors cursor) === map (expected model) (drop 1 (reverse (inits' path)))
] ++ case step of
Look ->
[ Cursor.green cursor === maybe (error "no focus") compile (modelAt path model)
, Text.unpack (Cursor.text cursor) === maybe "" source (modelAt path model)
, map observe (Cursor.children cursor)
=== [expected model (path ++ [index]) | index <- [0 .. length (maybe [] modelChildren (modelAt path model)) - 1]]
]
Lookup offset ->
let fresh = Cursor.fromGreen tree
position = fromIntegral offset
query = Range.fromLength position 0
in [ fmap observe (Cursor.tokenAt position (Cursor.root cursor)) === fmap observe (Cursor.tokenAt position fresh)
, fmap observe (Cursor.covering query (Cursor.root cursor)) === fmap observe (Cursor.covering query fresh)
]
_ -> []
inits' path = [take count path | count <- [0 .. length path]]
cursorProperties :: [(String, Property)]
cursorProperties =
[ ("cursor programs of moves and lookups against the model", property propProgram)
]