diff --git a/flake.nix b/flake.nix index 46aeb016..697c0794 100644 --- a/flake.nix +++ b/flake.nix @@ -37,9 +37,12 @@ pkgs, ... }: { - packages = + packages = let + internal = inputs.self.internal.${system}; + in { - default = inputs.self.internal.${system}.package; + default = internal.package; + inherit (internal) tx-build cardano-address testgen-hs; } // (inputs.nixpkgs.lib.optionalAttrs (system == "x86_64-linux") { default-x86_64-windows = inputs.self.internal.x86_64-windows.package; @@ -49,7 +52,9 @@ treefmt = {pkgs, ...}: { projectRootFile = "flake.nix"; - programs.alejandra.enable = true; + programs.alejandra.enable = true; # Nix + programs.ormolu.enable = true; # Haskell + programs.cabal-fmt.enable = true; programs.prettier.enable = true; settings.formatter.prettier.options = [ "--config" @@ -60,7 +65,7 @@ ]; programs.rustfmt.enable = true; programs.yamlfmt.enable = true; - programs.taplo.enable = true; + programs.taplo.enable = true; # TOML programs.shfmt.enable = true; }; }; diff --git a/nix/devshells.nix b/nix/devshells.nix index fb2cc96e..47ec4e90 100644 --- a/nix/devshells.nix +++ b/nix/devshells.nix @@ -50,6 +50,11 @@ in { category = "handy"; package = internal.tx-build; } + { + category = "handy"; + name = "testgen-hs"; + package = internal.testgen-hs; + } ]; language.c.compiler = diff --git a/nix/internal/unix.nix b/nix/internal/unix.nix index 5b1a0a70..b16aea9b 100644 --- a/nix/internal/unix.nix +++ b/nix/internal/unix.nix @@ -125,6 +125,33 @@ in rec { ln -sf $out/libexec/cardano-address $out/bin/ ''; + testgen-hs = let + patched-flake = let + unpatched = inputs.cardano-node; + in + (import inputs.flake-compat { + src = { + outPath = toString (pkgs.runCommandNoCC "source" {} '' + cp -r ${unpatched} $out + chmod -R +w $out + cd $out + echo ${lib.escapeShellArg (builtins.toJSON [targetSystem])} $out/nix/supported-systems.nix + cp -r ${../../testgen-hs} ./testgen-hs + sed -r '/^packages:/ a\ testgen-hs' -i cabal.project + sed -r 's/other-modules:\s*/ , /g' -i cardano-submit-api/cardano-submit-api.cabal + ''); + inherit (unpatched) rev shortRev lastModified lastModifiedDate; + }; + }) + .defaultNix; + in + { + x86_64-linux = patched-flake.hydraJobs.x86_64-linux.musl.testgen-hs; + x86_64-darwin = patched-flake.packages.x86_64-darwin.testgen-hs; + aarch64-darwin = patched-flake.packages.aarch64-darwin.testgen-hs; + } + .${targetSystem}; + tx-build = let onPath = with pkgs; [ bash diff --git a/testgen-hs/CLI.hs b/testgen-hs/CLI.hs new file mode 100644 index 00000000..692db24f --- /dev/null +++ b/testgen-hs/CLI.hs @@ -0,0 +1,149 @@ +module CLI + ( Command (..), + GenerateOptions (..), + Seed (..), + GenSize (..), + NumCases (..), + TypeCommand (..), + parse, + ) +where + +import Data.ByteString (ByteString) +import qualified Data.ByteString.Base16 as Base16 +import qualified Data.ByteString.Char8 as BS8 +import Options.Applicative as O + +data Command = Generate GenerateOptions | Deserialize ByteString deriving (Show) + +data GenerateOptions = GenerateOptions (Maybe Seed) GenSize NumCases TypeCommand deriving (Show) + +newtype Seed = Seed Int deriving (Show) + +newtype GenSize = GenSize Int deriving (Show) + +newtype NumCases = NumCases Int deriving (Show) + +data TypeCommand + = GHCInteger + | DataText + | ExampleADT + | ApplyTxErr'Byron + | ApplyTxErr'Shelley + | ApplyTxErr'Allegra + | ApplyTxErr'Mary + | ApplyTxErr'Alonzo + | ApplyTxErr'Babbage + | ApplyTxErr'Conway + deriving (Show) + +parse :: IO Command +parse = execParser opts + +------------------------------------------------------------------------------------- + +opts :: ParserInfo Command +opts = + info + (commandParser <**> helper) + ( fullDesc + <> progDesc "Test case generator for cross-checking CBOR (de)serializers" + ) + +commandParser :: Parser Command +commandParser = + subparser + ( mempty + <> ( command + "generate" + ( info + ( Generate + <$> optionsParser + <**> helper + ) + (progDesc "Generate random CBOR test cases") + ) + ) + <> ( command + "deserialize" + ( info + ( Deserialize + <$> argument (eitherReader parseHex) (metavar "CBOR_HEX") + <**> helper + ) + (progDesc "Deserialize CBOR of ‘HardForkApplyTxErr’ that you got from cardano-node") + ) + ) + ) + +optionsParser :: Parser GenerateOptions +optionsParser = + GenerateOptions + <$> optional + ( Seed + <$> option + auto + ( long "seed" + <> short 's' + <> metavar "SEED" + <> help "Random seed integer (UNIX timestamp by default)" + ) + ) + <*> ( GenSize + <$> option + auto + ( long "generator-size" + <> short 'g' + <> metavar "SIZE" + <> value 300 + <> help "Set the relative ‘size’ of the test cases" + ) + ) + <*> ( NumCases + <$> option + positive + ( long "number" + <> short 'n' + <> metavar "NUM" + <> value 10 + <> help "How many test cases to generate" + ) + ) + <*> typeCommandParser + +positive :: ReadM Int +positive = do + n <- auto + if n > 0 + then return n + else readerError "NUM must be positive" + +typeCommandParser :: Parser TypeCommand +typeCommandParser = + subparser + ( mempty + <> mkTypeCommand ApplyTxErr'Byron + <> mkTypeCommand ApplyTxErr'Shelley + <> mkTypeCommand ApplyTxErr'Allegra + <> mkTypeCommand ApplyTxErr'Mary + <> mkTypeCommand ApplyTxErr'Alonzo + <> mkTypeCommand ApplyTxErr'Babbage + <> mkTypeCommand ApplyTxErr'Conway + <> mkTypeCommand GHCInteger + <> mkTypeCommand DataText + <> mkTypeCommand ExampleADT + ) + +mkTypeCommand :: TypeCommand -> Mod CommandFields TypeCommand +mkTypeCommand cmd = + command + (replaceQuotes . show $ cmd) + (info (pure cmd) (progDesc ("Generate CBOR of " ++ show cmd))) + where + replaceQuotes = ((\c -> if c == '\'' then '_' else c) <$>) + +-- | Parse a hex-encoded ByteString – e.g. CBOR +parseHex :: String -> Either String ByteString +parseHex hexInput = + let bsInput = BS8.pack hexInput + in Base16.decode bsInput diff --git a/testgen-hs/Deserialize.hs b/testgen-hs/Deserialize.hs new file mode 100644 index 00000000..a6d14d63 --- /dev/null +++ b/testgen-hs/Deserialize.hs @@ -0,0 +1,36 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Deserialize (deserialize) where + +import Cardano.Api.Orphans () +import qualified Cardano.Chain.Slotting as CCS +import qualified Codec.CBOR.Decoding as C +import qualified Codec.CBOR.Read as C +import Data.ByteString (ByteString) +import qualified Data.ByteString.Base16 as B16 +import qualified Data.ByteString.Lazy as BL +import qualified Ouroboros.Consensus.Cardano.Block as OCCB +import Ouroboros.Consensus.Cardano.Node as OCCN +import qualified Ouroboros.Consensus.HardFork.Combinator.Mempool as OCHCM +import Ouroboros.Consensus.HardFork.Combinator.Serialisation.SerialiseNodeToClient () +import qualified Ouroboros.Consensus.Node.ProtocolInfo as OCNPI +import qualified Ouroboros.Consensus.Node.Serialisation as OCNS +import Test.Consensus.Cardano.Generators () + +deserialize :: ByteString -> Either String (OCHCM.HardForkApplyTxErr (OCCB.CardanoEras OCCB.StandardCrypto)) +deserialize cbor = + case C.deserialiseFromBytes hfcEnvelopeDecoder (BL.fromStrict cbor) of + Left err -> Left (show err) + Right ("", ok) -> Right ok + Right (remainder, _) -> Left ("Deserialization successful, but the following bytes remained: " <> (show . B16.encode . BL.toStrict) remainder) + +hfcEnvelopeDecoder :: forall s. C.Decoder s (OCHCM.HardForkApplyTxErr (OCCB.CardanoEras OCCB.StandardCrypto)) +hfcEnvelopeDecoder = + OCNS.decodeNodeToClient + @(OCCB.HardForkBlock (OCCB.CardanoEras OCCB.StandardCrypto)) + @(OCHCM.HardForkApplyTxErr (OCCB.CardanoEras OCCB.StandardCrypto)) + codecConfig + OCCN.CardanoNodeToClientVersion12 + where + byronEpochSlots = CCS.EpochSlots 21600 -- probably safe to hardcode in Conway…? + codecConfig = OCNPI.pClientInfoCodecConfig (OCCN.protocolClientInfoCardano byronEpochSlots) diff --git a/testgen-hs/Generators.hs b/testgen-hs/Generators.hs new file mode 100644 index 00000000..546f66d0 --- /dev/null +++ b/testgen-hs/Generators.hs @@ -0,0 +1,285 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Generators where + +import qualified Cardano.Api.Eon.ShelleyBasedEra as CAPI +import qualified Cardano.Api.Eras.Core as CAPI +import qualified Cardano.Api.InMode as CAPI +import qualified Cardano.Api.Modes as CAPI +import Cardano.Api.Orphans () +import qualified Cardano.Chain.Slotting as CCS +import qualified Cardano.TxSubmit.Types as CTT +import qualified Codec.CBOR.Encoding as C +import Codec.Serialise (Serialise) +import qualified Codec.Serialise +import Data.Aeson (ToJSON) +import qualified Data.Aeson as J +import Data.Text (Text) +import Data.Typeable (Typeable, typeOf) +import GHC.Generics (Generic) +import Generic.Random (GenericArbitraryU (..)) +import qualified Ouroboros.Consensus.Byron.Ledger as OCBL +import qualified Ouroboros.Consensus.Cardano.Block as OCCB +import Ouroboros.Consensus.Cardano.Node as OCCN +import qualified Ouroboros.Consensus.HardFork.Combinator.AcrossEras as O +import qualified Ouroboros.Consensus.HardFork.Combinator.Mempool as OCHCM +import Ouroboros.Consensus.HardFork.Combinator.Serialisation.SerialiseNodeToClient () +import qualified Ouroboros.Consensus.Ledger.SupportsMempool as OCLSM +import qualified Ouroboros.Consensus.Node.ProtocolInfo as OCNPI +import qualified Ouroboros.Consensus.Node.Serialisation as OCNS +import qualified Ouroboros.Consensus.Shelley.Ledger as OCSL +import Test.Consensus.Cardano.Generators () +import Test.QuickCheck (Arbitrary) +import qualified Test.QuickCheck as QC + +-- | We define our own type class, to be able to include multiple complex +-- encoders for our `newtype` wrappers under a single interface. +class (Typeable a) => OurCBOR a where + unwrappedType :: a -> String + ourToCBOR :: a -> C.Encoding + ourToJSON :: a -> J.Value + +------- Byron ------------------------------------------------------------------ + +newtype ApplyTxErr'Byron = ApplyTxErr'Byron (OCLSM.ApplyTxErr OCBL.ByronBlock) + deriving newtype (Eq, Show, ToJSON, Arbitrary) + +instance OurCBOR ApplyTxErr'Byron where + unwrappedType (ApplyTxErr'Byron a) = show . typeOf $ a + ourToCBOR (ApplyTxErr'Byron a) = hfcEnvelope . OCCB.ApplyTxErrByron $ a + ourToJSON (ApplyTxErr'Byron a) = + submitApiEnvelope . CAPI.ByronTxValidationError $ a + +------- Shelley ---------------------------------------------------------------- + +newtype ApplyTxErr'Shelley + = ApplyTxErr'Shelley + ( OCLSM.ApplyTxErr + ( OCSL.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.ShelleyEra) + (CAPI.ShelleyLedgerEra CAPI.ShelleyEra) + ) + ) + deriving newtype (Eq, Show, ToJSON, Arbitrary) + +instance OurCBOR ApplyTxErr'Shelley where + unwrappedType (ApplyTxErr'Shelley a) = show . typeOf $ a + ourToCBOR (ApplyTxErr'Shelley a) = hfcEnvelope . OCCB.ApplyTxErrShelley $ a + ourToJSON (ApplyTxErr'Shelley a) = + submitApiEnvelope . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraShelley $ a + +------- Allegra ---------------------------------------------------------------- + +newtype ApplyTxErr'Allegra + = ApplyTxErr'Allegra + ( OCLSM.ApplyTxErr + ( OCSL.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.AllegraEra) + (CAPI.ShelleyLedgerEra CAPI.AllegraEra) + ) + ) + deriving newtype (Eq, Show, ToJSON, Arbitrary) + +instance OurCBOR ApplyTxErr'Allegra where + unwrappedType (ApplyTxErr'Allegra a) = show . typeOf $ a + ourToCBOR (ApplyTxErr'Allegra a) = hfcEnvelope . OCCB.ApplyTxErrAllegra $ a + ourToJSON (ApplyTxErr'Allegra a) = + submitApiEnvelope . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraAllegra $ a + +------- Mary ------------------------------------------------------------------- + +newtype ApplyTxErr'Mary + = ApplyTxErr'Mary + ( OCLSM.ApplyTxErr + ( OCSL.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.MaryEra) + (CAPI.ShelleyLedgerEra CAPI.MaryEra) + ) + ) + deriving newtype (Eq, Show, ToJSON, Arbitrary) + +instance OurCBOR ApplyTxErr'Mary where + unwrappedType (ApplyTxErr'Mary a) = show . typeOf $ a + ourToCBOR (ApplyTxErr'Mary a) = hfcEnvelope . OCCB.ApplyTxErrMary $ a + ourToJSON (ApplyTxErr'Mary a) = + submitApiEnvelope . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraMary $ a + +------- Alonzo ----------------------------------------------------------------- + +newtype ApplyTxErr'Alonzo + = ApplyTxErr'Alonzo + ( OCLSM.ApplyTxErr + ( OCSL.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.AlonzoEra) + (CAPI.ShelleyLedgerEra CAPI.AlonzoEra) + ) + ) + deriving newtype (Eq, Show, ToJSON, Arbitrary) + +instance OurCBOR ApplyTxErr'Alonzo where + unwrappedType (ApplyTxErr'Alonzo a) = show . typeOf $ a + ourToCBOR (ApplyTxErr'Alonzo a) = hfcEnvelope . OCCB.ApplyTxErrAlonzo $ a + ourToJSON (ApplyTxErr'Alonzo a) = + submitApiEnvelope . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraAlonzo $ a + +------- Babbage ---------------------------------------------------------------- + +newtype ApplyTxErr'Babbage + = ApplyTxErr'Babbage + ( OCLSM.ApplyTxErr + ( OCSL.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.BabbageEra) + (CAPI.ShelleyLedgerEra CAPI.BabbageEra) + ) + ) + deriving newtype (Eq, Show, ToJSON, Arbitrary) + +instance OurCBOR ApplyTxErr'Babbage where + unwrappedType (ApplyTxErr'Babbage a) = show . typeOf $ a + ourToCBOR (ApplyTxErr'Babbage a) = hfcEnvelope . OCCB.ApplyTxErrBabbage $ a + ourToJSON (ApplyTxErr'Babbage a) = + submitApiEnvelope . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraBabbage $ a + +------- Conway ----------------------------------------------------------------- + +newtype ApplyTxErr'Conway + = ApplyTxErr'Conway + ( OCLSM.ApplyTxErr + ( OCSL.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.ConwayEra) + (CAPI.ShelleyLedgerEra CAPI.ConwayEra) + ) + ) + deriving newtype (Eq, Show, ToJSON, Arbitrary) + +instance OurCBOR ApplyTxErr'Conway where + unwrappedType (ApplyTxErr'Conway a) = show . typeOf $ a + ourToCBOR (ApplyTxErr'Conway a) = hfcEnvelope . OCCB.ApplyTxErrConway $ a + ourToJSON (ApplyTxErr'Conway a) = + submitApiEnvelope . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraConway $ a + +------- HardForkApplyTxErr ----------------------------------------------------- + +hfcEnvelope :: OCHCM.HardForkApplyTxErr (OCCB.CardanoEras OCCB.StandardCrypto) -> C.Encoding +hfcEnvelope wrapped = + OCNS.encodeNodeToClient + @(OCCB.HardForkBlock (OCCB.CardanoEras OCCB.StandardCrypto)) + @(OCHCM.HardForkApplyTxErr (OCCB.CardanoEras OCCB.StandardCrypto)) + codecConfig + OCCN.CardanoNodeToClientVersion12 + wrapped + where + byronEpochSlots = CCS.EpochSlots 21600 -- probably safe to hardcode in Conway…? + codecConfig = OCNPI.pClientInfoCodecConfig (OCCN.protocolClientInfoCardano byronEpochSlots) + +submitApiEnvelope :: CAPI.TxValidationError era -> J.Value +submitApiEnvelope = + J.toJSON + . CTT.TxSubmitFail + . CTT.TxCmdTxSubmitValidationError + . CAPI.TxValidationErrorInCardanoMode + +hfcEnvelopeToSubmitApiEnvelope :: OCHCM.HardForkApplyTxErr (OCCB.CardanoEras OCCB.StandardCrypto) -> J.Value +hfcEnvelopeToSubmitApiEnvelope = + J.toJSON + . CTT.TxSubmitFail + . CTT.TxCmdTxSubmitValidationError + . CAPI.fromConsensusApplyTxErr + +hfcEnvelopeShowInner :: OCHCM.HardForkApplyTxErr (OCCB.CardanoEras OCCB.StandardCrypto) -> (String, String) +hfcEnvelopeShowInner = go + where + go (OCCB.ApplyTxErrByron a) = (show . typeOf $ a, show a) + go (OCCB.ApplyTxErrShelley a) = (show . typeOf $ a, show a) + go (OCCB.ApplyTxErrAllegra a) = (show . typeOf $ a, show a) + go (OCCB.ApplyTxErrMary a) = (show . typeOf $ a, show a) + go (OCCB.ApplyTxErrAlonzo a) = (show . typeOf $ a, show a) + go (OCCB.ApplyTxErrBabbage a) = (show . typeOf $ a, show a) + go (OCCB.ApplyTxErrConway a) = (show . typeOf $ a, show a) + go (OCCB.ApplyTxErrWrongEra a) = (show . typeOf $ a, show a) + +------- TxValidationErrorInCardanoMode (all at once) --------------------------- + +instance Arbitrary CAPI.TxValidationErrorInCardanoMode where + arbitrary = + QC.frequency + [ ( 5, + CAPI.TxValidationErrorInCardanoMode + . CAPI.ByronTxValidationError + . (\(ApplyTxErr'Byron a) -> a) + <$> QC.arbitrary + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode + . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraShelley + . (\(ApplyTxErr'Shelley a) -> a) + <$> QC.arbitrary + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode + . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraAllegra + . (\(ApplyTxErr'Allegra a) -> a) + <$> QC.arbitrary + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode + . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraMary + . (\(ApplyTxErr'Mary a) -> a) + <$> QC.arbitrary + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode + . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraAlonzo + . (\(ApplyTxErr'Alonzo a) -> a) + <$> QC.arbitrary + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode + . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraBabbage + . (\(ApplyTxErr'Babbage a) -> a) + <$> QC.arbitrary + ), + ( 15, + CAPI.TxValidationErrorInCardanoMode + . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraConway + . (\(ApplyTxErr'Conway a) -> a) + <$> QC.arbitrary + ), + (5, CAPI.TxValidationEraMismatch <$> QC.arbitrary) + ] + +instance Arbitrary O.EraMismatch where + arbitrary = do + (a, b) <- QC.oneof [pure ("Byron", "Shelley"), pure ("Shelley", "Byron")] + pure (O.EraMismatch a b) + +------- ExampleADT ------------------------------------------------------------- + +data ExampleADT + = SAOne Integer + | SATwo String + | SAThree Double + deriving (Eq, Show, Generic, ToJSON, Serialise) + deriving (Arbitrary) via (GenericArbitraryU ExampleADT) + +instance OurCBOR ExampleADT where + unwrappedType = show . typeOf + ourToCBOR = Codec.Serialise.encode + ourToJSON = J.toJSON + +------- basic types ------------------------------------------------------------ + +instance OurCBOR Text where + unwrappedType = show . typeOf + ourToCBOR = Codec.Serialise.encode + ourToJSON = J.toJSON + +instance OurCBOR Integer where + unwrappedType = show . typeOf + ourToCBOR = Codec.Serialise.encode + ourToJSON = J.toJSON + +instance OurCBOR Double where + unwrappedType = show . typeOf + ourToCBOR = Codec.Serialise.encode + ourToJSON = J.toJSON diff --git a/testgen-hs/Main.hs b/testgen-hs/Main.hs new file mode 100644 index 00000000..88a1bb92 --- /dev/null +++ b/testgen-hs/Main.hs @@ -0,0 +1,114 @@ +module Main where + +import CLI (GenSize (..), NumCases (..), Seed (..)) +import qualified CLI +import qualified Codec.CBOR.Write as C +import Data.Aeson (FromJSON, ToJSON) +import qualified Data.Aeson as J +import qualified Data.Aeson.Encode.Pretty as J +import Data.ByteString (ByteString) +import qualified Data.ByteString.Base16 as B16 +import qualified Data.ByteString.Lazy as BL +import qualified Data.ByteString.Lazy.Char8 as B +import Data.Proxy (Proxy (..)) +import Data.Text (Text) +import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.Text.Encoding.Error as T +import Data.Time.Clock.POSIX (getPOSIXTime) +import qualified Deserialize as D +import GHC.Generics (Generic) +import qualified Generators as G +import System.Exit (exitFailure) +import System.IO (hPutStrLn, stderr) +import Test.QuickCheck (Arbitrary) +import qualified Test.QuickCheck as QC +import qualified Test.QuickCheck.Gen as QC (unGen) +import Test.QuickCheck.Instances.Text () +import qualified Test.QuickCheck.Random as QC (mkQCGen) + +main :: IO () +main = + CLI.parse >>= \case + CLI.Generate opts -> runGenerate opts + CLI.Deserialize cbor -> runDeserialize cbor + +data Output a = Output + { seed :: Int, + testCases :: [TestCase a] + } + deriving (Generic, Show, FromJSON, ToJSON) + +data TestCase a = TestCase + { cbor :: Text, + json :: J.Value, + haskellRepr :: Text, + typeTag :: Text + } + deriving (Generic, Show, FromJSON, ToJSON) + +runGenerate :: CLI.GenerateOptions -> IO () +runGenerate (CLI.GenerateOptions maybeSeed genSize numCases command) = do + seed <- case maybeSeed of + Just s -> return s + Nothing -> Seed . round . (* 1000.0) <$> getPOSIXTime + + ( case command of + CLI.ApplyTxErr'Byron -> writeRandom @G.ApplyTxErr'Byron Proxy + CLI.ApplyTxErr'Shelley -> writeRandom @G.ApplyTxErr'Shelley Proxy + CLI.ApplyTxErr'Allegra -> writeRandom @G.ApplyTxErr'Allegra Proxy + CLI.ApplyTxErr'Mary -> writeRandom @G.ApplyTxErr'Mary Proxy + CLI.ApplyTxErr'Alonzo -> writeRandom @G.ApplyTxErr'Alonzo Proxy + CLI.ApplyTxErr'Babbage -> writeRandom @G.ApplyTxErr'Babbage Proxy + CLI.ApplyTxErr'Conway -> writeRandom @G.ApplyTxErr'Conway Proxy + CLI.DataText -> writeRandom @Text Proxy + CLI.GHCInteger -> writeRandom @Integer Proxy + CLI.ExampleADT -> writeRandom @G.ExampleADT Proxy + ) + seed + genSize + numCases + +writeRandom :: forall a. (Arbitrary a, Show a, G.OurCBOR a) => Proxy a -> Seed -> GenSize -> NumCases -> IO () +writeRandom _ (Seed seed) (GenSize generatorSize) (NumCases numCases) = do + let qcGen = QC.mkQCGen seed + values :: [a] = QC.unGen (QC.vectorOf numCases QC.arbitrary) qcGen generatorSize + testCases :: [TestCase a] = mkTestCase <$> values + output = Output {seed, testCases} + B.putStrLn $ J.encodePretty' (J.defConfig {J.confIndent = J.Spaces 2}) output + +mkTestCase :: forall a. (Show a, G.OurCBOR a) => a -> TestCase a +mkTestCase a = + TestCase + { cbor = + T.decodeUtf8With T.lenientDecode + . B16.encode + . BL.toStrict + . C.toLazyByteString + . G.ourToCBOR + $ a, + haskellRepr = T.pack . show $ a, + json = G.ourToJSON a, + typeTag = T.pack . G.unwrappedType $ a + } + +runDeserialize :: ByteString -> IO () +runDeserialize cbor' = + case D.deserialize cbor' of + Left err -> do + hPutStrLn stderr err + exitFailure + Right a -> + let (typeTag', haskellRepr') = G.hfcEnvelopeShowInner a + in B.putStrLn $ + J.encodePretty' + (J.defConfig {J.confIndent = J.Spaces 2}) + TestCase + { cbor = + T.decodeUtf8With T.lenientDecode + . B16.encode + $ cbor', + haskellRepr = T.pack haskellRepr', + json = G.hfcEnvelopeToSubmitApiEnvelope a, + typeTag = T.pack typeTag' + } diff --git a/testgen-hs/testgen-hs.cabal b/testgen-hs/testgen-hs.cabal new file mode 100644 index 00000000..d554bc2a --- /dev/null +++ b/testgen-hs/testgen-hs.cabal @@ -0,0 +1,61 @@ +cabal-version: 3.0 +name: testgen-hs +version: 0.1.0.0 +build-type: Simple +synopsis: Test case generator for cross-checking CBOR (de)serializers + +executable testgen-hs + main-is: Main.hs + build-depends: + , aeson + , aeson-pretty + , base >=4.7 && <5 + , base16-bytestring + , bytestring + , cardano-api:internal + , cardano-binary + , cardano-ledger-binary + , cardano-ledger-byron + , cardano-ledger-conway + , cardano-ledger-shelley + , cardano-submit-api + , cborg + , generic-random + , optparse-applicative + , ouroboros-consensus + , ouroboros-consensus-cardano + , ouroboros-consensus-cardano:unstable-cardano-testlib + , ouroboros-network-api + , QuickCheck + , quickcheck-instances + , random + , serialise + , text + , time + + other-modules: + CLI + Deserialize + Generators + + default-extensions: + DeriveAnyClass + DeriveGeneric + DerivingStrategies + DerivingVia + FlexibleContexts + GeneralizedNewtypeDeriving + LambdaCase + NamedFieldPuns + OverloadedStrings + RankNTypes + ScopedTypeVariables + StandaloneDeriving + TypeApplications + + ghc-options: + -O2 -Werror -Wall -Wcompat -Widentities -Wincomplete-record-updates + -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints + -Wunused-packages + + default-language: Haskell2010