-
Notifications
You must be signed in to change notification settings - Fork 0
Add interactive shrink view to conformance-test-viewer #18
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: conformance-testing
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,284 @@ | ||||||||||
| {-# LANGUAGE DeriveFunctor #-} | ||||||||||
| {-# LANGUAGE DeriveFoldable #-} | ||||||||||
| {-# LANGUAGE DeriveTraversable #-} | ||||||||||
| {-# LANGUAGE RankNTypes #-} | ||||||||||
| {-# LANGUAGE ScopedTypeVariables #-} | ||||||||||
| {-# LANGUAGE StandaloneDeriving #-} | ||||||||||
| {-# LANGUAGE DeriveAnyClass #-} | ||||||||||
| {-# LANGUAGE DeriveGeneric #-} | ||||||||||
|
|
||||||||||
| {-# OPTIONS_GHC -Wno-missing-signatures -Wno-unused-top-binds -Wno-unused-imports -Wno-unused-matches -Wno-unused-local-binds #-} | ||||||||||
|
|
||||||||||
| module Main (main) where | ||||||||||
|
|
||||||||||
| import Prelude hiding (lookup) | ||||||||||
|
|
||||||||||
| import Data.Aeson (eitherDecode, encode, FromJSON, ToJSON, Value) | ||||||||||
| import Data.Aeson.Encode.Pretty (encodePretty) | ||||||||||
| import Data.Aeson.KeyMap (KeyMap) | ||||||||||
| import qualified Data.ByteString.Lazy as BS | ||||||||||
| import Data.Char (isDigit) | ||||||||||
| import Data.List (groupBy) | ||||||||||
| import Data.Proxy | ||||||||||
| import Data.RoseTree | ||||||||||
| import Data.Scientific (Scientific) | ||||||||||
| import GHC.Generics | ||||||||||
| import ShrinkIndex | ||||||||||
| import System.Console.ANSI (clearScreen, setCursorPosition) | ||||||||||
| import System.Console.GetOpt | ||||||||||
| import System.Environment (getArgs) | ||||||||||
| import System.Exit | ||||||||||
| import System.IO (hPutStr, hPutChar, hSetEcho, hFlush, hSetBuffering, stdin, stdout, stderr, BufferMode(..)) | ||||||||||
| import Test.QuickCheck (Arbitrary) | ||||||||||
| import Test.StateMachine.TreeDiff.Class (ediff, ToExpr(..)) | ||||||||||
| import Test.StateMachine.TreeDiff.Pretty (prettyEditExpr) | ||||||||||
| import qualified Text.PrettyPrint as PP (render) | ||||||||||
| import Text.Read (readEither) | ||||||||||
|
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| main :: IO () | ||||||||||
| main = do | ||||||||||
| opts <- getOptions | ||||||||||
|
|
||||||||||
| if optShowHelp opts | ||||||||||
| then showUsage >> exitSuccess | ||||||||||
| else pure () | ||||||||||
|
|
||||||||||
| if optShowVersion opts | ||||||||||
| then showVersion >> exitSuccess | ||||||||||
| else pure () | ||||||||||
|
|
||||||||||
| if optDebug opts | ||||||||||
| then analyzeTreeOf (Proxy :: Proxy Int) opts | ||||||||||
| else analyzeTreeOf (Proxy :: Proxy String) opts -- For now just a dummy implementation | ||||||||||
|
|
||||||||||
| -- Analyze the shrink tree of a value of any type that | ||||||||||
| -- implements `Arbitrary`, `FromJSON`, and `ToJSON`. | ||||||||||
| analyzeTreeOf | ||||||||||
| :: forall a. (ToJSON a, FromJSON a, Arbitrary a, Show a, ToExpr a) | ||||||||||
| => Proxy a -> Options -> IO () | ||||||||||
| analyzeTreeOf _ opts = do | ||||||||||
| input <- case optInputPath opts of | ||||||||||
| Nothing -> BS.getContents | ||||||||||
| Just iPath -> BS.readFile iPath | ||||||||||
|
|
||||||||||
| testCase <- case eitherDecode input of | ||||||||||
| Right ok -> pure (ok :: a) | ||||||||||
| Left err -> do | ||||||||||
| errPutStrLn $ "Input decoding error: " <> err | ||||||||||
| exitFailure | ||||||||||
|
|
||||||||||
| let tree = arbitraryShrinkTree testCase | ||||||||||
|
|
||||||||||
| case optMode opts of | ||||||||||
| ShowDescendant -> do | ||||||||||
| let result = lookup (optShrinkIndex opts) tree | ||||||||||
| descendant <- case result of | ||||||||||
| Nothing -> do | ||||||||||
| errPutStrLn $ "Descendant does not exist. :(" | ||||||||||
| exitFailure | ||||||||||
| Just ok -> pure ok | ||||||||||
|
|
||||||||||
| let bytes = if optPrettyPrint opts | ||||||||||
| then encodePretty descendant | ||||||||||
| else encode descendant | ||||||||||
|
|
||||||||||
| case optOutputPath opts of | ||||||||||
| Nothing -> BS.putStr bytes >> putStrLn "" | ||||||||||
| Just oPath -> BS.writeFile oPath bytes | ||||||||||
|
|
||||||||||
| Interactive -> do | ||||||||||
| hSetEcho stdout False | ||||||||||
| hSetBuffering stdin NoBuffering | ||||||||||
| let allShrinks = makeRoseTreeZipper $ applyRoseTreeWithIndex (,) $ makeRoseTree (\x -> (node x, branches x)) $ tree | ||||||||||
| interactWithShrinks allShrinks opts Nothing | ||||||||||
|
|
||||||||||
| interactWithShrinks | ||||||||||
| :: (Show a, ToExpr a) => RoseTreeZipper ([Int], a) -> Options -> Maybe String -> IO () | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this function can/should be decomposed into three pieces:
As a general rule, the more code you can move outside of |
||||||||||
| interactWithShrinks zipper opts mMessage = do | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am pretty sure |
||||||||||
| clearScreen | ||||||||||
| setCursorPosition 0 0 | ||||||||||
| let RoseTreeZipper tree@(RoseTree (index, u) _) _ prev = zipper | ||||||||||
| case prev of | ||||||||||
| Nothing -> putStrLn $ show u | ||||||||||
| Just (_,v) -> putStrLn $ PP.render $ prettyEditExpr $ ediff v u | ||||||||||
| putStrLn $ show index | ||||||||||
| case mMessage of | ||||||||||
| Nothing -> pure () | ||||||||||
| Just msg -> putStrLn msg | ||||||||||
|
Comment on lines
+107
to
+109
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The logic here is a little smelly. This is an error message from the end of previous invocation of the function, and yet here it is at the beginning of the function! Once the |
||||||||||
| -- mapM_ putStrLn $ flattenDepthFirst $ fmap mconcat $ prettyPrefixRoseTree tree | ||||||||||
| cmd <- readCommand | ||||||||||
| case cmd of | ||||||||||
| Quit -> pure () | ||||||||||
| ToParent -> case toParent zipper of | ||||||||||
| Just zipper' -> interactWithShrinks zipper' opts Nothing | ||||||||||
| Nothing -> interactWithShrinks zipper opts $ Just "already at the root!" | ||||||||||
|
Comment on lines
+114
to
+116
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This pattern is crying out for a helper. withZipper :: (Zipper -> Maybe Zipper) -> String -> Zipper -> ...and then:
Suggested change
|
||||||||||
| ToLeftSibling -> case toLeftSibling zipper of | ||||||||||
| Just zipper' -> interactWithShrinks zipper' opts Nothing | ||||||||||
| Nothing -> interactWithShrinks zipper opts $ Just "already at the first sibling!" | ||||||||||
| ToRightSibling -> case toRightSibling zipper of | ||||||||||
| Just zipper' -> interactWithShrinks zipper' opts Nothing | ||||||||||
| Nothing -> interactWithShrinks zipper opts $ Just "already at the last sibling!" | ||||||||||
| ToFirstChild -> case toFirstChild zipper of | ||||||||||
| Just zipper' -> interactWithShrinks zipper' opts Nothing | ||||||||||
| Nothing -> interactWithShrinks zipper opts $ Just "this node has no children!" | ||||||||||
| _ -> interactWithShrinks zipper opts Nothing | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please always spell out the whole enumeration:
Suggested change
otherwise if we ever add a new case, this code will silently break |
||||||||||
|
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| getOptions :: IO Options | ||||||||||
| getOptions = do | ||||||||||
| argv <- getArgs | ||||||||||
|
|
||||||||||
| let (actions, params, errs) = getOpt Permute options argv | ||||||||||
|
|
||||||||||
| if null errs | ||||||||||
| then pure () | ||||||||||
| else do | ||||||||||
| errPutStrLn "option error(s):" | ||||||||||
| mapM_ errPutStrLn errs | ||||||||||
| showUsage >> exitFailure | ||||||||||
|
|
||||||||||
| -- Options as parsed only from the flags. | ||||||||||
| opts <- case compose actions defaultOpts of | ||||||||||
| Right ok -> pure ok | ||||||||||
| Left err -> do | ||||||||||
| errPutStrLn $ "Option parsing error: " <> err | ||||||||||
| showUsage >> exitFailure | ||||||||||
|
|
||||||||||
| -- Adjust to account for the input path being passed as | ||||||||||
| -- an argument instead of a flag. | ||||||||||
| case params of | ||||||||||
| [] -> pure opts | ||||||||||
| [inputPath] -> pure $ opts { optInputPath = Just inputPath } | ||||||||||
| _ -> do | ||||||||||
| errPutStrLn $ "unrecognized arguments: " <> show params | ||||||||||
| showUsage >> exitFailure | ||||||||||
|
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| data Mode | ||||||||||
| = ShowDescendant | ||||||||||
| | Interactive | ||||||||||
| deriving (Eq, Show) | ||||||||||
|
|
||||||||||
| data Options = Options | ||||||||||
| { optShowHelp :: Bool -- ^ Show usage? | ||||||||||
| , optShowVersion :: Bool -- ^ Show version info? | ||||||||||
| , optInputPath :: Maybe FilePath | ||||||||||
| , optShrinkIndex :: ShrinkIndex | ||||||||||
| , optOutputPath :: Maybe FilePath | ||||||||||
| , optPrettyPrint :: Bool | ||||||||||
| , optDebug :: Bool | ||||||||||
| , optMode :: Mode | ||||||||||
| } deriving (Eq, Show) | ||||||||||
|
|
||||||||||
| defaultOpts :: Options | ||||||||||
| defaultOpts = Options | ||||||||||
| { optShowHelp = False | ||||||||||
| , optShowVersion = False | ||||||||||
| , optInputPath = Nothing | ||||||||||
| , optShrinkIndex = mempty | ||||||||||
| , optOutputPath = Nothing | ||||||||||
| , optPrettyPrint = True | ||||||||||
| , optDebug = False | ||||||||||
| , optMode = ShowDescendant | ||||||||||
| } | ||||||||||
|
|
||||||||||
| options :: [OptDescr (Options -> Either String Options)] | ||||||||||
| options = | ||||||||||
| [ let munge opts = pure $ opts { optShowHelp = True } | ||||||||||
| in Option ['?'] ["help"] (NoArg munge) | ||||||||||
| "show usage" | ||||||||||
|
|
||||||||||
| , let munge opts = pure $ opts { optShowVersion = True } | ||||||||||
| in Option [] ["version"] (NoArg munge) | ||||||||||
| "show version information" | ||||||||||
|
|
||||||||||
| , let munge mPath opts = pure $ opts { optInputPath = mPath } | ||||||||||
| in Option [] ["input"] (OptArg munge "FILE") | ||||||||||
| "read from FILE (default is stdin)" | ||||||||||
|
|
||||||||||
| , let | ||||||||||
| munge d opts = do | ||||||||||
| ix <- parseShrinkIndexOption d | ||||||||||
| pure $ opts { optShrinkIndex = ix } | ||||||||||
| in | ||||||||||
| Option [] ["shrink-index"] (ReqArg munge "STRING") | ||||||||||
| "comma delimited list of natural numbers, e.g. '1,3,3,7' (default is the empty list)" | ||||||||||
|
|
||||||||||
| , let munge mPath opts = pure $ opts { optOutputPath = mPath } | ||||||||||
| in Option [] ["output"] (OptArg munge "FILE") | ||||||||||
| "write to FILE (default is stdout)" | ||||||||||
|
|
||||||||||
| , let munge opts = pure $ opts { optPrettyPrint = False } | ||||||||||
| in Option [] ["notpretty"] (NoArg munge) | ||||||||||
| "do not pretty print output" | ||||||||||
|
|
||||||||||
| , let munge opts = pure $ opts { optDebug = True } | ||||||||||
| in Option [] ["debug"] (NoArg munge) | ||||||||||
| "Use trees of integers instead of test scripts" | ||||||||||
|
|
||||||||||
| , let munge opts = pure $ opts { optMode = Interactive } | ||||||||||
| in Option [] ["interactive"] (NoArg munge) | ||||||||||
| "navigate the shrink tree" | ||||||||||
| ] | ||||||||||
|
|
||||||||||
| -- Very permissive; given a string, interpret any maximal contiguous | ||||||||||
| -- substring of digits as a number and all other characters as delimiters. | ||||||||||
| -- parseShrinkIndex "1,2,34" == Right path [1,2,34] | ||||||||||
| -- parseShrinkIndex "1,,2,,,34" == Right path [1,2,34] | ||||||||||
| -- parseShrinkIndex "1 2 34" == Right path [1,2,34] | ||||||||||
| -- parseShrinkIndex "o1w2o34w" == Right path [1,2,34] | ||||||||||
| parseShrinkIndexOption :: String -> Either String ShrinkIndex | ||||||||||
| parseShrinkIndexOption = | ||||||||||
| fmap path . sequenceA . fmap readEither . filter (all isDigit) . groupBy bothDigits | ||||||||||
| where bothDigits u v = isDigit u && isDigit v | ||||||||||
|
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| -- Like putStrLn, but for stderr. | ||||||||||
| errPutStrLn :: String -> IO () | ||||||||||
| errPutStrLn msg = hPutStr stderr msg >> hPutChar stderr '\n' | ||||||||||
|
|
||||||||||
| showUsage :: IO () | ||||||||||
| showUsage = do | ||||||||||
| let header = "USAGE: " <> nameString <> " [--OPTION...] [PATH]" | ||||||||||
| putStrLn $ usageInfo header options | ||||||||||
|
|
||||||||||
| showVersion :: IO () | ||||||||||
| showVersion = putStrLn versionString | ||||||||||
|
|
||||||||||
| nameString :: String | ||||||||||
| nameString = "conformance-test-viewer" | ||||||||||
|
|
||||||||||
| versionString :: String | ||||||||||
| versionString = nameString <> "-0.0" | ||||||||||
|
|
||||||||||
|
|
||||||||||
|
|
||||||||||
| data Command | ||||||||||
| = NoOp | ||||||||||
| | ToParent | ||||||||||
|
Comment on lines
+262
to
+263
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about removing this constructor
Suggested change
and then use |
||||||||||
| | ToLeftSibling | ||||||||||
| | ToRightSibling | ||||||||||
| | ToFirstChild | ||||||||||
| | Quit | ||||||||||
| deriving (Eq, Show) | ||||||||||
|
|
||||||||||
| readCommand :: IO Command | ||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this instead have type
Suggested change
? Laziness will allow us to stream it off stdin, which then means we can drop the unnecessary |
||||||||||
| readCommand = do | ||||||||||
| c1 <- getChar | ||||||||||
| case c1 of | ||||||||||
| '\ESC' -> do | ||||||||||
| c2 <- getChar | ||||||||||
| c3 <- getChar | ||||||||||
| case (c2, c3) of | ||||||||||
| ('[', 'A') -> pure ToLeftSibling | ||||||||||
| ('[', 'B') -> pure ToRightSibling | ||||||||||
| ('[', 'C') -> pure ToFirstChild | ||||||||||
| ('[', 'D') -> pure ToParent | ||||||||||
|
Comment on lines
+274
to
+281
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess you're parsing ansi codes for arrows here? But a comment would go a long way! |
||||||||||
| _ -> pure NoOp | ||||||||||
| 'q' -> pure Quit | ||||||||||
| _ -> pure NoOp | ||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function is really, really cool!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree!