Repository: coinbase/rosetta-cli
Pull request #148 (2020-10-01 17:58:25)

[Cmd] Fix Error on Exit
Relates: #145 

This PR changes all commands to use `RunE` so that `cobra` can invoke a post process function. This also contains some minor cleanups enabled by using `RunE` instead of exiting manually from each command.


Patch:
diff --git a/cmd/check_construction.go b/cmd/check_construction.go
index 05bbbdd..15b1d0c 100644
--- a/cmd/check_construction.go
+++ b/cmd/check_construction.go
@@ -16,8 +16,8 @@ package cmd
 
 import (
 	"context"
+	"errors"
 	"fmt"
-	"log"
 	"time"
 
 	"github.com/coinbase/rosetta-cli/pkg/results"
@@ -49,13 +49,18 @@ Ethereum.
 Right now, this tool only supports transfer testing (for both account-based
 and UTXO-based blockchains). However, we plan to add support for testing
 arbitrary scenarios (i.e. staking, governance).`,
-		Run: runCheckConstructionCmd,
+		RunE: runCheckConstructionCmd,
 	}
 )
 
-func runCheckConstructionCmd(cmd *cobra.Command, args []string) {
+func runCheckConstructionCmd(cmd *cobra.Command, args []string) error {
 	if Config.Construction == nil {
-		log.Fatal("construction configuration is missing!")
+		return results.ExitConstruction(
+			Config,
+			nil,
+			nil,
+			errors.New("construction configuration is missing"),
+		)
 	}
 
 	ensureDataDirectoryExists()
@@ -71,23 +76,23 @@ func runCheckConstructionCmd(cmd *cobra.Command, args []string) {
 
 	_, _, fetchErr := fetcher.InitializeAsserter(ctx, Config.Network)
 	if fetchErr != nil {
-		results.ExitConstruction(
+		cancel()
+		return results.ExitConstruction(
 			Config,
 			nil,
 			nil,
 			fmt.Errorf("%w: unable to initialize asserter", fetchErr.Err),
-			1,
 		)
 	}
 
 	_, err := utils.CheckNetworkSupported(ctx, Config.Network, fetcher)
 	if err != nil {
-		results.ExitConstruction(
+		cancel()
+		return results.ExitConstruction(
 			Config,
 			nil,
 			nil,
 			fmt.Errorf("%w: unable to confirm network is supported", err),
-			1,
 		)
 	}
 
@@ -100,19 +105,23 @@ func runCheckConstructionCmd(cmd *cobra.Command, args []string) {
 		&SignalReceived,
 	)
 	if err != nil {
-		results.ExitConstruction(
+		return results.ExitConstruction(
 			Config,
 			nil,
 			nil,
 			fmt.Errorf("%w: unable to initialize construction tester", err),
-			1,
 		)
 	}
 
 	defer constructionTester.CloseDatabase(ctx)
 
 	if err := constructionTester.PerformBroadcasts(ctx); err != nil {
-		log.Fatalf("%s: unable to perform broadcasts", err.Error())
+		return results.ExitConstruction(
+			Config,
+			nil,
+			nil,
+			fmt.Errorf("%w: unable to perform broadcasts", err),
+		)
 	}
 
 	g, ctx := errgroup.WithContext(ctx)
@@ -149,5 +158,5 @@ func runCheckConstructionCmd(cmd *cobra.Command, args []string) {
 	go handleSignals(&sigListeners)
 
 	err = g.Wait()
-	constructionTester.HandleErr(err, &sigListeners)
+	return constructionTester.HandleErr(err, &sigListeners)
 }
diff --git a/cmd/check_data.go b/cmd/check_data.go
index 1418697..51c93fa 100644
--- a/cmd/check_data.go
+++ b/cmd/check_data.go
@@ -66,11 +66,11 @@ historical balance disabled to true, you must provide an
 absolute path to a JSON file containing initial balances with the
 bootstrap balance config. You can look at the examples folder for an example
 of what one of these files looks like.`,
-		Run: runCheckDataCmd,
+		RunE: runCheckDataCmd,
 	}
 )
 
