packages feed

dino-0.1: test/DiffTest.hs

{-# LANGUAGE TemplateHaskell #-}

{-# OPTIONS_GHC -Wno-missing-signatures #-}
{-# OPTIONS_GHC -Wno-orphans #-}

module DiffTest where

import Prelude

import Control.Monad (replicateM)
import qualified Data.HashMap.Strict as HM
import Data.Maybe (fromJust, isJust, isNothing)
import Data.Text (Text)
import qualified Data.Text as Text

import Dino.Pretty
import Dino.AST
import Dino.AST.Diff hiding (new)

import Test.Tasty.QuickCheck
import Test.Tasty.TH

instance Arbitrary NameType where
  arbitrary = oneof $ map pure [minBound .. maxBound]

-- | Generate a short text consisting of alphabetic characters
instance Arbitrary Text where
  arbitrary = do
    n <- choose (1,6)
    fmap Text.pack $ vectorOf n $ choose ('a', 'z')

  shrink "v" = []
  shrink _   = ["v"]

instance Arbitrary Field where
  arbitrary = do
    n <- choose (1,6)
    fmap Field $ vectorOf n $ choose ('a', 'z')

instance Arbitrary Constr where
  arbitrary = frequency
    [ (1, return List)
    , (1, return Tuple)
    , (3, Named <$> arbitrary <*> arbitrary)
    ]

  shrink List = []
  shrink _    = [List]

instance Arbitrary a => Arbitrary (Mapping Field a) where
  arbitrary = sized $ \s -> do
    n <- choose (0, s)
    Mapping Unimportant . HM.fromList <$>
      replicateM n ((,) <$> arbitrary <*> resize (s `div` n) arbitrary)

-- | Generate an 'AST' of bounded size
genAST :: Arbitrary a => Int -> Gen (AST a)
genAST s =
  frequency
    [ (1, Number <$> arbitrary)
    , (1, Text <$> arbitrary)
    , (s, ) $ do
        n <- choose (0, s)
        App <$> arbitrary <*> replicateM n (genAST (s `div` n))
    , (s, ) $ Let <$> arbitrary <*> genAST (s `div` 2) <*> genAST (s `div` 2)
    , (s, ) . fmap Record $ resize s arbitrary
    ]

shrinkAST :: Arbitrary a => AST a -> [AST a]
shrinkAST (Number _) = [Text ""]
shrinkAST (Text _) = []
shrinkAST (App c as) = as ++ map (uncurry App) (shrink (c, as))
shrinkAST (Let v a b) =
  [a, b] ++ [Let v' a' b' | (a', b') <- shrink (a, b), v' <- shrink v]
shrinkAST (Record (Mapping imp rec)) =
  HM.elems rec ++
  [App List $ HM.elems rec] ++
  map
    (Record . Mapping imp . HM.fromList)
    (shrinkList (\(f, a) -> map (f, ) $ shrink a) $ HM.toList rec)

instance Arbitrary a => Arbitrary (AST a) where
  arbitrary = sized genAST
  shrink = shrinkAST

oldOrNew :: Arbitrary a => a -> Gen a
oldOrNew a = frequency [(1, resize 3 arbitrary), (5, return a)]

class ArbitraryRelative a where
  -- | Generate a value by applying small changes to an existing value
  genRelative :: a -> Gen a

  default genRelative :: Arbitrary a => a -> Gen a
  genRelative = oldOrNew

instance (Arbitrary a, ArbitraryRelative a) => ArbitraryRelative [a] where
  genRelative as = do
    as' <- mapM (\a -> frequency [(4, return a), (1, genRelative a)]) as
    frequency
      [ (3, ) $ return as'
      , (1, ) $ do
          n <- choose (0, 2)
          return $ dropEnd n as'
      , (1, ) $ do
          n <- choose (0, 2)
          (as' ++) <$> replicateM n (resize 3 arbitrary)
      ]

instance (Arbitrary a, ArbitraryRelative a) => ArbitraryRelative (Maybe a) where
  genRelative Nothing  = oldOrNew Nothing
  genRelative (Just a) = frequency
    [ (1, return Nothing)
    , (4, Just <$> genRelative a)
    ]

instance (Arbitrary a, ArbitraryRelative a) =>
         ArbitraryRelative (Mapping Field a) where
  genRelative (Mapping imp m) =
    frequency
      [ ( 4
        , do m' <-
               fmap (HM.mapMaybe id) $
               flip traverse m $ \a ->
                 frequency
                   [ (3, Just <$> return a)
                   , (1, Just <$> genRelative a)
                   , (1, return Nothing)
                   ]
             Mapping _ m'' <- resize 3 arbitrary
             return $ Mapping imp $ HM.union m'' m')
      , (1, resize 5 arbitrary)
      ]

instance (ArbitraryRelative a, ArbitraryRelative b) =>
         ArbitraryRelative (a, b) where
  genRelative (a, b) = (,) <$> genRelative a <*> genRelative b

instance ArbitraryRelative Constr

instance Arbitrary a => ArbitraryRelative (AST a) where
  genRelative (Number a) = frequency
    [ (4, return $ Number a)
    , (2, Number <$> arbitrary)
    , (1, genAST 3)
    ]
  genRelative (Text a) = frequency
    [ (4, return $ Text a)
    , (2, Text <$> arbitrary)
    , (1, genAST 3)
    ]
  genRelative (App c as) = frequency
    [ (4, App <$> genRelative c <*> genRelative as)
    , (1, genAST 3)
    ]
  genRelative (Let v a b) = frequency
    [ (4, Let <$> oldOrNew v <*> genRelative a <*> genRelative b)
    , (1, genAST 3)
    ]
  genRelative (Record rec) = frequency
    [ (4, Record <$> genRelative rec)
    , (1, genAST 3)
    ]

genEdit :: Int -> Gen (Edit Int)
genEdit s = do
  ast <- genAST s
  rel <- genRelative ast
  maybe (genEdit s) return $ diff ast rel

-- | If the edit is empty, then @ast@ and @rel@ are equal
prop_diffEq1 =
  forAllShrink (genAST 8) shrinkAST $ \(a :: AST Int) ->
    forAllShrink (genRelative a) shrinkAST $ \rel ->
      isNothing (diff a rel) ==> a == rel

-- | If @ast@ and @rel@ are equal, then the edit is empty
prop_diffEq2 =
  forAllShrink (genAST 8) shrinkAST $ \(a :: AST Int) ->
    forAllShrink (genRelative a) shrinkAST $ \rel ->
      a == rel ==> isNothing (diff a rel)

-- | Applying the calculated edit to the original retrieves the new 'AST'
prop_diffApply (orig :: AST Int) =
  forAllShrink (genRelative orig) shrinkAST $ \new ->
    let e = diff orig new
     in isJust e ==> applyDiff (fromJust e) orig == Just new

-- TODO Test that the diff is "optimal". E.g. don't use `Replacement` when a
-- smaller representation of the diff exists.

-- TODO Property that tests when `applyDiff` must return `Nothing`. Probably
-- hard...

tests = $testGroupGenerator