diff --git a/flake.nix b/flake.nix index 220f2f5..ff3a38a 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,10 +52,12 @@ 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.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 fb2cc96..47ec4e9 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 5b1a0a7..15289f0 100644 --- a/nix/internal/unix.nix +++ b/nix/internal/unix.nix @@ -125,6 +125,32 @@ 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 + ''); + 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 0000000..bdaaf27 --- /dev/null +++ b/testgen-hs/CLI.hs @@ -0,0 +1,105 @@ +module CLI (Options (..), Seed (..), GenSize (..), NumCases (..), Command (..), parse) where + +import Options.Applicative as O + +newtype Seed = Seed Int deriving (Show) + +newtype GenSize = GenSize Int deriving (Show) + +newtype NumCases = NumCases Int deriving (Show) + +data Options = Options (Maybe Seed) GenSize NumCases Command deriving (Show) + +data Command + = GHCInteger + | DataText + | ExampleADT + | TxValidationErrorInCardanoMode + | ApplyTxError'Byron + | ApplyTxError'Shelley + | ApplyTxError'Allegra + | ApplyTxError'Mary + | ApplyTxError'Alonzo + | ApplyTxError'Babbage + | ApplyTxError'Conway + deriving (Show) + +parse :: IO Options +parse = execParser opts + +------------------------------------------------------------------------------------- + +opts :: ParserInfo Options +opts = + info + (optionsParser <**> helper) + ( fullDesc + <> progDesc "Test case generator for cross-checking CBOR (de)serializers" + ) + +optionsParser :: Parser Options +optionsParser = + Options + <$> 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" + ) + ) + <*> commandParser + +positive :: ReadM Int +positive = do + n <- auto + if n > 0 + then return n + else readerError "NUM must be positive" + +commandParser :: Parser Command +commandParser = + subparser + ( mempty + <> mkCommand ApplyTxError'Byron + <> mkCommand ApplyTxError'Shelley + <> mkCommand ApplyTxError'Allegra + <> mkCommand ApplyTxError'Mary + <> mkCommand ApplyTxError'Alonzo + <> mkCommand ApplyTxError'Babbage + <> mkCommand ApplyTxError'Conway + <> mkCommand TxValidationErrorInCardanoMode + <> mkCommand GHCInteger + <> mkCommand DataText + <> mkCommand ExampleADT + ) + +mkCommand :: Command -> Mod CommandFields Command +mkCommand cmd = + command + (replaceQuotes . show $ cmd) + (info (pure cmd) (progDesc ("Generate CBOR of " ++ show cmd))) + where + replaceQuotes = ((\c -> if c == '\'' then '_' else c) <$>) diff --git a/testgen-hs/Generators.hs b/testgen-hs/Generators.hs new file mode 100644 index 0000000..f138436 --- /dev/null +++ b/testgen-hs/Generators.hs @@ -0,0 +1,147 @@ +{-# 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 Cardano.Binary (FromCBOR (..), ToCBOR (..)) +import Cardano.Ledger.Binary (DecCBOR, EncCBOR) +import Codec.Serialise (Serialise) +import qualified Codec.Serialise +import Data.Aeson (ToJSON) +import GHC.Generics (Generic) +import Generic.Random (GenericArbitraryU (..)) +import qualified Ouroboros.Consensus.Byron.Ledger as Consensus +import qualified Ouroboros.Consensus.HardFork.Combinator.AcrossEras as O +import qualified Ouroboros.Consensus.Ledger.SupportsMempool as Consensus +import qualified Ouroboros.Consensus.Shelley.Ledger as Consensus +import Test.Consensus.Cardano.Generators () +import Test.QuickCheck (Arbitrary) +import qualified Test.QuickCheck as QC + +genTxValidationErrorInCardanoMode :: QC.Gen CAPI.TxValidationErrorInCardanoMode +genTxValidationErrorInCardanoMode = QC.arbitrary + +-- FIXME: where is it originally??? +-- FIXME: maybe here? https://github.com/IntersectMBO/ouroboros-consensus/blob/358305b09f8fa1a85f076b20a51b4af03e827071/ouroboros-consensus-cardano/src/byron/Ouroboros/Consensus/Byron/Node/Serialisation.hs#L175-L178 +-- instance ToCBOR CAPI.TxValidationErrorInCardanoMode where +-- toCBOR +-- deriving instance EncCBOR CAPI.TxValidationErrorInCardanoMode + +genApplyTxError'Byron :: QC.Gen (Consensus.ApplyTxErr Consensus.ByronBlock) +genApplyTxError'Byron = QC.arbitrary + +genApplyTxError'Shelley :: + QC.Gen + ( Consensus.ApplyTxErr + ( Consensus.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.ShelleyEra) + (CAPI.ShelleyLedgerEra CAPI.ShelleyEra) + ) + ) +genApplyTxError'Shelley = QC.arbitrary + +genApplyTxError'Allegra :: + QC.Gen + ( Consensus.ApplyTxErr + ( Consensus.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.AllegraEra) + (CAPI.ShelleyLedgerEra CAPI.AllegraEra) + ) + ) +genApplyTxError'Allegra = QC.arbitrary + +genApplyTxError'Mary :: + QC.Gen + ( Consensus.ApplyTxErr + ( Consensus.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.MaryEra) + (CAPI.ShelleyLedgerEra CAPI.MaryEra) + ) + ) +genApplyTxError'Mary = QC.arbitrary + +genApplyTxError'Alonzo :: + QC.Gen + ( Consensus.ApplyTxErr + ( Consensus.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.AlonzoEra) + (CAPI.ShelleyLedgerEra CAPI.AlonzoEra) + ) + ) +genApplyTxError'Alonzo = QC.arbitrary + +genApplyTxError'Babbage :: + QC.Gen + ( Consensus.ApplyTxErr + ( Consensus.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.BabbageEra) + (CAPI.ShelleyLedgerEra CAPI.BabbageEra) + ) + ) +genApplyTxError'Babbage = QC.arbitrary + +genApplyTxError'Conway :: + QC.Gen + ( Consensus.ApplyTxErr + ( Consensus.ShelleyBlock + (CAPI.ConsensusProtocol CAPI.ConwayEra) + (CAPI.ShelleyLedgerEra CAPI.ConwayEra) + ) + ) +genApplyTxError'Conway = QC.arbitrary + +instance Arbitrary CAPI.TxValidationErrorInCardanoMode where + arbitrary = + QC.frequency + [ ( 5, + CAPI.TxValidationErrorInCardanoMode . CAPI.ByronTxValidationError + <$> genApplyTxError'Byron + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraShelley + <$> genApplyTxError'Shelley + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraAllegra + <$> genApplyTxError'Allegra + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraMary + <$> genApplyTxError'Mary + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraAlonzo + <$> genApplyTxError'Alonzo + ), + ( 5, + CAPI.TxValidationErrorInCardanoMode . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraBabbage + <$> genApplyTxError'Babbage + ), + ( 15, + CAPI.TxValidationErrorInCardanoMode . CAPI.ShelleyTxValidationError CAPI.ShelleyBasedEraConway + <$> genApplyTxError'Conway + ), + (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) + +data ExampleADT + = SAOne Integer + | SATwo String + | SAThree Double + deriving (Eq, Show, Generic, ToJSON, Serialise, EncCBOR, DecCBOR) + deriving (Arbitrary) via (GenericArbitraryU ExampleADT) + +instance FromCBOR ExampleADT where + fromCBOR = Codec.Serialise.decode + +instance ToCBOR ExampleADT where + toCBOR = Codec.Serialise.encode diff --git a/testgen-hs/Main.hs b/testgen-hs/Main.hs new file mode 100644 index 0000000..7053d60 --- /dev/null +++ b/testgen-hs/Main.hs @@ -0,0 +1,89 @@ +module Main where + +import CLI (GenSize (..), NumCases (..), Seed (..)) +import qualified CLI +import Cardano.Ledger.Binary (EncCBOR) +import qualified Cardano.Ledger.Binary as CLB +import Codec.CBOR.Write as C +import Data.Aeson (FromJSON, ToJSON) +import qualified Data.Aeson.Encode.Pretty as J +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 Data.Time.Clock.POSIX (getPOSIXTime) +import Data.Typeable (typeRep) +import GHC.Generics (Generic) +import qualified Generators as G +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 >>= runCommand + +data Output a = Output + { typeTag :: Text, + seed :: Int, + testCases :: [TestCase a] + } + deriving (Generic, Show, FromJSON, ToJSON) + +data TestCase a = TestCase + { cbor :: Text, + json :: a, + haskellRepr :: Text + -- FIXME: , submitAPIRepr :: Data.Aeson.Value + } + deriving (Generic, Show, FromJSON, ToJSON) + +runCommand :: CLI.Options -> IO () +runCommand (CLI.Options maybeSeed genSize numCases command) = do + seed <- case maybeSeed of + Just s -> return s + Nothing -> (Seed . round) `fmap` getPOSIXTime + + ( case command of + CLI.ApplyTxError'Byron -> writeRandom G.genApplyTxError'Byron + CLI.ApplyTxError'Shelley -> writeRandom G.genApplyTxError'Shelley + CLI.ApplyTxError'Allegra -> writeRandom G.genApplyTxError'Allegra + CLI.ApplyTxError'Mary -> writeRandom G.genApplyTxError'Mary + CLI.ApplyTxError'Alonzo -> writeRandom G.genApplyTxError'Alonzo + CLI.ApplyTxError'Babbage -> writeRandom G.genApplyTxError'Babbage + CLI.ApplyTxError'Conway -> writeRandom G.genApplyTxError'Conway + CLI.TxValidationErrorInCardanoMode -> writeRandom (QC.arbitrary @Double) -- FIXME: G.genTxValidationErrorInCardanoMode + CLI.DataText -> writeRandom (QC.arbitrary @Text) + CLI.GHCInteger -> writeRandom (QC.arbitrary @Integer) + CLI.ExampleADT -> writeRandom (QC.arbitrary @G.ExampleADT) + ) + seed + genSize + numCases + +writeRandom :: forall a. (Show a, EncCBOR a, ToJSON a) => QC.Gen a -> Seed -> GenSize -> NumCases -> IO () +writeRandom genA (Seed seed) (GenSize generatorSize) (NumCases numCases) = do + let qcGen = QC.mkQCGen seed + values :: [a] = QC.unGen (QC.vectorOf numCases genA) qcGen generatorSize + testCases :: [TestCase a] = mkTestCase <$> values + output = Output {typeTag = T.pack . show . typeRep $ Proxy @a, seed, testCases} + B.putStrLn $ J.encodePretty' (J.defConfig {J.confIndent = J.Spaces 2}) output + +mkTestCase :: forall a. (Show a, EncCBOR a) => a -> TestCase a +mkTestCase a = + let haskellRepr = T.pack $ show a + -- XXX: we’re using the latest protocol version + protocolVersion :: CLB.Version = maxBound + cbor = + ( T.decodeUtf8Lenient + . B16.encode + . BL.toStrict + . C.toLazyByteString + . CLB.toPlainEncoding protocolVersion + . CLB.encCBOR + ) + a + in TestCase {cbor, haskellRepr, json = a} diff --git a/testgen-hs/testgen-hs.cabal b/testgen-hs/testgen-hs.cabal new file mode 100644 index 0000000..5ec022e --- /dev/null +++ b/testgen-hs/testgen-hs.cabal @@ -0,0 +1,52 @@ +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-conway + , cborg + , generic-random + , optparse-applicative + , ouroboros-consensus + , ouroboros-consensus-cardano + , ouroboros-consensus-cardano:unstable-cardano-testlib + , QuickCheck + , quickcheck-instances + , random + , serialise + , text + , time + + other-modules: + CLI + Generators + + default-extensions: + DeriveAnyClass + DeriveGeneric + DerivingVia + 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