mirror of
https://github.com/httprunner/httprunner.git
synced 2026-05-06 20:32:44 +08:00
feat: compress image data for html report
This commit is contained in:
@@ -1 +1 @@
|
||||
v5.0.0-beta-2506082208
|
||||
v5.0.0-beta-2506082348
|
||||
|
||||
38
report.go
38
report.go
@@ -12,6 +12,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/httprunner/httprunner/v5/internal/builtin"
|
||||
"github.com/httprunner/httprunner/v5/uixt"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -195,7 +196,7 @@ func (g *HTMLReportGenerator) parseLogTime(timeStr string) (time.Time, error) {
|
||||
return time.Time{}, fmt.Errorf("unable to parse time: %s", timeStr)
|
||||
}
|
||||
|
||||
// encodeImageToBase64 encodes an image file to base64 string
|
||||
// encodeImageToBase64 encodes an image file to base64 string with compression
|
||||
func (g *HTMLReportGenerator) encodeImageToBase64(imagePath string) string {
|
||||
// Convert relative path to absolute path
|
||||
if !filepath.IsAbs(imagePath) {
|
||||
@@ -207,13 +208,21 @@ func (g *HTMLReportGenerator) encodeImageToBase64(imagePath string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(imagePath)
|
||||
// Read and compress the image using the unified compression function
|
||||
// Enable resize with max width 800px for HTML reports
|
||||
compressedData, err := uixt.CompressImageFile(imagePath, true, 800)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("path", imagePath).Msg("failed to read image file")
|
||||
return ""
|
||||
log.Warn().Err(err).Str("path", imagePath).Msg("failed to compress image, using original")
|
||||
// Fallback to original image if compression fails
|
||||
data, readErr := os.ReadFile(imagePath)
|
||||
if readErr != nil {
|
||||
log.Warn().Err(readErr).Str("path", imagePath).Msg("failed to read image file")
|
||||
return ""
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
return base64.StdEncoding.EncodeToString(compressedData)
|
||||
}
|
||||
|
||||
// formatDuration formats duration from milliseconds to human readable format
|
||||
@@ -1612,14 +1621,14 @@ const htmlTemplate = `<!DOCTYPE html>
|
||||
|
||||
<div class="sub-action-content">
|
||||
<div class="sub-action-left">
|
||||
{{if $subAction.Arguments}}
|
||||
<div class="arguments">Arguments: {{safeHTML (toJSON $subAction.Arguments)}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if $subAction.Thought}}
|
||||
<div class="thought">{{$subAction.Thought}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if $subAction.Arguments}}
|
||||
<div class="arguments">Arguments: {{safeHTML (toJSON $subAction.Arguments)}}</div>
|
||||
{{end}}
|
||||
|
||||
{{if $subAction.ModelName}}
|
||||
<div class="model-name-container">
|
||||
<span class="model-label">🤖 Model:</span>
|
||||
@@ -1711,10 +1720,13 @@ const htmlTemplate = `<!DOCTYPE html>
|
||||
{{end}}
|
||||
|
||||
<!-- Screenshots -->
|
||||
{{if $step.Attachments}}{{if $step.Attachments.ScreenResults}}
|
||||
{{if $step.Attachments}}
|
||||
{{$attachments := $step.Attachments}}
|
||||
{{if eq (printf "%T" $attachments) "map[string]interface {}"}}
|
||||
{{if index $attachments "screen_results"}}
|
||||
<div class="screenshots-section">
|
||||
<h4>Screenshots</h4>
|
||||
{{range $screenshot := $step.Attachments.ScreenResults}}
|
||||
{{range $screenshot := index $attachments "screen_results"}}
|
||||
{{$base64Image := encodeImageBase64 $screenshot.ImagePath}}
|
||||
{{if $base64Image}}
|
||||
<div class="screenshot-item">
|
||||
@@ -1731,7 +1743,9 @@ const htmlTemplate = `<!DOCTYPE html>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
<!-- Step Logs -->
|
||||
{{$stepLogs := getStepLogs $step}}
|
||||
|
||||
@@ -90,16 +90,14 @@ func TestStartToGoal(t *testing.T) {
|
||||
- 连接线可以水平或垂直,但不能斜线,也不能跨过其他图案。
|
||||
- 连接线的转折次数不能超过两次。
|
||||
3. 游戏界面:
|
||||
- 游戏界面通常是一个矩形区域,内含多个图案或图标,排列成行和列。
|
||||
- 图案或图标在未选中状态下背景为白色,选中状态下背景为绿色。
|
||||
4. 重试机制:
|
||||
- 游戏失败后,可以点击「立即复活」按钮,观看视频广告;30秒,点击屏幕右上角关闭图标后可继续游戏。
|
||||
- 若无法再复活,可以点击「立即挑战」按钮,重新开始游戏。
|
||||
|
||||
注意事项:
|
||||
1、当连接错误时,顶部的红心会减少一个,需及时调整策略,避免红心变为0个后游戏失败
|
||||
2、不要连续 2 次点击同一个图案
|
||||
3、不要犯重复的错误
|
||||
- 游戏界面是一个矩形区域,内含多个图案或图标,排列成行和列;图案或图标在未选中状态下背景为白色,选中状态下背景为绿色。
|
||||
- 游戏界面下方是道具区域,共有 3 种道具,从左到右分别是:「高亮显示」、「随机打乱」、「减少种类」。
|
||||
4、游戏攻略:建议多次使用道具,可以降低游戏难度
|
||||
- 优先使用「减少种类」道具,可以将图案种类随机减少一种
|
||||
- 遇到困难时,推荐使用「随机打乱」道具,可以获得很多新的消除机会
|
||||
- 观看广告视频,待屏幕右上角出现「领取成功」后,点击其右侧的 X 即可关闭广告,继续游戏
|
||||
5、结束游戏
|
||||
- 游戏失败,且无法再「立即复活」后,游戏结束,停止游戏
|
||||
|
||||
请严格按照以上游戏规则,开始游戏
|
||||
`
|
||||
@@ -121,8 +119,8 @@ func TestStartToGoal(t *testing.T) {
|
||||
err := testCase.Dump2JSON("start_llk_game.json")
|
||||
require.Nil(t, err)
|
||||
|
||||
err = hrp.NewRunner(t).Run(testCase)
|
||||
assert.Nil(t, err)
|
||||
// err = hrp.NewRunner(t).Run(testCase)
|
||||
// assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestAIAction(t *testing.T) {
|
||||
|
||||
@@ -81,7 +81,6 @@ Target: User will give you a screenshot, an instruction and some previous logs i
|
||||
|
||||
Restriction:
|
||||
- Don't give extra actions or plans beyond the instruction. ONLY plan for what the instruction requires. For example, don't try to submit the form if the instruction is only to fill something.
|
||||
- Always give ONLY ONE action in ` + "`log`" + ` field (or null if no action should be done), instead of multiple actions. Supported actions are click, long_press, type, scroll, drag, press_home, press_back, wait, finished.
|
||||
- Don't repeat actions in the previous logs.
|
||||
- Bbox is the bounding box of the element to be located. It's an array of 4 numbers, representing [x1, y1, x2, y2] coordinates in 1000x1000 relative coordinates system.
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ func getScreenShotBuffer(driver IDriver) (compressedBufSource *bytes.Buffer, err
|
||||
}
|
||||
|
||||
// compress screenshot
|
||||
compressBufSource, err := compressImageBuffer(bufSource)
|
||||
compressBufSource, err := compressImageBufferWithOptions(bufSource, false, 800)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(code.DeviceScreenShotError,
|
||||
"compress screenshot failed %v", err)
|
||||
@@ -291,7 +291,8 @@ func saveScreenShot(raw *bytes.Buffer, screenshotPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error) {
|
||||
// compressImageBufferWithOptions compresses image buffer with advanced options
|
||||
func compressImageBufferWithOptions(raw *bytes.Buffer, enableResize bool, maxWidth int) (compressed *bytes.Buffer, err error) {
|
||||
rawSize := raw.Len()
|
||||
// decode image from buffer
|
||||
img, format, err := image.Decode(raw)
|
||||
@@ -299,28 +300,126 @@ func compressImageBuffer(raw *bytes.Buffer) (compressed *bytes.Buffer, err error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
// Get original image dimensions
|
||||
bounds := img.Bounds()
|
||||
originalWidth := bounds.Dx()
|
||||
originalHeight := bounds.Dy()
|
||||
|
||||
switch format {
|
||||
// compress image
|
||||
case "jpeg", "png":
|
||||
jpegOptions := &jpeg.Options{Quality: 60}
|
||||
err = jpeg.Encode(&buf, img, jpegOptions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Calculate new dimensions for compression if resize is enabled
|
||||
var newWidth, newHeight int
|
||||
var resizedImg image.Image = img
|
||||
|
||||
if enableResize && originalWidth > maxWidth {
|
||||
ratio := float64(maxWidth) / float64(originalWidth)
|
||||
newWidth = maxWidth
|
||||
newHeight = int(float64(originalHeight) * ratio)
|
||||
resizedImg = resizeImage(img, newWidth, newHeight)
|
||||
} else {
|
||||
newWidth = originalWidth
|
||||
newHeight = originalHeight
|
||||
}
|
||||
|
||||
// Determine JPEG quality based on image size for optimal compression
|
||||
jpegQuality := 60 // Default quality for better compression
|
||||
if newWidth*newHeight > 500000 { // For very large images, use lower quality
|
||||
jpegQuality = 50
|
||||
} else if newWidth*newHeight < 100000 { // For small images, use higher quality
|
||||
jpegQuality = 70
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
switch strings.ToLower(format) {
|
||||
case "jpeg", "jpg":
|
||||
// Use adaptive JPEG compression quality
|
||||
err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: jpegQuality})
|
||||
case "png":
|
||||
// Convert PNG to JPEG for better compression
|
||||
err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: jpegQuality})
|
||||
case "gif":
|
||||
// Keep GIF format but with reduced colors for better compression
|
||||
err = gif.Encode(&buf, resizedImg, &gif.Options{NumColors: 64})
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported image format: %s", format)
|
||||
// Default to JPEG for unknown formats
|
||||
err = jpeg.Encode(&buf, resizedImg, &jpeg.Options{Quality: jpegQuality})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
compressedSize := buf.Len()
|
||||
log.Debug().Int("rawSize", rawSize).Int("compressedSize", compressedSize).
|
||||
log.Debug().
|
||||
Int("rawSize", rawSize).
|
||||
Int("originalWidth", originalWidth).
|
||||
Int("originalHeight", originalHeight).
|
||||
Int("newWidth", newWidth).
|
||||
Int("newHeight", newHeight).
|
||||
Int("jpegQuality", jpegQuality).
|
||||
Int("compressedSize", compressedSize).
|
||||
Bool("resized", enableResize && originalWidth > maxWidth).
|
||||
Msg("compress image buffer")
|
||||
|
||||
// return compressed image buffer
|
||||
return &buf, nil
|
||||
}
|
||||
|
||||
// resizeImage resizes an image using simple nearest neighbor algorithm
|
||||
func resizeImage(src image.Image, width, height int) image.Image {
|
||||
srcBounds := src.Bounds()
|
||||
srcWidth := srcBounds.Dx()
|
||||
srcHeight := srcBounds.Dy()
|
||||
|
||||
// Create a new image with the target dimensions
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
|
||||
// Simple nearest neighbor resizing
|
||||
for y := 0; y < height; y++ {
|
||||
for x := 0; x < width; x++ {
|
||||
// Map destination coordinates to source coordinates
|
||||
srcX := x * srcWidth / width
|
||||
srcY := y * srcHeight / height
|
||||
|
||||
// Ensure we don't go out of bounds
|
||||
if srcX >= srcWidth {
|
||||
srcX = srcWidth - 1
|
||||
}
|
||||
if srcY >= srcHeight {
|
||||
srcY = srcHeight - 1
|
||||
}
|
||||
|
||||
// Copy pixel from source to destination
|
||||
dst.Set(x, y, src.At(srcBounds.Min.X+srcX, srcBounds.Min.Y+srcY))
|
||||
}
|
||||
}
|
||||
|
||||
return dst
|
||||
}
|
||||
|
||||
// CompressImageFile compresses an image file and returns the compressed data
|
||||
func CompressImageFile(imagePath string, enableResize bool, maxWidth int) ([]byte, error) {
|
||||
// Read the original image file
|
||||
file, err := os.Open(imagePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open image file: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Read file content into buffer
|
||||
var buf bytes.Buffer
|
||||
_, err = buf.ReadFrom(file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read image file: %w", err)
|
||||
}
|
||||
|
||||
// Compress using the buffer compression function
|
||||
compressedBuf, err := compressImageBufferWithOptions(&buf, enableResize, maxWidth)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to compress image: %w", err)
|
||||
}
|
||||
|
||||
return compressedBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
// MarkUIOperation add operation mark for UI operation
|
||||
func MarkUIOperation(driver IDriver, actionType option.ActionName, actionCoordinates []float64) error {
|
||||
if actionType == "" || len(actionCoordinates) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user