"""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)