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
56 changes: 42 additions & 14 deletions cardano-node/app/conformance-test-runner.hs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ import Cardano.Node.Protocol.Cardano (mkCardanoProtocolParams)
import Cardano.Node.Run ()
import Cardano.Node.Types (ConfigYamlFilePath (..), NodeProtocolConfiguration (..))
import Cardano.Protocol.Crypto (StandardCrypto)
import qualified Data.Aeson as Aeson
import Ouroboros.Consensus.Block (GetHeader (getHeader), Header)
import Ouroboros.Consensus.Block.Abstract
import Ouroboros.Consensus.Cardano (CardanoBlock)
import IssueTestBlock ()
import Ouroboros.Consensus.Cardano.Node (CardanoProtocolParams, protocolInfoCardano)
Expand Down Expand Up @@ -72,6 +74,7 @@ import System.Environment (getArgs)
import Test.Consensus.BlockTree (BlockTree (..), BlockTreeBranch (..), onTrunk,
prettyBlockTree)
import Test.Consensus.Genesis.Setup.GenChains
import Test.Consensus.Genesis.Tests (GenesisTestKey)
import Test.Consensus.OrphanInstances ()
import Test.Consensus.PeerSimulator.Config ()
import Test.Consensus.PeerSimulator.NodeLifecycle
Expand All @@ -84,13 +87,22 @@ import Test.Consensus.PointSchedule
import Test.Consensus.PointSchedule.Peers (PeerId (..), getPeerIds, peersOnlyHonest)
import Test.Consensus.PointSchedule.SinglePeer (SchedulePoint (..), scheduleBlockPoint,
scheduleHeaderPoint, scheduleTipPoint)
import qualified Test.Consensus.Serialize as Serialize
import Test.QuickCheck (generate, scale)
import Test.QuickCheck.Gen (Gen, unGen)
import Test.QuickCheck.Random (QCGen, newQCGen)
import Test.Util.TestBlock (TestBlock, unTestHash)

import ExitCodes
import Query
import Server (run)
import qualified ShrinkIndex as Ix
import ShrinkIndex (ShrinkIndex, ShrinkTree, makeShrinkTree)
-- TODO(nbloomf): ShrinkIndex was moved upstream to ouroboros-consensus-diffusion because it
-- is used in the serialization of test cases, but the local version is still in cardano-node.
-- We're importing both here qualified because both are used; this is not a long-term
-- solution but it is not yet clear where is the best place to put it.
import qualified ShrinkIndex as OldIx
import qualified Test.Consensus.Genesis.ShrinkIndex as Ix
Comment thread
nbloomf marked this conversation as resolved.
import Test.Consensus.Genesis.ShrinkIndex (ShrinkIndex, ShrinkTree, makeShrinkTree)

instance ( NodeInitStorage (CardanoBlock StandardCrypto)) => HasPointScheduleTestParams (CardanoBlock StandardCrypto) where
data ProtocolInfoArgs (CardanoBlock StandardCrypto) = CardanoInfoArgs (CardanoProtocolParams StandardCrypto)
Expand All @@ -117,7 +129,7 @@ instance ( NodeInitStorage (CardanoBlock StandardCrypto)) => HasPointScheduleTes
shrinkGenesisTest :: GenesisTestFull blk -> [GenesisTestFull blk]
shrinkGenesisTest _ = []

data TestResult = TestSuccess | TestFailure deriving (Eq, Ord, Show, Enum, Bounded)
data TestResult = TestSuccess | TestFailure deriving (Eq, Show)

-- | This function makes implicit reference to the fact that 'ExitCodes.Success'
-- is defined as the empty status flag pattern.
Expand Down Expand Up @@ -218,15 +230,19 @@ main = do
-- Generate a random RollBack test chain. We divide the test size by 10 here
-- because 'TestBlock's have a hardcoded size of 100---anything longer will
-- crash when being deserialized.
chain0 <- generate $ scale (`div` 10) $ do
gt <- genChains $ pure 1
pure $ gt {gtSchedule = rollbackSchedule 1 $ gtBlockTree gt}
seedGen <- newQCGen -- This will come from @testgen@ in the future.
let
quickCheckGenSize = 30
genChain = scale (flip div 10) $ do
gt <- genChains $ pure 1
pure $ gt {gtSchedule = rollbackSchedule 1 $ gtBlockTree gt}
chain0 = unGen genChain seedGen quickCheckGenSize

