diff --git a/vclusterops/adapter_pool.go b/vclusterops/adapter_pool.go index e4ff6f8..0b6db4b 100644 --- a/vclusterops/adapter_pool.go +++ b/vclusterops/adapter_pool.go @@ -85,7 +85,7 @@ func (pool *adapterPool) sendRequest(httpRequest *clusterHTTPRequest) error { if pool.logger.ForCli { // use context to check whether a step has completed ctx, cancelCtx := context.WithCancel(context.Background()) - go progressCheck(ctx, httpRequest.Name) + go progressCheck(ctx, httpRequest.Name, pool.logger) // cancel the progress check context when the result channel is closed defer cancelCtx() } @@ -115,7 +115,7 @@ func (pool *adapterPool) sendRequest(httpRequest *clusterHTTPRequest) error { // progressCheck checks whether a step (operation) has been completed. // Elapsed time of the step in seconds will be displayed. -func progressCheck(ctx context.Context, name string) { +func progressCheck(ctx context.Context, name string, logger vlog.Printer) { const progressCheckInterval = 5 startTime := time.Now() @@ -131,7 +131,7 @@ func progressCheck(ctx context.Context, name string) { return case tickTime := <-ticker.C: elapsedTime := tickTime.Sub(startTime) - vlog.PrintWithIndent("[%s] is still running. %.f seconds spent at this step.", + logger.PrintWithIndent("[%s] is still running. %.f seconds spent at this step.", name, elapsedTime.Seconds()) } } diff --git a/vclusterops/add_node.go b/vclusterops/add_node.go index bbf1f20..5722eb1 100644 --- a/vclusterops/add_node.go +++ b/vclusterops/add_node.go @@ -23,25 +23,17 @@ import ( "github.com/vertica/vcluster/vclusterops/vlog" ) -// VAddNodeOptions are the option arguments for the VAddNode API +// VAddNodeOptions represents the available options for VAddNode. type VAddNodeOptions struct { DatabaseOptions - // Hosts to add to database - NewHosts []string - // Name of the subcluster that the new nodes will be added to - SCName *string - // A primary up host that will be used to execute - // add_node operations. - Initiator string - DepotSize *string // like 10G - // Skip rebalance shards if true - SkipRebalanceShards *bool - // Use force remove if true - ForceRemoval *bool - - // Names of the existing nodes in the cluster. - // This options can be used to remove partially added nodes from catalog. - ExpectedNodeNames []string + NewHosts []string // Hosts to add to database + SCName *string // Name of the subcluster that the new nodes will be added to + Initiator string // A primary up host that will be used to execute add_node operations. + DepotSize *string // Depot size, e.g. 10G + SkipRebalanceShards *bool // Skip rebalance shards if true + ForceRemoval *bool // Use force remove if true + ExpectedNodeNames []string // Names of the existing nodes in the cluster. This option can be + // used to remove partially added nodes from catalog. } func VAddNodeOptionsFactory() VAddNodeOptions { @@ -119,7 +111,8 @@ func (o *VAddNodeOptions) validateAnalyzeOptions(logger vlog.Printer) error { return o.analyzeOptions() } -// VAddNode is the top-level API for adding node(s) to an existing database. +// VAddNode adds one or more nodes to an existing database. +// It returns a VCoordinationDatabase that contains catalog information and any error encountered. func (vcc *VClusterCommands) VAddNode(options *VAddNodeOptions) (VCoordinationDatabase, error) { vdb := makeVCoordinationDatabase() diff --git a/vclusterops/add_subcluster.go b/vclusterops/add_subcluster.go index 14635fc..d7d8d3a 100644 --- a/vclusterops/add_subcluster.go +++ b/vclusterops/add_subcluster.go @@ -176,7 +176,8 @@ func (options *VAddSubclusterOptions) validateAnalyzeOptions(config *ClusterConf return options.analyzeOptions() } -// VAddSubcluster can add a new subcluster to a running database +// VAddSubcluster adds to a running database a new subcluster with provided options. +// It returns any error encountered. func (vcc *VClusterCommands) VAddSubcluster(options *VAddSubclusterOptions) error { /* * - Produce Instructions diff --git a/vclusterops/cluster_config.go b/vclusterops/cluster_config.go index 5c6661a..a6b68c4 100644 --- a/vclusterops/cluster_config.go +++ b/vclusterops/cluster_config.go @@ -39,7 +39,7 @@ type Config struct { Databases ClusterConfig `yaml:"databases"` } -// ClusterConfig holds information of the databases in the cluster +// ClusterConfig is a map that stores configuration information for each database in the cluster. type ClusterConfig map[string]DatabaseConfig type DatabaseConfig struct { @@ -66,7 +66,8 @@ func MakeDatabaseConfig() DatabaseConfig { return DatabaseConfig{} } -// read config information from the YAML file +// ReadConfig reads cluster configuration information from a YAML-formatted file in configDirectory. +// It returns a ClusterConfig and any error encountered when reading and parsing the file. func ReadConfig(configDirectory string, logger vlog.Printer) (ClusterConfig, error) { configFilePath := filepath.Join(configDirectory, ConfigFileName) configBytes, err := os.ReadFile(configFilePath) @@ -103,7 +104,8 @@ func ReadConfig(configDirectory string, logger vlog.Printer) (ClusterConfig, err return clusterConfig, nil } -// write config information to the YAML file +// WriteConfig writes configuration information to configFilePath. +// It returns any write error encountered. func (c *ClusterConfig) WriteConfig(configFilePath string) error { var config Config config.Version = CurrentConfigFileVersion diff --git a/vclusterops/cluster_op.go b/vclusterops/cluster_op.go index 1b7abd8..cc75f65 100644 --- a/vclusterops/cluster_op.go +++ b/vclusterops/cluster_op.go @@ -380,9 +380,8 @@ func (opb *opHTTPSBase) validateAndSetUsernameAndPassword(opName string, useHTTP return nil } -// VClusterCommands is struct for all top-level admin commands (e.g. create db, -// add node, etc.). This is used to pass state around for the various APIs. We -// also use it for mocking in our unit test. +// VClusterCommands passes state around for all top-level administrator commands +// (e.g. create db, add node, etc.). type VClusterCommands struct { Log vlog.Printer } diff --git a/vclusterops/cluster_op_engine.go b/vclusterops/cluster_op_engine.go index 64a44a0..d9ce160 100644 --- a/vclusterops/cluster_op_engine.go +++ b/vclusterops/cluster_op_engine.go @@ -72,7 +72,7 @@ func (opEngine *VClusterOpEngine) run(logger vlog.Printer) error { return fmt.Errorf("finalize failed %w", err) } - vlog.PrintWithIndent("[%s] is successfully completed", op.getName()) + logger.PrintWithIndent("[%s] is successfully completed", op.getName()) } return nil diff --git a/vclusterops/coordinator_database.go b/vclusterops/coordinator_database.go index ee71aba..0e0acaf 100644 --- a/vclusterops/coordinator_database.go +++ b/vclusterops/coordinator_database.go @@ -26,14 +26,9 @@ import ( "golang.org/x/exp/maps" ) -/* VCoordinationDatabase contains a copy of some of the CAT::Database - * information from the catalog. It also contains a list of VCoordinationNodes. - * It is similar to the admintools VDatabase object. - * - * The create database command produces a VCoordinationDatabase. - * Start database, for example, consumes a VCoordinationDatabase. - * - */ +// VCoordinationDatabase represents catalog and node information for a database. The +// VCreateDatabase command returns a VCoordinationDatabase struct. Operations on +// an existing database (e.g. VStartDatabase) consume a VCoordinationDatabase struct. type VCoordinationDatabase struct { Name string // processed path prefixes @@ -345,11 +340,7 @@ func (vdb *VCoordinationDatabase) filterPrimaryNodes() { vdb.HostList = maps.Keys(vdb.HostNodeMap) } -/* VCoordinationNode contains a copy of the some of CAT::Node information - * from the database catalog (visible in the vs_nodes table). It is similar - * to the admintools VNode object. - * - */ +// VCoordinationNode represents node information from the database catalog. type VCoordinationNode struct { Name string `json:"name"` Address string @@ -427,7 +418,9 @@ func (vnode *VCoordinationNode) setFromNodeConfig(nodeConfig *NodeConfig, vdb *V } } -// WriteClusterConfig updates the yaml config file with the given vdb information +// WriteClusterConfig updates cluster configuration with the YAML-formatted file in configDir +// and writes to the log and stdout. +// It returns any error encountered. func (vdb *VCoordinationDatabase) WriteClusterConfig(configDir *string, logger vlog.Printer) error { /* build config information */ diff --git a/vclusterops/create_db.go b/vclusterops/create_db.go index 6c88c2b..94b2515 100644 --- a/vclusterops/create_db.go +++ b/vclusterops/create_db.go @@ -25,8 +25,8 @@ import ( "github.com/vertica/vcluster/vclusterops/vlog" ) -// A good rule of thumb is to use normal strings unless you need nil. -// Normal strings are easier and safer to use in Go. +// VCreateDatabaseOptions represents the available options when you create a database with +// VCreateDatabase. type VCreateDatabaseOptions struct { /* part 1: basic db info */ DatabaseOptions diff --git a/vclusterops/drop_db.go b/vclusterops/drop_db.go index 1b91655..e1d71c3 100644 --- a/vclusterops/drop_db.go +++ b/vclusterops/drop_db.go @@ -22,8 +22,7 @@ import ( "github.com/vertica/vcluster/vclusterops/util" ) -// A good rule of thumb is to use normal strings unless you need nil. -// Normal strings are easier and safer to use in Go. +// VDropDatabaseOptions adds to VCreateDatabaseOptions the option to force delete directories. type VDropDatabaseOptions struct { VCreateDatabaseOptions ForceDelete *bool // whether force delete directories @@ -37,7 +36,8 @@ func VDropDatabaseOptionsFactory() VDropDatabaseOptions { return opt } -// TODO: call this func when honor-user-input is implemented +// AnalyzeOptions verifies the host options for the VDropDatabaseOptions struct and +// returns any error encountered. func (options *VDropDatabaseOptions) AnalyzeOptions() error { hostAddresses, err := util.ResolveRawHostsToAddresses(options.RawHosts, options.Ipv6.ToBool()) if err != nil { diff --git a/vclusterops/fetch_node_state.go b/vclusterops/fetch_node_state.go index 40823c6..9e073ec 100644 --- a/vclusterops/fetch_node_state.go +++ b/vclusterops/fetch_node_state.go @@ -47,7 +47,8 @@ func (options *VFetchNodeStateOptions) validateAnalyzeOptions(vcc *VClusterComma return options.analyzeOptions() } -// VFetchNodeState fetches node states (e.g., up or down) in the cluster +// VFetchNodeState returns the node state (e.g., up or down) for each node in the cluster and any +// error encountered. func (vcc *VClusterCommands) VFetchNodeState(options *VFetchNodeStateOptions) ([]NodeInfo, error) { /* * - Produce Instructions diff --git a/vclusterops/https_poll_node_state_op.go b/vclusterops/https_poll_node_state_op.go index a8dcb7c..4f7ceec 100644 --- a/vclusterops/https_poll_node_state_op.go +++ b/vclusterops/https_poll_node_state_op.go @@ -140,7 +140,7 @@ func (op *httpsPollNodeStateOp) finalize(_ *opEngineExecContext) error { } func (op *httpsPollNodeStateOp) processResult(execContext *opEngineExecContext) error { - vlog.PrintWithIndent("[%s] expecting %d up host(s)", op.name, len(op.hosts)) + op.logger.PrintWithIndent("[%s] expecting %d up host(s)", op.name, len(op.hosts)) err := pollState(op, execContext) if err != nil { @@ -206,11 +206,11 @@ func (op *httpsPollNodeStateOp) shouldStopPolling() (bool, error) { } if upNodeCount < len(op.hosts) { - vlog.PrintWithIndent("[%s] %d host(s) up", op.name, upNodeCount) + op.logger.PrintWithIndent("[%s] %d host(s) up", op.name, upNodeCount) return false, nil } - vlog.PrintWithIndent("[%s] All nodes are up", op.name) + op.logger.PrintWithIndent("[%s] All nodes are up", op.name) return true, nil } diff --git a/vclusterops/nma_download_file_op.go b/vclusterops/nma_download_file_op.go index 56abe5c..b71c66d 100644 --- a/vclusterops/nma_download_file_op.go +++ b/vclusterops/nma_download_file_op.go @@ -55,8 +55,8 @@ type downloadFileRequestData struct { Parameters map[string]string `json:"parameters,omitempty"` } -// ClusterLeaseNotExpiredFailure is returned when an attempt is made to use a -// communal storage before the lease for it has expired. +// ClusterLeaseNotExpiredError is returned when you attempt to access a +// communal storage location when there is an active cluster lease on it. type ClusterLeaseNotExpiredError struct { Expiration string } @@ -68,8 +68,8 @@ func (e *ClusterLeaseNotExpiredError) Error() string { e.Expiration) } -// ReviveDBNodeCountMismatchError is returned when the number of nodes in new cluster -// does not match the number of nodes in original cluster +// ReviveDBNodeCountMismatchError is the error that is returned when the number of +// nodes in the revived cluster does not match the number of nodes in the original cluster. type ReviveDBNodeCountMismatchError struct { ReviveDBStep string FailureHost string diff --git a/vclusterops/node_info.go b/vclusterops/node_info.go index 8aafc76..bdd95a1 100644 --- a/vclusterops/node_info.go +++ b/vclusterops/node_info.go @@ -17,11 +17,10 @@ package vclusterops import mapset "github.com/deckarep/golang-set/v2" -// the following structs only hosts necessary information for this op +// NodeInfo represents information to identify a node. type NodeInfo struct { - Address string `json:"address"` - // vnode name, e.g., v_dbname_node0001 - Name string `json:"name"` + Address string `json:"address"` + Name string `json:"name"` // vnode name, e.g., v_dbname_node0001 State string `json:"state"` CatalogPath string `json:"catalog_path"` } diff --git a/vclusterops/re_ip.go b/vclusterops/re_ip.go index 6e6897b..ae94544 100644 --- a/vclusterops/re_ip.go +++ b/vclusterops/re_ip.go @@ -114,7 +114,8 @@ func (opt *VReIPOptions) validateAnalyzeOptions(logger vlog.Printer) error { return nil } -// VReIP changes nodes addresses (node address, control address, and control broadcast) +// VReIP changes the node address, control address, and control broadcast for a node. +// It returns any error encountered. func (vcc *VClusterCommands) VReIP(options *VReIPOptions) error { /* * - Produce Instructions @@ -237,7 +238,8 @@ type reIPRow struct { NewControlBroadcast string `json:"to_control_broadcast,omitempty"` } -// ReadReIPFile reads the re-ip file and build a list of ReIPInfo +// ReadReIPFile reads the re-IP file and builds a slice of ReIPInfo. +// It returns any error encountered. func (opt *VReIPOptions) ReadReIPFile(path string) error { if err := util.AbsPathCheck(path); err != nil { return fmt.Errorf("must specify an absolute path for the re-ip file") diff --git a/vclusterops/remove_node.go b/vclusterops/remove_node.go index 5fb4ae0..a4d6b68 100644 --- a/vclusterops/remove_node.go +++ b/vclusterops/remove_node.go @@ -24,16 +24,13 @@ import ( "github.com/vertica/vcluster/vclusterops/vlog" ) -// VRemoveNodeOptions are the option arguments for the VRemoveNode API +// VRemoveNodeOptions represents the available options to remove one or more nodes from +// the database. type VRemoveNodeOptions struct { DatabaseOptions - // Hosts to remove from database - HostsToRemove []string - // A primary up host that will be used to execute - // remove_node operations. - Initiator string - // whether force delete directories - ForceDelete *bool + HostsToRemove []string // Hosts to remove from database + Initiator string // A primary up host that will be used to execute remove_node operations. + ForceDelete *bool // whether force delete directories } func VRemoveNodeOptionsFactory() VRemoveNodeOptions { @@ -51,8 +48,9 @@ func (o *VRemoveNodeOptions) setDefaultValues() { *o.ForceDelete = true } -// ParseHostToRemoveList converts the string list of hosts, to remove, into a slice of strings. -// The hosts should be separated by comma, and will be converted to lower case. +// ParseHostToRemoveList converts a comma-separated string list of hosts into a slice of host names +// to remove from the database. During parsing, the hosts are converted to lowercase. +// It returns any parsing error encountered. func (o *VRemoveNodeOptions) ParseHostToRemoveList(hosts string) error { inputHostList, err := util.SplitHosts(hosts) if err != nil { diff --git a/vclusterops/remove_subcluster.go b/vclusterops/remove_subcluster.go index ee40588..f29a843 100644 --- a/vclusterops/remove_subcluster.go +++ b/vclusterops/remove_subcluster.go @@ -24,13 +24,12 @@ import ( "github.com/vertica/vcluster/vclusterops/vlog" ) -// VRemoveScOptions are the option arguments for the VRemoveSubcluster API +// VRemoveScOptions represents the available options when you remove a subcluster from a +// database. type VRemoveScOptions struct { DatabaseOptions - // Subcluster to remove from database - SubclusterToRemove *string - // whether force delete directories - ForceDelete *bool + SubclusterToRemove *string // subcluster to remove from database + ForceDelete *bool // whether force delete directories } func VRemoveScOptionsFactory() VRemoveScOptions { @@ -105,12 +104,11 @@ func (o *VRemoveScOptions) validateAnalyzeOptions(logger vlog.Printer) error { return o.setUsePassword(logger) } -/* -VRemoveSubcluster has three major phases: - 1. pre-check (check the subcluster name and get nodes for the subcluster) - 2. run VRemoveNode (refer to the instructions in VRemoveNode; Optional: if there are any nodes still associated with the subcluster) - 3. run drop subcluster (i.e., remove the subcluster name from catalog) -*/ +// VRemoveSubcluster removes a subcluster. It returns updated database catalog information and any error encountered. +// VRemoveSubcluster has three major phases: +// 1. Pre-check: check the subcluster name and get nodes for the subcluster. +// 2. Removes nodes: Optional. If there are any nodes still associated with the subcluster, runs VRemoveNode. +// 3. Drop the subcluster: Remove the subcluster name from the database catalog. func (vcc *VClusterCommands) VRemoveSubcluster(removeScOpt *VRemoveScOptions) (VCoordinationDatabase, error) { vdb := makeVCoordinationDatabase() diff --git a/vclusterops/revive_db.go b/vclusterops/revive_db.go index 0db4ac5..c0e6804 100644 --- a/vclusterops/revive_db.go +++ b/vclusterops/revive_db.go @@ -105,7 +105,8 @@ func (options *VReviveDatabaseOptions) validateAnalyzeOptions() error { return options.analyzeOptions() } -// VReviveDatabase can revive a database which has been terminated but its communal storage data still exists +// VReviveDatabase revives a database that was terminated but whose communal storage data still exists. +// It returns the database information retrieved from communal storage and any error encountered. func (vcc *VClusterCommands) VReviveDatabase(options *VReviveDatabaseOptions) (dbInfo string, err error) { /* * - Validate options diff --git a/vclusterops/start_db.go b/vclusterops/start_db.go index 8204a2b..037cb42 100644 --- a/vclusterops/start_db.go +++ b/vclusterops/start_db.go @@ -22,18 +22,12 @@ import ( "github.com/vertica/vcluster/vclusterops/vlog" ) -// Normal strings are easier and safer to use in Go. +// VStartDatabaseOptions represents the available options when you start a database +// with VStartDatabase. type VStartDatabaseOptions struct { - // basic db info - DatabaseOptions - - // timeout for polling the states of all nodes in the database in HTTPSPollNodeStateOp - StatePollingTimeout *int - - /* hidden option */ - - // whether trim the input host list based on the catalog info - TrimHostList *bool + DatabaseOptions // basic db info + StatePollingTimeout *int // timeout for polling the states of all nodes in the database in HTTPSPollNodeStateOp + TrimHostList *bool // whether trim the input host list based on the catalog info } func VStartDatabaseOptionsFactory() VStartDatabaseOptions { diff --git a/vclusterops/start_node.go b/vclusterops/start_node.go index b85b08a..38b0e6f 100644 --- a/vclusterops/start_node.go +++ b/vclusterops/start_node.go @@ -22,14 +22,12 @@ import ( "github.com/vertica/vcluster/vclusterops/vlog" ) -// Normal strings are easier and safer to use in Go. +// VStartNodesOptions represents the available options when you start one or more nodes +// with VStartNodes. type VStartNodesOptions struct { - // basic db info - DatabaseOptions - // A set of nodes(nodename - host) that we want to start in the database - Nodes map[string]string - // timeout for polling nodes that we want to start in httpsPollNodeStateOp - StatePollingTimeout int + DatabaseOptions // basic db info + Nodes map[string]string // A set of nodes(nodename - host) that we want to start in the database + StatePollingTimeout int // timeout for polling nodes that we want to start in httpsPollNodeStateOp } type VStartNodesInfo struct { @@ -84,8 +82,8 @@ func (options *VStartNodesOptions) analyzeOptions() (err error) { return nil } -// ParseNodesList builds and returns a map from a comma-separated list of nodes. -// Ex: vnodeName1=host1,vnodeName2=host2 ---> map[string]string{vnodeName1: host1, vnodeName2: host2} +// ParseNodesList builds and returns a map of nodes from a comma-separated list of nodes. +// For example, vnodeName1=host1,vnodeName2=host2 is converted to map[string]string{vnodeName1: host1, vnodeName2: host2} func (options *VStartNodesOptions) ParseNodesList(nodeListStr string) error { nodes, err := util.ParseKeyValueListStr(nodeListStr, "restart") if err != nil { @@ -109,9 +107,10 @@ func (options *VStartNodesOptions) validateAnalyzeOptions(logger vlog.Printer) e return options.analyzeOptions() } -// VStartNodes will start the given nodes for a cluster that has not yet lost -// cluster quorum. This will handle updating of the nodes IP in the vertica -// catalog if necessary. Use VStartDatabase if cluster quorum is lost. +// VStartNodes starts the given nodes for a cluster that has not yet lost +// cluster quorum and returns any error encountered. +// If necessary, it updates the node's IP in the Vertica catalog. +// If cluster quorum is already lost, use VStartDatabase. func (vcc *VClusterCommands) VStartNodes(options *VStartNodesOptions) error { /* * - Produce Instructions diff --git a/vclusterops/vcluster_database_options.go b/vclusterops/vcluster_database_options.go index ed119fd..634698f 100644 --- a/vclusterops/vcluster_database_options.go +++ b/vclusterops/vcluster_database_options.go @@ -248,8 +248,9 @@ func (opt *DatabaseOptions) validateConfigDir(commandName string) error { return nil } -// ParseHostList converts a string into a list of hosts. -// The hosts should be separated by comma, and will be converted to lower case +// ParseHostList converts a comma-separated string of hosts into a slice of host names. During parsing, +// the hosts names are converted to lowercase. +// It returns any parsing error encountered. func (opt *DatabaseOptions) ParseHostList(hosts string) error { inputHostList, err := util.SplitHosts(hosts) if err != nil { @@ -385,7 +386,8 @@ func (opt *DatabaseOptions) getDepotAndDataPrefix( return depotPrefix, dataPrefix, nil } -// GetDBConfig can read database configurations from vertica_cluster.yaml to the struct ClusterConfig +// GetDBConfig reads database configurations from vertica_cluster.yaml into a ClusterConfig struct. +// It returns the ClusterConfig and any error encountered. func (opt *DatabaseOptions) GetDBConfig(vcc VClusterCommands) (config *ClusterConfig, e error) { var configDir string diff --git a/vclusterops/vlog/printer.go b/vclusterops/vlog/printer.go index f69e16c..cdb5687 100644 --- a/vclusterops/vlog/printer.go +++ b/vclusterops/vlog/printer.go @@ -218,8 +218,10 @@ func (p *Printer) SetupOrDie(logFile string) { } // PrintWithIndent prints message to console only with an indentation -func PrintWithIndent(msg string, v ...any) { - // the indent level may be adjusted - const indentLevel = 2 - fmt.Printf("%*s%s\n", indentLevel, "", fmt.Sprintf(msg, v...)) +func (p *Printer) PrintWithIndent(msg string, v ...any) { + if p.ForCli { + // the indent level may be adjusted + const indentLevel = 2 + fmt.Printf("%*s%s\n", indentLevel, "", fmt.Sprintf(msg, v...)) + } } diff --git a/vclusterops/vlog/printer_test.go b/vclusterops/vlog/printer_test.go index 33174aa..bd7e196 100644 --- a/vclusterops/vlog/printer_test.go +++ b/vclusterops/vlog/printer_test.go @@ -16,11 +16,28 @@ package vlog import ( + "io" + "os" "testing" "github.com/stretchr/testify/assert" ) +// CaptureStdout returns the stdout of the function f as a string +func CaptureStdout(f func()) string { + originalStdout := os.Stdout + r, w, _ := os.Pipe() + os.Stdout = w + + f() + + w.Close() + out, _ := io.ReadAll(r) + os.Stdout = originalStdout + + return string(out) +} + func TestPasswordRedaction(t *testing.T) { // test pw redaction pw := "hunter2" @@ -35,3 +52,24 @@ func TestPasswordRedaction(t *testing.T) { assert.Len(t, unmaskedArgs, 2) assert.Equal(t, pw, unmaskedArgs[1]) } + +func TestPrintWithIndent(t *testing.T) { + var p Printer + + const testMessage = "test message" + + // when ForCli is false, PrintWithIndent() should not output to stdout + p.ForCli = false + output := CaptureStdout(func() { + p.PrintWithIndent(testMessage) + }) + assert.Empty(t, output) + + // when ForCli is true, + // PrintWithIndent() should output the message to stdout with indentation + p.ForCli = true + output = CaptureStdout(func() { + p.PrintWithIndent(testMessage) + }) + assert.Equal(t, output, " "+testMessage+"\n") +}