Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions cardano-node/app/conformance-test-viewer.hs
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
Copy link
Collaborator

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!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree!

:: (Show a, ToExpr a) => RoseTreeZipper ([Int], a) -> Options -> Maybe String -> IO ()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function can/should be decomposed into three pieces:

  1. folding Commands into state updates on the zipper
  2. generating output
  3. some IO plumbing to make it all work

As a general rule, the more code you can move outside of IO the better. Which makes this a prime candidate.

interactWithShrinks zipper opts mMessage = do
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am pretty sure opts is unused

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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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 withZipper helper is implemented, that would be a much better place to put this logic.

-- 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
ToParent -> case toParent zipper of
Just zipper' -> interactWithShrinks zipper' opts Nothing
Nothing -> interactWithShrinks zipper opts $ Just "already at the root!"
ToParent -> withZipper toParent "already at the root" zipper

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please always spell out the whole enumeration:

Suggested change
_ -> interactWithShrinks zipper opts Nothing
NoOpt -> interactWithShrinks zipper opts Nothing

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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about removing this constructor

Suggested change
= NoOp
| ToParent
= ToParent

and then use Maybe Command instead? Then we get the whole bevy of Maybe combinators like catMaybes or a foldable instance that automatically ignores the no-ops.

| ToLeftSibling
| ToRightSibling
| ToFirstChild
| Quit
deriving (Eq, Show)

readCommand :: IO Command
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this instead have type

Suggested change
readCommand :: IO Command
readCommand :: String -> [Command]

?

Laziness will allow us to stream it off stdin, which then means we can drop the unnecessary IO.

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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
28 changes: 22 additions & 6 deletions cardano-node/cardano-node.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -302,11 +302,12 @@ library conformance-testlib
import: project-config
hs-source-dirs: src-conformance-test
exposed-modules: ExitCodes
MiniProtocols
Options
Query
Server
ShrinkIndex
, MiniProtocols
, Options
, Query
, Server
, ShrinkIndex
, Data.RoseTree
build-depends: base
, containers
, comonad
Expand All @@ -318,7 +319,7 @@ library conformance-testlib
, ouroboros-consensus:{ouroboros-consensus, unstable-consensus-testlib}
, ouroboros-consensus-diffusion:unstable-consensus-conformance-testlib
, ouroboros-network-api
, aeson
-- , aeson
, bytestring
, ouroboros-network-framework
, network
Expand Down Expand Up @@ -360,6 +361,21 @@ executable conformance-test-runner
, QuickCheck
, cardano-api

executable conformance-test-viewer
import: project-config
hs-source-dirs: app
main-is: conformance-test-viewer.hs
build-depends: base
, cardano-node:conformance-testlib
, aeson
, aeson-pretty
, ansi-terminal
, bytestring
, pretty
, QuickCheck
, quickcheck-state-machine
, scientific

test-suite conformance-testlib-test
import: project-config
hs-source-dirs: test
Expand Down
Loading