| | |
| | | from typing import Dict |
| | | from django.conf import settings |
| | | import time |
| | | from datetime import datetime |
| | | |
| | | |
| | | def parse_message(summary: Dict, **kwargs): |
| | |
| | | pass_count = summary["stat"]["successes"] |
| | | fail_count = summary["stat"]["failures"] |
| | | error_count = summary["stat"]["errors"] |
| | | skipped_count = summary["stat"].get("skipped", 0) |
| | | expected_failures_count = summary["stat"].get("expectedFailures", 0) |
| | | unexpected_successes_count = summary["stat"].get("unexpectedSuccesses", 0) |
| | | duration = "%.2fs" % summary["time"]["duration"] |
| | | report_id = summary["report_id"] |
| | | base_url = settings.IM_REPORT_SETTING.get("base_url") |
| | | port = settings.IM_REPORT_SETTING.get("port") |
| | | report_url = f"{base_url}:{port}/api/lunarlink/reports/{report_id}" |
| | | executed = rows_count |
| | | fail_rate = "{:.2%}".format(fail_count / executed) |
| | | fail_rate = "{:.2%}".format(fail_count / executed) if executed > 0 else "0.00%" |
| | | success_rate = "{:.2%}".format(pass_count / executed) if executed > 0 else "0.00%" |
| | | case_count = kwargs.get("case_count") |
| | | creator = kwargs.get("creator", "") |
| | | updater = kwargs.get("updater", "") |
| | | |
| | | start_at = summary.get("time", {}).get("start_at", 0) |
| | | if start_at: |
| | | start_time = datetime.fromtimestamp(start_at).strftime("%Y-%m-%d %H:%M:%S") |
| | | end_time = datetime.fromtimestamp(start_at + summary["time"]["duration"]).strftime("%Y-%m-%d %H:%M:%S") |
| | | else: |
| | | start_time = "" |
| | | end_time = "" |
| | | |
| | | setup_hooks_duration = summary.get("time", {}).get("setup_hooks_duration", 0) |
| | | teardown_hooks_duration = summary.get("time", {}).get("teardown_hooks_duration", 0) |
| | | |
| | | platform_info = summary.get("platform", {}) |
| | | python_version = platform_info.get("python_version", "") |
| | | httprunner_version = platform_info.get("httprunner_version", "") |
| | | |
| | | avg_duration = 0 |
| | | if executed > 0: |
| | | avg_duration = summary["time"]["duration"] / executed |
| | | |
| | | failed_cases = [] |
| | | error_cases = [] |
| | | |
| | | details = summary.get("details", []) |
| | | for detail in details: |
| | | records = detail.get("records", []) |
| | | for record in records: |
| | | if record.get("status") in ["failure", "error"]: |
| | | case_name = record.get("name", "未知用例") |
| | | case_status = record.get("status", "") |
| | | meta_data = record.get("meta_data", {}) |
| | | case_url = meta_data.get("request", {}).get("url", "") |
| | | case_method = meta_data.get("request", {}).get("method", "") |
| | | case_error = record.get("attachment", "") |
| | | |
| | | if case_error: |
| | | case_error_lines = case_error.split('\n') |
| | | if len(case_error_lines) > 1: |
| | | case_error = case_error_lines[0] |
| | | if len(case_error_lines) > 2: |
| | | case_error += f"\n... (共{len(case_error_lines)}行错误信息)" |
| | | case_error = case_error.strip() |
| | | else: |
| | | case_error = "无错误信息" |
| | | |
| | | case_info = { |
| | | "name": case_name, |
| | | "url": case_url, |
| | | "method": case_method, |
| | | "error": case_error[:150] if case_error else "无错误信息" |
| | | } |
| | | |
| | | if case_status == "failure": |
| | | failed_cases.append(case_info) |
| | | elif case_status == "error": |
| | | error_cases.append(case_info) |
| | | |
| | | failed_cases = failed_cases[:10] |
| | | error_cases = error_cases[:10] |
| | | |
| | | return { |
| | | "task_name": task_name, |
| | |
| | | "error_count": error_count, |
| | | "fail_count": fail_count, |
| | | "fail_rate": fail_rate, |
| | | "success_rate": success_rate, |
| | | "report_url": report_url, |
| | | "creator": creator, |
| | | "updater": updater, |
| | | "start_time": start_time, |
| | | "end_time": end_time, |
| | | "skipped_count": skipped_count, |
| | | "expected_failures_count": expected_failures_count, |
| | | "unexpected_successes_count": unexpected_successes_count, |
| | | "setup_hooks_duration": setup_hooks_duration, |
| | | "teardown_hooks_duration": teardown_hooks_duration, |
| | | "python_version": python_version, |
| | | "httprunner_version": httprunner_version, |
| | | "avg_duration": avg_duration, |
| | | "failed_cases": failed_cases, |
| | | "error_cases": error_cases, |
| | | } |
| | | |
| | | |
| | |
| | | fail_count, |
| | | fail_rate, |
| | | report_url, |
| | | success_rate, |
| | | creator, |
| | | updater, |
| | | start_time, |
| | | failed_cases, |
| | | error_cases, |
| | | end_time, |
| | | skipped_count, |
| | | expected_failures_count, |
| | | unexpected_successes_count, |
| | | setup_hooks_duration, |
| | | teardown_hooks_duration, |
| | | python_version, |
| | | httprunner_version, |
| | | avg_duration, |
| | | ): |
| | | """ |
| | | 定制邮件报告消息模板(高级优化版) |
| | |
| | | :param fail_count: 失败接口个数 |
| | | :param fail_rate: 失败比例 |
| | | :param report_url: 报告链接 |
| | | :param success_rate: 成功率 |
| | | :param creator: 创建人 |
| | | :param updater: 更新人 |
| | | :param start_time: 开始时间 |
| | | :param failed_cases: 失败用例列表 |
| | | :param error_cases: 异常用例列表 |
| | | :param end_time: 结束时间 |
| | | :param skipped_count: 跳过用例数 |
| | | :param expected_failures_count: 预期失败数 |
| | | :param unexpected_successes_count: 意外成功数 |
| | | :param setup_hooks_duration: 前置钩子耗时 |
| | | :param teardown_hooks_duration: 后置钩子耗时 |
| | | :param python_version: Python版本 |
| | | :param httprunner_version: HttpRunner版本 |
| | | :param avg_duration: 平均用例耗时 |
| | | :return: 邮件主题及 HTML 内容 |
| | | """ |
| | | email_subject = f"【自动化测试报告】{task_name}" |
| | | |
| | | status_icon = "✅" if fail_count == 0 and error_count == 0 else ("⚠️" if fail_count == 0 else "❌") |
| | | |
| | | if error_count == 0 and fail_count == 0: |
| | | status_text = "测试通过" |
| | | status_color = "#2ecc71" |
| | | status_bg = "#e8f8f5" |
| | | elif fail_count == 0: |
| | | status_text = "部分异常" |
| | | status_color = "#f39c12" |
| | | status_bg = "#fef5e7" |
| | | else: |
| | | status_text = "测试失败" |
| | | status_color = "#e74c3c" |
| | | status_bg = "#fdedec" |
| | | |
| | | failed_cases_html = "" |
| | | if failed_cases: |
| | | failed_cases_html = "<div class='case-list'>" |
| | | for idx, case in enumerate(failed_cases, 1): |
| | | failed_cases_html += f""" |
| | | <div class='case-item'> |
| | | <div class='case-header'> |
| | | <span class='case-index'>{idx}</span> |
| | | <span class='case-name'>{case['name']}</span> |
| | | </div> |
| | | <div class='case-details'> |
| | | <span class='case-method'>{case['method']}</span> |
| | | <span class='case-url'>{case['url'][:60]}...</span> |
| | | </div> |
| | | <div class='case-error'>❌ {case['error']}</div> |
| | | </div> |
| | | """ |
| | | failed_cases_html += "</div>" |
| | | |
| | | error_cases_html = "" |
| | | if error_cases: |
| | | error_cases_html = "<div class='case-list'>" |
| | | for idx, case in enumerate(error_cases, 1): |
| | | error_cases_html += f""" |
| | | <div class='case-item'> |
| | | <div class='case-header'> |
| | | <span class='case-index'>{idx}</span> |
| | | <span class='case-name'>{case['name']}</span> |
| | | </div> |
| | | <div class='case-details'> |
| | | <span class='case-method'>{case['method']}</span> |
| | | <span class='case-url'>{case['url'][:60]}...</span> |
| | | </div> |
| | | <div class='case-error'>⚠️ {case['error']}</div> |
| | | </div> |
| | | """ |
| | | error_cases_html += "</div>" |
| | | |
| | | email_content = f"""<!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <title>自动化测试报告</title> |
| | | <!-- 引入 Google 字体 --> |
| | | <link href="https://fonts.googleapis.com/css?family=Roboto:400,500,700&display=swap" rel="stylesheet"> |
| | | <style> |
| | | body {{ |
| | | margin: 0; |
| | | padding: 0; |
| | | background-color: #f5f7fa; |
| | | font-family: 'Roboto', sans-serif; |
| | | font-family: 'Roboto', 'Microsoft YaHei', sans-serif; |
| | | color: #333; |
| | | }} |
| | | .container {{ |
| | | max-width: 700px; |
| | | max-width: 800px; |
| | | margin: 40px auto; |
| | | background: #fff; |
| | | border-radius: 12px; |
| | | border-radius: 16px; |
| | | overflow: hidden; |
| | | box-shadow: 0 8px 24px rgba(0,0,0,0.1); |
| | | box-shadow: 0 10px 40px rgba(0,0,0,0.1); |
| | | }} |
| | | .header {{ |
| | | background: linear-gradient(135deg, #4facfe, #00f2fe); |
| | | padding: 30px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | padding: 40px 30px; |
| | | text-align: center; |
| | | color: #fff; |
| | | }} |
| | | .header h1 {{ |
| | | margin: 0; |
| | | font-size: 28px; |
| | | font-size: 32px; |
| | | font-weight: 700; |
| | | letter-spacing: 1px; |
| | | }} |
| | | .header p {{ |
| | | margin: 8px 0 0; |
| | | .header .subtitle {{ |
| | | margin: 12px 0 0; |
| | | font-size: 16px; |
| | | opacity: 0.95; |
| | | }} |
| | | .status-badge {{ |
| | | display: inline-block; |
| | | margin-top: 20px; |
| | | padding: 8px 24px; |
| | | background: rgba(255,255,255,0.2); |
| | | border-radius: 20px; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | }} |
| | | .content {{ |
| | | padding: 30px 40px; |
| | | padding: 40px 50px; |
| | | }} |
| | | .section-title {{ |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #2c3e50; |
| | | margin-bottom: 20px; |
| | | padding-bottom: 10px; |
| | | border-bottom: 2px solid #f0f0f0; |
| | | }} |
| | | .summary-grid {{ |
| | | display: grid; |
| | | grid-template-columns: repeat(4, 1fr); |
| | | gap: 20px; |
| | | margin-bottom: 30px; |
| | | }} |
| | | .summary-grid-extended {{ |
| | | display: grid; |
| | | grid-template-columns: repeat(3, 1fr); |
| | | gap: 20px; |
| | | margin-top: 20px; |
| | | margin-bottom: 30px; |
| | | }} |
| | | .card {{ |
| | | background: #f9fafc; |
| | | border-radius: 8px; |
| | | padding: 20px; |
| | | background: #f8f9fa; |
| | | border-radius: 12px; |
| | | padding: 24px 16px; |
| | | text-align: center; |
| | | box-shadow: 0 2px 6px rgba(0,0,0,0.05); |
| | | box-shadow: 0 2px 8px rgba(0,0,0,0.05); |
| | | transition: transform 0.2s ease, box-shadow 0.2s ease; |
| | | }} |
| | | .card:hover {{ |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 12px rgba(0,0,0,0.1); |
| | | }} |
| | | .card .label {{ |
| | | font-size: 14px; |
| | | color: #777; |
| | | margin-bottom: 8px; |
| | | font-size: 13px; |
| | | color: #7f8c8d; |
| | | margin-bottom: 10px; |
| | | display: block; |
| | | font-weight: 500; |
| | | }} |
| | | .card .value {{ |
| | | font-size: 24px; |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | line-height: 1; |
| | | }} |
| | | .value.success {{ |
| | | color: #2ecc71; |
| | |
| | | .value.warning {{ |
| | | color: #f39c12; |
| | | }} |
| | | .value.primary {{ |
| | | color: #667eea; |
| | | }} |
| | | .info-table {{ |
| | | width: 100%; |
| | | border-collapse: collapse; |
| | | margin-bottom: 30px; |
| | | }} |
| | | .info-table td {{ |
| | | padding: 12px 0; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | }} |
| | | .info-table td:first-child {{ |
| | | color: #7f8c8d; |
| | | font-weight: 500; |
| | | width: 120px; |
| | | }} |
| | | .info-table td:last-child {{ |
| | | color: #2c3e50; |
| | | font-weight: 600; |
| | | }} |
| | | .progress-bar {{ |
| | | width: 100%; |
| | | height: 8px; |
| | | background: #e0e0e0; |
| | | border-radius: 4px; |
| | | overflow: hidden; |
| | | margin: 20px 0; |
| | | }} |
| | | .progress-fill {{ |
| | | height: 100%; |
| | | background: linear-gradient(90deg, #2ecc71, #27ae60); |
| | | border-radius: 4px; |
| | | transition: width 0.3s ease; |
| | | }} |
| | | .case-list {{ |
| | | margin-top: 15px; |
| | | }} |
| | | .case-item {{ |
| | | background: #fff; |
| | | border: 1px solid #e9ecef; |
| | | border-radius: 8px; |
| | | padding: 16px; |
| | | margin-bottom: 12px; |
| | | transition: box-shadow 0.2s ease; |
| | | }} |
| | | .case-item:hover {{ |
| | | box-shadow: 0 2px 8px rgba(0,0,0,0.08); |
| | | }} |
| | | .case-header {{ |
| | | display: flex; |
| | | align-items: center; |
| | | margin-bottom: 10px; |
| | | }} |
| | | .case-index {{ |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 24px; |
| | | height: 24px; |
| | | background: #667eea; |
| | | color: #fff; |
| | | border-radius: 50%; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | margin-right: 10px; |
| | | }} |
| | | .case-name {{ |
| | | font-weight: 600; |
| | | color: #2c3e50; |
| | | font-size: 14px; |
| | | flex: 1; |
| | | }} |
| | | .case-details {{ |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | margin-bottom: 8px; |
| | | }} |
| | | .case-method {{ |
| | | display: inline-block; |
| | | padding: 4px 10px; |
| | | background: #e8f4f8; |
| | | color: #2980b9; |
| | | border-radius: 4px; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | }} |
| | | .case-url {{ |
| | | flex: 1; |
| | | color: #7f8c8d; |
| | | font-size: 12px; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | }} |
| | | .case-error {{ |
| | | background: #fdedec; |
| | | color: #c0392b; |
| | | padding: 10px 12px; |
| | | border-radius: 6px; |
| | | font-size: 12px; |
| | | font-family: 'Courier New', monospace; |
| | | white-space: pre-wrap; |
| | | word-break: break-word; |
| | | line-height: 1.5; |
| | | }} |
| | | .no-cases {{ |
| | | text-align: center; |
| | | padding: 30px; |
| | | color: #95a5a6; |
| | | font-size: 14px; |
| | | }} |
| | | .report-btn {{ |
| | | display: block; |
| | | width: 240px; |
| | | width: 280px; |
| | | margin: 30px auto 0; |
| | | text-align: center; |
| | | padding: 15px; |
| | | background-color: #4a90e2; |
| | | padding: 16px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: #fff; |
| | | text-decoration: none; |
| | | border-radius: 6px; |
| | | font-weight: 500; |
| | | transition: background 0.3s ease; |
| | | border-radius: 8px; |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | transition: transform 0.2s ease, box-shadow 0.2s ease; |
| | | box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3); |
| | | }} |
| | | .report-btn:hover {{ |
| | | background-color: #3b7dd8; |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); |
| | | }} |
| | | .footer {{ |
| | | text-align: center; |
| | | font-size: 12px; |
| | | color: #aaa; |
| | | padding: 20px; |
| | | background: #f0f2f5; |
| | | font-size: 13px; |
| | | color: #95a5a6; |
| | | padding: 25px; |
| | | background: #f8f9fa; |
| | | border-top: 1px solid #e9ecef; |
| | | }} |
| | | .divider {{ |
| | | height: 1px; |
| | | background: #e9ecef; |
| | | margin: 30px 0; |
| | | }} |
| | | .icon {{ |
| | | font-size: 20px; |
| | | margin-right: 8px; |
| | | }} |
| | | @media (max-width: 768px) {{ |
| | | .container {{ |
| | | margin: 20px; |
| | | }} |
| | | .content {{ |
| | | padding: 30px 25px; |
| | | }} |
| | | .summary-grid {{ |
| | | grid-template-columns: 1fr 1fr; |
| | | grid-template-columns: repeat(2, 1fr); |
| | | }} |
| | | .summary-grid-extended {{ |
| | | grid-template-columns: repeat(2, 1fr); |
| | | }} |
| | | }} |
| | | @media (max-width: 480px) {{ |
| | | .summary-grid {{ |
| | | grid-template-columns: 1fr; |
| | | }} |
| | | .summary-grid-extended {{ |
| | | grid-template-columns: 1fr; |
| | | }} |
| | | .header h1 {{ |
| | | font-size: 24px; |
| | | }} |
| | | }} |
| | | </style> |
| | |
| | | <body> |
| | | <div class="container"> |
| | | <div class="header"> |
| | | <h1>自动化测试报告</h1> |
| | | <p>{task_name}</p> |
| | | <h1>🚀 自动化测试报告</h1> |
| | | <p class="subtitle">{task_name}</p> |
| | | <div class="status-badge">{status_icon} {status_text}</div> |
| | | </div> |
| | | |
| | | <div class="content"> |
| | | <div class="section-title">📊 测试概览</div> |
| | | <div class="summary-grid"> |
| | | <div class="card"> |
| | | <span class="label">总耗时</span> |
| | | <span class="value">{duration}</span> |
| | | <span class="value primary">{duration}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">用例个数</span> |
| | | <span class="label">用例总数</span> |
| | | <span class="value">{case_count}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">失败比例</span> |
| | | <span class="value warning">{fail_rate}</span> |
| | | <span class="label">成功率</span> |
| | | <span class="value success">{success_rate}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">成功接口</span> |
| | | <span class="label">失败率</span> |
| | | <span class="value error">{fail_rate}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">✅ 成功</span> |
| | | <span class="value success">{pass_count}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">异常接口</span> |
| | | <span class="value error">{error_count}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">失败接口</span> |
| | | <span class="label">❌ 失败</span> |
| | | <span class="value error">{fail_count}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">⚠️ 异常</span> |
| | | <span class="value warning">{error_count}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">📈 通过率</span> |
| | | <span class="value success">{success_rate}</span> |
| | | </div> |
| | | </div> |
| | | <a href="{report_url}" class="report-btn">点击查看详细报告</a> |
| | | |
| | | <div class="summary-grid-extended"> |
| | | <div class="card"> |
| | | <span class="label">⏭️ 跳过</span> |
| | | <span class="value" style="color: #95a5a6;">{skipped_count}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">🎯 预期失败</span> |
| | | <span class="value" style="color: #f39c12;">{expected_failures_count}</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">🎉 意外成功</span> |
| | | <span class="value" style="color: #3498db;">{unexpected_successes_count}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="summary-grid-extended"> |
| | | <div class="card"> |
| | | <span class="label">⚡ 平均耗时</span> |
| | | <span class="value primary">{avg_duration:.3f}s</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">🔧 前置钩子</span> |
| | | <span class="value" style="color: #7f8c8d;">{setup_hooks_duration:.3f}s</span> |
| | | </div> |
| | | <div class="card"> |
| | | <span class="label">🔧 后置钩子</span> |
| | | <span class="value" style="color: #7f8c8d;">{teardown_hooks_duration:.3f}s</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="divider"></div> |
| | | |
| | | <div class="section-title">📋 详细信息</div> |
| | | <table class="info-table"> |
| | | <tr> |
| | | <td><span class="icon">📝</span>任务名称</td> |
| | | <td>{task_name}</td> |
| | | </tr> |
| | | <tr> |
| | | <td><span class="icon">⏱️</span>执行耗时</td> |
| | | <td>{duration}</td> |
| | | </tr> |
| | | <tr> |
| | | <td><span class="icon">⚡</span>平均耗时</td> |
| | | <td>{avg_duration:.3f}s</td> |
| | | </tr> |
| | | <tr> |
| | | <td><span class="icon">📅</span>开始时间</td> |
| | | <td>{start_time if start_time else '未记录'}</td> |
| | | </tr> |
| | | <tr> |
| | | <td><span class="icon">🏁</span>结束时间</td> |
| | | <td>{end_time if end_time else '未记录'}</td> |
| | | </tr> |
| | | <tr> |
| | | <td><span class="icon">👤</span>创建人</td> |
| | | <td>{creator if creator else '未记录'}</td> |
| | | </tr> |
| | | <tr> |
| | | <td><span class="icon">✏️</span>更新人</td> |
| | | <td>{updater if updater else '未记录'}</td> |
| | | </tr> |
| | | <tr> |
| | | <td><span class="icon">🐍</span>Python版本</td> |
| | | <td>{python_version if python_version else '未记录'}</td> |
| | | </tr> |
| | | <tr> |
| | | <td><span class="icon">🏃</span>HttpRunner版本</td> |
| | | <td>{httprunner_version if httprunner_version else '未记录'}</td> |
| | | </tr> |
| | | </table> |
| | | |
| | | <div class="divider"></div> |
| | | |
| | | <div class="section-title">📈 测试结果分析</div> |
| | | <div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 20px;"> |
| | | <div style="display: flex; justify-content: space-between; margin-bottom: 10px;"> |
| | | <span style="color: #7f8c8d; font-size: 14px;">测试通过率</span> |
| | | <span style="color: #2ecc71; font-weight: 700; font-size: 18px;">{success_rate}</span> |
| | | </div> |
| | | <div class="progress-bar"> |
| | | <div class="progress-fill" style="width: {success_rate};"></div> |
| | | </div> |
| | | <div style="display: flex; justify-content: space-between; font-size: 12px; color: #95a5a6;"> |
| | | <span>成功: {pass_count}</span> |
| | | <span>失败: {fail_count}</span> |
| | | <span>异常: {error_count}</span> |
| | | </div> |
| | | </div> |
| | | |
| | | {f'<div class="divider"></div><div class="section-title">❌ 失败用例列表</div>{failed_cases_html}' if failed_cases else ''} |
| | | {f'<div class="divider"></div><div class="section-title">⚠️ 异常用例列表</div>{error_cases_html}' if error_cases else ''} |
| | | |
| | | <a href="{report_url}" class="report-btn">👉 点击查看详细报告</a> |
| | | </div> |
| | | |
| | | <div class="footer"> |
| | | 本邮件由系统自动发出,请勿回复 |
| | | <p>本邮件由系统自动发出,请勿回复</p> |
| | | <p style="margin-top: 8px;">如有疑问,请联系相关负责人</p> |
| | | </div> |
| | | </div> |
| | | </body> |