241 lines
8.5 KiB
Python
241 lines
8.5 KiB
Python
"""Markdown report generator for E2E test results."""
|
|
import logging
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import List, Dict, Any, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ReportGenerator:
|
|
"""Generate markdown reports for E2E test execution."""
|
|
|
|
def __init__(self, report_dir: Path, test_name: str = "E2E Tenant Test"):
|
|
"""
|
|
Initialize the report generator.
|
|
|
|
Args:
|
|
report_dir: Directory to save reports
|
|
test_name: Name of the test suite
|
|
"""
|
|
self.report_dir = Path(report_dir)
|
|
self.report_dir.mkdir(parents=True, exist_ok=True)
|
|
self.test_name = test_name
|
|
self.steps: List[Dict[str, Any]] = []
|
|
self.cors_errors: List[Dict[str, Any]] = []
|
|
self.start_time: Optional[datetime] = None
|
|
self.end_time: Optional[datetime] = None
|
|
|
|
def start_test(self) -> None:
|
|
"""Mark the start of the test."""
|
|
self.start_time = datetime.now()
|
|
logger.info(f"Starting test: {self.test_name}")
|
|
|
|
def end_test(self) -> None:
|
|
"""Mark the end of the test."""
|
|
self.end_time = datetime.now()
|
|
logger.info(f"Test completed: {self.test_name}")
|
|
|
|
def start_step(self, step_name: str) -> None:
|
|
"""
|
|
Mark the start of a test step.
|
|
|
|
Args:
|
|
step_name: Name of the step
|
|
"""
|
|
step = {
|
|
"name": step_name,
|
|
"status": "in_progress",
|
|
"start_time": datetime.now(),
|
|
"end_time": None,
|
|
"message": "",
|
|
"screenshot": None,
|
|
}
|
|
self.steps.append(step)
|
|
logger.info(f"Starting step: {step_name}")
|
|
|
|
def end_step(
|
|
self,
|
|
status: str,
|
|
message: str = "",
|
|
screenshot_path: Optional[str] = None,
|
|
) -> None:
|
|
"""
|
|
Mark the end of the current test step.
|
|
|
|
Args:
|
|
status: "PASS", "FAIL", or "SKIP"
|
|
message: Additional message about the step
|
|
screenshot_path: Path to screenshot if captured
|
|
"""
|
|
if not self.steps:
|
|
logger.warning("No step to end")
|
|
return
|
|
|
|
step = self.steps[-1]
|
|
step["status"] = status
|
|
step["end_time"] = datetime.now()
|
|
step["message"] = message
|
|
step["screenshot"] = screenshot_path
|
|
|
|
duration = (step["end_time"] - step["start_time"]).total_seconds()
|
|
logger.info(f"Step '{step['name']}' completed: {status} ({duration:.2f}s)")
|
|
|
|
def add_cors_errors(self, errors: List[Dict[str, Any]]) -> None:
|
|
"""
|
|
Add CORS errors to the report.
|
|
|
|
Args:
|
|
errors: List of CORS error dictionaries
|
|
"""
|
|
self.cors_errors.extend(errors)
|
|
if errors:
|
|
logger.warning(f"Added {len(errors)} CORS errors to report")
|
|
|
|
def generate_report(self) -> Path:
|
|
"""
|
|
Generate the markdown report.
|
|
|
|
Returns:
|
|
Path to the generated report file
|
|
"""
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
report_file = self.report_dir / f"e2e_report_{timestamp}.md"
|
|
|
|
content = self._build_report_content()
|
|
report_file.write_text(content, encoding="utf-8")
|
|
|
|
logger.info(f"Report generated: {report_file}")
|
|
return report_file
|
|
|
|
def _build_report_content(self) -> str:
|
|
"""Build the markdown report content."""
|
|
lines = []
|
|
|
|
# Header
|
|
lines.append(f"# {self.test_name} Report")
|
|
lines.append("")
|
|
lines.append(f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
if self.start_time and self.end_time:
|
|
duration = (self.end_time - self.start_time).total_seconds()
|
|
lines.append(f"**Duration:** {duration:.2f} seconds")
|
|
|
|
lines.append("")
|
|
|
|
# Summary
|
|
lines.append("## Summary")
|
|
lines.append("")
|
|
|
|
total_steps = len(self.steps)
|
|
passed = sum(1 for s in self.steps if s["status"] == "PASS")
|
|
failed = sum(1 for s in self.steps if s["status"] == "FAIL")
|
|
skipped = sum(1 for s in self.steps if s["status"] == "SKIP")
|
|
|
|
lines.append(f"- **Total Steps:** {total_steps}")
|
|
lines.append(f"- **Passed:** {passed} ✅")
|
|
lines.append(f"- **Failed:** {failed} ❌")
|
|
lines.append(f"- **Skipped:** {skipped} ⏭️")
|
|
lines.append(f"- **CORS Errors:** {len(self.cors_errors)}")
|
|
lines.append("")
|
|
|
|
# Overall status
|
|
if failed > 0:
|
|
lines.append("**Overall Status:** ❌ FAILED")
|
|
else:
|
|
lines.append("**Overall Status:** ✅ PASSED")
|
|
lines.append("")
|
|
|
|
# Steps detail
|
|
lines.append("## Test Steps")
|
|
lines.append("")
|
|
|
|
for i, step in enumerate(self.steps, 1):
|
|
status_icon = "✅" if step["status"] == "PASS" else "❌" if step["status"] == "FAIL" else "⏭️"
|
|
lines.append(f"### {i}. {step['name']} {status_icon}")
|
|
lines.append("")
|
|
|
|
if step["start_time"] and step["end_time"]:
|
|
duration = (step["end_time"] - step["start_time"]).total_seconds()
|
|
lines.append(f"**Duration:** {duration:.2f}s")
|
|
lines.append("")
|
|
|
|
if step["message"]:
|
|
lines.append(f"**Message:** {step['message']}")
|
|
lines.append("")
|
|
|
|
if step["screenshot"]:
|
|
screenshot_rel = Path(step["screenshot"]).name
|
|
lines.append(f"**Screenshot:** `{screenshot_rel}`")
|
|
lines.append("")
|
|
lines.append(f"![{step['name']}]({screenshot_rel})")
|
|
lines.append("")
|
|
|
|
lines.append("---")
|
|
lines.append("")
|
|
|
|
# CORS Errors section
|
|
lines.append("## CORS Issues")
|
|
lines.append("")
|
|
|
|
if self.cors_errors:
|
|
lines.append(f"**{len(self.cors_errors)} CORS error(s) detected:**")
|
|
lines.append("")
|
|
|
|
for i, error in enumerate(self.cors_errors, 1):
|
|
lines.append(f"### Error {i}")
|
|
lines.append("")
|
|
lines.append(f"- **Type:** {error.get('type', 'unknown')}")
|
|
lines.append(f"- **Message:** ```{error.get('text', 'N/A')}```")
|
|
if error.get("location"):
|
|
lines.append(f"- **Location:** {error['location']}")
|
|
lines.append("")
|
|
else:
|
|
lines.append("✅ No CORS errors detected during test execution.")
|
|
lines.append("")
|
|
|
|
# Recommendations
|
|
lines.append("## Recommendations")
|
|
lines.append("")
|
|
|
|
if failed > 0:
|
|
lines.append("### Failed Steps")
|
|
lines.append("")
|
|
for step in self.steps:
|
|
if step["status"] == "FAIL":
|
|
lines.append(f"- **{step['name']}:** {step['message']}")
|
|
# Check for DNS/hosts file issues
|
|
if "DNS/Hostname issue" in step["message"]:
|
|
lines.append("")
|
|
lines.append("**Hosts File Fix Required:**")
|
|
lines.append("```")
|
|
lines.append("127.0.0.1 mondaytest.local.openedx.io")
|
|
lines.append("127.0.0.1 studio.mondaytest.local.openedx.io")
|
|
lines.append("127.0.0.1 mondaytest.apps.local.openedx.io")
|
|
lines.append("```")
|
|
lines.append("")
|
|
|
|
if self.cors_errors:
|
|
lines.append("### CORS Issues")
|
|
lines.append("")
|
|
lines.append("CORS errors were detected. Possible solutions:")
|
|
lines.append("")
|
|
lines.append("1. Verify `CORS_ORIGIN_WHITELIST` includes both `lms_domain` and `apps_domain` origins")
|
|
lines.append("2. Check that the tenant config has correct CORS settings")
|
|
lines.append("3. Enable the `cors_fix.py` plugin for global CORS settings")
|
|
lines.append("4. Verify the MFE is using the correct LMS_BASE_URL for the tenant")
|
|
lines.append("")
|
|
|
|
lines.append("### Next Steps")
|
|
lines.append("")
|
|
if failed == 0 and not self.cors_errors:
|
|
lines.append("- ✅ All tests passed. The tenant workflow is functioning correctly.")
|
|
else:
|
|
lines.append("- Review failed steps and error messages above")
|
|
lines.append("- Check screenshots for visual context")
|
|
lines.append("- Verify tenant configuration in Django admin")
|
|
lines.append("- Run tests again after fixes")
|
|
lines.append("")
|
|
|
|
return "\n".join(lines)
|