func (entry *SummaryEntry) ValueString(summary SummaryType) string {
	switch summary {
	case SummaryTypeSimple:
		return "<" + entry.Value + ">"
	default:
		return ": " + entry.Value
	}
}
ValueString returns value string as ": value" or "<value>" depending on summary type
package config
import (
	"context"
	"fmt"
	"io"
	"math"
	"os"
	"path/filepath"
	"strconv"
	"sync"
	"time"
	"github.com/InVisionApp/tabular"
	"github.com/buger/jsonparser"
	jsoniter "github.com/json-iterator/go"
	"github.com/pkg/errors"
	"github.com/qlik-oss/gopherciser/action"
	"github.com/qlik-oss/gopherciser/connection"
	"github.com/qlik-oss/gopherciser/enummap"
	"github.com/qlik-oss/gopherciser/helpers"
	"github.com/qlik-oss/gopherciser/logger"
	"github.com/qlik-oss/gopherciser/scenario"
	"github.com/qlik-oss/gopherciser/scheduler"
	"github.com/qlik-oss/gopherciser/session"
	"github.com/qlik-oss/gopherciser/statistics"
	"github.com/qlik-oss/gopherciser/synced"
	"github.com/qlik-oss/gopherciser/users"
	"github.com/qlik-oss/gopherciser/version"
	"github.com/shiena/ansicolor"
)
type (
	LogFormatType int
	SummaryType int
	SummaryHeaderEntry struct {
		FullName string
		ColSize  int
	}
	SummaryHeader map[string]*SummaryHeaderEntry
	SummaryActionDataEntry struct {
		Action      string
		Label       string
		AppGUID     string
		SuccessRate string
		AvgResp     string
		Requests    string
		Errs        string
		Warns       string
		Sent        string
		Received    string
	}
	SummaryRequestDataEntry struct {
		Method   string
		Path     string
		AvgResp  string
		Requests string
		Sent     string
		Received string
	}
	LogSettings struct {
		Traffic        bool            `json:"traffic,omitempty" displayname:"Traffic log" doc-key:"config.settings.logs.traffic"`
		Debug          bool            `json:"debug,omitempty" displayname:"Debug log" doc-key:"config.settings.logs.debug"`
		TrafficMetrics bool            `json:"metrics,omitempty" displayname:"Traffic metrics log" doc-key:"config.settings.logs.metrics"`
		Regression     bool            `json:"regression,omitempty" displayname:"Regression log" doc-key:"config.settings.logs.regression"`
		FileName       synced.Template `json:"filename" displayname:"Log filename" displayelement:"savefile" doc-key:"config.settings.logs.filename"`
		Format         LogFormatType   `json:"format,omitempty" displayname:"Log format" doc-key:"config.settings.logs.format"`
		Summary        SummaryType     `json:"summary,omitempty" displayname:"Summary type" doc-key:"config.settings.logs.summary"`
	}
	OutputsSettings struct {
		Dir string `json:"dir" displayname:"Output directory" doc-key:"config.settings.outputs.dir"`
	}
	Settings struct {
		Timeout         int             `json:"timeout" displayname:"WebSocket timeout" doc-key:"config.settings.timeout"`  
		LogSettings     LogSettings     `json:"logs" doc-key:"config.settings.logs"`
		OutputsSettings OutputsSettings `json:"outputs,omitempty" doc-key:"config.settings.outputs"`
		MaxErrorCount   uint64          `json:"maxerrors,omitempty" doc-key:"config.settings.maxerrors"`
	}
	hookData struct {
		Vars               map[string]interface{}
		Scheduler          map[string]interface{}
		ConnectionSettings *connection.ConnectionSettings
		Counters           *statistics.ExecutionCounters
	}
	Hooks struct {
		Pre  *Hook `json:"preexecute" doc-key:"config.hooks.preexecute" displayname:"Pre-execution Hook"`
		Post *Hook `json:"postexecute" doc-key:"config.hooks.postexecute" displayname:"Post-execution Hook"`
		data hookData
	}
	cfgCore struct {
		Scenario           []scenario.Action             `json:"scenario"`
		Settings           Settings                      `json:"settings"`
		LoginSettings      users.UserGenerator           `json:"loginSettings"`
		ConnectionSettings connection.ConnectionSettings `json:"connectionSettings"`
		Hooks              Hooks                         `json:"hooks"`
	}
	Config struct {
		*cfgCore
		Scheduler scheduler.IScheduler `json:"scheduler"`
		CustomLoggers []*logger.Logger `json:"-"`
		Counters statistics.ExecutionCounters `json:"-"`
		ValidationWarnings []string `json:"-"`
		Options struct {
			AcceptNoScheduler bool
		} `json:"-"`
		Cancel func(msg string) `json:"-"`
	}
	SummaryEntry struct {
		longTitle  string  
		shortTitle string  
		Value      string
		Color      string
	}
)
const (
	ansiReset      = "\x1b[0m"
	ansiStatus     = "\x1b[21;30;47m"
	ansiBoldRed    = "\x1b[1;31m"
	ansiBoldYellow = "\x1b[1;33m"
	ansiBoldBlue   = "\x1b[1;34m"
	ansiBoldWhite  = "\x1b[1;37m"
)
const (
	LogFormatTSVFile LogFormatType = iota
	LogFormatTSVConsole
	LogFormatJSONFile
	LogFormatJSONConsole
	LogFormatColorConsole
	LogFormatTSVFileJSONConsole
	LogFormatNoLogs
	LogFormatOnlyStatus
)
const (
	SummaryTypeDefault SummaryType = iota
	SummaryTypeNone
	SummaryTypeSimple
	SummaryTypeExtended
	SummaryTypeFull
)
var (
	ansiWriter = ansicolor.NewAnsiColorWriter(os.Stdout)
	jsonit     = jsoniter.ConfigCompatibleWithStandardLibrary
)
func (value LogFormatType) GetEnumMap() *enummap.EnumMap {
	logFormatEnum, _ := enummap.NewEnumMap(map[string]int{
		"tsvfile":     int(LogFormatTSVFile),
		"tsvconsole":  int(LogFormatTSVConsole),
		"jsonfile":    int(LogFormatJSONFile),
		"jsonconsole": int(LogFormatJSONConsole),
		"console":     int(LogFormatColorConsole),
		"combined":    int(LogFormatTSVFileJSONConsole),
		"no":          int(LogFormatNoLogs),
		"onlystatus":  int(LogFormatOnlyStatus),
	})
	return logFormatEnum
}
func (value SummaryType) GetEnumMap() *enummap.EnumMap {
	summaryTypeEnum, _ := enummap.NewEnumMap(map[string]int{
		"default":  int(SummaryTypeDefault),  
		"none":     int(SummaryTypeNone),
		"simple":   int(SummaryTypeSimple),
		"extended": int(SummaryTypeExtended),
		"full":     int(SummaryTypeFull),
	})
	return summaryTypeEnum
}
func (value *SummaryType) UnmarshalJSON(arg []byte) error {
	i, err := value.GetEnumMap().UnMarshal(arg)
	if err != nil {
		return errors.WithStack(err)
	}
	*value = SummaryType(i)
	return nil
}
func (value SummaryType) MarshalJSON() ([]byte, error) {
	str, err := value.GetEnumMap().String(int(value))
	return []byte(fmt.Sprintf(`"%s"`, str)), errors.Wrapf(err, "failed to marshal SummaryType<%d>", value)
}
func (format *LogFormatType) UnmarshalJSON(arg []byte) error {
	i, err := format.GetEnumMap().UnMarshal(arg)
	if err != nil {
		return errors.WithStack(err)
	}
	*format = LogFormatType(i)
	return nil
}
func (format LogFormatType) MarshalJSON() ([]byte, error) {
	str, err := format.GetEnumMap().String(int(format))
	return []byte(fmt.Sprintf(`"%s"`, str)), errors.Wrapf(err, "failed to marshal LogFormatType<%d>", format)
}
func NewExampleConfig() (*Config, error) {
	openHub := scenario.Action{}
	openHub.Type = scenario.ActionOpenHub
	openHub.Label = "open hub"
	openHub.Settings = scenario.OpenHubSettings{}
	appSelection, err := session.NewAppSelection(session.AppModeName, "myapp", nil)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	openApp := scenario.Action{
		Settings: scenario.OpenAppSettings{
			AppSelection: *appSelection,
		},
	}
	openApp.Type = scenario.ActionOpenApp
	openApp.Label = "open app"
	think := scenario.Action{
		Settings: scenario.ThinkTimeSettings{
			DistributionSettings: helpers.DistributionSettings{
				Type:      helpers.UniformDistribution,
				Mean:      15.0,
				Deviation: 5.0,
			},
		},
	}
	think.Type = scenario.ActionThinkTime
	think.Label = "think for 10-15s"
	changeSheet := scenario.Action{
		Settings: scenario.ChangeSheetSettings{ID: "QWERTY"},
	}
	changeSheet.Type = scenario.ActionChangeSheet
	changeSheet.Label = "change sheet to analysis sheet"
	selectAction := scenario.Action{
		Settings: scenario.SelectionSettings{
			ID:   "uvxyz",
			Type: scenario.RandomFromEnabled,
			Min:  1,
			Max:  10,
		},
	}
	selectAction.Type = scenario.ActionSelect
	selectAction.Label = "select 1-10 values in object uvxyz"
	logFileName, err := synced.New("scenarioresult.tsv")
	if err != nil {
		return nil, err
	}
	cfg := &Config{
		cfgCore: &cfgCore{
			ConnectionSettings: connection.ConnectionSettings{
				Mode:           connection.WS,
				WsSettings:     nil,
				Server:         "localhost",
				VirtualProxy:   "header",
				Security:       true,
				Allowuntrusted: true,
				Headers: map[string]string{
					"Qlik-User-Header": "{{.UserName}}",
				},
			},
			LoginSettings: users.NewUserGeneratorPrefix("testuser"),
			Settings: Settings{
				Timeout: 300,
				LogSettings: LogSettings{
					FileName: *logFileName,
				},
			},
			Scenario: []scenario.Action{
				openHub,
				think,
				openApp,
				think,
				changeSheet,
				think,
				selectAction,
			},
		},
		Scheduler: &scheduler.SimpleScheduler{
			Scheduler: scheduler.Scheduler{
				SchedType: scheduler.SchedSimple,
				TimeBuf: scheduler.TimeBuffer{
					Mode:     scheduler.TimeBufOnError,
					Duration: helpers.TimeDuration(10 * time.Second),
				},
				InstanceNumber: 1,
			},
			Settings: scheduler.SimpleSchedSettings{
				ExecutionTime:   -1,
				Iterations:      10,
				RampupDelay:     7.0,
				ConcurrentUsers: 10,
				ReuseUsers:      false,
			},
		},
		CustomLoggers:      nil,
		Counters:           statistics.ExecutionCounters{},
		ValidationWarnings: nil,
	}
	return cfg, nil
}
func NewEmptyConfig() (*Config, error) {
	logFileName, err := synced.New("scenarioresult.tsv")
	if err != nil {
		return nil, err
	}
	cfg := &Config{
		cfgCore: &cfgCore{
			ConnectionSettings: connection.ConnectionSettings{
				Mode:           connection.WS,
				WsSettings:     nil,
				Server:         "localhost",
				VirtualProxy:   "",
				Security:       true,
				Allowuntrusted: true,
				Headers:        map[string]string{},
			},
			LoginSettings: users.NewUserGeneratorPrefix("testuser"),
			Settings: Settings{
				Timeout: 300,
				LogSettings: LogSettings{
					FileName: *logFileName,
				},
			},
			Scenario: []scenario.Action{},
		},
		Scheduler: &scheduler.SimpleScheduler{
			Scheduler: scheduler.Scheduler{
				SchedType: scheduler.SchedSimple,
				TimeBuf: scheduler.TimeBuffer{
					Mode:     scheduler.TimeBufOnError,
					Duration: helpers.TimeDuration(10 * time.Second),
				},
				InstanceNumber: 1,
			},
			Settings: scheduler.SimpleSchedSettings{
				ExecutionTime:   -1,
				Iterations:      1,
				RampupDelay:     1,
				ConcurrentUsers: 1,
				ReuseUsers:      false,
			},
		},
		CustomLoggers:      nil,
		Counters:           statistics.ExecutionCounters{},
		ValidationWarnings: nil,
	}
	return cfg, nil
}
func (cfg *Config) UnmarshalJSON(arg []byte) error {
	var core cfgCore
	if err := jsonit.Unmarshal(arg, &core); err != nil {
		return errors.Wrap(err, "Failed unmarshaling config")
	}
	cfg.cfgCore = &core
	if cfg.Settings.LogSettings.Regression {
		cfg.Options.AcceptNoScheduler = true
	}
	rawsched, _, _, err := jsonparser.Get(arg, "scheduler")
	if err != nil {
		if cfg.Options.AcceptNoScheduler {
			return nil
		}
		return errors.Wrap(err, "no scheduler in config")
	}
	cfg.Scheduler, err = scheduler.UnmarshalScheduler(rawsched)
	if err != nil {
		return errors.Wrap(err, "failed unmarhaling scheduler")
	}
	return nil
}
func (cfg *Config) SetTrafficLogging() {
	cfg.Settings.LogSettings.Traffic = true
}
func (cfg *Config) SetTrafficMetricsLogging() {
	cfg.Settings.LogSettings.TrafficMetrics = true
}
func (cfg *Config) SetDebugLogging() {
	cfg.Settings.LogSettings.Debug = true
}
func (cfg *Config) SetRegressionLogging() {
	cfg.Settings.LogSettings.Regression = true
}
func (cfg *Config) validateScheduler() error {
	if cfg.Scheduler == nil {
		if cfg.Options.AcceptNoScheduler {
			return nil
		} else {
			return errors.Errorf("No scheduler defined")
		}
	}
	w, err := cfg.Scheduler.Validate()
	cfg.ValidationWarnings = append(cfg.ValidationWarnings, w...)
	if err != nil {
		return errors.Wrap(err, "Scheduler settings validation failed")
	}
	if cfg.Scheduler.RequireScenario() {
		if cfg.Scenario == nil || len(cfg.Scenario) < 1 {
			return errors.Errorf("No scenario items defined")
		}
	}
	return nil
}
func (cfg *Config) Validate() error {
	cfg.ValidationWarnings = make([]string, 0)
	if err := cfg.validateScheduler(); err != nil {
		return err
	}
	if cfg.LoginSettings.Settings == nil {
		return errors.Errorf("No LoginSettings defined")
	}
	if err := cfg.LoginSettings.Settings.Validate(); err != nil {
		return errors.Wrap(err, "LoginSettings validation failed")
	}
	if cfg.ConnectionSettings.Server == "" {
		return errors.Errorf("Empty server name, server name is required")
	}
	if err := cfg.ConnectionSettings.Validate(); err != nil {
		return errors.Wrap(err, "ConnectionSettings validation failed")
	}
	for _, v := range cfg.Scenario {
		if w, err := v.Validate(); err != nil {
			return errors.WithStack(err)
		} else if len(w) > 0 {
			cfg.ValidationWarnings = append(cfg.ValidationWarnings, w...)
		}
	}
	if w, err := cfg.Hooks.Validate(); err != nil {
		return errors.WithStack(err)
	} else if len(w) > 0 {
		cfg.ValidationWarnings = append(cfg.ValidationWarnings, w...)
	}
	return nil
}
func (cfg *Config) TestConnection(ctx context.Context) error {
	user := cfg.LoginSettings.GetNext(&cfg.Counters)
	cfg.Settings.LogSettings.Format = LogFormatNoLogs
	log, err := setupLogging(ctx, cfg.Settings.LogSettings, cfg.CustomLoggers, nil, nil)
	if err != nil {
		return errors.WithStack(err)
	}
	if log == nil {
		return errors.New("setup logging returned nil logger")
	}
	sessionState := session.New(ctx, "", time.Duration(cfg.Settings.Timeout)*time.Second, user, 1, 1,
		cfg.ConnectionSettings.VirtualProxy, false, &cfg.Counters)
	logEntry := log.NewLogEntry()
	sessionState.SetLogEntry(logEntry)
	sessionState.LogEntry.Session = &logger.SessionEntry{}
	headers, err := cfg.ConnectionSettings.GetHeaders(sessionState, "")
	if err != nil {
		return errors.Wrap(err, "failed to generate authentication headers")
	}
	host, err := cfg.ConnectionSettings.GetHost()
	if err != nil {
		return errors.Wrap(err, "failed to extract hostname")
	}
	sessionState.HeaderJar.SetHeader(host, headers)
	client, err := session.DefaultClient(cfg.ConnectionSettings.Allowuntrusted, sessionState)
	if err != nil {
		return errors.Wrap(err, "failed to set up REST client")
	}
	sessionState.Rest.SetClient(client)
	actionState := &action.State{}
	sessionState.CurrentActionState = actionState
	errs := make([]error, 0)
	for _, connFunc := range scenario.GetConnTestFuncs() {
		if err = connFunc(&cfg.ConnectionSettings, sessionState, actionState); err == nil {
			break
		}
		errs = append(errs, err)
	}
	if len(errs) < len(scenario.GetConnTestFuncs()) {
		return nil
	}
	errorString := fmt.Sprintf("Failed to connect using %d connection test functions.", len(errs))
	var i int
	for i, err = range errs {
		errorString = fmt.Sprintf("%s\n\nError #%d: %s", errorString, i+1, err)
	}
	return errors.New(errorString)
}
func (cfg *Config) Execute(ctx context.Context, templateData interface{}) error {
	timeout := time.Duration(cfg.Settings.Timeout) * time.Second
	log, err := setupLogging(ctx, cfg.Settings.LogSettings, cfg.CustomLoggers, templateData, &cfg.Counters)
	if err != nil {
		return errors.WithStack(err)
	}
	if log == nil {
		return errors.New("setup logging returned nil logger")
	}
	defer func() {
		if err := log.Close(); err != nil {
			_, _ = fmt.Fprintln(os.Stderr, "Error closing log: ", err)
		}
	}()
	entry := logger.NewLogEntry(log)
	entry.LogInfo("GopherciserVersion", version.Version)
	for _, warning := range cfg.ValidationWarnings {
		entry.LogInfo("ScriptValidationWarning", warning)
	}
	script, err := jsonit.Marshal(cfg)
	if err != nil {
		entry.Logf(logger.WarningLevel, "failed to Marshal config for logging: %v", err)
	} else {
		entry.LogInfo("Script", string(script))
	}
	outputsDir, err := setupOutputs(cfg.Settings.OutputsSettings)
	if err != nil {
		return errors.WithStack(err)
	}
	summaryType := cfg.Settings.LogSettings.getSummaryType()
	if err := cfg.SetupStatistics(summaryType); err != nil {
		return errors.WithStack(err)
	}
	defer summary(log, summaryType, time.Now(), &cfg.Counters)
	if cfg.Settings.MaxErrorCount > 0 {
		var once sync.Once
		cfg.Counters.SetMaxErrors(cfg.Settings.MaxErrorCount, func(msg string) {
			once.Do(func() {
				if cfg.Cancel == nil {
					entry.LogError(errors.Errorf("max errors surpassed, but no function to cancel execution is set!"))
					return
				}
				entry.Log(logger.ErrorLevel, msg)
				cfg.Cancel(msg)
			})
		})
	}
	cfg.PopulateHookData()
	if cfg.Hooks.Pre != nil {
		hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.Settings.Timeout*int(time.Second)))
		defer cancel()
		if err := cfg.Hooks.Pre.Execute(hookCtx, entry, &cfg.Hooks.data, cfg.ConnectionSettings.Allowuntrusted); err != nil {
			return err
		}
	}
	if cfg.Hooks.Post != nil {
		defer func() {
			hookCtx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.Settings.Timeout*int(time.Second)))
			defer cancel()
			if err := cfg.Hooks.Post.Execute(hookCtx, entry, &cfg.Hooks.data, cfg.ConnectionSettings.Allowuntrusted); err != nil {
				entry.LogError(err)
			}
		}()
	}
	execErr := cfg.Scheduler.Execute(
		ctx, log, timeout, cfg.Scenario, outputsDir, cfg.LoginSettings, &cfg.ConnectionSettings, &cfg.Counters,
	)
	if execErr != nil {
		return errors.WithStack(execErr)
	}
	return nil
}
func (cfg *Config) PopulateHookData() {
	cfg.Hooks.data.Vars = make(map[string]interface{})
	cfg.Hooks.data.Scheduler = make(map[string]interface{})
	cfg.Scheduler.PopulateHookData(cfg.Hooks.data.Scheduler)
	cfg.Hooks.data.ConnectionSettings = &cfg.ConnectionSettings
	cfg.Hooks.data.Counters = &cfg.Counters
}
func (cfg *Config) SetupStatistics(summary SummaryType) error {
	switch summary {
	case SummaryTypeExtended:
		cfg.Counters.StatisticsCollector = statistics.NewCollector()
		return errors.WithStack(cfg.Counters.StatisticsCollector.SetLevel(statistics.StatsLevelOn))
	case SummaryTypeFull:
		cfg.Counters.StatisticsCollector = statistics.NewCollector()
		return errors.WithStack(cfg.Counters.StatisticsCollector.SetLevel(statistics.StatsLevelFull))
	}
	return nil
}
func (settings *LogSettings) shouldLogStatus() bool {
	if settings == nil {
		return true
	}
	switch settings.Format {
	case LogFormatNoLogs:
		return false
	case LogFormatTSVConsole:
		return false
	case LogFormatJSONConsole:
		return false
	case LogFormatColorConsole:
		return false
	case LogFormatTSVFileJSONConsole:
		return false
	default:
		return true
	}
}
func (settings *LogSettings) getSummaryType() SummaryType {
	switch settings.Summary {
	case SummaryTypeDefault:
		return SummaryTypeSimple
	case SummaryTypeNone:
		return settings.Summary
	case SummaryTypeSimple:
		return settings.Summary
	case SummaryTypeExtended:
		return settings.Summary
	case SummaryTypeFull:
		return settings.Summary
	default:  
		if !settings.shouldLogStatus() {
			return SummaryTypeNone
		}
		return SummaryTypeSimple
	}
}
func (entry *SummaryEntry) Title(summary SummaryType) string {
	switch summary {
	case SummaryTypeSimple:
		return entry.shortTitle
	default:
		return fmt.Sprintf("%-20s", entry.longTitle)
	}
}