fix: optimize report command to avoid creating timestamp directories

- Implement lazy loading for directory creation in config.go
- Add logFile parameter to InitLogger for better control
- Use dynamic directory existence check instead of flags
- Report command now uses console-only logging to prevent directory creation
- Support both JSON and colorized console output formats
- Maintain backward compatibility for all other commands

Changes:
- config.go: Convert directory paths to getter methods with lazy creation
- logger.go: Add logFile parameter and improve logging control
- cmd/root.go: Detect report command and disable file logging
- uixt/*: Update all references to use new getter methods

Fixes the issue where 'hrp report results/' would create unwanted timestamp directories
This commit is contained in:
lilong.129
2025-06-10 12:06:08 +08:00
parent 6588d95154
commit f5f6d177ab
9 changed files with 84 additions and 36 deletions
+3 -1
View File
@@ -46,7 +46,9 @@ GitHub: https://github.com/httprunner/httprunner
Copyright © 2017-present debugtalk. Apache-2.0 License.`, Copyright © 2017-present debugtalk. Apache-2.0 License.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
hrp.InitLogger(logLevel, logJSON) // For report command, don't create log files to avoid creating directories
enableLogFile := cmd.Name() != "report"
hrp.InitLogger(logLevel, logJSON, enableLogFile)
}, },
Version: version.GetVersionInfo(), Version: version.GetVersionInfo(),
TraverseChildren: true, // parses flags on all parents before executing child command TraverseChildren: true, // parses flags on all parents before executing child command
+51 -17
View File
@@ -21,12 +21,13 @@ const (
type Config struct { type Config struct {
RootDir string RootDir string
ResultsDir string ResultsDir string
ResultsPath string resultsPath string
DownloadsPath string downloadsPath string
ScreenShotsPath string screenShotsPath string
StartTime time.Time StartTime time.Time
ActionLogFilePath string ActionLogFilePath string
DeviceActionLogFilePath string DeviceActionLogFilePath string
mu sync.Mutex
} }
var ( var (
@@ -48,25 +49,58 @@ func GetConfig() *Config {
startTimeStr := cfg.StartTime.Format("20060102150405") startTimeStr := cfg.StartTime.Format("20060102150405")
cfg.ResultsDir = filepath.Join(ResultsDirName, startTimeStr) cfg.ResultsDir = filepath.Join(ResultsDirName, startTimeStr)
cfg.ResultsPath = filepath.Join(cfg.RootDir, cfg.ResultsDir) cfg.resultsPath = filepath.Join(cfg.RootDir, cfg.ResultsDir)
cfg.DownloadsPath = filepath.Join(cfg.RootDir, filepath.Join(DownloadsDirName, startTimeStr)) cfg.downloadsPath = filepath.Join(cfg.RootDir, filepath.Join(DownloadsDirName, startTimeStr))
cfg.ScreenShotsPath = filepath.Join(cfg.ResultsPath, ScreenshotsDirName) cfg.screenShotsPath = filepath.Join(cfg.resultsPath, ScreenshotsDirName)
cfg.ActionLogFilePath = filepath.Join(cfg.ResultsDir, ActionLogDirName) cfg.ActionLogFilePath = filepath.Join(cfg.ResultsDir, ActionLogDirName)
cfg.DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor" cfg.DeviceActionLogFilePath = "/sdcard/Android/data/io.appium.uiautomator2.server/files/hodor"
// create results directory
if err := builtin.EnsureFolderExists(cfg.ResultsPath); err != nil {
log.Fatal().Err(err).Msg("create results directory failed")
}
if err := builtin.EnsureFolderExists(cfg.DownloadsPath); err != nil {
log.Fatal().Err(err).Msg("create downloads directory failed")
}
if err := builtin.EnsureFolderExists(cfg.ScreenShotsPath); err != nil {
log.Fatal().Err(err).Msg("create screenshots directory failed")
}
globalConfig = cfg globalConfig = cfg
}) })
return globalConfig return globalConfig
} }
// ResultsPath returns the results path and creates the directory if it doesn't exist
func (c *Config) ResultsPath() string {
c.mu.Lock()
defer c.mu.Unlock()
// Check if directory exists, create if it doesn't
if _, err := os.Stat(c.resultsPath); os.IsNotExist(err) {
if err := builtin.EnsureFolderExists(c.resultsPath); err != nil {
log.Error().Err(err).Str("path", c.resultsPath).Msg("failed to create results directory")
} else {
log.Info().Str("path", c.resultsPath).Msg("create folder")
}
}
return c.resultsPath
}
// DownloadsPath returns the downloads path and creates the directory if it doesn't exist
func (c *Config) DownloadsPath() string {
c.mu.Lock()
defer c.mu.Unlock()
// Check if directory exists, create if it doesn't
if _, err := os.Stat(c.downloadsPath); os.IsNotExist(err) {
if err := builtin.EnsureFolderExists(c.downloadsPath); err != nil {
log.Error().Err(err).Str("path", c.downloadsPath).Msg("failed to create downloads directory")
}
}
return c.downloadsPath
}
// ScreenShotsPath returns the screenshots path and creates the directory if it doesn't exist
func (c *Config) ScreenShotsPath() string {
c.mu.Lock()
defer c.mu.Unlock()
// Check if directory exists, create if it doesn't
if _, err := os.Stat(c.screenShotsPath); os.IsNotExist(err) {
if err := builtin.EnsureFolderExists(c.screenShotsPath); err != nil {
log.Error().Err(err).Str("path", c.screenShotsPath).Msg("failed to create screenshots directory")
}
}
return c.screenShotsPath
}
+1 -1
View File
@@ -1 +1 @@
v5.0.0-beta-2506101103 v5.0.0-beta-2506101206
+23 -11
View File
@@ -15,7 +15,7 @@ import (
"github.com/httprunner/httprunner/v5/internal/config" "github.com/httprunner/httprunner/v5/internal/config"
) )
func InitLogger(logLevel string, logJSON bool) { func InitLogger(logLevel string, logJSON bool, logFile bool) {
// Error Logging with Stacktrace // Error Logging with Stacktrace
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
@@ -39,40 +39,52 @@ func InitLogger(logLevel string, logJSON bool) {
TimeFormat: time.RFC3339Nano, TimeFormat: time.RFC3339Nano,
NoColor: noColor, NoColor: noColor,
} }
msg = "log with colorized console and file output" if logFile {
msg = "log with colorized console and file output"
} else {
msg = "log with colorized console output only"
}
} else { } else {
// default logger // default logger
consoleWriter = os.Stderr consoleWriter = os.Stderr
msg = "log with json console and file output" if logFile {
msg = "log with json console and file output"
} else {
msg = "log with json console output only"
}
} }
// parse console log level // parse console log level
consoleLevel := parseLogLevel(logLevel) consoleLevel := parseLogLevel(logLevel)
// If logFile is false, use console-only logger
if !logFile {
log.Logger = zerolog.New(consoleWriter).With().Timestamp().Logger().Level(consoleLevel)
log.Info().Msg(msg)
return
}
// file writer - write to results/taskID/hrp.log // file writer - write to results/taskID/hrp.log
cfg := config.GetConfig() cfg := config.GetConfig()
logFilePath := filepath.Join(cfg.ResultsPath, "hrp.log") logFilePath := filepath.Join(cfg.ResultsPath(), "hrp.log")
// create or open log file // create or open log file
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666) logFileWriter, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o666)
if err != nil { if err != nil {
// if file creation failed, use console logger only // if file creation failed, use console logger only
log.Logger = zerolog.New(consoleWriter).With().Timestamp().Logger().Level(consoleLevel) log.Logger = zerolog.New(consoleWriter).With().Timestamp().Logger().Level(consoleLevel)
log.Error().Err(err).Str("logFilePath", logFilePath).Msg("create log file failed") log.Error().Err(err).Str("logFilePath", logFilePath).Msg(msg)
} else { } else {
// create a custom writer that applies different log levels // create a custom writer that applies different log levels
multiWriter := &leveledMultiWriter{ multiWriter := &leveledMultiWriter{
consoleWriter: consoleWriter, consoleWriter: consoleWriter,
consoleLevel: consoleLevel, consoleLevel: consoleLevel,
fileWriter: logFile, fileWriter: logFileWriter,
fileLevel: zerolog.DebugLevel, fileLevel: zerolog.DebugLevel,
} }
log.Logger = zerolog.New(multiWriter).With().Timestamp().Logger() log.Logger = zerolog.New(multiWriter).With().Timestamp().Logger()
log.Info().Str("logFilePath", logFilePath).Msg("log file created successfully") log.Info().Str("logFilePath", logFilePath).Msg(msg)
} }
log.Info().Msg(msg)
log.Info().Str("console_log_level", strings.ToUpper(logLevel)).Str("file_log_level", "DEBUG").Msg("logger initialized with different levels")
} }
// parseLogLevel converts string log level to zerolog.Level // parseLogLevel converts string log level to zerolog.Level
+1 -1
View File
@@ -807,7 +807,7 @@ func (ad *ADBDriver) ScreenRecord(opts ...option.ActionOption) (videoPath string
filePath = options.ScreenRecordPath filePath = options.ScreenRecordPath
} else { } else {
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000) timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
filePath = filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) filePath = filepath.Join(config.GetConfig().ScreenShotsPath(), fmt.Sprintf("%s.mp4", timestamp))
} }
var ctx context.Context var ctx context.Context
+2 -2
View File
@@ -70,7 +70,7 @@ func (dExt *XTDriver) GetScreenResult(opts ...option.ActionOption) (screenResult
fileName = builtin.GenNameWithTimestamp("%d_screenshot") fileName = builtin.GenNameWithTimestamp("%d_screenshot")
} }
imagePath := filepath.Join( imagePath := filepath.Join(
config.GetConfig().ScreenShotsPath, config.GetConfig().ScreenShotsPath(),
fmt.Sprintf("%s.%s", fileName, "jpeg"), fmt.Sprintf("%s.%s", fileName, "jpeg"),
) )
go func() { go func() {
@@ -436,7 +436,7 @@ func MarkUIOperation(driver IDriver, actionType option.ActionName, actionCoordin
// create screenshot save path // create screenshot save path
timestamp := builtin.GenNameWithTimestamp("%d") timestamp := builtin.GenNameWithTimestamp("%d")
imagePath := filepath.Join( imagePath := filepath.Join(
config.GetConfig().ScreenShotsPath, config.GetConfig().ScreenShotsPath(),
fmt.Sprintf("action_%s_pre_%s.png", timestamp, actionType), fmt.Sprintf("action_%s_pre_%s.png", timestamp, actionType),
) )
+1 -1
View File
@@ -141,7 +141,7 @@ func postHandler(driver IDriver, actionType option.ActionName, options *option.A
// save compressed screenshot to file // save compressed screenshot to file
timestamp := builtin.GenNameWithTimestamp("%d") timestamp := builtin.GenNameWithTimestamp("%d")
imagePath := filepath.Join( imagePath := filepath.Join(
config.GetConfig().ScreenShotsPath, config.GetConfig().ScreenShotsPath(),
fmt.Sprintf("action_%s_post_%s.png", timestamp, actionType), fmt.Sprintf("action_%s_post_%s.png", timestamp, actionType),
) )
+1 -1
View File
@@ -305,7 +305,7 @@ var (
func DownloadFileByUrl(fileUrl string) (filePath string, err error) { func DownloadFileByUrl(fileUrl string) (filePath string, err error) {
hash := md5.Sum([]byte(fileUrl)) hash := md5.Sum([]byte(fileUrl))
fileName := fmt.Sprintf("%x", hash) fileName := fmt.Sprintf("%x", hash)
filePath = filepath.Join(config.GetConfig().DownloadsPath, fileName) filePath = filepath.Join(config.GetConfig().DownloadsPath(), fileName)
// get or create file lock // get or create file lock
lockI, _ := fileLocks.LoadOrStore(filePath, &sync.Mutex{}) lockI, _ := fileLocks.LoadOrStore(filePath, &sync.Mutex{})
+1 -1
View File
@@ -910,7 +910,7 @@ func (wd *WDADriver) triggerWDALog(data map[string]interface{}) (rawResp []byte,
func (wd *WDADriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) { func (wd *WDADriver) ScreenRecord(opts ...option.ActionOption) (videoPath string, err error) {
log.Info().Msg("WDADriver.ScreenRecord") log.Info().Msg("WDADriver.ScreenRecord")
timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000) timestamp := time.Now().Format("20060102_150405") + fmt.Sprintf("_%03d", time.Now().UnixNano()/1e6%1000)
fileName := filepath.Join(config.GetConfig().ScreenShotsPath, fmt.Sprintf("%s.mp4", timestamp)) fileName := filepath.Join(config.GetConfig().ScreenShotsPath(), fmt.Sprintf("%s.mp4", timestamp))
options := option.NewActionOptions(opts...) options := option.NewActionOptions(opts...)
duration := time.Duration(options.Duration * float64(time.Second)) duration := time.Duration(options.Duration * float64(time.Second))