let tree = makeShrinkTree shrinkGenesisTest chain0
inputIndex = optShrinkIndex opts
-- Note that both no index and the empty index must
-- return the original chain. See [NOTE: shrink-index-properties]
chain <- case Ix.lookup (fold inputIndex) tree of
chain <- case Ix.lookup (foldMap convertShrinkIndex inputIndex) tree of
Nothing -> do
putStrLn "Incorrect shrink index"
exitWithStatus BadUsage
Expand All @@ -239,10 +255,11 @@ main = do
(optSimPeerPort opts)
(optOutputTopologyFile opts)
chain
seedGen
case res of
Left _ -> exitWithStatus InternalError
Right testRes -> do
mightContinueShrinking <- case indexUpdate testRes tree (fold inputIndex) of
mightContinueShrinking <- case indexUpdate testRes tree (foldMap convertShrinkIndex inputIndex) of
ContinueShrinkingWith ix -> do
print ix
pure $ S.singleton ContinueShrinking
Expand All @@ -254,11 +271,18 @@ main = do
let isGlobalSuccess =
testRes == TestSuccess &&
(isNothing inputIndex || inputIndex == Just mempty)
testVersion = Serialize.TestVersion 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let's pull this off the test suite too, and verify that the one in our testfile agrees with it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wonderig, you mean adding a TestSuiteData field for it?

In general, how should we go about when the work depends on upstream changes? My take would be to just make then in a new branch on the other package and work it out locally, but then the question remains of how reviewers could attempt to compile. For example we could change the source-repository-package to point to the corresponding revision. Or just trust the submitter managed to compile locally and the changes the PR depends on will eventually land.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks like it didn't happen, but there was supposed to be a test version attached to each conformance test, which we could cross reference to ensure that the serialized version is talking to the same version. Maybe sync with @ninioArtillero about the best place to get this data to line up.


