From abebf8dd6ea930066d1f376f648d183298f4a67d Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 31 Oct 2025 11:00:53 -0400 Subject: [PATCH 01/19] Implement `liftToIntegration` We use this function to mainly lift the two functions we want to use outside of a property testing context: - createTestnetEnv - cardanoTestnet --- cardano-testnet/src/Testnet/Start/Cardano.hs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cardano-testnet/src/Testnet/Start/Cardano.hs b/cardano-testnet/src/Testnet/Start/Cardano.hs index bb3ddea34c9..ef4dcb3ed40 100644 --- a/cardano-testnet/src/Testnet/Start/Cardano.hs +++ b/cardano-testnet/src/Testnet/Start/Cardano.hs @@ -23,6 +23,8 @@ module Testnet.Start.Cardano , getDefaultAlonzoGenesis , getDefaultShelleyGenesis , retryOnAddressInUseError + + , liftToIntegration ) where @@ -80,8 +82,12 @@ testMinimumConfigurationRequirements :: () => CardanoTestnetOptions -> m () testMinimumConfigurationRequirements options = withFrozenCallStack $ do when (cardanoNumPools options < 1) $ do - H.note_ "Need at least one SPO node to produce blocks, but got none." - H.failure + throwM $ MinimumConfigRequirementsError "Need at least one SPO node to produce blocks, but got none." + +liftToIntegration :: RIO ResourceMap a -> H.Integration a +liftToIntegration r = do + rMap <- lift $ lift getInternalState + liftIOAnnotated $ runRIO rMap r createTestnetEnv :: () => HasCallStack From 4719832551b381fad8f9906c9263f830aedc465a Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 31 Oct 2025 14:07:41 -0400 Subject: [PATCH 02/19] Implement mkConfig and mkConfigAbs --- cardano-testnet/src/Testnet/Start/Types.hs | 25 +++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/cardano-testnet/src/Testnet/Start/Types.hs b/cardano-testnet/src/Testnet/Start/Types.hs index b9e557be742..8189b8f0564 100644 --- a/cardano-testnet/src/Testnet/Start/Types.hs +++ b/cardano-testnet/src/Testnet/Start/Types.hs @@ -41,6 +41,8 @@ module Testnet.Start.Types , NodeConfiguration , NodeConfigurationYaml , mkConf + , mkConfigAbs + , mkConfig ) where import Cardano.Api hiding (cardanoEra) @@ -58,6 +60,7 @@ import qualified Data.Text as Text import Data.Word import GHC.Stack import qualified Network.HTTP.Simple as HTTP +import System.Directory (createDirectory, doesDirectoryExist, makeAbsolute) import System.FilePath (addTrailingPathSeparator) import Testnet.Filepath @@ -285,18 +288,34 @@ data Conf = Conf , updateTimestamps :: UpdateTimestamps } deriving (Eq, Show) --- | Create a 'Conf' from a temporary absolute path, with Genesis Hashes enabled --- and updating time stamps disabled. -- Logs the argument in the test. mkConf :: (HasCallStack, MonadTest m) => FilePath -> m Conf mkConf tempAbsPath' = withFrozenCallStack $ do H.note_ tempAbsPath' - pure $ Conf + pure $ mkConfig tempAbsPath' + +-- | Create a 'Conf' from a temporary absolute path, with Genesis Hashes enabled +-- and updating time stamps disabled. +mkConfig :: FilePath -> Conf +mkConfig tempAbsPath' = + Conf { genesisHashesPolicy = WithHashes , tempAbsPath = TmpAbsolutePath (addTrailingPathSeparator tempAbsPath') , updateTimestamps = DontUpdateTimestamps } +mkConfigAbs :: FilePath -> IO Conf +mkConfigAbs userOutputDir = do + absUserOutputDir <- makeAbsolute userOutputDir + dirExists <- doesDirectoryExist absUserOutputDir + let conf = mkConfig absUserOutputDir + if dirExists then + -- Happens when the environment has previously been created by the user + return conf + else do + createDirectory absUserOutputDir + return conf + -- | @anyEraToString (AnyCardanoEra ByronEra)@ returns @"byron"@ anyEraToString :: AnyCardanoEra -> String anyEraToString (AnyCardanoEra e) = eraToString e From 6fec6698a353495625069fd23c8b123b1275d105 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 31 Oct 2025 14:09:01 -0400 Subject: [PATCH 03/19] Remove runTestnet usage in runCardanoOptions and testnetRoutine from createEnvOptions This removes the unnecessary Integration monad --- cardano-testnet/src/Parsers/Run.hs | 36 +++++++++++++++--------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cardano-testnet/src/Parsers/Run.hs b/cardano-testnet/src/Parsers/Run.hs index 52fa3bc81ca..866e7c4c129 100644 --- a/cardano-testnet/src/Parsers/Run.hs +++ b/cardano-testnet/src/Parsers/Run.hs @@ -1,6 +1,5 @@ {-# LANGUAGE GADTs #-} {-# LANGUAGE LambdaCase #-} -{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} module Parsers.Run @@ -11,13 +10,14 @@ module Parsers.Run ) where import Cardano.CLI.Environment +import Control.Monad import Data.Default.Class (def) import Data.Foldable import Options.Applicative import qualified Options.Applicative as Opt - -import Testnet.Property.Run +import RIO (runRIO) +import RIO.Orphans import Testnet.Start.Cardano import Testnet.Start.Types @@ -60,8 +60,8 @@ createEnvOptions CardanoTestnetCreateEnvOptions , createEnvGenesisOptions=genesisOptions , createEnvOutputDir=outputDir , createEnvCreateEnvOptions=ceOptions - } = - testnetRoutine (UserProvidedEnv outputDir) $ \conf -> do + } = do + conf <- mkConfigAbs outputDir createTestnetEnv testnetOptions genesisOptions ceOptions -- Do not add hashes to the main config file, so that genesis files @@ -70,25 +70,25 @@ createEnvOptions CardanoTestnetCreateEnvOptions runCardanoOptions :: CardanoTestnetCliOptions -> IO () runCardanoOptions CardanoTestnetCliOptions - { cliTestnetOptions=testnetOptions@CardanoTestnetOptions{cardanoOutputDir} + { cliTestnetOptions=testnetOptions , cliGenesisOptions=genesisOptions , cliNodeEnvironment=env - , cliUpdateTimestamps=updateTimestamps - } = + , cliUpdateTimestamps=updateTimestamps' + } = do case env of - NoUserProvidedEnv -> + NoUserProvidedEnv -> do -- Create the sandbox, then run cardano-testnet. -- It is not necessary to honor `cliUpdateTimestamps` here, because -- the genesis files will be created with up-to-date stamps already. - runTestnet cardanoOutputDir $ \conf -> do - createTestnetEnv - testnetOptions genesisOptions def - conf - cardanoTestnet testnetOptions conf - UserProvidedEnv nodeEnvPath -> + conf <- mkConfigAbs "testnet" + runRIO () $ createTestnetEnv + testnetOptions genesisOptions def + conf + withResourceMap (\rm -> void . runRIO rm $ cardanoTestnet testnetOptions conf) + UserProvidedEnv nodeEnvPath -> do -- Run cardano-testnet in the sandbox provided by the user -- In that case, 'cardanoOutputDir' is not used - runTestnet (UserProvidedEnv nodeEnvPath) $ \conf -> - cardanoTestnet + conf <- mkConfigAbs nodeEnvPath + withResourceMap (\rm -> void . runRIO rm $ cardanoTestnet testnetOptions - conf{updateTimestamps=updateTimestamps} + conf{updateTimestamps=updateTimestamps'}) From 43ebfa1d8e937aed43162cc7634e7189b6e19c8b Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 31 Oct 2025 14:36:56 -0400 Subject: [PATCH 04/19] Implement liftIOAnnotated which annotates exceptions with a callstack Add Testnet.Process.RunIO which exposes execCli functions without the MonadTest constraint --- cardano-testnet/src/Testnet/Process/RunIO.hs | 284 +++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 cardano-testnet/src/Testnet/Process/RunIO.hs diff --git a/cardano-testnet/src/Testnet/Process/RunIO.hs b/cardano-testnet/src/Testnet/Process/RunIO.hs new file mode 100644 index 00000000000..98d5c924976 --- /dev/null +++ b/cardano-testnet/src/Testnet/Process/RunIO.hs @@ -0,0 +1,284 @@ + +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Testnet.Process.RunIO + ( execCli' + , execCli_ + , mkExecConfig + , procNode + , liftIOAnnotated + ) where + +import Prelude +import Data.Aeson (eitherDecode) +import Data.Monoid (Last (..)) +import Hedgehog.Extras.Internal.Plan (Component (..), Plan (..)) +import RIO +import System.FilePath (takeDirectory) +import System.FilePath.Posix (()) +import System.Process (CreateProcess (..)) + +import Control.Exception.Annotated (exceptionWithCallStack) +import qualified Data.ByteString.Lazy as LBS +import qualified Data.List as L +import qualified Data.Text as T +import qualified GHC.Stack as GHC +import qualified Hedgehog.Extras.Stock.OS as OS +import qualified Hedgehog.Extras.Stock.IO.Network.Sprocket as IO +import Hedgehog.Extras.Test.Process (ExecConfig(..)) +import qualified System.Directory as IO +import qualified System.Environment as IO +import qualified System.Exit as IO +import qualified System.IO.Unsafe as IO +import qualified System.Process as IO + + + +defaultExecConfig :: ExecConfig +defaultExecConfig = ExecConfig + { execConfigEnv = mempty + , execConfigCwd = mempty + } + + +mkExecConfig :: () + => MonadIO m + => FilePath + -> IO.Sprocket + -> Int -- ^ Network id + -> m ExecConfig +mkExecConfig tempBaseAbsPath sprocket networkId = do + env' <- liftIOAnnotated IO.getEnvironment + + return ExecConfig + { execConfigEnv = Last $ Just $ + [ ("CARDANO_NODE_SOCKET_PATH", IO.sprocketArgumentName sprocket) + , ("CARDANO_NODE_NETWORK_ID", show networkId) + ] + -- The environment must be passed onto child process on Windows in order to + -- successfully start that process. + <> env' + , execConfigCwd = Last $ Just tempBaseAbsPath + } + + +execCli' + :: MonadIO m + => ExecConfig + -> [String] + -> m String +execCli' execConfig = GHC.withFrozenCallStack $ execFlex' execConfig "cardano-cli" "CARDANO_CLI" + +execCli_ + :: HasCallStack + => MonadIO m + => [String] + -> m () +execCli_ = GHC.withFrozenCallStack $ void . liftIOAnnotated . runRIO () . execCli + +execCli + :: HasCallStack + => [String] + -> RIO env String +execCli = GHC.withFrozenCallStack $ execFlex "cardano-cli" "CARDANO_CLI" + +-- | Create a process returning its stdout. +-- +-- Being a 'flex' function means that the environment determines how the process is launched. +-- +-- When running in a nix environment, the 'envBin' argument describes the environment variable +-- that defines the binary to use to launch the process. +-- +-- When running outside a nix environment, the `pkgBin` describes the name of the binary +-- to launch via cabal exec. +execFlex + :: String + -> String + -> [String] + -> RIO env String +execFlex = execFlex' defaultExecConfig + +execFlex' + :: MonadIO m + => ExecConfig + -> String + -> String + -> [String] + -> m String +execFlex' execConfig pkgBin envBin arguments = GHC.withFrozenCallStack $ do + (exitResult, stdout', _stderr) <- execFlexAny' execConfig pkgBin envBin arguments + case exitResult of + IO.ExitFailure exitCode -> throwString $ + unlines $ + [ "Process exited with non-zero exit-code: " ++ show @Int exitCode ] + ++ (if L.null stdout' then [] else ["━━━━ stdout ━━━━" , stdout']) + ++ (if L.null _stderr then [] else ["━━━━ stderr ━━━━" , _stderr]) + IO.ExitSuccess -> return stdout' + +-- | Run a process, returning its exit code, its stdout, and its stderr. + +-- Contrary to @execFlex'@, this function doesn't fail if the call fails. +-- So, if you want to test something negative, this is the function to use. +execFlexAny' + :: HasCallStack + => MonadIO m + => ExecConfig + -> String -- ^ @pkgBin@: name of the binary to launch via 'cabal exec' + -> String -- ^ @envBin@: environment variable defining the binary to launch the process, when in Nix + -> [String] + -> m (ExitCode, String, String) -- ^ exit code, stdout, stderr +execFlexAny' execConfig pkgBin envBin arguments = GHC.withFrozenCallStack $ do + cp <- procFlex' execConfig pkgBin envBin arguments + --H.annotate . ("━━━━ command ━━━━\n" <>) $ case IO.cmdspec cp of + -- IO.ShellCommand cmd -> cmd + -- IO.RawCommand cmd args -> cmd <> " " <> L.unwords (argQuote <$> args) + liftIOAnnotated $ IO.readCreateProcessWithExitCode cp "" + + + +procFlex' + :: HasCallStack + => MonadIO m + => ExecConfig + -> String + -- ^ Cabal package name corresponding to the executable + -> String + -- ^ Environment variable pointing to the binary to run + -> [String] + -- ^ Arguments to the CLI command + -> m CreateProcess + -- ^ Captured stdout +procFlex' execConfig pkg binaryEnv arguments = GHC.withFrozenCallStack $ do + bin <- binFlex pkg binaryEnv + return (IO.proc bin arguments) + { IO.env = getLast $ execConfigEnv execConfig + , IO.cwd = getLast $ execConfigCwd execConfig + -- this allows sending signals to the created processes, without killing the test-suite process + , IO.create_group = True + } + +-- | Compute the path to the binary given a package name or an environment variable override. +binFlex + :: HasCallStack + => MonadIO m + => String + -- ^ Package name + -> String + -- ^ Environment variable pointing to the binary to run + -> m FilePath + -- ^ Path to executable +binFlex pkg binaryEnv = do + maybeEnvBin <- liftIOAnnotated $ IO.lookupEnv binaryEnv + case maybeEnvBin of + Just envBin -> return envBin + Nothing -> binDist pkg binaryEnv + +-- | Discover the location of the plan.json file. +planJsonFile :: String +planJsonFile = IO.unsafePerformIO $ do + maybeBuildDir <- liftIOAnnotated $ IO.lookupEnv "CABAL_BUILDDIR" + case maybeBuildDir of + Just buildDir -> return $ ".." buildDir "cache/plan.json" + Nothing -> findDefaultPlanJsonFile +{-# NOINLINE planJsonFile #-} + + +-- | Find the nearest plan.json going upwards from the current directory. +findDefaultPlanJsonFile :: IO FilePath +findDefaultPlanJsonFile = IO.getCurrentDirectory >>= go + where go :: FilePath -> IO FilePath + go d = do + let planRelPath = "dist-newstyle/cache/plan.json" + file = d planRelPath + exists <- IO.doesFileExist file + if exists + then return file + else do + let parent = takeDirectory d + if parent == d + then return planRelPath + else go parent + +addExeSuffix :: String -> String +addExeSuffix s = if ".exe" `L.isSuffixOf` s + then s + else s <> exeSuffix + + +exeSuffix :: String +exeSuffix = if OS.isWin32 then ".exe" else "" + +-- | Consult the "plan.json" generated by cabal to get the path to the executable corresponding. +-- to a haskell package. It is assumed that the project has already been configured and the +-- executable has been built. +-- Throws an exception on failure. +binDist + :: HasCallStack + => MonadIO m + => String + -- ^ Package name + -> String + -- ^ Environment variable pointing to the binary to run (used for error messages only) + -> m FilePath + -- ^ Path to executable +binDist pkg binaryEnv = do + doesPlanExist <- liftIOAnnotated $ IO.doesFileExist planJsonFile + unless doesPlanExist $ + error $ "Could not find plan.json in the path: " + <> planJsonFile + <> ". Please run \"cabal build " + <> pkg + <> "\" if you are working with sources. Otherwise define " + <> binaryEnv + <> " and have it point to the executable you want." + contents <- liftIOAnnotated $ LBS.readFile planJsonFile + + case eitherDecode contents of + Right plan -> case L.filter matching (plan & installPlan) of + (component:_) -> case component & binFile of + Just bin -> return $ addExeSuffix (T.unpack bin) + Nothing -> error $ "missing \"bin-file\" key in plan component: " <> show component <> " in the plan in: " <> planJsonFile + [] -> error $ "Cannot find \"component-name\" key with the value \"exe:" <> pkg <> "\" in the plan in: " <> planJsonFile + Left message -> error $ "Cannot decode plan in " <> planJsonFile <> ": " <> message + where matching :: Component -> Bool + matching component = case componentName component of + Just name -> name == "exe:" <> T.pack pkg + Nothing -> False + + + +procNode + :: (HasCallStack) + => [String] + -- ^ Arguments to the CLI command + -> RIO env CreateProcess + -- ^ Captured stdout +procNode = GHC.withFrozenCallStack $ procFlex "cardano-node" "CARDANO_NODE" + + +-- | Create a 'CreateProcess' describing how to start a process given the Cabal package name +-- corresponding to the executable, an environment variable pointing to the executable, +-- and an argument list. +-- +-- The actual executable used will the one specified by the environment variable, but if +-- the environment variable is not defined, it will be found instead by consulting the +-- "plan.json" generated by cabal. It is assumed that the project has already been +-- configured and the executable has been built. +procFlex + :: HasCallStack + => String + -- ^ Cabal package name corresponding to the executable + -> String + -- ^ Environment variable pointing to the binary to run + -> [String] + -- ^ Arguments to the CLI command + -> RIO env CreateProcess + -- ^ Captured stdout +procFlex = procFlex' defaultExecConfig + + +liftIOAnnotated :: (HasCallStack, MonadIO m) => IO a -> m a +liftIOAnnotated action = GHC.withFrozenCallStack $ + liftIO $ action `catch` (\(e :: SomeException) -> throwM $ exceptionWithCallStack e) \ No newline at end of file From f831850cab6d158ab8f8aa74e560adff912c9595 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 31 Oct 2025 14:38:28 -0400 Subject: [PATCH 05/19] Remove testnetRoutine --- cardano-testnet/src/Testnet/Property/Run.hs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/cardano-testnet/src/Testnet/Property/Run.hs b/cardano-testnet/src/Testnet/Property/Run.hs index c52717d7fc8..476fbe47c19 100644 --- a/cardano-testnet/src/Testnet/Property/Run.hs +++ b/cardano-testnet/src/Testnet/Property/Run.hs @@ -3,7 +3,6 @@ module Testnet.Property.Run ( runTestnet - , testnetRoutine -- Ignore tests on various OSs , ignoreOn , ignoreOnWindows @@ -119,21 +118,7 @@ testnetProperty env runTn = void $ runTn conf H.failure -- Intentional failure to force failure report --- | Runs a routine, which is supposed to end in finite duration -testnetRoutine :: UserProvidedEnv -> (Conf -> H.Integration ()) -> IO () -testnetRoutine env runRoutine = void . H.check $ case env of - NoUserProvidedEnv -> - integrationWorkspace "testnet" $ mkConf >=> runRoutine - UserProvidedEnv userOutputDir -> integration $ do - absUserOutputDir <- H.evalIO $ makeAbsolute userOutputDir - dirExists <- H.evalIO $ doesDirectoryExist absUserOutputDir - if dirExists then - -- Happens when the environment has previously been created by the user - H.note_ $ "Reusing " <> absUserOutputDir - else do - liftIO $ createDirectory absUserOutputDir - H.note_ $ "Created " <> absUserOutputDir - mkConf absUserOutputDir >>= runRoutine + -- Ignore properties on various OSs From e86447d647081a337de8ee0a95ef893eb51f5823 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 31 Oct 2025 14:39:16 -0400 Subject: [PATCH 06/19] Implement asyncRegister_ This differs from hedgehog-extras's asyncRegister function in that the thread is linked before it is cancelled. --- cardano-testnet/src/Testnet/Runtime.hs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cardano-testnet/src/Testnet/Runtime.hs b/cardano-testnet/src/Testnet/Runtime.hs index 4ca93877e14..ca5a5434dca 100644 --- a/cardano-testnet/src/Testnet/Runtime.hs +++ b/cardano-testnet/src/Testnet/Runtime.hs @@ -14,6 +14,8 @@ module Testnet.Runtime ( startNode , startLedgerNewEpochStateLogging , NodeStartFailure (..) + -- Exposed for testing purposes + , asyncRegister_ ) where import Cardano.Api @@ -362,3 +364,20 @@ instance (L.EraTxOut ledgerera, L.EraGov ledgerera, L.EraCertState ledgerera, L. , "rewardUpdate" .= nesRu , "currentStakeDistribution" .= nesPd ] + + +-- | Runs an action in background, and registers its cancellation to 'MonadResource'. +asyncRegister_ :: HasCallStack + => MonadResource m + => IO a -- ^ Action to run in background + -> m (ReleaseKey, H.Async a) +asyncRegister_ act = GHC.withFrozenCallStack $ do + allocate + (do a <- H.async act + H.link a + return a + ) + cleanUp + where + cleanUp :: H.Async a -> IO () + cleanUp = H.cancel \ No newline at end of file From 5b970e8340e0f7f100cb4674fde3f3dce50c97ed Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 31 Oct 2025 14:54:38 -0400 Subject: [PATCH 07/19] Add hprop_asyncRegister_sanity_check --- .../Cardano/Testnet/Test/SanityCheck.hs | 45 +++++++++++++++++-- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/SanityCheck.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/SanityCheck.hs index 3260099705d..4254664b59e 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/SanityCheck.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/SanityCheck.hs @@ -1,11 +1,13 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DisambiguateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} module Cardano.Testnet.Test.SanityCheck - ( hprop_ledger_events_sanity_check + ( hprop_asyncRegister_sanity_check + , hprop_ledger_events_sanity_check ) where import Cardano.Api @@ -13,12 +15,16 @@ import Cardano.Api import Cardano.Testnet import Prelude - +import RIO +import Control.Monad.Trans.Resource +import Control.Monad.Trans.Resource.Internal import Data.Default.Class -import GHC.IO.Exception (IOException) +import Data.Time.Clock +import GHC.Conc (threadStatus, ThreadStatus (..)) import GHC.Stack import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Runtime import Testnet.Start.Types import Hedgehog @@ -50,6 +56,7 @@ hprop_ledger_events_sanity_check = integrationRetryWorkspace 2 "ledger-events-sa TestnetRuntime{configurationFile, testnetNodes} <- createAndRunTestnet fastTestnetOptions shelleyOptions conf + nr@TestnetNode{nodeSprocket} <- H.headM testnetNodes let socketPath = nodeSocketPath nr @@ -88,3 +95,35 @@ foldBlocksAccumulator _ _ allEvents _ _ = filterPoolReap :: LedgerEvent -> Bool filterPoolReap (PoolReap _) = True filterPoolReap _ = False + + +hprop_asyncRegister_sanity_check :: Property +hprop_asyncRegister_sanity_check = + withTests 1 . property $ lift $ do + + beforeForkedThread <- getCurrentTime + (internalState,tId) <- runResourceT $ do + s <- getInternalState + (_,asyncA) <- asyncRegister_ (threadDelay 10_000_000) + let tId = asyncThreadId asyncA + return (s,tId) + afterForkedThread <- getCurrentTime + let diff' = diffUTCTime afterForkedThread beforeForkedThread + -- The forked thread (asyncRegister_) may be some long running IO action. + -- When the ResourceT block has finished this action should be cancelled. + -- Therefore we check to see that the thread is indeed cancelled when the + -- ResourceT block has finished. + when (diff' >= 5) $ + throwString $ "Forked thread took too long: " <> show diff' + + stat <- threadStatus tId + + case stat of + ThreadFinished -> return () + _ -> throwString $ "Async thread not finished as expected, status: " <> show stat + + rMap <- readIORef internalState + case rMap of + ReleaseMapClosed -> return () + _ -> throwString "Release map should be closed" + From 3e6b8a478a90376590222251348d5e56e43cd7c9 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 31 Oct 2025 14:55:48 -0400 Subject: [PATCH 08/19] Remove Integration monad return type from createTestnetEnv and cardanoTestnet --- cardano-testnet/src/Testnet/Start/Cardano.hs | 132 ++++++++++--------- 1 file changed, 70 insertions(+), 62 deletions(-) diff --git a/cardano-testnet/src/Testnet/Start/Cardano.hs b/cardano-testnet/src/Testnet/Start/Cardano.hs index ef4dcb3ed40..7e1faf80ba8 100644 --- a/cardano-testnet/src/Testnet/Start/Cardano.hs +++ b/cardano-testnet/src/Testnet/Start/Cardano.hs @@ -41,7 +41,8 @@ import Ouroboros.Network.PeerSelection.RelayAccessPoint (RelayAccessPo import Prelude hiding (lines) import Control.Concurrent (threadDelay) -import Control.Monad +import Control.Monad +import Control.Monad.Catch import Data.Aeson import qualified Data.Aeson.Encode.Pretty as A import qualified Data.Aeson.KeyMap as A @@ -57,32 +58,36 @@ import qualified Data.Time.Clock as DTC import GHC.Stack import qualified System.Directory as IO import System.FilePath (()) -import qualified System.Info as OS import Testnet.Components.Configuration import qualified Testnet.Defaults as Defaults import Testnet.Filepath import Testnet.Handlers (interruptNodesOnSigINT) -import Testnet.Process.Run (execCli', execCli_, mkExecConfig) +import Testnet.Process.RunIO (execCli', execCli_, liftIOAnnotated, mkExecConfig) import Testnet.Property.Assert (assertChainExtended, assertExpectedSposInLedgerState) import Testnet.Runtime as TR import Testnet.Start.Types import Testnet.Types as TR hiding (shelleyGenesis) -import Hedgehog (MonadTest) -import qualified Hedgehog as H import qualified Hedgehog.Extras as H import qualified Hedgehog.Extras.Stock.IO.Network.Port as H + +import RIO (RIO(..),runRIO, throwString, MonadUnliftIO) +import Control.Monad.Trans.Resource (getInternalState, MonadResource) +import Testnet.Orphans () +import RIO.Orphans (ResourceMap) +import UnliftIO.Async + -- | There are certain conditions that need to be met in order to run -- a valid node cluster. testMinimumConfigurationRequirements :: () => HasCallStack - => MonadTest m + => MonadIO m => CardanoTestnetOptions -> m () testMinimumConfigurationRequirements options = withFrozenCallStack $ do when (cardanoNumPools options < 1) $ do - throwM $ MinimumConfigRequirementsError "Need at least one SPO node to produce blocks, but got none." + throwString "Need at least one SPO node to produce blocks, but got none." liftToIntegration :: RIO ResourceMap a -> H.Integration a liftToIntegration r = do @@ -91,11 +96,13 @@ liftToIntegration r = do createTestnetEnv :: () => HasCallStack + => MonadIO m + => MonadThrow m => CardanoTestnetOptions -> GenesisOptions -> CreateEnvOptions -> Conf - -> H.Integration () + -> m () createTestnetEnv testnetOptions@CardanoTestnetOptions { cardanoNodeEra=asbe @@ -114,11 +121,12 @@ createTestnetEnv testMinimumConfigurationRequirements testnetOptions AnyShelleyBasedEra sbe <- pure asbe + _ <- createSPOGenesisAndFiles testnetOptions genesisOptions onChainParams (TmpAbsolutePath tmpAbsPath) - configurationFile <- H.noteShow $ tmpAbsPath "configuration.yaml" + let configurationFile = tmpAbsPath "configuration.yaml" -- Add Byron, Shelley and Alonzo genesis hashes to node configuration config' <- case genesisHashesPolicy of WithHashes -> createConfigJson (TmpAbsolutePath tmpAbsPath) sbe @@ -128,22 +136,23 @@ createTestnetEnv "EnableP2P" (Bool $ topologyType == P2PTopology) config' - H.evalIO $ LBS.writeFile configurationFile $ A.encodePretty $ Object config + + liftIOAnnotated . LBS.writeFile configurationFile $ A.encodePretty $ Object config -- Create network topology, with abstract IDs in lieu of addresses let nodeIds = fst <$> zip [1..] cardanoNodes forM_ nodeIds $ \i -> do let nodeDataDir = tmpAbsPath Defaults.defaultNodeDataDir i - H.evalIO $ IO.createDirectoryIfMissing True nodeDataDir + liftIOAnnotated $ IO.createDirectoryIfMissing True nodeDataDir let producers = NodeId <$> filter (/= i) nodeIds case topologyType of DirectTopology -> let topology = Direct.RealNodeTopology producers - in H.lbsWriteFile (nodeDataDir "topology.json") $ A.encodePretty topology + in liftIOAnnotated . LBS.writeFile (nodeDataDir "topology.json") $ A.encodePretty topology P2PTopology -> let topology = Defaults.defaultP2PTopology producers - in H.lbsWriteFile (nodeDataDir "topology.json") $ A.encodePretty topology + in liftIOAnnotated . LBS.writeFile (nodeDataDir "topology.json") $ A.encodePretty topology -- | Starts a number of nodes, as configured by the value of the 'cardanoNodes' -- field in the 'CardanoTestnetOptions' argument. Regarding this field, you can either: @@ -211,11 +220,15 @@ createTestnetEnv -- > ├── configuration.json -- > ├── current-stake-pools.json -- > └── module -cardanoTestnet :: () - => HasCallStack +cardanoTestnet + :: HasCallStack + => MonadUnliftIO m + => MonadResource m + => MonadCatch m + => MonadFail m => CardanoTestnetOptions -- ^ The options to use -> Conf -- ^ Path to the test sandbox - -> H.Integration TestnetRuntime + -> m TestnetRuntime cardanoTestnet testnetOptions Conf @@ -232,9 +245,11 @@ cardanoTestnet byronGenesisFile = tmpAbsPath "byron-genesis.json" shelleyGenesisFile = tmpAbsPath "shelley-genesis.json" - H.note_ OS.os - - shelleyGenesis@ShelleyGenesis{sgNetworkMagic} <- H.readJsonFileOk shelleyGenesisFile + sBytes <- liftIOAnnotated (LBS.readFile shelleyGenesisFile) + shelleyGenesis@ShelleyGenesis{sgNetworkMagic} + <- case eitherDecode sBytes of + Right sg -> return sg + Left err -> throwString $ "Could not decode shelley genesis file: " <> shelleyGenesisFile <> " Error: " <> err let testnetMagic :: Int = fromIntegral sgNetworkMagic wallets <- forM [1..3] $ \idx -> do @@ -248,7 +263,7 @@ cardanoTestnet , "--out-file", paymentAddrFile ] - paymentAddr <- H.readFile paymentAddrFile + paymentAddr <- liftIOAnnotated $ readFile paymentAddrFile pure $ PaymentKeyInfo { paymentKeyInfoPair = utxoKeys @@ -262,13 +277,13 @@ cardanoTestnet forM_ portNumbers $ \(i, portNumber) -> do let nodeDataDir = tmpAbsPath Defaults.defaultNodeDataDir i - H.evalIO $ IO.createDirectoryIfMissing True nodeDataDir - H.writeFile (nodeDataDir "port") (show portNumber) + liftIOAnnotated $ IO.createDirectoryIfMissing True nodeDataDir + liftIOAnnotated $ writeFile (nodeDataDir "port") (show portNumber) let idToRemoteAddressDirect :: () - => MonadTest m => HasCallStack + => MonadIO m => NodeId -> m RemoteAddress idToRemoteAddressDirect (NodeId i) = case lookup i portNumbers of Just port -> pure $ RemoteAddress @@ -277,10 +292,9 @@ cardanoTestnet , raValency = 1 } Nothing -> do - H.note_ $ "Found node id that was unaccounted for: " ++ show i - H.failure + throwString $ "Found node id that was unaccounted for: " ++ show i idToRemoteAddressP2P :: () - => MonadTest m + => MonadIO m => HasCallStack => NodeId -> m RelayAccessPoint idToRemoteAddressP2P (NodeId i) = case lookup i portNumbers of @@ -288,51 +302,54 @@ cardanoTestnet (showIpv4Address testnetDefaultIpv4Address) port Nothing -> do - H.note_ $ "Found node id that was unaccounted for: " ++ show i - H.failure + throwString $ "Found node id that was unaccounted for: " ++ show i -- Implement concrete topology from abstract one, if necessary forM_ portNumbers $ \(i, _port) -> do let topologyPath = tmpAbsPath Defaults.defaultNodeDataDir i "topology.json" -- Try to decode either a direct topology file, or a P2P one - H.readJsonFile topologyPath >>= \case + tBytes <- liftIOAnnotated $ LBS.readFile topologyPath + case eitherDecode tBytes of Right (abstractTopology :: Direct.NetworkTopology NodeId) -> do topology <- mapM idToRemoteAddressDirect abstractTopology - H.lbsWriteFile topologyPath $ encode topology + liftIOAnnotated . LBS.writeFile topologyPath $ encode topology Left _ -> - H.readJsonFile topologyPath >>= \case + case eitherDecode tBytes of Right (abstractTopology :: P2P.NetworkTopology NodeId) -> do topology <- mapM idToRemoteAddressP2P abstractTopology - H.lbsWriteFile topologyPath $ encode topology + liftIOAnnotated . LBS.writeFile topologyPath $ encode topology Left e -> -- There can be multiple reasons for why both decodings have failed. -- Here we assume, very optimistically, that the user has already -- instantiated it with a concrete topology file. - H.note_ $ "Could not decode topology file. This may be okay. Reason for decoding failure is:\n" ++ e + -- TODO: It is suspicious that this decoding can fail. Investigate further. + liftIOAnnotated . putStrLn $ "Could not decode topology file: " <> topologyPath <> ". This may be okay. Reason for decoding failure is:\n" ++ e -- If necessary, update the time stamps in Byron and Shelley Genesis files. -- This is a QoL feature so that users who edit their configuration files don't -- have to manually set up the start times themselves. when (updateTimestamps == UpdateTimestamps) $ do - currentTime <- H.noteShowIO DTC.getCurrentTime - startTime <- H.noteShow $ DTC.addUTCTime startTimeOffsetSeconds currentTime + currentTime <- liftIOAnnotated DTC.getCurrentTime + let startTime = DTC.addUTCTime startTimeOffsetSeconds currentTime -- Update start time in Byron genesis file eByron <- runExceptT $ Byron.readGenesisData byronGenesisFile - (byronGenesis', _byronHash) <- H.leftFail eByron + (byronGenesis', _byronHash) <- + case eByron of + Right bg -> return bg + Left err -> throwString $ "Could not read byron genesis data from file: " <> byronGenesisFile <> " Error: " <> show err let byronGenesis = byronGenesis'{gdStartTime = startTime} - H.lbsWriteFile byronGenesisFile $ canonicalEncodePretty byronGenesis + liftIOAnnotated . LBS.writeFile byronGenesisFile $ canonicalEncodePretty byronGenesis -- Update start time in Shelley genesis file (which has been read already) let shelleyGenesis' = shelleyGenesis{sgSystemStart = startTime} - H.lbsWriteFile shelleyGenesisFile $ A.encodePretty shelleyGenesis' + liftIOAnnotated . LBS.writeFile shelleyGenesisFile $ A.encodePretty shelleyGenesis' - eTestnetNodes <- H.forConcurrently (zip [1..] portNumbersWithNodeOptions) $ \(i, (nodeOptions, port)) -> do + eTestnetNodes <- forConcurrently (zip [1..] portNumbersWithNodeOptions) $ \(i, (nodeOptions, port)) -> do let nodeName = Defaults.defaultNodeName i nodeDataDir = tmpAbsPath Defaults.defaultNodeDataDir i nodePoolKeysDir = tmpAbsPath Defaults.defaultSpoKeysDir i - H.note_ $ "Node name: " <> nodeName let (mKeys, spoNodeCliArgs) = case nodeOptions of RelayNodeOptions{} -> (Nothing, []) @@ -356,25 +373,22 @@ cardanoTestnet ] <> spoNodeCliArgs <> extraCliArgs nodeOptions + pure $ eRuntime <&> \rt -> rt{poolKeys=mKeys} let (failedNodes, testnetNodes') = partitionEithers eTestnetNodes unless (null failedNodes) $ do - H.noteShow_ . vsep $ prettyError <$> failedNodes - H.failure + throwString $ "Some nodes failed to start:\n" ++ show (vsep $ prettyError <$> failedNodes) -- Interrupt cardano nodes when the main process is interrupted - H.evalIO $ interruptNodesOnSigINT testnetNodes' - H.annotateShow $ nodeSprocket <$> testnetNodes' + liftIOAnnotated $ interruptNodesOnSigINT testnetNodes' -- FIXME: use foldEpochState waiting for chain extensions - now <- H.noteShowIO DTC.getCurrentTime - deadline <- H.noteShow $ DTC.addUTCTime 45 now + now <- liftIOAnnotated DTC.getCurrentTime + let deadline = DTC.addUTCTime 45 now forM_ (map nodeStdout testnetNodes') $ \nodeStdoutFile -> do assertChainExtended deadline nodeLoggingFormat nodeStdoutFile - H.noteShowIO_ DTC.getCurrentTime - let runtime = TestnetRuntime { configurationFile = File nodeConfigFile , shelleyGenesisFile = tmpAbsPath Defaults.defaultGenesisFilepath ShelleyEra @@ -386,22 +400,18 @@ cardanoTestnet let tempBaseAbsPath = makeTmpBaseAbsPath $ TmpAbsolutePath tmpAbsPath - node1sprocket <- H.headM $ testnetSprockets runtime + let node1sprocket = head $ testnetSprockets runtime execConfig <- mkExecConfig tempBaseAbsPath node1sprocket testnetMagic forM_ wallets $ \wallet -> do - H.cat . signingKeyFp $ paymentKeyInfoPair wallet - H.cat . verificationKeyFp $ paymentKeyInfoPair wallet - utxos <- execCli' execConfig + execCli' execConfig [ "latest", "query", "utxo" , "--address", Text.unpack $ paymentKeyInfoAddr wallet , "--cardano-mode" ] - H.note_ utxos - - stakePoolsFp <- H.note $ tmpAbsPath "current-stake-pools.json" + let stakePoolsFp = tmpAbsPath "current-stake-pools.json" assertExpectedSposInLedgerState stakePoolsFp nPools execConfig @@ -427,15 +437,15 @@ createAndRunTestnet :: () -> Conf -- ^ Path to the test sandbox -> H.Integration TestnetRuntime createAndRunTestnet testnetOptions genesisOptions conf = do - createTestnetEnv - testnetOptions genesisOptions def - conf - cardanoTestnet testnetOptions conf + liftToIntegration $ do + createTestnetEnv + testnetOptions genesisOptions def + conf + cardanoTestnet testnetOptions conf -- | Retry an action when `NodeAddressAlreadyInUseError` gets thrown from an action retryOnAddressInUseError :: forall m a. HasCallStack - => MonadTest m => MonadIO m => ExceptT NodeStartFailure m a -- ^ action being retried -> ExceptT NodeStartFailure m a @@ -444,7 +454,6 @@ retryOnAddressInUseError act = withFrozenCallStack $ go maximumTimeout retryTime go :: HasCallStack => NominalDiffTime -> NominalDiffTime -> ExceptT NodeStartFailure m a go timeout interval | timeout <= 0 = withFrozenCallStack $ do - H.note_ "Exceeded timeout when retrying node start" act | otherwise = withFrozenCallStack $ do !time <- liftIO DTC.getCurrentTime @@ -454,7 +463,6 @@ retryOnAddressInUseError act = withFrozenCallStack $ go maximumTimeout retryTime !time' <- liftIO DTC.getCurrentTime let elapsedTime = time' `diffUTCTime` time newTimeout = timeout - elapsedTime - H.note_ $ "Retrying on 'address in use' error, timeout: " <> show newTimeout go newTimeout interval e -> throwError e From 8270a2b205b8a1719830dfc1f826cc7f1a68cee3 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Mon, 3 Nov 2025 09:34:25 -0400 Subject: [PATCH 09/19] Remove MonadTest constraint from createSPOGenesisAndFiles --- .../src/Testnet/Components/Configuration.hs | 99 +++++++++++-------- 1 file changed, 57 insertions(+), 42 deletions(-) diff --git a/cardano-testnet/src/Testnet/Components/Configuration.hs b/cardano-testnet/src/Testnet/Components/Configuration.hs index 200e67138b7..e2b0766b66c 100644 --- a/cardano-testnet/src/Testnet/Components/Configuration.hs +++ b/cardano-testnet/src/Testnet/Components/Configuration.hs @@ -33,11 +33,12 @@ import Cardano.Network.PeerSelection.Bootstrap import Cardano.Network.PeerSelection.PeerTrustable import qualified Cardano.Node.Configuration.Topology as NonP2P import qualified Cardano.Node.Configuration.TopologyP2P as P2P +import Cardano.Node.Protocol.Byron import Ouroboros.Network.NodeToNode (DiffusionMode (..)) import Ouroboros.Network.PeerSelection.LedgerPeers import Ouroboros.Network.PeerSelection.State.LocalRootPeers -import Control.Exception.Safe (MonadCatch) +import Control.Exception import Control.Monad import Control.Monad.Extra import Data.Aeson @@ -45,7 +46,6 @@ import qualified Data.Aeson as Aeson import qualified Data.Aeson.Encode.Pretty as A import Data.Aeson.Key hiding (fromString) import Data.Aeson.KeyMap hiding (map) -import Data.Bifunctor (first) import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import qualified Data.List as List @@ -57,25 +57,25 @@ import Data.Word (Word64) import GHC.Stack (HasCallStack) import qualified GHC.Stack as GHC import qualified Network.HTTP.Simple as HTTP +import RIO ( MonadThrow, throwM) import qualified System.Directory as System import System.FilePath.Posix (takeDirectory, ()) + import Testnet.Blockfrost (blockfrostToGenesis) import qualified Testnet.Defaults as Defaults import Testnet.Filepath -import Testnet.Process.Run (execCli_) +import Testnet.Process.RunIO (execCli_, liftIOAnnotated) import Testnet.Start.Types -import Hedgehog -import qualified Hedgehog as H import qualified Hedgehog.Extras.Stock.OS as OS import qualified Hedgehog.Extras.Stock.Time as DTC -import qualified Hedgehog.Extras.Test.Base as H -import qualified Hedgehog.Extras.Test.File as H -- | Returns JSON encoded hashes of the era, as well as the hard fork configuration toggle. createConfigJson :: () - => (MonadTest m, MonadIO m, HasCallStack) + => HasCallStack + => MonadIO m + => MonadThrow m => TmpAbsolutePath -> ShelleyBasedEra era -- ^ The era used for generating the hard fork configuration toggle -> m (KeyMap Aeson.Value) @@ -93,7 +93,7 @@ createConfigJson (TmpAbsolutePath tempAbsPath) sbe = GHC.withFrozenCallStack $ d , Defaults.defaultYamlHardforkViaConfig sbe ] where - getHash :: (MonadTest m, MonadIO m) => CardanoEra a -> Text.Text -> m (KeyMap Value) + getHash :: MonadIO m => CardanoEra a -> Text.Text -> m (KeyMap Value) getHash e = getShelleyGenesisHash (tempAbsPath Defaults.defaultGenesisFilepath e) createConfigJsonNoHash :: () @@ -104,22 +104,25 @@ createConfigJsonNoHash = Defaults.defaultYamlHardforkViaConfig -- Generate hashes for genesis.json files getByronGenesisHash - :: (H.MonadTest m, MonadIO m) + :: MonadIO m + => MonadThrow m => FilePath -> m (KeyMap Aeson.Value) getByronGenesisHash path = do e <- runExceptT $ readGenesisData path - (_, genesisHash) <- H.leftFail e - let genesisHash' = unGenesisHash genesisHash - pure . singleton "ByronGenesisHash" $ toJSON genesisHash' + case e of + Left err -> throwM $ GenesisReadError path err + Right (_, genesisHash) -> do + let genesisHash' = unGenesisHash genesisHash + pure . singleton "ByronGenesisHash" $ toJSON genesisHash' getShelleyGenesisHash - :: (H.MonadTest m, MonadIO m) + :: MonadIO m => FilePath -> Text -> m (KeyMap Aeson.Value) getShelleyGenesisHash path key = do - content <- H.evalIO $ BS.readFile path + content <- liftIOAnnotated $ BS.readFile path let genesisHash = Crypto.hashWith id content :: Crypto.Hash Crypto.Blake2b_256 BS.ByteString pure . singleton (fromText key) $ toJSON genesisHash @@ -130,32 +133,35 @@ startTimeOffsetSeconds = if OS.isWin32 then 90 else 15 -- | A start time and 'ShelleyGenesis' value that are fit to pass to 'cardanoTestnet' getDefaultShelleyGenesis :: () - => HasCallStack => MonadIO m - => MonadTest m => AnyShelleyBasedEra -> Word64 -- ^ The max supply -> GenesisOptions -> m ShelleyGenesis getDefaultShelleyGenesis asbe maxSupply opts = do - currentTime <- H.noteShowIO DTC.getCurrentTime - startTime <- H.noteShow $ DTC.addUTCTime startTimeOffsetSeconds currentTime + currentTime <- liftIOAnnotated DTC.getCurrentTime + let startTime = DTC.addUTCTime startTimeOffsetSeconds currentTime return $ Defaults.defaultShelleyGenesis asbe startTime maxSupply opts -- | An 'AlonzoGenesis' value that is fit to pass to 'cardanoTestnet' getDefaultAlonzoGenesis :: () => HasCallStack - => MonadTest m + => MonadThrow m => ShelleyBasedEra era -> m AlonzoGenesis getDefaultAlonzoGenesis sbe = - H.evalEither $ first prettyError (Defaults.defaultAlonzoGenesis sbe) + case Defaults.defaultAlonzoGenesis sbe of + Right genesis -> return genesis + Left err -> throwM err + numSeededUTxOKeys :: Int numSeededUTxOKeys = 3 createSPOGenesisAndFiles - :: (MonadTest m, MonadCatch m, MonadIO m, HasCallStack) + :: MonadIO m + => HasCallStack + => MonadThrow m => CardanoTestnetOptions -- ^ The options to use -> GenesisOptions -> TestnetOnChainParams @@ -164,11 +170,12 @@ createSPOGenesisAndFiles createSPOGenesisAndFiles testnetOptions genesisOptions@GenesisOptions{genesisTestnetMagic} onChainParams - (TmpAbsolutePath tempAbsPath) = GHC.withFrozenCallStack $ do + (TmpAbsolutePath tempAbsPath) = do AnyShelleyBasedEra sbe <- pure cardanoNodeEra let genesisShelleyDirAbs = takeDirectory inputGenesisShelleyFp - genesisShelleyDir <- H.createDirectoryIfMissing genesisShelleyDirAbs + + genesisShelleyDir <- liftIOAnnotated $ System.createDirectoryIfMissing True genesisShelleyDirAbs >> pure genesisShelleyDirAbs let -- At least there should be a delegator per DRep -- otherwise some won't be representing anybody numStakeDelegators = max 3 (fromIntegral cardanoNumDReps) :: Int @@ -184,23 +191,20 @@ createSPOGenesisAndFiles alonzoGenesis' <- getDefaultAlonzoGenesis sbe let conwayGenesis' = Defaults.defaultConwayGenesis - (alonzoGenesis, conwayGenesis, shelleyGenesis) <- resolveOnChainParams onChainParams - (alonzoGenesis', conwayGenesis', shelleyGenesis') + (alonzoGenesis, conwayGenesis, shelleyGenesis) + <- resolveOnChainParams onChainParams + (alonzoGenesis', conwayGenesis', shelleyGenesis') -- Write Genesis files to disk, so they can be picked up by create-testnet-data - H.evalIO $ do + liftIOAnnotated $ do LBS.writeFile inputGenesisAlonzoFp $ A.encodePretty alonzoGenesis LBS.writeFile inputGenesisConwayFp $ A.encodePretty conwayGenesis LBS.writeFile inputGenesisShelleyFp $ A.encodePretty shelleyGenesis - H.note_ $ "Number of pools: " <> show nPoolNodes - H.note_ $ "Number of stake delegators: " <> show numStakeDelegators - H.note_ $ "Number of seeded UTxO keys: " <> show numSeededUTxOKeys - let era = toCardanoEra sbe - currentTime <- H.noteShowIO DTC.getCurrentTime - startTime <- H.noteShow $ DTC.addUTCTime startTimeOffsetSeconds currentTime + currentTime <- liftIOAnnotated DTC.getCurrentTime + let startTime = DTC.addUTCTime startTimeOffsetSeconds currentTime execCli_ $ [ eraToString sbe, "genesis", "create-testnet-data" ] @@ -225,9 +229,6 @@ createSPOGenesisAndFiles ] (\fp -> liftIO $ whenM (System.doesFileExist fp) (System.removeFile fp)) - files <- H.listDirectory tempAbsPath - forM_ files H.note - return genesisShelleyDir where inputGenesisShelleyFp = genesisInputFilepath ShelleyEra @@ -287,23 +288,37 @@ mkTopologyConfig numNodes allPorts port True = A.encodePretty topologyP2P DontUseBootstrapPeers Nothing + +data BlockfrostParamsError = BlockfrostParamsDecodeError FilePath String + deriving Show + +instance Exception BlockfrostParamsError where + displayException (BlockfrostParamsDecodeError fp err) = + "Failed to decode Blockfrost on-chain parameters from file " + <> fp + <> ": " + <> err + -- | Resolves different kinds of user-provided on-chain parameters -- into a unified, consistent set of Genesis files resolveOnChainParams :: () - => (MonadTest m, MonadIO m) => HasCallStack + => MonadIO m + => MonadThrow m => TestnetOnChainParams -> (AlonzoGenesis, ConwayGenesis, ShelleyGenesis) -> m (AlonzoGenesis, ConwayGenesis, ShelleyGenesis) resolveOnChainParams onChainParams geneses = case onChainParams of - DefaultParams -> pure geneses + DefaultParams -> do + pure geneses OnChainParamsFile file -> do - eParams <- H.readJsonFile file - params <- H.leftFail eParams - pure $ blockfrostToGenesis geneses params + eParams <- eitherDecode <$> liftIOAnnotated (LBS.readFile file) + case eParams of + Right params -> pure $ blockfrostToGenesis geneses params + Left err -> throwM $ BlockfrostParamsDecodeError file err OnChainParamsMainnet -> do - mainnetParams <- H.evalIO $ HTTP.getResponseBody <$> HTTP.httpJSON mainnetParamsRequest + mainnetParams <- liftIOAnnotated $ HTTP.getResponseBody <$> HTTP.httpJSON mainnetParamsRequest pure $ blockfrostToGenesis geneses mainnetParams From 37ade3d99b3cb6bd6a4ba2a2435158f94f962d29 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Mon, 3 Nov 2025 09:35:19 -0400 Subject: [PATCH 10/19] Remove MonadTest constraint from startNode and startLedgerNewEpochStateLogging --- cardano-testnet/src/Testnet/Runtime.hs | 63 ++++++++++++-------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/cardano-testnet/src/Testnet/Runtime.hs b/cardano-testnet/src/Testnet/Runtime.hs index ca5a5434dca..e2e5ecd2592 100644 --- a/cardano-testnet/src/Testnet/Runtime.hs +++ b/cardano-testnet/src/Testnet/Runtime.hs @@ -43,6 +43,7 @@ import GHC.Stack import qualified GHC.Stack as GHC import Network.Socket (HostAddress, PortNumber) import Prettyprinter (unAnnotate) +import RIO (runRIO) import qualified System.Directory as IO import System.FilePath import qualified System.IO as IO @@ -50,15 +51,13 @@ import qualified System.Process as IO import Testnet.Filepath import qualified Testnet.Ping as Ping -import Testnet.Process.Run +import Testnet.Process.Run (ProcessError (..), initiateProcess) +import Testnet.Process.RunIO (procNode, liftIOAnnotated) import Testnet.Types (TestnetNode (..), TestnetRuntime (configurationFile), showIpv4Address, testnetSprockets) -import Hedgehog (MonadTest) -import qualified Hedgehog as H import Hedgehog.Extras.Stock.IO.Network.Sprocket (Sprocket (..)) import qualified Hedgehog.Extras.Stock.IO.Network.Sprocket as H -import qualified Hedgehog.Extras.Test.Base as H import qualified Hedgehog.Extras.Test.Concurrent as H data NodeStartFailure @@ -105,7 +104,6 @@ startNode => MonadResource m => MonadCatch m => MonadFail m - => MonadTest m => TmpAbsolutePath -- ^ The temporary absolute path -> String @@ -151,14 +149,14 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do , "--port", show port , "--host-addr", showIpv4Address ipv4 ] - - nodeProcess <- newExceptT . fmap (first ExecutableRelatedFailure) . try $ procNode completeNodeCmd + nodeProcess <- newExceptT . fmap (first ExecutableRelatedFailure) . try $ runRIO () $ procNode completeNodeCmd -- The port number if it is obtained using 'H.randomPort', it is firstly bound to and then closed. The closing -- and release in the operating system is done asynchronously and can be slow. Here we wait until the port - -- is out of CLOSING state. - H.note_ $ "Waiting for port " <> show port <> " to be available before starting node" - H.assertM $ Ping.waitForPortClosed 30 0.1 port + + isClosed <- liftIOAnnotated $ Ping.waitForPortClosed 30 0.1 port + unless isClosed $ + throwString $ "Port is still in use after 30 seconds before starting node: " <> show port (Just stdIn, _, _, hProcess, _) <- firstExceptT ProcessRelatedFailure $ initiateProcess @@ -175,18 +173,18 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do >>= hoistMaybe (NodeExecutableError $ "startNode:" <+> pretty node <+> "'s process did not start.") -- We then log the pid in the temp dir structure. - liftIO $ IO.writeFile nodePidFile $ show pid + liftIOAnnotated $ IO.writeFile nodePidFile $ show pid -- Wait for socket to be created eSprocketError <- - H.evalIO $ + liftIOAnnotated $ Ping.waitForSprocket 120 -- timeout 0.2 -- check interval sprocket -- If we do have anything on stderr, fail. - stdErrContents <- liftIO $ IO.readFile nodeStderrFile + stdErrContents <- liftIOAnnotated $ IO.readFile nodeStderrFile unless (null stdErrContents) $ throwError $ mkNodeNonEmptyStderrError stdErrContents @@ -282,7 +280,6 @@ startLedgerNewEpochStateLogging :: HasCallStack => MonadCatch m => MonadResource m - => MonadTest m => TestnetRuntime -> FilePath -- ^ tmp workspace directory -> m () @@ -292,29 +289,25 @@ startLedgerNewEpochStateLogging testnetRuntime tmpWorkspace = withFrozenCallStac logFile = logDir "ledger-epoch-state.log" diffFile = logDir "ledger-epoch-state-diffs.log" - H.evalIO (IO.doesDirectoryExist logDir) >>= \case + liftIOAnnotated $ IO.doesDirectoryExist logDir >>= \case True -> pure () False -> do - H.note_ $ "Log directory does not exist: " <> logDir <> " - cannot start logging epoch states" - H.failure - - H.evalIO (IO.doesFileExist logFile) >>= \case - True -> do - H.note_ $ "Epoch states logging to " <> logFile <> " is already started." - False -> do - H.evalIO $ appendFile logFile "" - socketPath <- H.noteM $ H.sprocketSystemName <$> H.headM (testnetSprockets testnetRuntime) - - _ <- H.asyncRegister_ . runExceptT $ - foldEpochState - (configurationFile testnetRuntime) - (Api.File socketPath) - Api.QuickValidation - (EpochNo maxBound) - Nothing - (handler logFile diffFile) - - H.note_ $ "Started logging epoch states to: " <> logFile <> "\nEpoch state diffs are logged to: " <> diffFile + throwString $ "Log directory does not exist: " <> logDir <> " - cannot start logging epoch states" + + liftIOAnnotated $ IO.doesFileExist logFile >>= \case + True -> return () + False -> liftIO $ appendFile logFile "" + + let socketPath = H.sprocketSystemName $ head (testnetSprockets testnetRuntime) + let act = runExceptT $ + foldEpochState + (configurationFile testnetRuntime) + (Api.File socketPath) + Api.QuickValidation + (EpochNo maxBound) + Nothing + (handler logFile diffFile) + void $ asyncRegister_ act where handler :: FilePath -- ^ log file -> FilePath -- ^ diff file From 8e04ac0b793a9341fb399a6d8bc2b7936920de12 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Mon, 3 Nov 2025 09:40:43 -0400 Subject: [PATCH 11/19] Remove MonadTest constraints from assertion functions --- .../src/Testnet/Property/Assert.hs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/cardano-testnet/src/Testnet/Property/Assert.hs b/cardano-testnet/src/Testnet/Property/Assert.hs index 37095dcb008..a989abce132 100644 --- a/cardano-testnet/src/Testnet/Property/Assert.hs +++ b/cardano-testnet/src/Testnet/Property/Assert.hs @@ -22,7 +22,6 @@ import Prelude hiding (lines) import qualified Control.Concurrent as IO import Control.Monad -import Control.Monad.Catch (MonadCatch) import Control.Monad.Trans.Reader (ReaderT) import Control.Monad.Trans.Resource (ResourceT) import Data.Aeson (Value, (.:)) @@ -39,17 +38,16 @@ import qualified Data.Time.Clock as DTC import Data.Type.Equality import Data.Word (Word8) import GHC.Stack as GHC +import RIO (throwString) -import Testnet.Process.Run +import Testnet.Process.RunIO import Testnet.Start.Types import Hedgehog (MonadTest) import qualified Hedgehog as H import Hedgehog.Extras.Internal.Test.Integration (IntegrationState) import qualified Hedgehog.Extras.Stock.IO.File as IO -import Hedgehog.Extras.Test.Base (failMessage) import qualified Hedgehog.Extras.Test.Base as H -import qualified Hedgehog.Extras.Test.File as H import Hedgehog.Extras.Test.Process (ExecConfig) newlineBytes :: Word8 @@ -65,23 +63,23 @@ fileJsonGrep fp f = do return $ L.any f jsons assertByDeadlineIOCustom - :: (MonadTest m, MonadIO m, HasCallStack) + :: (MonadIO m, HasCallStack) => String -> DTC.UTCTime -> IO Bool -> m () assertByDeadlineIOCustom str deadline f = withFrozenCallStack $ do - success <- H.evalIO f + success <- liftIOAnnotated f unless success $ do - currentTime <- H.evalIO DTC.getCurrentTime + currentTime <- liftIOAnnotated DTC.getCurrentTime if currentTime < deadline then do - H.evalIO $ IO.threadDelay 1_000_000 + liftIOAnnotated $ IO.threadDelay 1_000_000 assertByDeadlineIOCustom str deadline f else do - H.annotateShow currentTime - H.failMessage GHC.callStack $ "Condition not met by deadline: " <> str + throwString $ "Condition not met by deadline: " <> str -- | A sanity check that confirms that there are the expected number of SPOs in the ledger state assertExpectedSposInLedgerState - :: (MonadTest m, MonadCatch m, MonadIO m, HasCallStack) + :: HasCallStack + => MonadIO m => FilePath -- ^ Stake pools query output filepath -> NumPools -> ExecConfig @@ -92,21 +90,21 @@ assertExpectedSposInLedgerState output (NumPools numExpectedPools) execConfig = , "--out-file", output ] - poolSet <- H.evalEither =<< H.evalIO (Aeson.eitherDecodeFileStrict' @(Set PoolId) output) - - H.cat output - - let numPoolsInLedgerState = Set.size poolSet - unless (numPoolsInLedgerState == numExpectedPools) $ - failMessage GHC.callStack - $ unlines [ "Expected number of stake pools not found in ledger state" - , "Expected: ", show numExpectedPools - , "Actual: ", show numPoolsInLedgerState - ] + ePoolSet <- liftIOAnnotated (Aeson.eitherDecodeFileStrict' @(Set PoolId) output) + case ePoolSet of + Left err -> + throwString $ "Failed to decode stake pools from ledger state: " <> err + Right poolSet -> do + let numPoolsInLedgerState = Set.size poolSet + unless (numPoolsInLedgerState == numExpectedPools) $ + throwString $ unlines + [ "Expected number of stake pooFvls not found in ledger state" + , "Expected: ", show numExpectedPools + , "Actual: ", show numPoolsInLedgerState + ] assertChainExtended :: HasCallStack - => H.MonadTest m => MonadIO m => DTC.UTCTime -> NodeLoggingFormat From d1006cc8748402d41a3a15ff5f8a6479731d885c Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Mon, 3 Nov 2025 09:41:29 -0400 Subject: [PATCH 12/19] Propagate liftToIntegration and liftIOAnnotated --- .../src/Cardano/Node/Protocol/Byron.hs | 4 ++++ cardano-testnet/cardano-testnet.cabal | 9 +++++++- cardano-testnet/src/Testnet/Defaults.hs | 5 ++++ cardano-testnet/src/Testnet/Orphans.hs | 8 +++++++ .../Cardano/Testnet/Test/Cli/KesPeriodInfo.hs | 5 ++-- .../Testnet/Test/Cli/LeadershipSchedule.hs | 5 ++-- .../Cardano/Testnet/Test/DumpConfig.hs | 6 +++-- .../Cardano/Testnet/Test/Gov/NoConfidence.hs | 9 ++++---- .../Cardano/Testnet/Test/MainnetParams.hs | 5 ++-- .../Cardano/Testnet/Test/Node/Shutdown.hs | 23 +++++++++++-------- .../Cardano/Testnet/Test/P2PTopology.hs | 5 ++-- .../Cardano/Testnet/Test/UpdateTimeStamps.hs | 5 ++-- 12 files changed, 63 insertions(+), 26 deletions(-) create mode 100644 cardano-testnet/src/Testnet/Orphans.hs diff --git a/cardano-node/src/Cardano/Node/Protocol/Byron.hs b/cardano-node/src/Cardano/Node/Protocol/Byron.hs index c6a3a766833..6342184fcf4 100644 --- a/cardano-node/src/Cardano/Node/Protocol/Byron.hs +++ b/cardano-node/src/Cardano/Node/Protocol/Byron.hs @@ -36,6 +36,7 @@ import Cardano.Tracing.OrphanInstances.Shelley () import Ouroboros.Consensus.Cardano import qualified Ouroboros.Consensus.Cardano as Consensus +import Control.Exception import qualified Data.ByteString.Lazy as LB import Data.Maybe (fromMaybe) import Data.Text (Text) @@ -167,6 +168,9 @@ data ByronProtocolInstantiationError = | SigningKeyFilepathNotSpecified deriving Show +instance Exception ByronProtocolInstantiationError where + displayException = docToString . prettyError + instance Error ByronProtocolInstantiationError where prettyError (CanonicalDecodeFailure fp failure) = "Canonical decode failure in " <> pshow fp diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index d81890ce526..f4fc06364da 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -38,6 +38,7 @@ library build-depends: aeson , aeson-pretty + , annotated-exception , ansi-terminal , bytestring , cardano-api ^>= 10.18 @@ -86,6 +87,8 @@ library , process , resourcet , retry + , rio + , rio-orphans , safe-exceptions , scientific , si-timers @@ -98,6 +101,7 @@ library , time , transformers , transformers-except + , unliftio , vector , yaml @@ -111,17 +115,20 @@ library Testnet.EpochStateProcessing Testnet.Filepath Testnet.Handlers + Testnet.Orphans Testnet.Ping Testnet.Process.Cli.DRep Testnet.Process.Cli.Keys Testnet.Process.Cli.SPO Testnet.Process.Cli.Transaction + Testnet.Process.RunIO Testnet.Process.Run Testnet.Property.Assert Testnet.Property.Run Testnet.Property.Util Testnet.Runtime Testnet.Start.Byron + Testnet.Start.Cardano Testnet.Start.Types Testnet.SubmitApi Testnet.TestQueryCmds @@ -130,7 +137,6 @@ library other-modules: Parsers.Cardano Parsers.Help Parsers.Version - Testnet.Start.Cardano Testnet.TestEnumGenerator Paths_cardano_testnet @@ -265,6 +271,7 @@ test-suite cardano-testnet-test , monad-control , mtl , process + , resourcet , regex-compat , rio , tasty ^>= 1.5 diff --git a/cardano-testnet/src/Testnet/Defaults.hs b/cardano-testnet/src/Testnet/Defaults.hs index d8e74829c58..37d42672161 100644 --- a/cardano-testnet/src/Testnet/Defaults.hs +++ b/cardano-testnet/src/Testnet/Defaults.hs @@ -74,6 +74,7 @@ import Ouroboros.Network.PeerSelection.State.LocalRootPeers (HotValenc import Prelude +import Control.Exception (Exception (..)) import Control.Monad.Identity (Identity) import Data.Aeson (ToJSON (..), Value, (.=)) import qualified Data.Aeson as Aeson @@ -107,6 +108,10 @@ newtype AlonzoGenesisError = AlonzoGenErrTooMuchPrecision Rational deriving Show +instance Exception AlonzoGenesisError where + displayException = Api.docToString . Api.prettyError + + defaultAlonzoGenesis :: ShelleyBasedEra era -> Either AlonzoGenesisError AlonzoGenesis defaultAlonzoGenesis sbe = do let genesis = Api.alonzoGenesisDefaults (toCardanoEra sbe) diff --git a/cardano-testnet/src/Testnet/Orphans.hs b/cardano-testnet/src/Testnet/Orphans.hs new file mode 100644 index 00000000000..622e9df0b8f --- /dev/null +++ b/cardano-testnet/src/Testnet/Orphans.hs @@ -0,0 +1,8 @@ +{-# OPTIONS_GHC -Wno-orphans #-} + +module Testnet.Orphans () where + +import RIO (RIO(..), liftIO) + +instance MonadFail (RIO env) where + fail = liftIO . fail \ No newline at end of file diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/KesPeriodInfo.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/KesPeriodInfo.hs index 1b565c3112c..f43d1f40be4 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/KesPeriodInfo.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/KesPeriodInfo.hs @@ -22,8 +22,8 @@ import Prelude import Control.Monad import qualified Data.Aeson as Aeson -import qualified Data.Aeson as J import qualified Data.Aeson.Encode.Pretty as Aeson +import qualified Data.Aeson as J import Data.Default.Class import Data.Function import qualified Data.Map.Strict as Map @@ -39,6 +39,7 @@ import Testnet.Process.Cli.SPO import Testnet.Process.Run (execCli, execCli', mkExecConfig) import Testnet.Property.Util (decodeEraUTxO, integrationRetryWorkspace) import Testnet.Runtime +import Testnet.Start.Cardano (liftToIntegration) import Testnet.Types import Hedgehog (Property) @@ -255,7 +256,7 @@ hprop_kes_period_info = integrationRetryWorkspace 2 "kes-period-info" $ \tempAbs , "--out-file", testSpoOperationalCertFp ] - jsonBS <- Aeson.encodePretty . Aeson.Object <$> createConfigJson tempAbsPath sbe + jsonBS <- liftToIntegration $ Aeson.encodePretty . Aeson.Object <$> createConfigJson tempAbsPath sbe H.lbsWriteFile (unFile configurationFile) jsonBS newNodePortNumber <- H.randomPort testnetDefaultIpv4Address eRuntime <- runExceptT . retryOnAddressInUseError $ diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/LeadershipSchedule.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/LeadershipSchedule.hs index fe9d5d5be13..1794f0cbd23 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/LeadershipSchedule.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/LeadershipSchedule.hs @@ -45,6 +45,7 @@ import Testnet.Process.Run (execCli, execCli', mkExecConfig) import Testnet.Property.Assert import Testnet.Property.Util (decodeEraUTxO, integrationRetryWorkspace) import Testnet.Runtime +import Testnet.Start.Cardano import Testnet.Types import Hedgehog (Property, (===)) @@ -256,8 +257,8 @@ hprop_leadershipSchedule = integrationRetryWorkspace 2 "leadership-schedule" $ \ , "--operational-certificate-issue-counter-file", testSpoOperationalCertFp , "--out-file", testSpoOperationalCertFp ] - - jsonBS <- Aeson.encodePretty . Aeson.Object <$> createConfigJson tempAbsPath sbe + jsonBS <- liftToIntegration $ + Aeson.encodePretty . Aeson.Object <$> createConfigJson tempAbsPath sbe H.lbsWriteFile (unFile configurationFile) jsonBS newNodePort <- H.randomPort testnetDefaultIpv4Address eRuntime <- runExceptT . retryOnAddressInUseError $ diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/DumpConfig.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/DumpConfig.hs index 49803562ea6..32f91d89bda 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/DumpConfig.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/DumpConfig.hs @@ -25,6 +25,7 @@ import System.FilePath (()) import Testnet.Components.Configuration (startTimeOffsetSeconds) import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Start.Cardano (liftToIntegration) import Testnet.Start.Types (GenesisHashesPolicy (..), GenesisOptions (..), UserProvidedEnv (..)) @@ -43,7 +44,8 @@ hprop_dump_config = integrationRetryWorkspace 2 "dump-config-files" $ \tmpDir -> -- Generate the sandbox conf <- mkConf tmpDir - createTestnetEnv + + liftToIntegration $ createTestnetEnv testnetOptions genesisOptions def -- Do not add hashes to the main config file, so that genesis files -- can be modified without having to recompute hashes every time. @@ -69,6 +71,6 @@ hprop_dump_config = integrationRetryWorkspace 2 "dump-config-files" $ \tmpDir -> H.lbsWriteFile shelleyGenesisFile $ encodePretty shelleyGenesis -- Run testnet with generated config - runtime <- cardanoTestnet testnetOptions conf + runtime <- liftToIntegration $ cardanoTestnet testnetOptions conf nodesProduceBlocks tmpDir runtime diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/NoConfidence.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/NoConfidence.hs index a704d0b7b9f..b1c42520543 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/NoConfidence.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/NoConfidence.hs @@ -40,6 +40,7 @@ import qualified Testnet.Process.Cli.SPO as SPO import Testnet.Process.Cli.Transaction import qualified Testnet.Process.Run as H import Testnet.Property.Util (integrationWorkspace) +import Testnet.Start.Cardano (liftToIntegration) import Testnet.Start.Types import Testnet.Types @@ -101,18 +102,18 @@ hprop_gov_no_confidence = integrationWorkspace "no-confidence" $ \tempAbsBasePat let comKeyCred1 = L.KeyHashObj comKeyHash1 committeeThreshold = unsafeBoundedRational 0.5 committee = L.Committee (Map.fromList [(comKeyCred1, EpochNo 100)]) committeeThreshold - - createTestnetEnv fastTestnetOptions genesisOptions def conf + + liftToIntegration $ createTestnetEnv fastTestnetOptions genesisOptions def conf H.rewriteJsonFile (tempAbsBasePath' "conway-genesis.json") $ \conwayGenesis -> conwayGenesis { L.cgCommittee = committee } - + TestnetRuntime { testnetMagic , testnetNodes , wallets=wallet0:_wallet1:_ , configurationFile - } <- cardanoTestnet fastTestnetOptions conf + } <- liftToIntegration $ cardanoTestnet fastTestnetOptions conf poolNode1 <- H.headM testnetNodes poolSprocket1 <- H.noteShow $ nodeSprocket poolNode1 diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/MainnetParams.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/MainnetParams.hs index 27273214675..de858bf7d00 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/MainnetParams.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/MainnetParams.hs @@ -17,6 +17,7 @@ import Lens.Micro ((^?)) import Testnet.Process.Run (execCli', mkExecConfig) import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Start.Cardano (liftToIntegration) import Testnet.Start.Types (CreateEnvOptions (..), GenesisOptions (..), UserProvidedEnv (..), TestnetOnChainParams (..)) @@ -38,14 +39,14 @@ hprop_mainnet_params = integrationRetryWorkspace 2 "mainnet-params" $ \tmpDir -> -- Generate the sandbox conf <- mkConf tmpDir - createTestnetEnv + liftToIntegration $ createTestnetEnv testnetOptions genesisOptions createEnvOptions conf -- Run testnet with mainnet on-chain params TestnetRuntime { testnetNodes , testnetMagic - } <- cardanoTestnet testnetOptions conf + } <- liftToIntegration $ cardanoTestnet testnetOptions conf -- Get a running node TestnetNode{nodeSprocket} <- H.headM testnetNodes diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/Shutdown.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/Shutdown.hs index 3a3416820b6..85f3bf18656 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/Shutdown.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/Shutdown.hs @@ -13,7 +13,6 @@ module Cardano.Testnet.Test.Node.Shutdown import Cardano.Api import Cardano.Testnet -import qualified Cardano.Testnet as Testnet import Prelude @@ -36,11 +35,12 @@ import qualified System.IO as IO import qualified System.Process as IO import System.Process (interruptProcessGroupOf) -import Testnet.Components.Configuration +import qualified Testnet.Components.Configuration as Testnet import Testnet.Defaults import Testnet.Process.Run (execCli_, initiateProcess, procNode) import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Byron +import Testnet.Start.Cardano import Testnet.Start.Types import Hedgehog (Property, (===)) @@ -53,6 +53,7 @@ import qualified Hedgehog.Extras.Test.Concurrent as H import qualified Hedgehog.Extras.Test.File as H import qualified Hedgehog.Extras.Test.Process as H import qualified Hedgehog.Extras.Test.TestWatchdog as H +import Testnet.Process.RunIO (liftIOAnnotated) {- HLINT ignore "Redundant <&>" -} @@ -106,8 +107,8 @@ hprop_shutdown = integrationRetryWorkspace 2 "shutdown" $ \tempAbsBasePath' -> H -- 2. Create Alonzo genesis alonzoBabbageTestGenesisJsonTargetFile <- H.noteShow $ tempAbsPath' shelleyDir "genesis.alonzo.spec.json" - gen <- Testnet.getDefaultAlonzoGenesis sbe - H.evalIO $ LBS.writeFile alonzoBabbageTestGenesisJsonTargetFile $ encode gen + gen <- liftToIntegration $ Testnet.getDefaultAlonzoGenesis sbe + liftIOAnnotated $ LBS.writeFile alonzoBabbageTestGenesisJsonTargetFile $ encode gen -- 2. Create Conway genesis conwayBabbageTestGenesisJsonTargetFile <- H.noteShow $ tempAbsPath' shelleyDir "genesis.conway.spec.json" @@ -121,16 +122,20 @@ hprop_shutdown = integrationRetryWorkspace 2 "shutdown" $ \tempAbsBasePath' -> H , "--start-time", formatIso8601 startTime ] - byronGenesisHash <- getByronGenesisHash $ byronGenesisOutputDir "genesis.json" + byronGenesisHash <- liftToIntegration $ Testnet.getByronGenesisHash $ byronGenesisOutputDir "genesis.json" + -- Move the files to the paths expected by 'defaultYamlHardforkViaConfig' below H.renameFile (byronGenesisOutputDir "genesis.json") (tempAbsPath' defaultGenesisFilepath ByronEra) H.renameFile (tempAbsPath' "shelley/genesis.json") (tempAbsPath' defaultGenesisFilepath ShelleyEra) H.renameFile (tempAbsPath' "shelley/genesis.alonzo.json") (tempAbsPath' defaultGenesisFilepath AlonzoEra) H.renameFile (tempAbsPath' "shelley/genesis.conway.json") (tempAbsPath' defaultGenesisFilepath ConwayEra) - shelleyGenesisHash <- getShelleyGenesisHash (tempAbsPath' defaultGenesisFilepath ShelleyEra) "ShelleyGenesisHash" - alonzoGenesisHash <- getShelleyGenesisHash (tempAbsPath' defaultGenesisFilepath AlonzoEra) "AlonzoGenesisHash" - + (shelleyGenesisHash,alonzoGenesisHash) <- + liftToIntegration $ do + shelleyGenesisHash <- Testnet.getShelleyGenesisHash (tempAbsPath' defaultGenesisFilepath ShelleyEra) "ShelleyGenesisHash" + alonzoGenesisHash <- Testnet.getShelleyGenesisHash (tempAbsPath' defaultGenesisFilepath AlonzoEra) "AlonzoGenesisHash" + return (shelleyGenesisHash, alonzoGenesisHash) + let finalYamlConfig :: LBS.ByteString finalYamlConfig = encode . Object $ mconcat [ byronGenesisHash @@ -164,7 +169,7 @@ hprop_shutdown = integrationRetryWorkspace 2 "shutdown" $ \tempAbsBasePath' -> H eProcess <- runExceptT $ initiateProcess process case eProcess of Left e -> H.failMessage GHC.callStack $ mconcat ["Failed to initiate node process: ", show e] - Right (mStdin, _mStdout, _mStderr, pHandle, _releaseKey) -> do + Right (mStdin, _mStdout, _mStderr, pHandle, _) -> do H.threadDelay $ 10 * 1000000 mExitCodeRunning <- H.evalIO $ IO.getProcessExitCode pHandle diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/P2PTopology.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/P2PTopology.hs index 236eaa3556c..1c0852ac696 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/P2PTopology.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/P2PTopology.hs @@ -19,6 +19,7 @@ import System.FilePath (()) import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types (CreateEnvOptions (..), GenesisOptions (..), NodeId, UserProvidedEnv (..), TopologyType (..)) +import Testnet.Start.Cardano (liftToIntegration) import qualified Hedgehog as H import qualified Hedgehog.Extras as H @@ -36,13 +37,13 @@ hprop_p2p_topology = integrationRetryWorkspace 2 "p2p-topology" $ \tmpDir -> H.r -- Generate the sandbox conf <- mkConf tmpDir - createTestnetEnv testnetOptions genesisOptions createEnvOptions conf + liftToIntegration $ createTestnetEnv testnetOptions genesisOptions createEnvOptions conf -- Check that the topology is indeed P2P eTopology <- H.readJsonFile someTopologyFile (_topology :: P2P.NetworkTopology NodeId) <- H.leftFail eTopology -- Run testnet with generated config - runtime <- cardanoTestnet testnetOptions conf + runtime <- liftToIntegration $ cardanoTestnet testnetOptions conf nodesProduceBlocks tmpDir runtime diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/UpdateTimeStamps.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/UpdateTimeStamps.hs index c4b177d034b..b2a341c95cd 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/UpdateTimeStamps.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/UpdateTimeStamps.hs @@ -17,6 +17,7 @@ import GHC.Float (double2Int) import Testnet.Components.Configuration (startTimeOffsetSeconds) import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Start.Cardano (liftToIntegration) import Testnet.Start.Types (UpdateTimestamps (..), GenesisHashesPolicy (..), GenesisOptions (..), UserProvidedEnv (..)) @@ -34,7 +35,7 @@ hprop_update_time_stamps = integrationRetryWorkspace 2 "update-time-stamps" $ \t -- Generate the sandbox conf <- mkConf tmpDir - createTestnetEnv + liftToIntegration $ createTestnetEnv testnetOptions genesisOptions def -- Do not add hashes to the main config file, so that genesis files -- can be modified without having to recompute hashes every time. @@ -45,6 +46,6 @@ hprop_update_time_stamps = integrationRetryWorkspace 2 "update-time-stamps" $ \t H.threadDelay $ double2Int $ realToFrac startTimeOffsetSeconds * 1_000_000 * 1.2 -- Run testnet and specify to update time stamps before starting - runtime <- cardanoTestnet testnetOptions conf{updateTimestamps = UpdateTimestamps} + runtime <- liftToIntegration $ cardanoTestnet testnetOptions conf{updateTimestamps = UpdateTimestamps} nodesProduceBlocks tmpDir runtime From b8c8b5331aff72b5905b77422d171bfa775a4eec Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Mon, 3 Nov 2025 10:09:02 -0400 Subject: [PATCH 13/19] Fix build issues --- cardano-testnet/src/Testnet/Runtime.hs | 9 ++++++--- cardano-testnet/src/Testnet/Start/Cardano.hs | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/cardano-testnet/src/Testnet/Runtime.hs b/cardano-testnet/src/Testnet/Runtime.hs index e2e5ecd2592..3a1f139ddcb 100644 --- a/cardano-testnet/src/Testnet/Runtime.hs +++ b/cardano-testnet/src/Testnet/Runtime.hs @@ -37,7 +37,7 @@ import Data.Algorithm.Diff import Data.Algorithm.DiffOutput import Data.Bifunctor (first) import qualified Data.ByteString.Lazy.Char8 as BSC -import Data.List (isInfixOf) +import Data.List (isInfixOf, uncons) import qualified Data.List as List import GHC.Stack import qualified GHC.Stack as GHC @@ -297,8 +297,11 @@ startLedgerNewEpochStateLogging testnetRuntime tmpWorkspace = withFrozenCallStac liftIOAnnotated $ IO.doesFileExist logFile >>= \case True -> return () False -> liftIO $ appendFile logFile "" - - let socketPath = H.sprocketSystemName $ head (testnetSprockets testnetRuntime) + + let socketPath = case uncons (testnetSprockets testnetRuntime) of + Just (sprocket, _) -> H.sprocketSystemName sprocket + Nothing -> throwString "No testnet sprocket available" + let act = runExceptT $ foldEpochState (configurationFile testnetRuntime) diff --git a/cardano-testnet/src/Testnet/Start/Cardano.hs b/cardano-testnet/src/Testnet/Start/Cardano.hs index 7e1faf80ba8..1cc48519e3e 100644 --- a/cardano-testnet/src/Testnet/Start/Cardano.hs +++ b/cardano-testnet/src/Testnet/Start/Cardano.hs @@ -50,6 +50,7 @@ import qualified Data.ByteString.Lazy as LBS import Data.Default.Class (def) import Data.Either import Data.Functor +import Data.List (uncons) import Data.MonoTraversable (Element, MonoFunctor, omap) import qualified Data.Text as Text import Data.Time (diffUTCTime) @@ -400,7 +401,9 @@ cardanoTestnet let tempBaseAbsPath = makeTmpBaseAbsPath $ TmpAbsolutePath tmpAbsPath - let node1sprocket = head $ testnetSprockets runtime + node1sprocket <- case uncons $ testnetSprockets runtime of + Just (sprocket, _) -> pure sprocket + Nothing -> throwString "No testnet sprocket available" execConfig <- mkExecConfig tempBaseAbsPath node1sprocket testnetMagic forM_ wallets $ \wallet -> do From 1333cdc5349a2de08cc699029af2a3a2a038b511 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Wed, 5 Nov 2025 16:19:49 -0400 Subject: [PATCH 14/19] Review changes --- .../src/Testnet/Components/Query.hs | 5 +- cardano-testnet/src/Testnet/Orphans.hs | 4 +- cardano-testnet/src/Testnet/Process/RunIO.hs | 48 +++++++++++-------- cardano-testnet/src/Testnet/Runtime.hs | 36 +++++++------- cardano-testnet/src/Testnet/Start/Cardano.hs | 3 +- cardano-testnet/src/Testnet/Start/Types.hs | 14 +++--- .../cardano-testnet-test.hs | 2 + 7 files changed, 62 insertions(+), 50 deletions(-) diff --git a/cardano-testnet/src/Testnet/Components/Query.hs b/cardano-testnet/src/Testnet/Components/Query.hs index d3c75246318..14131b95a72 100644 --- a/cardano-testnet/src/Testnet/Components/Query.hs +++ b/cardano-testnet/src/Testnet/Components/Query.hs @@ -47,7 +47,7 @@ import Cardano.Api as Api hiding (txId) import Cardano.Api.Ledger (Credential, DRepState, EpochInterval (..), KeyRole (DRepRole)) import qualified Cardano.Api.Ledger as L import qualified Cardano.Api.UTxO as Utxo - +import Testnet.Runtime import Cardano.Ledger.Api (ConwayGovState) import qualified Cardano.Ledger.Api as L import qualified Cardano.Ledger.Conway.Governance as L @@ -253,13 +253,12 @@ getEpochStateView :: HasCallStack => MonadResource m => MonadTest m - => MonadCatch m => NodeConfigFile In -- ^ node Yaml configuration file path -> SocketPath -- ^ node socket path -> m EpochStateView getEpochStateView nodeConfigFile socketPath = withFrozenCallStack $ do epochStateView <- H.evalIO $ newIORef Nothing - H.asyncRegister_ . runExceptT . foldEpochState nodeConfigFile socketPath QuickValidation (EpochNo maxBound) Nothing + void . asyncRegister_ . runExceptT . foldEpochState nodeConfigFile socketPath QuickValidation (EpochNo maxBound) Nothing $ \epochState slotNumber blockNumber -> do liftIO . writeIORef epochStateView $ Just (epochState, slotNumber, blockNumber) pure ConditionNotMet diff --git a/cardano-testnet/src/Testnet/Orphans.hs b/cardano-testnet/src/Testnet/Orphans.hs index 622e9df0b8f..4c9b8292a88 100644 --- a/cardano-testnet/src/Testnet/Orphans.hs +++ b/cardano-testnet/src/Testnet/Orphans.hs @@ -2,7 +2,7 @@ module Testnet.Orphans () where -import RIO (RIO(..), liftIO) +import RIO (RIO(..), throwString) instance MonadFail (RIO env) where - fail = liftIO . fail \ No newline at end of file + fail = throwString \ No newline at end of file diff --git a/cardano-testnet/src/Testnet/Process/RunIO.hs b/cardano-testnet/src/Testnet/Process/RunIO.hs index 98d5c924976..293ca06241b 100644 --- a/cardano-testnet/src/Testnet/Process/RunIO.hs +++ b/cardano-testnet/src/Testnet/Process/RunIO.hs @@ -1,4 +1,6 @@ +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} @@ -44,6 +46,7 @@ defaultExecConfig = ExecConfig mkExecConfig :: () + => HasCallStack => MonadIO m => FilePath -> IO.Sprocket @@ -65,7 +68,8 @@ mkExecConfig tempBaseAbsPath sprocket networkId = do execCli' - :: MonadIO m + :: HasCallStack + => MonadIO m => ExecConfig -> [String] -> m String @@ -94,7 +98,8 @@ execCli = GHC.withFrozenCallStack $ execFlex "cardano-cli" "CARDANO_CLI" -- When running outside a nix environment, the `pkgBin` describes the name of the binary -- to launch via cabal exec. execFlex - :: String + :: HasCallStack + => String -> String -> [String] -> RIO env String @@ -102,6 +107,7 @@ execFlex = execFlex' defaultExecConfig execFlex' :: MonadIO m + => HasCallStack => ExecConfig -> String -> String @@ -215,8 +221,7 @@ exeSuffix = if OS.isWin32 then ".exe" else "" -- executable has been built. -- Throws an exception on failure. binDist - :: HasCallStack - => MonadIO m + :: (HasCallStack, MonadIO m) => String -- ^ Package name -> String @@ -233,20 +238,25 @@ binDist pkg binaryEnv = do <> "\" if you are working with sources. Otherwise define " <> binaryEnv <> " and have it point to the executable you want." - contents <- liftIOAnnotated $ LBS.readFile planJsonFile - - case eitherDecode contents of - Right plan -> case L.filter matching (plan & installPlan) of - (component:_) -> case component & binFile of - Just bin -> return $ addExeSuffix (T.unpack bin) - Nothing -> error $ "missing \"bin-file\" key in plan component: " <> show component <> " in the plan in: " <> planJsonFile - [] -> error $ "Cannot find \"component-name\" key with the value \"exe:" <> pkg <> "\" in the plan in: " <> planJsonFile - Left message -> error $ "Cannot decode plan in " <> planJsonFile <> ": " <> message - where matching :: Component -> Bool - matching component = case componentName component of - Just name -> name == "exe:" <> T.pack pkg - Nothing -> False + Plan{installPlan} <- eitherDecode <$> liftIOAnnotated (LBS.readFile planJsonFile) + >>= \case + Left message -> error $ "Cannot decode plan in " <> planJsonFile <> ": " <> message + Right plan -> pure plan + + let componentName = "exe:" <> fromString pkg + case findComponent componentName installPlan of + Just Component{binFile=Just binFilePath} -> pure . addExeSuffix $ T.unpack binFilePath + Just component@Component{binFile=Nothing} -> + error $ "missing \"bin-file\" key in plan component: " <> show component <> " in the plan in: " <> planJsonFile + Nothing -> + error $ "Cannot find \"component-name\" key with the value \"exe:" <> pkg <> "\" in the plan in: " <> planJsonFile + where + findComponent :: Text -> [Component] -> Maybe Component + findComponent _ [] = Nothing + findComponent needle (c@Component{componentName, components}:topLevelComponents) + | componentName == Just needle = Just c + | otherwise = findComponent needle topLevelComponents <|> findComponent needle components procNode @@ -278,7 +288,7 @@ procFlex -- ^ Captured stdout procFlex = procFlex' defaultExecConfig - +-- This will also catch async exceptions as well. liftIOAnnotated :: (HasCallStack, MonadIO m) => IO a -> m a liftIOAnnotated action = GHC.withFrozenCallStack $ - liftIO $ action `catch` (\(e :: SomeException) -> throwM $ exceptionWithCallStack e) \ No newline at end of file + liftIOAnnotated $ action `catch` (\(e :: SomeException) -> throwM $ exceptionWithCallStack e) \ No newline at end of file diff --git a/cardano-testnet/src/Testnet/Runtime.hs b/cardano-testnet/src/Testnet/Runtime.hs index 3a1f139ddcb..9019cd0b707 100644 --- a/cardano-testnet/src/Testnet/Runtime.hs +++ b/cardano-testnet/src/Testnet/Runtime.hs @@ -156,7 +156,7 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do isClosed <- liftIOAnnotated $ Ping.waitForPortClosed 30 0.1 port unless isClosed $ - throwString $ "Port is still in use after 30 seconds before starting node: " <> show port + throwString $ "Port is still in use after 30 seconds before starting node: " <> show port (Just stdIn, _, _, hProcess, _) <- firstExceptT ProcessRelatedFailure $ initiateProcess @@ -278,7 +278,6 @@ createSubdirectoryIfMissingNew parent subdirectory = GHC.withFrozenCallStack $ d -- Idempotent. startLedgerNewEpochStateLogging :: HasCallStack - => MonadCatch m => MonadResource m => TestnetRuntime -> FilePath -- ^ tmp workspace directory @@ -294,23 +293,24 @@ startLedgerNewEpochStateLogging testnetRuntime tmpWorkspace = withFrozenCallStac False -> do throwString $ "Log directory does not exist: " <> logDir <> " - cannot start logging epoch states" - liftIOAnnotated $ IO.doesFileExist logFile >>= \case + liftIOAnnotated (IO.doesFileExist logFile) >>= \case True -> return () - False -> liftIO $ appendFile logFile "" - - let socketPath = case uncons (testnetSprockets testnetRuntime) of - Just (sprocket, _) -> H.sprocketSystemName sprocket - Nothing -> throwString "No testnet sprocket available" - - let act = runExceptT $ - foldEpochState - (configurationFile testnetRuntime) - (Api.File socketPath) - Api.QuickValidation - (EpochNo maxBound) - Nothing - (handler logFile diffFile) - void $ asyncRegister_ act + False -> do + liftIOAnnotated $ appendFile logFile "" + + let socketPath = case uncons (testnetSprockets testnetRuntime) of + Just (sprocket, _) -> H.sprocketSystemName sprocket + Nothing -> throwString "No testnet sprocket available" + + void $ asyncRegister_ . runExceptT $ + foldEpochState + (configurationFile testnetRuntime) + (Api.File socketPath) + Api.QuickValidation + (EpochNo maxBound) + Nothing + (handler logFile diffFile) + where handler :: FilePath -- ^ log file -> FilePath -- ^ diff file diff --git a/cardano-testnet/src/Testnet/Start/Cardano.hs b/cardano-testnet/src/Testnet/Start/Cardano.hs index 1cc48519e3e..0a45b72b21c 100644 --- a/cardano-testnet/src/Testnet/Start/Cardano.hs +++ b/cardano-testnet/src/Testnet/Start/Cardano.hs @@ -80,6 +80,7 @@ import Testnet.Orphans () import RIO.Orphans (ResourceMap) import UnliftIO.Async + -- | There are certain conditions that need to be met in order to run -- a valid node cluster. testMinimumConfigurationRequirements :: () @@ -90,7 +91,7 @@ testMinimumConfigurationRequirements options = withFrozenCallStack $ do when (cardanoNumPools options < 1) $ do throwString "Need at least one SPO node to produce blocks, but got none." -liftToIntegration :: RIO ResourceMap a -> H.Integration a +liftToIntegration :: HasCallStack => RIO ResourceMap a -> H.Integration a liftToIntegration r = do rMap <- lift $ lift getInternalState liftIOAnnotated $ runRIO rMap r diff --git a/cardano-testnet/src/Testnet/Start/Types.hs b/cardano-testnet/src/Testnet/Start/Types.hs index 8189b8f0564..c3f94515e6c 100644 --- a/cardano-testnet/src/Testnet/Start/Types.hs +++ b/cardano-testnet/src/Testnet/Start/Types.hs @@ -42,7 +42,6 @@ module Testnet.Start.Types , NodeConfigurationYaml , mkConf , mkConfigAbs - , mkConfig ) where import Cardano.Api hiding (cardanoEra) @@ -52,6 +51,7 @@ import Cardano.Ledger.Conway.Genesis (ConwayGenesis) import Prelude import Control.Exception (throw) +import Control.Monad (unless) import qualified Data.Aeson as Aeson import Data.Aeson.Types (parseFail) import Data.Char (toLower) @@ -288,7 +288,8 @@ data Conf = Conf , updateTimestamps :: UpdateTimestamps } deriving (Eq, Show) --- Logs the argument in the test. +-- | Same as mkConfig except that it renders the path +-- when failing in a property test. mkConf :: (HasCallStack, MonadTest m) => FilePath -> m Conf mkConf tempAbsPath' = withFrozenCallStack $ do H.note_ tempAbsPath' @@ -304,17 +305,16 @@ mkConfig tempAbsPath' = , updateTimestamps = DontUpdateTimestamps } +-- | Create a 'Conf' from an absolute path, with Genesis Hashes enabled +-- and updating time stamps disabled. mkConfigAbs :: FilePath -> IO Conf mkConfigAbs userOutputDir = do absUserOutputDir <- makeAbsolute userOutputDir dirExists <- doesDirectoryExist absUserOutputDir let conf = mkConfig absUserOutputDir - if dirExists then - -- Happens when the environment has previously been created by the user - return conf - else do + unless dirExists $ createDirectory absUserOutputDir - return conf + pure conf -- | @anyEraToString (AnyCardanoEra ByronEra)@ returns @"byron"@ anyEraToString :: AnyCardanoEra -> String diff --git a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs index fb74ff0ff81..70112c0b16f 100644 --- a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs +++ b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs @@ -12,6 +12,7 @@ import qualified Cardano.Testnet.Test.Cli.Plutus.Scripts import qualified Cardano.Testnet.Test.Cli.Query import qualified Cardano.Testnet.Test.Cli.QuerySlotNumber import qualified Cardano.Testnet.Test.Cli.StakeSnapshot +import qualified Cardano.Testnet.Test.SanityCheck import qualified Cardano.Testnet.Test.Cli.Transaction import qualified Cardano.Testnet.Test.Cli.Transaction.RegisterDeregisterStakeAddress import qualified Cardano.Testnet.Test.DumpConfig @@ -56,6 +57,7 @@ tests = do [ T.testGroup "Spec" [ T.testGroup "Ledger Events" [ ignoreOnWindows "Sanity Check" LedgerEvents.hprop_ledger_events_sanity_check + , ignoreOnWindows "Async Register" Cardano.Testnet.Test.SanityCheck.hprop_asyncRegister_sanity_check -- FIXME this tests gets stuck - investigate why -- , ignoreOnWindows "Treasury Growth" Gov.prop_check_if_treasury_is_growing -- TODO: Replace foldBlocks with checkConditionResult From 68068698f6e402fd3493159f84fb81f37d128e67 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Thu, 6 Nov 2025 14:29:44 -0400 Subject: [PATCH 15/19] Replace liftIO with liftIOAnnotated --- bench/locli/locli.cabal | 2 +- cabal.project | 3 ++- .../cardano-node-chairman.cabal | 2 +- cardano-node/cardano-node.cabal | 2 +- cardano-testnet/cardano-testnet.cabal | 2 +- .../src/Testnet/Components/Configuration.hs | 2 +- .../src/Testnet/Components/Query.hs | 3 ++- cardano-testnet/src/Testnet/Ping.hs | 11 ++++++----- cardano-testnet/src/Testnet/Process/Run.hs | 3 ++- cardano-testnet/src/Testnet/Property/Run.hs | 6 +++--- cardano-testnet/src/Testnet/Runtime.hs | 18 +++++++++--------- cardano-testnet/src/Testnet/Start/Cardano.hs | 6 +++--- .../Testnet/Test/Cli/Plutus/CostCalculation.hs | 13 +++++++------ .../Cardano/Testnet/Test/Cli/Query.hs | 3 ++- .../Testnet/Test/Gov/CommitteeAddNew.hs | 3 ++- .../Cardano/Testnet/Test/Gov/InfoAction.hs | 3 ++- .../Testnet/Test/Gov/ProposeNewConstitution.hs | 5 +++-- .../Test/Gov/Transaction/HashMismatch.hs | 3 ++- .../Testnet/Test/Gov/TreasuryWithdrawal.hs | 3 ++- 19 files changed, 52 insertions(+), 41 deletions(-) diff --git a/bench/locli/locli.cabal b/bench/locli/locli.cabal index 336d0186b4d..cfa9cf27f03 100644 --- a/bench/locli/locli.cabal +++ b/bench/locli/locli.cabal @@ -212,7 +212,7 @@ test-suite test-locli build-depends: cardano-prelude , containers , hedgehog - , hedgehog-extras ^>= 0.8 + , hedgehog-extras ^>= 0.9 , locli , text diff --git a/cabal.project b/cabal.project index 25874410b4e..738515108ff 100644 --- a/cabal.project +++ b/cabal.project @@ -13,7 +13,7 @@ repository cardano-haskell-packages -- See CONTRIBUTING for information about these, including some Nix commands -- you need to run if you change them index-state: - , hackage.haskell.org 2025-06-24T21:06:59Z + , hackage.haskell.org 2025-06-27T07:48:39Z , cardano-haskell-packages 2025-09-18T12:21:32Z packages: @@ -60,6 +60,7 @@ package plutus-scripts-bench allow-newer: , katip:Win32 + , hedgehog-extras if impl (ghc >= 9.12) allow-newer: diff --git a/cardano-node-chairman/cardano-node-chairman.cabal b/cardano-node-chairman/cardano-node-chairman.cabal index 22cd7bdeb4a..69051a5ef57 100644 --- a/cardano-node-chairman/cardano-node-chairman.cabal +++ b/cardano-node-chairman/cardano-node-chairman.cabal @@ -75,7 +75,7 @@ test-suite chairman-tests , data-default-class , filepath , hedgehog - , hedgehog-extras ^>= 0.8 + , hedgehog-extras ^>= 0.9 , network , process , random diff --git a/cardano-node/cardano-node.cabal b/cardano-node/cardano-node.cabal index a70b48240d2..0fc68a857ed 100644 --- a/cardano-node/cardano-node.cabal +++ b/cardano-node/cardano-node.cabal @@ -265,7 +265,7 @@ test-suite cardano-node-test , filepath , hedgehog , hedgehog-corpus - , hedgehog-extras ^>= 0.8 + , hedgehog-extras ^>= 0.9 , iproute , mtl , ouroboros-consensus diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index f4fc06364da..f90d07e7526 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -71,7 +71,7 @@ library , extra , filepath , hedgehog - , hedgehog-extras ^>= 0.8 + , hedgehog-extras ^>= 0.9 , http-conduit , lens-aeson , microlens diff --git a/cardano-testnet/src/Testnet/Components/Configuration.hs b/cardano-testnet/src/Testnet/Components/Configuration.hs index e2b0766b66c..a21de9d5813 100644 --- a/cardano-testnet/src/Testnet/Components/Configuration.hs +++ b/cardano-testnet/src/Testnet/Components/Configuration.hs @@ -227,7 +227,7 @@ createSPOGenesisAndFiles [ inputGenesisShelleyFp, inputGenesisAlonzoFp, inputGenesisConwayFp , tempAbsPath "byron.genesis.spec.json" -- Created by create-testnet-data ] - (\fp -> liftIO $ whenM (System.doesFileExist fp) (System.removeFile fp)) + (\fp -> liftIOAnnotated $ whenM (System.doesFileExist fp) (System.removeFile fp)) return genesisShelleyDir where diff --git a/cardano-testnet/src/Testnet/Components/Query.hs b/cardano-testnet/src/Testnet/Components/Query.hs index 14131b95a72..21878386517 100644 --- a/cardano-testnet/src/Testnet/Components/Query.hs +++ b/cardano-testnet/src/Testnet/Components/Query.hs @@ -76,6 +76,7 @@ import GHC.Stack import Lens.Micro (Lens', to, (^.)) import Testnet.Property.Assert +import Testnet.Process.RunIO (liftIOAnnotated) import Testnet.Types import Hedgehog @@ -260,7 +261,7 @@ getEpochStateView nodeConfigFile socketPath = withFrozenCallStack $ do epochStateView <- H.evalIO $ newIORef Nothing void . asyncRegister_ . runExceptT . foldEpochState nodeConfigFile socketPath QuickValidation (EpochNo maxBound) Nothing $ \epochState slotNumber blockNumber -> do - liftIO . writeIORef epochStateView $ Just (epochState, slotNumber, blockNumber) + liftIOAnnotated . writeIORef epochStateView $ Just (epochState, slotNumber, blockNumber) pure ConditionNotMet pure $ EpochStateView nodeConfigFile socketPath epochStateView diff --git a/cardano-testnet/src/Testnet/Ping.hs b/cardano-testnet/src/Testnet/Ping.hs index 5f54fb62e5a..4cb62e44841 100644 --- a/cardano-testnet/src/Testnet/Ping.hs +++ b/cardano-testnet/src/Testnet/Ping.hs @@ -42,6 +42,7 @@ import qualified Network.Mux.Types as Mux import Network.Socket (AddrInfo (..), PortNumber, StructLinger (..)) import qualified Network.Socket as Socket import Prettyprinter +import Testnet.Process.RunIO (liftIOAnnotated) import qualified Hedgehog.Extras.Stock.IO.Network.Socket as IO import qualified Hedgehog.Extras.Stock.IO.Network.Sprocket as IO @@ -65,7 +66,7 @@ pingNode :: MonadIO m => TestnetMagic -- ^ testnet magic -> IO.Sprocket -- ^ node sprocket -> m (Either PingClientError ()) -- ^ '()' means success -pingNode networkMagic sprocket = liftIO $ bracket +pingNode networkMagic sprocket = liftIOAnnotated $ bracket (Socket.socket (Socket.addrFamily peer) Socket.Stream Socket.defaultProtocol) Socket.close (\sd -> handle (pure . Left . PceException) $ withTimeoutSerial $ \timeoutfn -> do @@ -143,7 +144,7 @@ waitForSprocket :: MonadIO m -> MT.DiffTime -- ^ interval -> IO.Sprocket -> m (Either IOException ()) -waitForSprocket timeout interval sprocket = liftIO $ do +waitForSprocket timeout interval sprocket = liftIOAnnotated $ do lastResult <- newIORef (Right ()) _ <- MT.timeout timeout $ loop lastResult readIORef lastResult @@ -158,7 +159,7 @@ waitForSprocket timeout interval sprocket = liftIO $ do -- | Check if the sprocket can be connected to. Returns an exception thrown during the connection attempt. checkSprocket :: MonadIO m => IO.Sprocket -> m (Either IOException ()) -checkSprocket sprocket = liftIO $ do +checkSprocket sprocket = liftIOAnnotated $ do let AddrInfo{addrFamily, addrSocketType, addrProtocol, addrAddress} = sprocketToAddrInfo sprocket bracket (Socket.socket addrFamily addrSocketType addrProtocol) Socket.close $ \sock -> do -- Capture only synchronous exceptions from the connection attempt. @@ -179,10 +180,10 @@ waitForPortClosed -> MT.DiffTime -- ^ check interval -> PortNumber -> m Bool -- ^ 'True' if port is closed, 'False' if timeout was reached before that -waitForPortClosed timeout interval portNumber = liftIO $ do +waitForPortClosed timeout interval portNumber = liftIOAnnotated $ do let retryPolicy = R.constantDelay (round @Double $ realToFrac interval) <> R.limitRetries (ceiling $ toRational timeout / toRational interval) fmap not . R.retrying retryPolicy (const pure) $ \_ -> - liftIO (IO.isPortOpen (fromIntegral portNumber)) + liftIOAnnotated (IO.isPortOpen (fromIntegral portNumber)) data PingClientError = PceDecodingError diff --git a/cardano-testnet/src/Testnet/Process/Run.hs b/cardano-testnet/src/Testnet/Process/Run.hs index c13c54db87a..b27b066d21b 100644 --- a/cardano-testnet/src/Testnet/Process/Run.hs +++ b/cardano-testnet/src/Testnet/Process/Run.hs @@ -41,6 +41,7 @@ import System.IO import qualified System.IO.Unsafe as IO import qualified System.Process as IO import System.Process +import Testnet.Process.RunIO (liftIOAnnotated) import Hedgehog (MonadTest) import qualified Hedgehog.Extras as H @@ -227,7 +228,7 @@ initiateProcess -> ExceptT ProcessError m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle, ReleaseKey) initiateProcess cp = do (mhStdin, mhStdout, mhStderr, hProcess) - <- handlesExceptT resourceAndIOExceptionHandlers . liftIO $ IO.createProcess cp + <- handlesExceptT resourceAndIOExceptionHandlers . liftIOAnnotated $ IO.createProcess cp releaseKey <- handlesExceptT resourceAndIOExceptionHandlers . register $ IO.cleanupProcess (mhStdin, mhStdout, mhStderr, hProcess) diff --git a/cardano-testnet/src/Testnet/Property/Run.hs b/cardano-testnet/src/Testnet/Property/Run.hs index 476fbe47c19..fd54acc79bb 100644 --- a/cardano-testnet/src/Testnet/Property/Run.hs +++ b/cardano-testnet/src/Testnet/Property/Run.hs @@ -16,7 +16,6 @@ import Prelude import qualified Control.Concurrent as IO import qualified Control.Concurrent.STM as STM import Control.Monad -import Control.Monad.IO.Class import Control.Monad.Trans.Resource import Data.Bool (bool) import Data.String (IsString (..)) @@ -44,6 +43,7 @@ import Test.Tasty.ExpectedFailure (wrapTest) import qualified Test.Tasty.Hedgehog as H import Test.Tasty.Providers (testPassed) import Test.Tasty.Runners (Result (resultShortDescription), TestTree) +import Testnet.Process.RunIO runTestnet :: UserProvidedEnv -> (Conf -> H.Integration TestnetRuntime) -> IO () runTestnet env tn = do @@ -107,14 +107,14 @@ testnetProperty env runTn = -- Happens when the environment has previously been created by the user H.note_ $ "Reusing " <> absUserOutputDir else do - liftIO $ createDirectory absUserOutputDir + liftIOAnnotated $ createDirectory absUserOutputDir H.note_ $ "Created " <> absUserOutputDir conf <- mkConf absUserOutputDir forkAndRunTestnet conf where forkAndRunTestnet conf = do -- Fork a thread to keep alive indefinitely any resources allocated by testnet. - void $ H.evalM . liftResourceT . resourceForkIO . forever . liftIO $ IO.threadDelay 10000000 + void $ H.evalM . liftResourceT . resourceForkIO . forever . liftIOAnnotated $ IO.threadDelay 10000000 void $ runTn conf H.failure -- Intentional failure to force failure report diff --git a/cardano-testnet/src/Testnet/Runtime.hs b/cardano-testnet/src/Testnet/Runtime.hs index 9019cd0b707..94fbd008cee 100644 --- a/cardano-testnet/src/Testnet/Runtime.hs +++ b/cardano-testnet/src/Testnet/Runtime.hs @@ -123,8 +123,8 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do socketDir = makeSocketDir tp logDir = makeLogDir tp - liftIO $ createDirectoryIfMissingNew_ $ logDir node - void . liftIO $ createSubdirectoryIfMissingNew tempBaseAbsPath (socketDir node) + liftIOAnnotated $ createDirectoryIfMissingNew_ $ logDir node + void . liftIOAnnotated $ createSubdirectoryIfMissingNew tempBaseAbsPath (socketDir node) let nodeStdoutFile = logDir node "stdout.log" nodeStderrFile = logDir node "stderr.log" @@ -154,7 +154,7 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do -- The port number if it is obtained using 'H.randomPort', it is firstly bound to and then closed. The closing -- and release in the operating system is done asynchronously and can be slow. Here we wait until the port - isClosed <- liftIOAnnotated $ Ping.waitForPortClosed 30 0.1 port + isClosed <- liftIOAnnotated $ Ping.waitForPortClosed 45 0.1 port unless isClosed $ throwString $ "Port is still in use after 30 seconds before starting node: " <> show port @@ -169,7 +169,7 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do -- We force the evaluation of initiateProcess so we can be sure that -- the process has started. This allows us to read stderr in order -- to fail early on errors generated from the cardano-node binary. - pid <- liftIO (IO.getPid hProcess) + pid <- liftIOAnnotated (IO.getPid hProcess) >>= hoistMaybe (NodeExecutableError $ "startNode:" <+> pretty node <+> "'s process did not start.") -- We then log the pid in the temp dir structure. @@ -216,7 +216,7 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do closeHandlesOnError :: MonadIO m => [IO.Handle] -> ExceptT e m a -> ExceptT e m a closeHandlesOnError handles action = catchE action $ \e -> do - liftIO $ mapM_ IO.hClose handles + liftIOAnnotated $ mapM_ IO.hClose handles throwE e -- Sometimes even when we close the files manually, the operating system still holds the lock for some @@ -239,7 +239,7 @@ startNode tp node ipv4 port _testnetMagic nodeCmd = GHC.withFrozenCallStack $ do path' = if n > 0 then path <> "-" <> show n <> extension else fullPath - r <- fmap (first FileRelatedFailure) . try . liftIO $ IO.openFile path' mode + r <- fmap (first FileRelatedFailure) . try . liftIOAnnotated $ IO.openFile path' mode case r of Right h -> pure h Left e @@ -321,7 +321,7 @@ startLedgerNewEpochStateLogging testnetRuntime tmpWorkspace = withFrozenCallStac handler outputFp diffFp anes@(AnyNewEpochState !sbe !nes _) _ (BlockNo blockNo) = handleException $ do let prettyNes = shelleyBasedEraConstraints sbe (encodePretty nes) blockLabel = "#### BLOCK " <> show blockNo <> " ####" - liftIO . BSC.appendFile outputFp $ BSC.unlines [BSC.pack blockLabel, prettyNes, ""] + liftIOAnnotated . BSC.appendFile outputFp $ BSC.unlines [BSC.pack blockLabel, prettyNes, ""] -- store epoch state for logging of differences mPrevEpochState <- get @@ -329,14 +329,14 @@ startLedgerNewEpochStateLogging testnetRuntime tmpWorkspace = withFrozenCallStac forM_ mPrevEpochState $ \(AnyNewEpochState sbe' pnes _) -> do let prettyPnes = shelleyBasedEraConstraints sbe' (encodePretty pnes) difference = calculateEpochStateDiff prettyPnes prettyNes - liftIO . appendFile diffFp $ unlines [blockLabel, difference, ""] + liftIOAnnotated . appendFile diffFp $ unlines [blockLabel, difference, ""] pure ConditionNotMet where -- | Handle all sync exceptions and log them into the log file. We don't want to fail the test just -- because logging has failed. handleException = handle $ \(e :: SomeException) -> do - liftIO $ appendFile outputFp $ "Ledger new epoch logging failed - caught exception:\n" + liftIOAnnotated $ appendFile outputFp $ "Ledger new epoch logging failed - caught exception:\n" <> displayException e <> "\n" pure ConditionMet diff --git a/cardano-testnet/src/Testnet/Start/Cardano.hs b/cardano-testnet/src/Testnet/Start/Cardano.hs index 0a45b72b21c..14d03a86d86 100644 --- a/cardano-testnet/src/Testnet/Start/Cardano.hs +++ b/cardano-testnet/src/Testnet/Start/Cardano.hs @@ -460,11 +460,11 @@ retryOnAddressInUseError act = withFrozenCallStack $ go maximumTimeout retryTime | timeout <= 0 = withFrozenCallStack $ do act | otherwise = withFrozenCallStack $ do - !time <- liftIO DTC.getCurrentTime + !time <- liftIOAnnotated DTC.getCurrentTime catchError act $ \case NodeAddressAlreadyInUseError _ -> do - liftIO $ threadDelay (round $ interval * 1_000_000) - !time' <- liftIO DTC.getCurrentTime + liftIOAnnotated $ threadDelay (round $ interval * 1_000_000) + !time' <- liftIOAnnotated DTC.getCurrentTime let elapsedTime = time' `diffUTCTime` time newTimeout = timeout - elapsedTime go newTimeout interval diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Plutus/CostCalculation.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Plutus/CostCalculation.hs index dce3a27407a..d152b083811 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Plutus/CostCalculation.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Plutus/CostCalculation.hs @@ -40,6 +40,7 @@ import Testnet.Components.Query (findLargestUtxoForPaymentKey, getEpoc import Testnet.Process.Cli.Transaction (TxOutAddress (..), mkSpendOutputsOnlyTx, retrieveTransactionId, signTx, submitTx) import Testnet.Process.Run (execCli', mkExecConfig) +import Testnet.Process.RunIO (liftIOAnnotated) import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types (eraToString) import Testnet.Types (PaymentKeyInfo (paymentKeyInfoAddr), paymentKeyInfoPair, @@ -83,7 +84,7 @@ hprop_ref_plutus_cost_calculation = integrationRetryWorkspace 2 "ref plutus scri refScriptSizeWork <- H.createDirectoryIfMissing $ work "ref-script-publish" plutusV3Script <- - File <$> liftIO (makeAbsolute "test/cardano-testnet-test/files/plutus/v3/always-succeeds.plutus") + File <$> liftIOAnnotated (makeAbsolute "test/cardano-testnet-test/files/plutus/v3/always-succeeds.plutus") let scriptPublishUTxOAmount = 10_000_000 @@ -231,7 +232,7 @@ hprop_included_plutus_cost_calculation = integrationRetryWorkspace 2 "included p includedScriptLockWork <- H.createDirectoryIfMissing $ work "included-script-lock" plutusV3Script <- - File <$> liftIO (makeAbsolute "test/cardano-testnet-test/files/plutus/v3/always-succeeds.plutus") + File <$> liftIOAnnotated (makeAbsolute "test/cardano-testnet-test/files/plutus/v3/always-succeeds.plutus") let includedScriptLockAmount = 10_000_000 enoughAmountForFees = 2_000_000 -- Needs to be more than min ada @@ -341,12 +342,12 @@ hprop_included_simple_script_cost_calculation = integrationRetryWorkspace 2 "inc -- We write a simple script that allows any of the two payment keys to spend the money - addrHash1 <- H.evalEitherM $ liftIO $ runExceptT $ paymentKeyInfoHash wallet0 - addrHash2 <- H.evalEitherM $ liftIO $ runExceptT $ paymentKeyInfoHash wallet1 + addrHash1 <- H.evalEitherM $ liftIOAnnotated $ runExceptT $ paymentKeyInfoHash wallet0 + addrHash2 <- H.evalEitherM $ liftIOAnnotated $ runExceptT $ paymentKeyInfoHash wallet1 simpleScriptLockWork <- H.createDirectoryIfMissing $ work "simple-script-lock" let simpleScript = File $ simpleScriptLockWork "simple-script.json" - liftIO $ encodeFile (unFile simpleScript) $ generateSimpleAnyKeyScript [addrHash1, addrHash2] + liftIOAnnotated $ encodeFile (unFile simpleScript) $ generateSimpleAnyKeyScript [addrHash1, addrHash2] -- We now submit a transaction to the script address let lockedAmount = 10_000_000 @@ -443,7 +444,7 @@ hprop_included_simple_script_cost_calculation = integrationRetryWorkspace 2 "inc paymentKeyInfoHash :: PaymentKeyInfo -> ExceptT String IO Text paymentKeyInfoHash wallet = do - vkBs <- liftIO $ BS.readFile (unFile $ verificationKey $ paymentKeyInfoPair wallet) + vkBs <- liftIOAnnotated $ BS.readFile (unFile $ verificationKey $ paymentKeyInfoPair wallet) svk <- liftEither $ first show $ deserialiseAnyVerificationKey vkBs return $ decodeLatin1 $ diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Query.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Query.hs index d1e10d11df7..48274f65297 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Query.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Cli/Query.hs @@ -58,6 +58,7 @@ import qualified Testnet.Defaults as Defaults import Testnet.Process.Cli.Transaction (TxOutAddress (..), mkSimpleSpendOutputsOnlyTx, mkSpendOutputsOnlyTx, retrieveTransactionId, signTx, submitTx) import Testnet.Process.Run (execCli', execCliStdoutToJson, mkExecConfig) +import Testnet.Process.RunIO (liftIOAnnotated) import Testnet.Property.Util (integrationWorkspace) import Testnet.Start.Types (GenesisOptions (..), NumPools (..), cardanoNumPools) import Testnet.TestQueryCmds (TestQueryCmds (..), forallQueryCommands) @@ -330,7 +331,7 @@ hprop_cli_queries = integrationWorkspace "cli-queries" $ \tempAbsBasePath' -> H. do -- Set up files and vars refScriptSizeWork <- H.createDirectoryIfMissing $ work "ref-script-size-test" - plutusV3Script <- File <$> liftIO (makeAbsolute "test/cardano-testnet-test/files/plutus/v3/always-succeeds.plutus") + plutusV3Script <- File <$> liftIOAnnotated (makeAbsolute "test/cardano-testnet-test/files/plutus/v3/always-succeeds.plutus") let transferAmount = Coin 10_000_000 -- Submit a transaction to publish the reference script txBody <- mkSpendOutputsOnlyTx execConfig epochStateView sbe refScriptSizeWork "tx-body" wallet1 diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/CommitteeAddNew.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/CommitteeAddNew.hs index f1b89b3c8a8..3f60d5fe081 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/CommitteeAddNew.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/CommitteeAddNew.hs @@ -45,6 +45,7 @@ import qualified Testnet.Process.Cli.SPO as SPO import Testnet.Process.Cli.SPO (createStakeKeyRegistrationCertificate) import Testnet.Process.Cli.Transaction (retrieveTransactionId, signTx, submitTx) import Testnet.Process.Run (addEnvVarsToConfig, execCli', mkExecConfig) +import Testnet.Process.RunIO (liftIOAnnotated) import Testnet.Property.Util (integrationWorkspace) import Testnet.Start.Types (GenesisOptions (..), cardanoNumPools) import Testnet.Types @@ -108,7 +109,7 @@ hprop_constitutional_committee_add_new = integrationWorkspace "constitutional-co gov <- H.createDirectoryIfMissing $ work "governance" let proposalAnchorDataIpfsHash = "QmexFJuEn5RtnHEqpxDcqrazdHPzAwe7zs2RxHLfMH5gBz" - proposalAnchorFile <- H.noteM $ liftIO $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" + proposalAnchorFile <- H.noteM $ liftIOAnnotated $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" updateCommitteeFp <- H.note $ gov "update-cc.action" diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/InfoAction.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/InfoAction.hs index fff611ac500..5ead843ce79 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/InfoAction.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/InfoAction.hs @@ -38,6 +38,7 @@ import Testnet.Process.Cli.Keys import Testnet.Process.Cli.SPO (createStakeKeyRegistrationCertificate) import Testnet.Process.Cli.Transaction (retrieveTransactionId) import Testnet.Process.Run (addEnvVarsToConfig, execCli', mkExecConfig) +import Testnet.Process.RunIO (liftIOAnnotated) import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types import Testnet.Types @@ -87,7 +88,7 @@ hprop_ledger_events_info_action = integrationRetryWorkspace 2 "info-hash" $ \tem gov <- H.createDirectoryIfMissing $ work "governance" let proposalAnchorDataIpfsHash = "QmexFJuEn5RtnHEqpxDcqrazdHPzAwe7zs2RxHLfMH5gBz" - proposalAnchorFile <- H.noteM $ liftIO $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" + proposalAnchorFile <- H.noteM $ liftIOAnnotated $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" infoActionFp <- H.note $ work gov "info.action" diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitution.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitution.hs index 83038acf9df..532dbf564e4 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitution.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/ProposeNewConstitution.hs @@ -46,6 +46,7 @@ import Testnet.Process.Cli.Keys import Testnet.Process.Cli.SPO (createStakeKeyRegistrationCertificate) import Testnet.Process.Cli.Transaction import Testnet.Process.Run (addEnvVarsToConfig, execCli', mkExecConfig) +import Testnet.Process.RunIO (liftIOAnnotated) import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types import Testnet.Types @@ -110,9 +111,9 @@ hprop_ledger_events_propose_new_constitution = integrationRetryWorkspace 2 "prop constitutionActionFp <- H.note $ gov "constitution.action" let proposalAnchorDataIpfsHash = "QmexFJuEn5RtnHEqpxDcqrazdHPzAwe7zs2RxHLfMH5gBz" - proposalAnchorFile <- H.noteM $ liftIO $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" + proposalAnchorFile <- H.noteM $ liftIOAnnotated $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" let constitutionAnchorDataIpfsHash = "QmXGkenkhh3NsotVwbNGToGsPuvJLgRT9aAz5ToyKAqdWP" - constitutionAnchorFile <- H.noteM $ liftIO $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" + constitutionAnchorFile <- H.noteM $ liftIOAnnotated $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" constitutionHash <- execCli' execConfig [ "hash", "anchor-data", "--file-binary", constitutionAnchorFile diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/Transaction/HashMismatch.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/Transaction/HashMismatch.hs index 9b4b8119df0..96bcf7728d3 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/Transaction/HashMismatch.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/Transaction/HashMismatch.hs @@ -26,6 +26,7 @@ import Test.Cardano.CLI.Hash (serveFilesWhile, tamperBase16Hash) import Testnet.Components.Query import Testnet.Process.Cli.Keys import Testnet.Process.Run (addEnvVarsToConfig, execCli', execCliAny, mkExecConfig) +import Testnet.Process.RunIO (liftIOAnnotated) import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types import Testnet.Types @@ -75,7 +76,7 @@ hprop_transaction_build_wrong_hash = integrationRetryWorkspace 2 "wrong-hash" $ gov <- H.createDirectoryIfMissing $ work "governance" let proposalAnchorDataIpfsHash = "QmexFJuEn5RtnHEqpxDcqrazdHPzAwe7zs2RxHLfMH5gBz" - proposalAnchorFile <- H.noteM $ liftIO $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" + proposalAnchorFile <- H.noteM $ liftIOAnnotated $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" infoActionFp <- H.note $ work gov "info.action" diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/TreasuryWithdrawal.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/TreasuryWithdrawal.hs index 28c1d046109..d40c9b5d56f 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/TreasuryWithdrawal.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Gov/TreasuryWithdrawal.hs @@ -43,6 +43,7 @@ import Testnet.Process.Cli.Keys (cliStakeAddressKeyGen) import Testnet.Process.Cli.SPO (createStakeKeyRegistrationCertificate) import Testnet.Process.Cli.Transaction (retrieveTransactionId) import Testnet.Process.Run (addEnvVarsToConfig, execCli', mkExecConfig) +import Testnet.Process.RunIO (liftIOAnnotated) import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types import Testnet.Types @@ -91,7 +92,7 @@ hprop_ledger_events_treasury_withdrawal = integrationRetryWorkspace 2 "treasury gov <- H.createDirectoryIfMissing $ work "governance" let proposalAnchorDataIpfsHash = "QmexFJuEn5RtnHEqpxDcqrazdHPzAwe7zs2RxHLfMH5gBz" - proposalAnchorFile <- H.noteM $ liftIO $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" + proposalAnchorFile <- H.noteM $ liftIOAnnotated $ makeAbsolute $ "test" "cardano-testnet-test" "files" "sample-proposal-anchor" treasuryWithdrawalActionFp <- H.note $ work gov "treasury-withdrawal.action" From 81be8a401af9648ad73a917e87231e77f459dcdc Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Fri, 7 Nov 2025 11:17:46 +0100 Subject: [PATCH 16/19] Fix infinite loop --- cardano-testnet/src/Testnet/Process/RunIO.hs | 32 +++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/cardano-testnet/src/Testnet/Process/RunIO.hs b/cardano-testnet/src/Testnet/Process/RunIO.hs index 293ca06241b..275b9f54460 100644 --- a/cardano-testnet/src/Testnet/Process/RunIO.hs +++ b/cardano-testnet/src/Testnet/Process/RunIO.hs @@ -5,7 +5,7 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -module Testnet.Process.RunIO +module Testnet.Process.RunIO ( execCli' , execCli_ , mkExecConfig @@ -14,27 +14,29 @@ module Testnet.Process.RunIO ) where import Prelude -import Data.Aeson (eitherDecode) -import Data.Monoid (Last (..)) -import Hedgehog.Extras.Internal.Plan (Component (..), Plan (..)) -import RIO -import System.FilePath (takeDirectory) -import System.FilePath.Posix (()) -import System.Process (CreateProcess (..)) import Control.Exception.Annotated (exceptionWithCallStack) +import Data.Aeson (eitherDecode) import qualified Data.ByteString.Lazy as LBS import qualified Data.List as L +import Data.Monoid (Last (..)) import qualified Data.Text as T import qualified GHC.Stack as GHC -import qualified Hedgehog.Extras.Stock.OS as OS -import qualified Hedgehog.Extras.Stock.IO.Network.Sprocket as IO -import Hedgehog.Extras.Test.Process (ExecConfig(..)) import qualified System.Directory as IO import qualified System.Environment as IO import qualified System.Exit as IO +import System.FilePath (takeDirectory) +import System.FilePath.Posix (()) import qualified System.IO.Unsafe as IO import qualified System.Process as IO +import System.Process (CreateProcess (..)) + +import Hedgehog.Extras.Internal.Plan (Component (..), Plan (..)) +import qualified Hedgehog.Extras.Stock.IO.Network.Sprocket as IO +import qualified Hedgehog.Extras.Stock.OS as OS +import Hedgehog.Extras.Test.Process (ExecConfig (..)) + +import RIO @@ -116,8 +118,8 @@ execFlex' execFlex' execConfig pkgBin envBin arguments = GHC.withFrozenCallStack $ do (exitResult, stdout', _stderr) <- execFlexAny' execConfig pkgBin envBin arguments case exitResult of - IO.ExitFailure exitCode -> throwString $ - unlines $ + IO.ExitFailure exitCode -> throwString $ + unlines $ [ "Process exited with non-zero exit-code: " ++ show @Int exitCode ] ++ (if L.null stdout' then [] else ["━━━━ stdout ━━━━" , stdout']) ++ (if L.null _stderr then [] else ["━━━━ stderr ━━━━" , _stderr]) @@ -289,6 +291,6 @@ procFlex procFlex = procFlex' defaultExecConfig -- This will also catch async exceptions as well. -liftIOAnnotated :: (HasCallStack, MonadIO m) => IO a -> m a +liftIOAnnotated :: (HasCallStack, MonadIO m) => IO a -> m a liftIOAnnotated action = GHC.withFrozenCallStack $ - liftIOAnnotated $ action `catch` (\(e :: SomeException) -> throwM $ exceptionWithCallStack e) \ No newline at end of file + liftIO $ action `catch` (\(e :: SomeException) -> throwM $ exceptionWithCallStack e) From 43697cc28aac906acbe56fd53aff9a18185d28d7 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 7 Nov 2025 08:37:17 -0400 Subject: [PATCH 17/19] Bump iohkNix and hackageNix --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 088771904d5..f6a57ba9e82 100644 --- a/flake.lock +++ b/flake.lock @@ -289,11 +289,11 @@ "hackageNix": { "flake": false, "locked": { - "lastModified": 1750944318, - "narHash": "sha256-DwjXWJqd3+Uhvx1OewJDMGxtny20vQvRF4iB+H8a3fs=", + "lastModified": 1762515617, + "narHash": "sha256-tl+UEK5D2oBqQU70sKROkWQj31wLbwr6C43p0cdffKo=", "owner": "input-output-hk", "repo": "hackage.nix", - "rev": "1df55daef81b543cf3ccab4b1a5a536e32d8ce2a", + "rev": "2205bcd146bf3cb58035a6e759e23c43bb1bf3c7", "type": "github" }, "original": { @@ -621,11 +621,11 @@ "sodium": "sodium" }, "locked": { - "lastModified": 1751421193, - "narHash": "sha256-rklXDo12dfukaSqcEyiYbze3ffRtTl2/WAAQCWfkGiw=", + "lastModified": 1757407040, + "narHash": "sha256-rSHOQli+iffMmneSF/Ov8Uci6APaROWen+EfRb5mmiU=", "owner": "input-output-hk", "repo": "iohk-nix", - "rev": "64ca6f4c0c6db283e2ec457c775bce75173fb319", + "rev": "a94259528eb6d37073512d1767f14fd8ea12a8f0", "type": "github" }, "original": { From 1d5b8a92fd7b68da2f2bb685a6ad45804bf975a8 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Fri, 7 Nov 2025 14:07:14 +0100 Subject: [PATCH 18/19] Fix liftToIntegration to report the exception location to Hedgehog --- cardano-testnet/src/Testnet/Start/Cardano.hs | 38 ++++++++++---------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/cardano-testnet/src/Testnet/Start/Cardano.hs b/cardano-testnet/src/Testnet/Start/Cardano.hs index 14d03a86d86..09a640249a2 100644 --- a/cardano-testnet/src/Testnet/Start/Cardano.hs +++ b/cardano-testnet/src/Testnet/Start/Cardano.hs @@ -6,6 +6,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} +{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} @@ -32,17 +33,18 @@ import Cardano.Api import Cardano.Api.Byron (GenesisData (..)) import qualified Cardano.Api.Byron as Byron -import Cardano.Node.Configuration.Topology (RemoteAddress(..)) +import Cardano.Node.Configuration.Topology (RemoteAddress (..)) import qualified Cardano.Node.Configuration.Topology as Direct import qualified Cardano.Node.Configuration.TopologyP2P as P2P import Cardano.Prelude (canonicalEncodePretty) -import Ouroboros.Network.PeerSelection.RelayAccessPoint (RelayAccessPoint(..)) +import Ouroboros.Network.PeerSelection.RelayAccessPoint (RelayAccessPoint (..)) import Prelude hiding (lines) import Control.Concurrent (threadDelay) -import Control.Monad +import Control.Monad import Control.Monad.Catch +import Control.Monad.Trans.Resource (MonadResource, getInternalState) import Data.Aeson import qualified Data.Aeson.Encode.Pretty as A import qualified Data.Aeson.KeyMap as A @@ -64,6 +66,7 @@ import Testnet.Components.Configuration import qualified Testnet.Defaults as Defaults import Testnet.Filepath import Testnet.Handlers (interruptNodesOnSigINT) +import Testnet.Orphans () import Testnet.Process.RunIO (execCli', execCli_, liftIOAnnotated, mkExecConfig) import Testnet.Property.Assert (assertChainExtended, assertExpectedSposInLedgerState) import Testnet.Runtime as TR @@ -72,13 +75,12 @@ import Testnet.Types as TR hiding (shelleyGenesis) import qualified Hedgehog.Extras as H import qualified Hedgehog.Extras.Stock.IO.Network.Port as H +import Hedgehog.Internal.Property (failException) - -import RIO (RIO(..),runRIO, throwString, MonadUnliftIO) -import Control.Monad.Trans.Resource (getInternalState, MonadResource) -import Testnet.Orphans () -import RIO.Orphans (ResourceMap) -import UnliftIO.Async +import RIO (MonadUnliftIO, RIO (..), runRIO, throwString) +import RIO.Orphans (ResourceMap) +import UnliftIO.Async +import UnliftIO.Exception (stringException) -- | There are certain conditions that need to be met in order to run @@ -91,10 +93,10 @@ testMinimumConfigurationRequirements options = withFrozenCallStack $ do when (cardanoNumPools options < 1) $ do throwString "Need at least one SPO node to produce blocks, but got none." -liftToIntegration :: HasCallStack => RIO ResourceMap a -> H.Integration a -liftToIntegration r = do - rMap <- lift $ lift getInternalState - liftIOAnnotated $ runRIO rMap r +liftToIntegration :: HasCallStack => RIO ResourceMap a -> H.Integration a +liftToIntegration r = do + rMap <- lift $ lift getInternalState + catch @_ @SomeException (runRIO rMap r) (withFrozenCallStack $ failException . toException . stringException . displayException) createTestnetEnv :: () => HasCallStack @@ -222,7 +224,7 @@ createTestnetEnv -- > ├── configuration.json -- > ├── current-stake-pools.json -- > └── module -cardanoTestnet +cardanoTestnet :: HasCallStack => MonadUnliftIO m => MonadResource m @@ -248,8 +250,8 @@ cardanoTestnet shelleyGenesisFile = tmpAbsPath "shelley-genesis.json" sBytes <- liftIOAnnotated (LBS.readFile shelleyGenesisFile) - shelleyGenesis@ShelleyGenesis{sgNetworkMagic} - <- case eitherDecode sBytes of + shelleyGenesis@ShelleyGenesis{sgNetworkMagic} + <- case eitherDecode sBytes of Right sg -> return sg Left err -> throwString $ "Could not decode shelley genesis file: " <> shelleyGenesisFile <> " Error: " <> err let testnetMagic :: Int = fromIntegral sgNetworkMagic @@ -337,8 +339,8 @@ cardanoTestnet -- Update start time in Byron genesis file eByron <- runExceptT $ Byron.readGenesisData byronGenesisFile - (byronGenesis', _byronHash) <- - case eByron of + (byronGenesis', _byronHash) <- + case eByron of Right bg -> return bg Left err -> throwString $ "Could not read byron genesis data from file: " <> byronGenesisFile <> " Error: " <> show err let byronGenesis = byronGenesis'{gdStartTime = startTime} From a8f627041c36e6e5052deac1a9d5efdf3aa51a24 Mon Sep 17 00:00:00 2001 From: Mateusz Galazyn Date: Sat, 8 Nov 2025 00:23:49 +0100 Subject: [PATCH 19/19] Generalise liftToIntegration --- cardano-testnet/src/Testnet/Start/Cardano.hs | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/cardano-testnet/src/Testnet/Start/Cardano.hs b/cardano-testnet/src/Testnet/Start/Cardano.hs index 09a640249a2..f9e3db1d846 100644 --- a/cardano-testnet/src/Testnet/Start/Cardano.hs +++ b/cardano-testnet/src/Testnet/Start/Cardano.hs @@ -9,6 +9,7 @@ {-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE TypeOperators #-} +{-# LANGUAGE RankNTypes #-} module Testnet.Start.Cardano ( CardanoTestnetCliOptions(..) @@ -44,7 +45,7 @@ import Prelude hiding (lines) import Control.Concurrent (threadDelay) import Control.Monad import Control.Monad.Catch -import Control.Monad.Trans.Resource (MonadResource, getInternalState) +import Control.Monad.Trans.Resource (MonadResource (..), getInternalState, runInternalState) import Data.Aeson import qualified Data.Aeson.Encode.Pretty as A import qualified Data.Aeson.KeyMap as A @@ -75,10 +76,9 @@ import Testnet.Types as TR hiding (shelleyGenesis) import qualified Hedgehog.Extras as H import qualified Hedgehog.Extras.Stock.IO.Network.Port as H -import Hedgehog.Internal.Property (failException) +import Hedgehog.Internal.Property (failException, MonadTest) -import RIO (MonadUnliftIO, RIO (..), runRIO, throwString) -import RIO.Orphans (ResourceMap) +import RIO (MonadUnliftIO, throwString) import UnliftIO.Async import UnliftIO.Exception (stringException) @@ -93,10 +93,12 @@ testMinimumConfigurationRequirements options = withFrozenCallStack $ do when (cardanoNumPools options < 1) $ do throwString "Need at least one SPO node to produce blocks, but got none." -liftToIntegration :: HasCallStack => RIO ResourceMap a -> H.Integration a -liftToIntegration r = do - rMap <- lift $ lift getInternalState - catch @_ @SomeException (runRIO rMap r) (withFrozenCallStack $ failException . toException . stringException . displayException) +liftToIntegration :: (HasCallStack, MonadCatch m, MonadResource m, MonadTest m) + => (forall n. (MonadCatch n, MonadResource n, MonadUnliftIO n, MonadFail n) => n a) + -> m a +liftToIntegration act = do + internalState <- liftResourceT getInternalState + catch @_ @SomeException (liftIO $ runInternalState act internalState) (withFrozenCallStack $ failException . toException . stringException . displayException) createTestnetEnv :: () => HasCallStack