From bfa6a0e4cd4ed06b07981f824b2686a43ebe5a9a Mon Sep 17 00:00:00 2001 From: "lilong.129" Date: Wed, 25 Jun 2025 14:40:04 +0800 Subject: [PATCH 01/16] feat: add case.json support for HTML report generation and enhance logging for step execution --- internal/version/VERSION | 2 +- report.go | 285 +++++++++++++++++++++++++++++++++++---- runner.go | 4 +- 3 files changed, 264 insertions(+), 27 deletions(-) diff --git a/internal/version/VERSION b/internal/version/VERSION index 54d05ab6..5b1c7b2a 100644 --- a/internal/version/VERSION +++ b/internal/version/VERSION @@ -1 +1 @@ -v5.0.0-beta-2506251207 +v5.0.0-beta-2506251440 diff --git a/report.go b/report.go index a7aa5fe8..ea5a964e 100644 --- a/report.go +++ b/report.go @@ -41,6 +41,7 @@ type HTMLReportGenerator struct { ReportDir string SummaryContent string // Raw summary.json content for download LogContent string // Raw hrp.log content for download + CaseContent string // Raw case.json content for display } // LogEntry represents a single log entry @@ -72,6 +73,11 @@ func NewHTMLReportGenerator(summaryFile, logFile string) (*HTMLReportGenerator, } } + // Load case.json data if exists + if err := generator.loadCaseData(); err != nil { + log.Warn().Err(err).Msg("failed to load case data, continuing without case display") + } + return generator, nil } @@ -208,7 +214,24 @@ func (g *HTMLReportGenerator) loadLogData() error { return scanner.Err() } -// getStepLogs filters log entries for a specific test step based on step boundaries +// loadCaseData loads test case data from case.json file +func (g *HTMLReportGenerator) loadCaseData() error { + caseFile := filepath.Join(g.ReportDir, "case.json") + if !builtin.FileExists(caseFile) { + return nil // case.json is optional + } + + data, err := os.ReadFile(caseFile) + if err != nil { + return err + } + + // Store the case content for display + g.CaseContent = string(data) + return nil +} + +// getStepLogs filters log entries for a specific test step using prefix matching and time range filtering func (g *HTMLReportGenerator) getStepLogs(stepName string, startTime int64, elapsed int64) []LogEntry { if len(g.LogData) == 0 { return nil @@ -217,8 +240,19 @@ func (g *HTMLReportGenerator) getStepLogs(stepName string, startTime int64, elap var stepLogs []LogEntry var inCurrentStep bool = false - // Simple approach: use step start/end markers for precise boundaries + // Calculate step end time (startTime + elapsed, both in milliseconds) + endTime := startTime + elapsed + + // Convert step times to time.Time for comparison + // The startTime from step result is in milliseconds timestamp + stepStartTime := time.UnixMilli(startTime) + stepEndTime := time.UnixMilli(endTime) + + // Use step start/end markers with prefix matching for precise boundaries for _, logEntry := range g.LogData { + // Parse log entry timestamp for time range validation + logTime, timeParseErr := g.parseLogTime(logEntry.Time) + // Check for step boundaries to control inclusion if logEntry.Message == RUN_STEP_START { if stepFieldValue, exists := logEntry.Fields["step"].(string); exists { @@ -228,7 +262,7 @@ func (g *HTMLReportGenerator) getStepLogs(stepName string, startTime int64, elap stepLogs = append(stepLogs, logEntry) continue } else if inCurrentStep { - // This is a different step starting, we're done + // This is a different step starting, we're done with current step break } } @@ -240,31 +274,30 @@ func (g *HTMLReportGenerator) getStepLogs(stepName string, startTime int64, elap if strings.HasPrefix(stepName, stepFieldValue) { stepLogs = append(stepLogs, logEntry) inCurrentStep = false - continue + break // End of current step, stop processing } } } - // Only include logs when we're in the current step + // Include logs when we're in the current step AND within the time range if inCurrentStep { - stepLogs = append(stepLogs, logEntry) + // Apply time range filtering if time parsing succeeded + if timeParseErr == nil { + // Only include logs within the step time range + if (logTime.Equal(stepStartTime) || logTime.After(stepStartTime)) && + (logTime.Equal(stepEndTime) || logTime.Before(stepEndTime)) { + stepLogs = append(stepLogs, logEntry) + } + } else { + // If time parsing failed, include all logs in the step boundary + stepLogs = append(stepLogs, logEntry) + } } } - // Sort logs by time, then by original index for stable ordering + // Sort logs by original index to maintain chronological order sort.Slice(stepLogs, func(i, j int) bool { - timeI, errI := g.parseLogTime(stepLogs[i].Time) - timeJ, errJ := g.parseLogTime(stepLogs[j].Time) - - if errI != nil || errJ != nil { - return stepLogs[i].LogIndex < stepLogs[j].LogIndex - } - - if timeI.Equal(timeJ) { - // For same timestamps, use original log index to maintain order - return stepLogs[i].LogIndex < stepLogs[j].LogIndex - } - return timeI.Before(timeJ) + return stepLogs[i].LogIndex < stepLogs[j].LogIndex }) return stepLogs @@ -514,6 +547,9 @@ func (g *HTMLReportGenerator) GenerateReport(outputFile string) error { "getLogContentBase64": func() string { return base64.StdEncoding.EncodeToString([]byte(g.LogContent)) }, + "getCaseContentBase64": func() string { + return base64.StdEncoding.EncodeToString([]byte(g.CaseContent)) + }, "safeHTML": func(s string) template.HTML { return template.HTML(s) }, @@ -1908,6 +1944,105 @@ const htmlTemplate = ` object-fit: contain; } + .json-modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.8); + overflow: auto; + } + + .json-modal-content { + background-color: #fefefe; + margin: 2% auto; + padding: 0; + border: none; + border-radius: 12px; + width: 90%; + max-width: 1000px; + max-height: 90%; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + display: flex; + flex-direction: column; + } + + .json-modal-header { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + padding: 20px 30px; + border-radius: 12px 12px 0 0; + display: flex; + justify-content: space-between; + align-items: center; + } + + .json-modal-title { + font-size: 1.5em; + font-weight: 600; + margin: 0; + } + + .json-modal-body { + padding: 0; + flex: 1; + overflow: hidden; + display: flex; + flex-direction: column; + } + + .json-content { + background: #f8f9fa; + margin: 0; + padding: 20px; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; + font-size: 13px; + line-height: 1.5; + color: #333; + overflow: auto; + flex: 1; + white-space: pre; + border-radius: 0 0 12px 12px; + } + + .json-toolbar { + background: #e9ecef; + padding: 10px 20px; + border-top: 1px solid #dee2e6; + display: flex; + justify-content: space-between; + align-items: center; + } + + .json-toolbar button { + background: #007bff; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 0.9em; + transition: background-color 0.3s; + } + + .json-toolbar button:hover { + background: #0056b3; + } + + .json-toolbar .copy-status { + font-size: 0.9em; + color: #28a745; + opacity: 0; + transition: opacity 0.3s; + } + + .json-toolbar .copy-status.show { + opacity: 1; + } + .close { position: absolute; top: 15px; @@ -1919,6 +2054,22 @@ const htmlTemplate = ` cursor: pointer; } + .json-close { + color: white; + font-size: 30px; + font-weight: bold; + cursor: pointer; + transition: color 0.3s; + background: none; + border: none; + padding: 0; + line-height: 1; + } + + .json-close:hover { + color: #ffcccc; + } + .close:hover, .close:focus { color: #bbb; @@ -2187,7 +2338,7 @@ const htmlTemplate = `
-
📥 Download
+
📥 Download & View
+ {{if getCaseContentBase64}} + + {{end}}
@@ -2714,10 +2871,31 @@ const htmlTemplate = ` + +
+
+
+

📋 Test Case JSON

+ +
+
+
+
+ + +
+ ✅ Copied! +
+

+            
+
+
+