{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE MonoLocalBinds #-} {-# LANGUAGE NoMonomorphismRestriction #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE UndecidableInstances #-} {-# OPTIONS -Wno-orphans#-} module PrettyParseTests where import Control.Monad import Data.Algorithm.Diff import Data.Algorithm.DiffOutput import Data.Char import Data.Fix import qualified Data.List.NonEmpty as NE import Data.Text (Text, pack) import Generic.Random import Nix.Atoms import Nix.Expr import Nix.Parser import Nix.Pretty import Test.QuickCheck.Instances.Semigroup () import Test.QuickCheck.Instances.Text () import qualified Test.QuickCheck.Property as P import Test.Tasty import Test.Tasty.QuickCheck hiding (Success, Failure) import Text.Megaparsec (Pos, SourcePos, mkPos) import Text.PrettyPrint.ANSI.Leijen ((), text) import qualified Text.PrettyPrint.ANSI.Leijen as P import qualified Text.Show.Pretty as PS -- Instead of using the Generic arbitrary instance (which doesn't exist anyway -- for Text), we use a different generator which just prints sensible looking -- variable names custom :: GenList '[Text] custom = asciiText :@ Nil asciiString :: Gen String asciiString = do n <- choose (1, 15) replicateM n (elements ['a'..'z']) asciiText :: Gen Text asciiText = pack <$> asciiString pcustom :: GenList '[Pos] pcustom = arbitrary :@ Nil -- | This generator generates selects one of the constructors uniformly and -- also decreases the size of the generator by dividing by the branching -- factor. This ensures sensible termination. genArb :: (GArbitrary (Options 'Sized '[Text]) a, GUniformWeight a) => Gen a genArb = genericArbitraryWith (setGenerators custom sizedOpts) uniform -- Might want to replace this instance with a constant value instance Arbitrary Pos where arbitrary = mkPos <$> (getSmall <$> arbitrary `suchThat` (> 0)) instance Arbitrary (f (Fix f)) => Arbitrary (Fix f) where arbitrary = genArb instance Arbitrary f => Arbitrary (NString f) where arbitrary = genArb instance Arbitrary SourcePos where arbitrary = genericArbitraryWith (setGenerators pcustom sizedOpts) uniform instance Arbitrary f => Arbitrary (Binding f) where arbitrary = genArb instance Arbitrary f => Arbitrary (NKeyName f) where arbitrary = oneof [ DynamicKey <$> arbitrary , StaticKey <$> asciiText ] instance Arbitrary f => Arbitrary (Params f) where arbitrary = oneof [ Param <$> asciiText , ParamSet <$> listOf ((,) <$> asciiText <*> arbitrary) <*> arbitrary <*> oneof [pure Nothing, Just <$> asciiText] ] instance Arbitrary NAtom where arbitrary = oneof [ NInt <$> arbitrary `suchThat` (>= 0) , NFloat <$> arbitrary `suchThat` (>= 0) , NBool <$> arbitrary , pure NNull ] instance Arbitrary NUnaryOp where arbitrary = genArb instance Arbitrary NBinaryOp where arbitrary = genArb instance (Arbitrary f) => Arbitrary (Antiquoted Text f) where arbitrary = genArb instance (Arbitrary f) => Arbitrary (Antiquoted (NString f) f) where arbitrary = genArb -- This is written by hand so we can use `fairList` rather than the normal -- list Arbitrary instance which makes the generator terminate. The -- distribution is not scientifically chosen. instance Arbitrary f => Arbitrary (NExprF f) where arbitrary = sized $ \n -> if n < 2 then oneof [genConstant, genStr, genSym, genLiteralPath, genEnvPath ] else frequency [ ( 1, genConstant) , ( 1, genSym) , ( 4, resize (n `div` 3) genIf) , (10, genRecSet ) , (20, genSet ) , ( 5, genList ) , ( 2, genUnary ) , ( 2, resize (n `div` 3) genBinary ) , ( 3, resize (n `div` 3) genSelect ) , (20, resize (n `div` 2) genAbs ) , ( 2, resize (n `div` 2) genHasAttr ) , (10, resize (n `div` 2) genLet ) , (10, resize (n `div` 2) genWith ) , ( 1, resize (n `div` 2) genAssert) ] where genConstant = NConstant <$> arbitrary genStr = NStr <$> arbitrary genSym = NSym <$> asciiText genList = NList <$> fairList arbitrary genSet = NSet <$> fairList arbitrary genRecSet = NRecSet <$> fairList arbitrary genLiteralPath = NLiteralPath . ("./" ++) <$> asciiString genEnvPath = NEnvPath <$> asciiString genUnary = NUnary <$> arbitrary <*> arbitrary genBinary = NBinary <$> arbitrary <*> arbitrary <*> arbitrary genSelect = NSelect <$> arbitrary <*> arbitrary <*> arbitrary genHasAttr = NHasAttr <$> arbitrary <*> arbitrary genAbs = NAbs <$> arbitrary <*> arbitrary genLet = NLet <$> fairList arbitrary <*> arbitrary genIf = NIf <$> arbitrary <*> arbitrary <*> arbitrary genWith = NWith <$> arbitrary <*> arbitrary genAssert = NAssert <$> arbitrary <*> arbitrary -- | Useful when there are recursive positions at each element of the list as -- it divides the size by the length of the generated list. fairList :: Gen a -> Gen [a] fairList g = do s <- getSize k <- choose (0, s) -- Use max here to avoid dividing by zero when there is the empty list resize (s `div` max 1 k) $ vectorOf k g equivUpToNormalization :: NExpr -> NExpr -> Bool equivUpToNormalization x y = normalize x == normalize y normalize :: NExpr -> NExpr normalize = cata $ \case NConstant (NInt n) | n < 0 -> Fix (NUnary NNeg (Fix (NConstant (NInt (negate n))))) NConstant (NFloat n) | n < 0 -> Fix (NUnary NNeg (Fix (NConstant (NFloat (negate n))))) NSet binds -> Fix (NSet (map normBinding binds)) NRecSet binds -> Fix (NRecSet (map normBinding binds)) NLet binds r -> Fix (NLet (map normBinding binds) r) NAbs params r -> Fix (NAbs (normParams params) r) r -> Fix r where normBinding (NamedVar path r pos) = NamedVar (NE.map normKey path) r pos normBinding (Inherit mr names pos) = Inherit mr (map normKey names) pos normKey (DynamicKey quoted) = DynamicKey (normAntiquotedString quoted) normKey (StaticKey name) = StaticKey name normAntiquotedString :: Antiquoted (NString NExpr) NExpr -> Antiquoted (NString NExpr) NExpr normAntiquotedString (Plain (DoubleQuoted [EscapedNewline])) = EscapedNewline normAntiquotedString (Plain (DoubleQuoted strs)) = let strs' = map normAntiquotedText strs in if strs == strs' then Plain (DoubleQuoted strs) else normAntiquotedString (Plain (DoubleQuoted strs')) normAntiquotedString r = r normAntiquotedText :: Antiquoted Text NExpr -> Antiquoted Text NExpr normAntiquotedText (Plain "\n") = EscapedNewline normAntiquotedText (Plain "''\n") = EscapedNewline normAntiquotedText r = r normParams (ParamSet binds var (Just "")) = ParamSet binds var Nothing normParams r = r -- | Test that parse . pretty == id up to attribute position information. prop_prettyparse :: NExpr -> P.Result prop_prettyparse p = let prog = show (pretty p) in case parse (pack prog) of Failure s -> P.rejected { P.reason = show $ text "Parse failed:" text (show s) P.<$> P.indent 2 (pretty p) } Success v | equivUpToNormalization p v -> P.succeeded | otherwise -> let pp = normalise prog pv = normalise (show (pretty v)) in (P.liftBool (pp == pv)) { P.reason = show $ text "----------------------------------------" P.<$> text "Expr before:" P.<$> P.indent 2 (text (PS.ppShow p)) P.<$> text "----------------------------------------" P.<$> text "Expr after:" P.<$> P.indent 2 (text (PS.ppShow v)) P.<$> text "----------------------------------------" P.<$> text "Pretty before:" P.<$> P.indent 2 (text prog) P.<$> text "----------------------------------------" P.<$> text "Pretty after:" P.<$> P.indent 2 (pretty v) P.<$> text "----------------------------------------" P.<$> text "Normalised before:" P.<$> P.indent 2 (text pp) P.<$> text "----------------------------------------" P.<$> text "Normalised after:" P.<$> P.indent 2 (text pv) P.<$> text "========================================" P.<$> text "Normalised diff:" P.<$> text (ppDiff (diff pp pv)) P.<$> text "========================================" } where pretty = prettyNix parse = parseNixText normalise = unlines . map (reverse . dropWhile isSpace . reverse) . lines diff :: String -> String -> [Diff [String]] diff s1 s2 = getDiff (map (:[]) (lines s1)) (map (:[]) (lines s2)) tests :: Int -> TestTree tests n = testProperty "Pretty/Parse Property" $ withMaxSuccess n prop_prettyparse