-func runCheckDataCmd(cmd *cobra.Command, args []string) {
+func runCheckDataCmd(cmd *cobra.Command, args []string) error {
 	ensureDataDirectoryExists()
 	ctx, cancel := context.WithCancel(context.Background())
 
@@ -84,12 +84,12 @@ func runCheckDataCmd(cmd *cobra.Command, args []string) {
 
 	_, _, fetchErr := fetcher.InitializeAsserter(ctx, Config.Network)
 	if fetchErr != nil {
-		results.ExitData(
+		cancel()
+		return results.ExitData(
 			Config,
 			nil,
 			nil,
 			fmt.Errorf("%w: unable to initialize asserter", fetchErr.Err),
-			1,
 			"",
 			"",
 		)
@@ -97,12 +97,12 @@ func runCheckDataCmd(cmd *cobra.Command, args []string) {
 
 	networkStatus, err := utils.CheckNetworkSupported(ctx, Config.Network, fetcher)
 	if err != nil {
-		results.ExitData(
+		cancel()
+		return results.ExitData(
 			Config,
 			nil,
 			nil,
 			fmt.Errorf("%w: unable to confirm network", err),
-			1,
 			"",
 			"",
 		)
@@ -166,5 +166,5 @@ func runCheckDataCmd(cmd *cobra.Command, args []string) {
 
 	// HandleErr will exit if we should not attempt
 	// to find missing operations.
-	dataTester.HandleErr(ctx, err, &sigListeners)
+	return dataTester.HandleErr(ctx, err, &sigListeners)
 }
diff --git a/cmd/configuration_create.go b/cmd/configuration_create.go
index 99e56d9..0bba6bc 100644
--- a/cmd/configuration_create.go
+++ b/cmd/configuration_create.go
@@ -15,7 +15,7 @@
 package cmd
 
 import (
-	"log"
+	"fmt"
 
 	"github.com/coinbase/rosetta-cli/configuration"
 
@@ -27,13 +27,15 @@ var (
 	configurationCreateCmd = &cobra.Command{
 		Use:   "configuration:create",
 		Short: "Create a default configuration file at the provided path",
-		Run:   runConfigurationCreateCmd,
+		RunE:  runConfigurationCreateCmd,
 		Args:  cobra.ExactArgs(1),
 	}
 )
 
-func runConfigurationCreateCmd(cmd *cobra.Command, args []string) {
+func runConfigurationCreateCmd(cmd *cobra.Command, args []string) error {
 	if err := utils.SerializeAndWrite(args[0], configuration.DefaultConfiguration()); err != nil {
-		log.Fatalf("%s: unable to save configuration file to %s", err.Error(), args[0])
+		return fmt.Errorf("%w: unable to save configuration file to %s", err, args[0])
 	}
+
+	return nil
 }
diff --git a/cmd/configuration_validate.go b/cmd/configuration_validate.go
index 178d279..932e56c 100644
--- a/cmd/configuration_validate.go
+++ b/cmd/configuration_validate.go
@@ -15,10 +15,11 @@
 package cmd
 
 import (
-	"log"
+	"fmt"
 
 	"github.com/coinbase/rosetta-cli/configuration"
 
+	"github.com/fatih/color"
 	"github.com/spf13/cobra"
 )
 
@@ -26,16 +27,17 @@ var (
 	configurationValidateCmd = &cobra.Command{
 		Use:   "configuration:validate",
 		Short: "Ensure a configuration file at the provided path is formatted correctly",
-		Run:   runConfigurationValidateCmd,
+		RunE:  runConfigurationValidateCmd,
 		Args:  cobra.ExactArgs(1),
 	}
 )
 
-func runConfigurationValidateCmd(cmd *cobra.Command, args []string) {
+func runConfigurationValidateCmd(cmd *cobra.Command, args []string) error {
 	_, err := configuration.LoadConfiguration(args[0])
 	if err != nil {
-		log.Fatalf("%s: unable to save configuration file to %s", err.Error(), args[0])
+		return fmt.Errorf("%w: unable to save configuration file to %s", err, args[0])
 	}
 
-	log.Println("Configuration file validated!")
+	color.Green("Configuration file validated!")
+	return nil
 }
diff --git a/cmd/utils_asserter_configuration.go b/cmd/utils_asserter_configuration.go
index d92e998..063a0af 100644
--- a/cmd/utils_asserter_configuration.go
+++ b/cmd/utils_asserter_configuration.go
@@ -16,11 +16,12 @@ package cmd
 
 import (
 	"context"
-	"log"
+	"fmt"
 	"time"
 
 	"github.com/coinbase/rosetta-sdk-go/fetcher"
 	"github.com/coinbase/rosetta-sdk-go/utils"
+	"github.com/fatih/color"
 	"github.com/spf13/cobra"
 )
 
@@ -36,12 +37,12 @@ have been added in an update instead of silently erroring.
 
 To use this command, simply provide an absolute path as the argument for where
 the configuration file should be saved (in JSON).`,
-		Run:  runCreateConfigurationCmd,
+		RunE: runCreateConfigurationCmd,
 		Args: cobra.ExactArgs(1),
 	}
 )
 
-func runCreateConfigurationCmd(cmd *cobra.Command, args []string) {
+func runCreateConfigurationCmd(cmd *cobra.Command, args []string) error {
 	ctx := context.Background()
 
 	// Create a new fetcher
@@ -55,15 +56,18 @@ func runCreateConfigurationCmd(cmd *cobra.Command, args []string) {
 	// Initialize the fetcher's asserter
 	_, _, fetchErr := newFetcher.InitializeAsserter(ctx, Config.Network)
 	if fetchErr != nil {
-		log.Fatalf("%s: failed to initialize asserter", fetchErr.Err.Error())
+		return fmt.Errorf("%w: failed to initialize asserter", fetchErr.Err)
 	}
 
 	configuration, err := newFetcher.Asserter.ClientConfiguration()
 	if err != nil {
-		log.Fatalf("%s: unable to generate spec", err.Error())
+		return fmt.Errorf("%w: unable to generate spec", err)
 	}
 
 	if err := utils.SerializeAndWrite(args[0], configuration); err != nil {
-		log.Fatalf("%s: unable to serialize asserter configuration", err.Error())
+		return fmt.Errorf("%w: unable to serialize asserter configuration", err)
 	}
+
+	color.Green("Configuration file saved!")
+	return nil
 }
diff --git a/cmd/utils_train_zstd.go b/cmd/utils_train_zstd.go
index d1bec9a..a24f926 100644
--- a/cmd/utils_train_zstd.go
+++ b/cmd/utils_train_zstd.go
@@ -16,11 +16,13 @@ package cmd
 
 import (
 	"context"
+	"fmt"
 	"log"
 	"path"
 	"strconv"
 
 	"github.com/coinbase/rosetta-sdk-go/storage"
+	"github.com/fatih/color"
 	"github.com/spf13/cobra"
 )
 
@@ -43,12 +45,12 @@ The arguments for this command are:
 
 You can learn more about dictionary compression on the Zstandard
 website: https://github.com/facebook/zstd#the-case-for-small-data-compression`,
-		Run:  runTrainZstdCmd,
+		RunE: runTrainZstdCmd,
 		Args: cobra.MinimumNArgs(trainArgs),
 	}
 )
 
-func runTrainZstdCmd(cmd *cobra.Command, args []string) {
+func runTrainZstdCmd(cmd *cobra.Command, args []string) error {
 	ctx := context.Background()
 
 	namespace := args[0]
@@ -56,7 +58,7 @@ func runTrainZstdCmd(cmd *cobra.Command, args []string) {
 	dictionaryPath := path.Clean(args[2])
 	maxItems, err := strconv.Atoi(args[3])
 	if err != nil {
-		log.Fatalf("%s: unable to convert max items to integer", err.Error())
+		return fmt.Errorf("%w: unable to convert max items to integer", err)
 	}
 
 	compressorEntries := []*storage.CompressorEntry{}
@@ -80,6 +82,9 @@ func runTrainZstdCmd(cmd *cobra.Command, args []string) {
 		compressorEntries,
 	)
 	if err != nil {
-		log.Fatal(err)
+		return fmt.Errorf("%w: badger training failed", err)
 	}
+
+	color.Green("Training successful!")
+	return nil
 }
diff --git a/cmd/view_account.go b/cmd/view_account.go
index 55e1da2..6d844c9 100644
--- a/cmd/view_account.go
+++ b/cmd/view_account.go
@@ -41,21 +41,21 @@ any account by providing a JSON representation of a types.AccountIdentifier
 For example, you could run view:account '{"address":"interesting address"}' 1000
 to lookup the balance of an interesting address at block 1000. Allowing the
 address to specified as JSON allows for querying by SubAccountIdentifier.`,
-		Run:  runViewAccountCmd,
+		RunE: runViewAccountCmd,
 		Args: cobra.MinimumNArgs(1),
 	}
 )
 
-func runViewAccountCmd(cmd *cobra.Command, args []string) {
+func runViewAccountCmd(cmd *cobra.Command, args []string) error {
 	ctx := context.Background()
 
 	account := &types.AccountIdentifier{}
 	if err := json.Unmarshal([]byte(args[0]), account); err != nil {
-		log.Fatal(fmt.Errorf("%w: unable to unmarshal account %s", err, args[0]))
+		return fmt.Errorf("%w: unable to unmarshal account %s", err, args[0])
 	}
 
 	if err := asserter.AccountIdentifier(account); err != nil {
-		log.Fatal(fmt.Errorf("%w: invalid account identifier %+v", err, account))
+		return fmt.Errorf("%w: invalid account identifier %s", err, types.PrintStruct(account))
 	}
 
 	// Create a new fetcher
@@ -69,19 +69,19 @@ func runViewAccountCmd(cmd *cobra.Command, args []string) {
 	// Initialize the fetcher's asserter
 	_, _, fetchErr := newFetcher.InitializeAsserter(ctx, Config.Network)
 	if fetchErr != nil {
-		log.Fatal(fetchErr.Err)
+		return fmt.Errorf("%w: unable to initialize asserter", fetchErr.Err)
 	}
 
 	_, err := utils.CheckNetworkSupported(ctx, Config.Network, newFetcher)
 	if err != nil {
-		log.Fatalf("%s: unable to confirm network is supported", err.Error())
+		return fmt.Errorf("%w: unable to confirm network is supported", err)
 	}
 
 	var lookupBlock *types.PartialBlockIdentifier
 	if len(args) > 1 {
 		index, err := strconv.ParseInt(args[1], 10, 64)
 		if err != nil {
-			log.Fatal(fmt.Errorf("%w: unable to parse index %s", err, args[0]))
+			return fmt.Errorf("%w: unable to parse index %s", err, args[0])
 		}
 
 		lookupBlock = &types.PartialBlockIdentifier{Index: &index}
@@ -94,11 +94,13 @@ func runViewAccountCmd(cmd *cobra.Command, args []string) {
 		lookupBlock,
 	)
 	if fetchErr != nil {
-		log.Fatal(fmt.Errorf("%w: unable to fetch account %+v", fetchErr.Err, account))
+		return fmt.Errorf("%w: unable to fetch account %+v", fetchErr.Err, account)
 	}
 
 	log.Printf("Amounts: %s\n", types.PrettyPrintStruct(amounts))
 	log.Printf("Coins: %s\n", types.PrettyPrintStruct(coins))
 	log.Printf("Metadata: %s\n", types.PrettyPrintStruct(metadata))
 	log.Printf("Balance Fetched At: %s\n", types.PrettyPrintStruct(block))
+
+	return nil
 }
diff --git a/cmd/view_block.go b/cmd/view_block.go
index 77f72dd..1a67114 100644
--- a/cmd/view_block.go
+++ b/cmd/view_block.go
@@ -41,16 +41,16 @@ of the block is correct before printing.
 
 If this command errors, it is likely because the block you are trying to
 fetch is formatted incorrectly.`,
-		Run:  runViewBlockCmd,
+		RunE: runViewBlockCmd,
 		Args: cobra.ExactArgs(1),
 	}
 )
 
-func runViewBlockCmd(cmd *cobra.Command, args []string) {
+func runViewBlockCmd(cmd *cobra.Command, args []string) error {
 	ctx := context.Background()
 	index, err := strconv.ParseInt(args[0], 10, 64)
 	if err != nil {
-		log.Fatal(fmt.Errorf("%w: unable to parse index %s", err, args[0]))
+		return fmt.Errorf("%w: unable to parse index %s", err, args[0])
 	}
 
 	// Create a new fetcher
@@ -68,12 +68,12 @@ func runViewBlockCmd(cmd *cobra.Command, args []string) {
 	// the asserter what are valid responses.
 	_, _, fetchErr := newFetcher.InitializeAsserter(ctx, Config.Network)
 	if fetchErr != nil {
-		log.Fatal(fetchErr.Err)
+		return fmt.Errorf("%w: unable to initialize asserter", fetchErr.Err)
 	}
 
 	_, err = utils.CheckNetworkSupported(ctx, Config.Network, newFetcher)
 	if err != nil {
-		log.Fatalf("%s: unable to confirm network is supported", err.Error())
+		return fmt.Errorf("%w: unable to confirm network is supported", err)
 	}
 
 	// Fetch the specified block with retries (automatically
@@ -93,7 +93,7 @@ func runViewBlockCmd(cmd *cobra.Command, args []string) {
 		},
 	)
 	if fetchErr != nil {
-		log.Fatal(fmt.Errorf("%w: unable to fetch block", fetchErr.Err))
+		return fmt.Errorf("%w: unable to fetch block", fetchErr.Err)
 	}
 
 	log.Printf("Current Block: %s\n", types.PrettyPrintStruct(block))
@@ -103,7 +103,7 @@ func runViewBlockCmd(cmd *cobra.Command, args []string) {
 	p := parser.New(newFetcher.Asserter, func(*types.Operation) bool { return false })
 	changes, err := p.BalanceChanges(ctx, block, false)
 	if err != nil {
-		log.Fatal(fmt.Errorf("%w: unable to calculate balance changes", err))
+		return fmt.Errorf("%w: unable to calculate balance changes", err)
 	}
 
 	log.Printf("Balance Changes: %s\n", types.PrettyPrintStruct(changes))
@@ -116,4 +116,6 @@ func runViewBlockCmd(cmd *cobra.Command, args []string) {
 			types.PrettyPrintStruct(parser.GroupOperations(tx)),
 		)
 	}
+
+	return nil
 }
diff --git a/cmd/view_networks.go b/cmd/view_networks.go
index c11c4ff..7764ef8 100644
--- a/cmd/view_networks.go
+++ b/cmd/view_networks.go
@@ -16,6 +16,8 @@ package cmd
 
 import (
 	"context"
+	"errors"
+	"fmt"
 	"log"
 	"time"
 
@@ -35,11 +37,11 @@ status from all available networks and prints it to the terminal.
 
 If this command errors, it is likely because the /network/* endpoints are
 not formatted correctly.`,
-		Run: runViewNetworksCmd,
+		RunE: runViewNetworksCmd,
 	}
 )
 
-func runViewNetworksCmd(cmd *cobra.Command, args []string) {
+func runViewNetworksCmd(cmd *cobra.Command, args []string) error {
 	ctx := context.Background()
 
 	f := fetcher.New(
@@ -52,11 +54,11 @@ func runViewNetworksCmd(cmd *cobra.Command, args []string) {
 	// Attempt to fetch network list
 	networkList, fetchErr := f.NetworkListRetry(ctx, nil)
 	if fetchErr != nil {
-		log.Fatalf("%s: unable to fetch network list", fetchErr.Err.Error())
+		return fmt.Errorf("%w: unable to fetch network list", fetchErr.Err)
 	}
 
 	if len(networkList.NetworkIdentifiers) == 0 {
-		log.Fatal("no networks available")
+		return errors.New("no networks available")
 	}
 
 	for _, network := range networkList.NetworkIdentifiers {
@@ -67,7 +69,7 @@ func runViewNetworksCmd(cmd *cobra.Command, args []string) {
 			nil,
 		)
 		if fetchErr != nil {
-			log.Fatalf("%s: unable to get network options", fetchErr.Err.Error())
+			return fmt.Errorf("%w: unable to get network options", fetchErr.Err)
 		}
 
 		log.Printf("Network options: %s\n", types.PrettyPrintStruct(networkOptions))
@@ -78,9 +80,11 @@ func runViewNetworksCmd(cmd *cobra.Command, args []string) {
 			nil,
 		)
 		if fetchErr != nil {
-			log.Fatalf("%s: unable to get network status", fetchErr.Err.Error())
+			return fmt.Errorf("%w: unable to get network status", fetchErr.Err)
 		}
 
 		log.Printf("Network status: %s\n", types.PrettyPrintStruct(networkStatus))
 	}
+
+	return nil
 }
diff --git a/main.go b/main.go
index a68c3ee..1d25c27 100644
--- a/main.go
+++ b/main.go
@@ -15,13 +15,17 @@
 package main
 
 import (
-	"log"
+	"os"
 
 	"github.com/coinbase/rosetta-cli/cmd"
+
+	"github.com/fatih/color"
 )
 
 func main() {
-	if err := cmd.Execute(); err != nil {
-		log.Fatal(err)
+	err := cmd.Execute()
+	if err != nil {
+		color.Red("Command Failed: %s", err.Error())
+		os.Exit(1)
 	}
 }
diff --git a/pkg/results/construction_results.go b/pkg/results/construction_results.go
index 50139b6..04ec06f 100644
--- a/pkg/results/construction_results.go
+++ b/pkg/results/construction_results.go
@@ -296,8 +296,7 @@ func ExitConstruction(
 	counterStorage *storage.CounterStorage,
 	jobStorage *storage.JobStorage,
 	err error,
-	status int,
-) {
+) error {
 	results := ComputeCheckConstructionResults(
 		config,
 		err,
@@ -309,5 +308,5 @@ func ExitConstruction(
 		results.Output(config.Construction.ResultsOutputFile)
 	}
 
-	os.Exit(status)
+	return err
 }
diff --git a/pkg/results/data_results.go b/pkg/results/data_results.go
index 2422946..3266a65 100644
--- a/pkg/results/data_results.go
+++ b/pkg/results/data_results.go
@@ -571,10 +571,9 @@ func ExitData(
 	counterStorage *storage.CounterStorage,
 	balanceStorage *storage.BalanceStorage,
 	err error,
-	status int,
 	endCondition configuration.CheckDataEndCondition,
 	endConditionDetail string,
-) {
+) error {
 	results := ComputeCheckDataResults(
 		config,
 		err,
@@ -588,5 +587,5 @@ func ExitData(
 		results.Output(config.Data.ResultsOutputFile)
 	}
 
-	os.Exit(status)
+	return err
 }


Test patch:
diff --git a/pkg/tester/construction.go b/pkg/tester/construction.go
index e286075..50fff81 100644
--- a/pkg/tester/construction.go
+++ b/pkg/tester/construction.go
@@ -21,7 +21,6 @@ import (
 	"fmt"
 	"log"
 	"net/http"
-	"os"
 	"time"
 
 	"github.com/coinbase/rosetta-cli/configuration"
@@ -486,15 +485,18 @@ func (t *ConstructionTester) returnFunds(
 func (t *ConstructionTester) HandleErr(
 	err error,
 	sigListeners *[]context.CancelFunc,
-) {
+) error {
 	if *t.signalReceived {
-		color.Red("Check halted")
-		os.Exit(1)
-		return
+		return results.ExitConstruction(
+			t.config,
+			t.counterStorage,
+			t.jobStorage,
+			errors.New("check halted"),
+		)
 	}
 
 	if !t.reachedEndConditions {
-		results.ExitConstruction(t.config, t.counterStorage, t.jobStorage, err, 1)
+		return results.ExitConstruction(t.config, t.counterStorage, t.jobStorage, err)
 	}
 
 	// We optimistically run the ReturnFunds function on the coordinator
@@ -505,5 +507,5 @@ func (t *ConstructionTester) HandleErr(
 		sigListeners,
 	)
 
-	results.ExitConstruction(t.config, t.counterStorage, t.jobStorage, nil, 0)
+	return results.ExitConstruction(t.config, t.counterStorage, t.jobStorage, nil)
 }
diff --git a/pkg/tester/data.go b/pkg/tester/data.go
index 03569ce..07b4e56 100644
--- a/pkg/tester/data.go
+++ b/pkg/tester/data.go
@@ -22,7 +22,6 @@ import (
 	"log"
 	"math/big"
 	"net/http"
-	"os"
 	"time"
 
 	"github.com/coinbase/rosetta-cli/configuration"
@@ -513,11 +512,16 @@ func (t *DataTester) WatchEndConditions(
 // HandleErr is called when `check:data` returns an error.
 // If historical balance lookups are enabled, HandleErr will attempt to
 // automatically find any missing balance-changing operations.
-func (t *DataTester) HandleErr(ctx context.Context, err error, sigListeners *[]context.CancelFunc) {
+func (t *DataTester) HandleErr(ctx context.Context, err error, sigListeners *[]context.CancelFunc) error {
 	if *t.signalReceived {
-		color.Red("Check halted")
-		os.Exit(1)
-		return
+		return results.ExitData(
+			t.config,
+			t.counterStorage,
+			t.balanceStorage,
+			errors.New("check halted"),
+			"",
+			"",
+		)
 	}
 
 	if (err == nil || errors.Is(err, context.Canceled)) &&
@@ -531,12 +535,11 @@ func (t *DataTester) HandleErr(ctx context.Context, err error, sigListeners *[]c
 	}
 
 	if len(t.endCondition) != 0 {
-		results.ExitData(
+		return results.ExitData(
 			t.config,
 			t.counterStorage,
 			t.balanceStorage,
 			nil,
-			0,
 			t.endCondition,
 			t.endConditionDetail,
 		)
@@ -544,22 +547,43 @@ func (t *DataTester) HandleErr(ctx context.Context, err error, sigListeners *[]c
 
 	fmt.Printf("\n")
 	if t.reconcilerHandler.InactiveFailure == nil {
-		results.ExitData(t.config, t.counterStorage, t.balanceStorage, err, 1, "", "")
+		return results.ExitData(
+			t.config,
+			t.counterStorage,
+			t.balanceStorage,
+			err,
+			"",
+			"",
+		)
 	}
 
 	if !t.historicalBalanceEnabled {
 		color.Yellow(
 			"Can't find the block missing operations automatically, please enable historical balance lookup",
 		)
-		results.ExitData(t.config, t.counterStorage, t.balanceStorage, err, 1, "", "")
+		return results.ExitData(
+			t.config,
+			t.counterStorage,
+			t.balanceStorage,
+			err,
+			"",
+			"",
+		)
 	}
 
 	if t.config.Data.InactiveDiscrepencySearchDisabled {
 		color.Yellow("Search for inactive reconciliation discrepency is disabled")
-		results.ExitData(t.config, t.counterStorage, t.balanceStorage, err, 1, "", "")
+		return results.ExitData(
+			t.config,
+			t.counterStorage,
+			t.balanceStorage,
+			err,
+			"",
+			"",
+		)
 	}
 
-	t.FindMissingOps(ctx, err, sigListeners)
+	return t.FindMissingOps(ctx, err, sigListeners)
 }
 
 // FindMissingOps logs the types.BlockIdentifier of a block
@@ -569,7 +593,7 @@ func (t *DataTester) FindMissingOps(
 	ctx context.Context,
 	originalErr error,
 	sigListeners *[]context.CancelFunc,
-) {
+) error {
 	color.Cyan("Searching for block with missing operations...hold tight")
 	badBlock, err := t.recursiveOpSearch(
 		ctx,
@@ -580,7 +604,14 @@ func (t *DataTester) FindMissingOps(
 	)
 	if err != nil {
 		color.Yellow("%s: could not find block with missing ops", err.Error())
-		results.ExitData(t.config, t.counterStorage, t.balanceStorage, originalErr, 1, "", "")
+		return results.ExitData(
+			t.config,
+			t.counterStorage,
+			t.balanceStorage,
+			originalErr,
+			"",
+			"",
+		)
 	}
 
 	color.Yellow(
@@ -590,7 +621,14 @@ func (t *DataTester) FindMissingOps(
 		badBlock.Hash,
 	)
 
-	results.ExitData(t.config, t.counterStorage, t.balanceStorage, originalErr, 1, "", "")
+	return results.ExitData(
+		t.config,
+		t.counterStorage,
+		t.balanceStorage,
+		originalErr,
+		"",
+		"",
+	)
 }
 
 func (t *DataTester) recursiveOpSearch(