-- When @testgen@ is implemented and we have real test case input, the
-- missing seed and test key will come from there.
serialize :: GenesisTestFull TestBlock -> Aeson.Value
serialize testCase = Serialize.serializeReifiedTestCase
Serialize.FormatVersionOne $ Serialize.toReifiedTestCase @_ @() -- TODO(nbloomf): test key type will be GenesisTestKey
(error "main: test key not available") testVersion (gtBlockTree testCase)
(gtSchedule testCase) ix (error "main: seed not available")
case (optMinimalTestOutput opts, not isGlobalSuccess, Ix.lookup ix tree) of
-- TODO: Encoding is commented out for now because
-- the test file format is not available yet.
(Just minimalTestFilePath, True, Just chain') -> pure () -- encodeFile minimalTestFilePath chain'
(Nothing, True, Just chain') -> pure () -- print $ encode chain'
(Just minimalTestFilePath, True, Just chain') -> encodeFile minimalTestFilePath $ serialize chain'
(Nothing, True, Just chain') -> print $ encode $ serialize chain'
_ -> pure ()
pure mempty
exitWithStatus . Flags $ testResultToFlag testRes <> mightContinueShrinking
Expand Down Expand Up @@ -396,10 +420,9 @@ runServer nutPort firstPort outputTopologyPath (GenesisTest {gtSchedule, gtSecur
for_ peerServers $ uninterruptibleCancel . snd

-- Return the test's acceptance criteria.

-- This should be parsed out of the test file parameter and computed from
-- the 'StateView', but is currently hard coded for convenience.
pure . boolToTestResult $ not . onTrunk gtBlockTree $ getTipPoint $ castTip tip
pure . boolToTestResult $ not . onTrunk gtBlockTree $ getTipPoint $ castTip tiprporate feedback)

--------------------------------------------------------------------------------
-- The remainder of this file is copied from the ouroboros-consensus
Expand Down Expand Up @@ -435,3 +458,8 @@ rollbackSchedule n blockTree =
banalSchedulePoints = concatMap banalSchedulePoints' . AF.toOldestFirst
banalSchedulePoints' :: blk -> [SchedulePoint blk]
banalSchedulePoints' block = [scheduleTipPoint block, scheduleHeaderPoint block, scheduleBlockPoint block]

-- | The 'ShrinkIndex' module is being moved to @ouroboros-consensus-diffusion@;
-- this is a temporary shim until all references to the local module are removed.
convertShrinkIndex :: OldIx.ShrinkIndex -> Ix.ShrinkIndex
convertShrinkIndex (OldIx.Ix index) = Ix.path $ toList index
117 changes: 68 additions & 49 deletions cardano-node/app/conformance-test-viewer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeOperators #-}

module Main (main) where

Expand All @@ -13,23 +15,31 @@ import Prelude hiding (lookup)
import Control.Error.Util (failWith)
import Control.Monad.Except (ExceptT(), runExceptT, throwError, MonadError(..))
import Control.Monad.IO.Class (MonadIO(..), liftIO)
import Data.Aeson (eitherDecode, FromJSON(..), ToJSON(..))
import Data.Aeson (eitherDecode, FromJSON(..), ToJSON(..), withText)
import qualified Data.Aeson as Aeson
import Data.Aeson.Encode.Pretty (encodePretty)
import qualified Data.Aeson.KeyMap as Aeson
import qualified Data.ByteString.Lazy as BS
import Data.Char (isDigit)
import Data.Function (on)
import Data.Proxy
import Data.List (groupBy)
import qualified Data.Text as T () -- TODO: Waiting for test key parsing.
import qualified ExitCodes as Exit
import Options.Applicative
import Ouroboros.Consensus.Byron.Ledger.Block
import qualified Ouroboros.Network.AnchoredFragment as AF
import ShrinkIndex
import System.Environment (getArgs)
import System.IO (hPutStr, hPutStrLn, stderr)
import Test.Consensus.Genesis.Setup (ConformanceTest(..))
import Test.Consensus.Genesis.Tests (GenesisTestKey, testSuite)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Just flagging that GenesisTestKey does not contain all the tests: we are missing the SmokeTestKey. This is not relevant to this PR though.

As a note, we are still to define another Key type to gather all keys.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

BTW, now such key is found under TestSuite.All

import Test.Consensus.Genesis.TestSuite (at, getTest)
import Test.Consensus.OrphanInstances ()
import Test.Consensus.PointSchedule (GenesisTest, PointSchedule)
import Test.QuickCheck (Arbitrary(..))
import Test.Consensus.PeerSimulator.StateView (StateView(..))
import Test.Consensus.PointSchedule (GenesisTest(..), GenesisTestFull)
import qualified Test.Consensus.Serialize as Serialize
import qualified Test.QuickCheck.Gen as QC
import Text.Read (readEither)
import Test.Util.TestBlock (TestBlock)

nameString, versionString :: String
nameString = "conformance-test-viewer"
Expand All @@ -41,7 +51,6 @@ data Options = Options
{ optInputPath :: Maybe FilePath -- ^ Where to read input (default stdin)
, optShrinkIndex :: ShrinkIndex -- ^ Which shrink of the input to analyze
, optOutputPath :: Maybe FilePath -- ^ Where to write output (default stdout)
, optTestCaseType :: TestCaseType -- ^ For testing; specify a test case type
, optMode :: Mode
} deriving (Eq, Show)

Expand Down Expand Up @@ -81,24 +90,16 @@ optionParser = Options
[ metavar "FILE_PATH"
, help "File path for writing output"
]))
<*> (option (eitherReader parseTestCaseType)
(long "type" <> mconcat
[ short 't'
, value GenesisTestTC
, metavar "TYPE"
, help "Which type of test case to parse"
]))
<*> pure ShowDescendant



main :: IO ()
main = do
args <- getArgs
opts <- getOptions args
opts <- getArgs >>= getOptions

result <- runExceptT $ do
testCase <- getInputTestCase (optTestCaseType opts) (optInputPath opts)
testCase <- getInputTestCase (optInputPath opts)
shrinkResult <- analyzeShrinkTree (optMode opts) (optShrinkIndex opts) testCase
writeOutputTestCase (optOutputPath opts) shrinkResult

Expand All @@ -114,7 +115,10 @@ main = do
-- we have to instantiate the input at a specific type. To achieve this
-- we hide the concrete type `a` behind an existential.
data ViewableTestCase where
TestCase :: (FromJSON a, ToJSON a, Arbitrary a) => a -> ViewableTestCase
TestCase
:: (a ~ GenesisTestFull blk, AF.HasHeader blk, Show blk)
=> Serialize.ReifiedTestCase GenesisTestKey Serialize.BlockRep
-> a -> (a -> [a]) -> ViewableTestCase



Expand All @@ -131,44 +135,66 @@ getOptions args = do
execCompletion compl nameString >>= putStr
Exit.exitWithStatus Exit.Success

-- | Determine from the program options what type the input test
-- case should be instantiated at, and then read it.
getInputTestCase
:: (MonadError String m, MonadIO m)
=> TestCaseType -> Maybe FilePath -> m ViewableTestCase
getInputTestCase testCaseType inputPath = do
case testCaseType of
IntTC -> readInputTestCase (Proxy :: Proxy Int) inputPath
StringTC -> readInputTestCase (Proxy :: Proxy String) inputPath
GenesisTestTC -> error "Genesis test not yet implemented!" -- readInputTestCase @(GenesisTest ByronBlock (PointSchedule ByronBlock)) -- TODO

-- | Read and parse a JSON-encoded test case either
-- from a file or from stdin.
readInputTestCase
:: forall a m. (MonadError String m, MonadIO m)
=> (ToJSON a, FromJSON a, Arbitrary a)
=> Proxy a -> Maybe FilePath -> m ViewableTestCase
readInputTestCase _ inputPath = do
=> Maybe FilePath -> m ViewableTestCase
getInputTestCase inputPath = do
input <- liftIO $ maybe BS.getContents BS.readFile inputPath
fmap TestCase $ case eitherDecode input of
Right ok -> pure (ok :: a)
rawJson <- case eitherDecode input of
Right ok -> pure (ok :: Aeson.Value)
Left err -> throwError $ "Input decoding error: " <> err

-- | Analyze the shrink tree of a value of any type that
-- implements `Arbitrary`, `FromJSON`, and `ToJSON`;
_rawKey <- case rawJson of
Aeson.Object o -> case Aeson.lookup "key" o of
Just (Aeson.String k) -> pure k
_ -> throwError "Malformed JSON: missing string field \"key\""
_ -> throwError "Malformed JSON: must be an object"
testKey <- error "getInputTestCase: test key parsing not yet implemented"
-- TODO(nbloomf): fix this once test keys are implemented
-- case parseKeyName (T.unpack rawKey) of
-- Just (k :: GenesisTestKey) -> pure k
-- Nothing -> throwError $ "Unrecognized test case key: " <> T.unpack rawKey

reifiedTestCase :: Serialize.ReifiedTestCase GenesisTestKey Serialize.BlockRep
<- case eitherDecode input of
Right ok -> pure ok
Left err -> throwError $ "Input decoding error: " <> err

let
-- The StateView is not used in the existing shrinkers, but if it ever
-- is we will need to update the serialized test cases to include it.
-- See @ouroboros-consensus-diffusion:Test.Consensus.PointSchedule.Shrinking@
stateView :: StateView TestBlock
stateView = error "getInputTestCase: Cannot construct an accurate StateView"

generator = ctGenerator conformanceTest
Serialize.Seed seed = Serialize.rtcSeed reifiedTestCase
conformanceTest = getTest . at testSuite $ testKey
-- QuickCheck's default size is 30, which we adjust to get the initial test case.
genesisTest = QC.unGen generator seed (ctMaxSize conformanceTest 30)
shrinker val = ctShrinker conformanceTest val stateView

pure $ TestCase reifiedTestCase genesisTest shrinker

-- | Analyze the shrink tree of a value of any viewable test case;
-- returns the resulting test case.
analyzeShrinkTree
:: (Monad m)
=> Mode -> ShrinkIndex -> ViewableTestCase -> ExceptT String m ViewableTestCase
analyzeShrinkTree mode shrinkIndex (TestCase testCase) = fmap TestCase $
case mode of
analyzeShrinkTree mode shrinkIndex (TestCase key testCase shrinker) =
let makeTestCase x = TestCase key x shrinker
in fmap makeTestCase $ case mode of
ShowDescendant -> failWith "Descendant does not exist. :(" $
lookup shrinkIndex $ arbitraryShrinkTree testCase
lookup shrinkIndex $ ShrinkIndex.makeShrinkTree shrinker testCase

writeOutputTestCase
:: (MonadIO m) => Maybe FilePath -> ViewableTestCase -> m ()
writeOutputTestCase outputPath (TestCase testCase) = do
let bytes = encodePretty testCase
writeOutputTestCase outputPath (TestCase reifiedTestCase testCase _) = do
let
Serialize.ReifiedTestCase {..} = reifiedTestCase

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Avoid using RecordWildCards per the style guide (Damian also explicitly asked for this on our upstream PR). NamedFieldPuns is kosher BTW.

bytes = encodePretty $ Serialize.serializeReifiedTestCase
Serialize.FormatVersionOne $ Serialize.toReifiedTestCase rtcTestKey rtcTestVersion
(gtBlockTree testCase) (gtSchedule testCase) rtcShrinkIndex rtcSeed
liftIO $ case outputPath of
Nothing -> BS.putStr bytes >> putStrLn ""
Just oPath -> BS.writeFile oPath bytes
Expand All @@ -185,10 +211,3 @@ parseShrinkIndexOption :: String -> Either String ShrinkIndex
parseShrinkIndexOption =
fmap path . traverse readEither . filter (all isDigit) . groupBy bothDigits
where bothDigits = on (&&) isDigit

parseTestCaseType :: String -> Either String TestCaseType
parseTestCaseType symbol = case symbol of
"int" -> Right IntTC
"string" -> Right StringTC
"genesis" -> Right GenesisTestTC
_ -> Left $ "Unrecognized test case type \"" <> symbol <> "\""
5 changes: 3 additions & 2 deletions cardano-node/cardano-node.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,6 @@ library conformance-testlib
, ouroboros-consensus:{ouroboros-consensus, unstable-consensus-testlib}
, ouroboros-consensus-diffusion:unstable-consensus-conformance-testlib
, ouroboros-network-api
, aeson
, bytestring
, ouroboros-network-framework
, network
Expand Down Expand Up @@ -393,9 +392,11 @@ executable conformance-test-viewer
, errors
, mtl
, optparse-applicative
, ouroboros-consensus-cardano

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm pretty sure we need this one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm in favor of keeping redundant imports in general... just for convenience. This could easily be cleaned-up when editing for upstream submission.

, ouroboros-consensus:unstable-consensus-testlib
, ouroboros-consensus-diffusion:unstable-consensus-conformance-testlib
, ouroboros-network-api
, QuickCheck
, text

executable conformance-test-gen
import: project-config
Expand Down
2 changes: 1 addition & 1 deletion cardano-node/src-conformance-test/ShrinkIndex.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
-- | Indexing the shrinking tree
module ShrinkIndex
( ShrinkTree,
ShrinkIndex,
ShrinkIndex(Ix),
makeShrinkTree,
arbitraryShrinkTree,
arbitraryShrinkIndexWithin,
Expand Down
Loading