update
This commit is contained in:
208
tests/sender/test_newsletter_tracking.py
Normal file
208
tests/sender/test_newsletter_tracking.py
Normal file
@@ -0,0 +1,208 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Integration test for newsletter with tracking.
|
||||
Tests the full flow of generating a newsletter with tracking enabled.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
# Add backend directory to path
|
||||
backend_dir = Path(__file__).parent.parent / 'backend'
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
# Mock the tracking service to avoid database dependency
|
||||
class MockTrackingService:
|
||||
"""Mock tracking service for testing"""
|
||||
|
||||
@staticmethod
|
||||
def create_newsletter_tracking(newsletter_id, subscriber_email, article_links=None):
|
||||
"""Mock create_newsletter_tracking function"""
|
||||
link_tracking_map = {}
|
||||
|
||||
if article_links:
|
||||
for i, article in enumerate(article_links):
|
||||
link_tracking_map[article['url']] = f"mock-link-{i}"
|
||||
|
||||
return {
|
||||
'pixel_tracking_id': 'mock-pixel-123',
|
||||
'link_tracking_map': link_tracking_map,
|
||||
'newsletter_id': newsletter_id,
|
||||
'subscriber_email': subscriber_email
|
||||
}
|
||||
|
||||
# Import after setting up path
|
||||
from tracking_integration import inject_tracking_pixel, replace_article_links, generate_tracking_urls
|
||||
from jinja2 import Template
|
||||
|
||||
|
||||
def test_newsletter_with_tracking():
|
||||
"""Test generating a newsletter with tracking enabled"""
|
||||
print("\n" + "="*70)
|
||||
print("NEWSLETTER TRACKING INTEGRATION TEST")
|
||||
print("="*70)
|
||||
|
||||
# Mock article data
|
||||
articles = [
|
||||
{
|
||||
'title': 'Munich Tech Summit Announces 2025 Dates',
|
||||
'author': 'Tech Reporter',
|
||||
'link': 'https://example.com/tech-summit',
|
||||
'summary': 'The annual Munich Tech Summit will return in 2025 with exciting new features.',
|
||||
'source': 'Munich Tech News',
|
||||
'published_at': datetime.now()
|
||||
},
|
||||
{
|
||||
'title': 'New Public Transport Routes Launched',
|
||||
'author': 'Transport Desk',
|
||||
'link': 'https://example.com/transport-routes',
|
||||
'summary': 'MVG announces three new bus routes connecting suburban areas.',
|
||||
'source': 'Munich Transport',
|
||||
'published_at': datetime.now()
|
||||
}
|
||||
]
|
||||
|
||||
# Configuration
|
||||
newsletter_id = 'test-newsletter-2025-11-11'
|
||||
subscriber_email = 'test@example.com'
|
||||
api_url = 'http://localhost:5001'
|
||||
|
||||
print(f"\nNewsletter ID: {newsletter_id}")
|
||||
print(f"Subscriber: {subscriber_email}")
|
||||
print(f"Articles: {len(articles)}")
|
||||
print(f"API URL: {api_url}")
|
||||
|
||||
# Step 1: Generate tracking URLs
|
||||
print("\n" + "-"*70)
|
||||
print("Step 1: Generate tracking data")
|
||||
print("-"*70)
|
||||
|
||||
tracking_data = generate_tracking_urls(
|
||||
articles=articles,
|
||||
newsletter_id=newsletter_id,
|
||||
subscriber_email=subscriber_email,
|
||||
tracking_service=MockTrackingService
|
||||
)
|
||||
|
||||
print(f"✓ Pixel tracking ID: {tracking_data['pixel_tracking_id']}")
|
||||
print(f"✓ Link tracking map: {len(tracking_data['link_tracking_map'])} links")
|
||||
for url, tracking_id in tracking_data['link_tracking_map'].items():
|
||||
print(f" - {url} → {tracking_id}")
|
||||
|
||||
# Step 2: Load and render template
|
||||
print("\n" + "-"*70)
|
||||
print("Step 2: Render newsletter template")
|
||||
print("-"*70)
|
||||
|
||||
template_path = Path(__file__).parent / 'newsletter_template.html'
|
||||
with open(template_path, 'r', encoding='utf-8') as f:
|
||||
template_content = f.read()
|
||||
|
||||
template = Template(template_content)
|
||||
|
||||
now = datetime.now()
|
||||
template_data = {
|
||||
'date': now.strftime('%A, %B %d, %Y'),
|
||||
'year': now.year,
|
||||
'article_count': len(articles),
|
||||
'articles': articles,
|
||||
'unsubscribe_link': 'http://localhost:3000/unsubscribe',
|
||||
'website_link': 'http://localhost:3000',
|
||||
'tracking_enabled': True
|
||||
}
|
||||
|
||||
html = template.render(**template_data)
|
||||
print("✓ Template rendered")
|
||||
|
||||
# Step 3: Inject tracking pixel
|
||||
print("\n" + "-"*70)
|
||||
print("Step 3: Inject tracking pixel")
|
||||
print("-"*70)
|
||||
|
||||
html = inject_tracking_pixel(
|
||||
html,
|
||||
tracking_data['pixel_tracking_id'],
|
||||
api_url
|
||||
)
|
||||
|
||||
pixel_url = f"{api_url}/api/track/pixel/{tracking_data['pixel_tracking_id']}"
|
||||
if pixel_url in html:
|
||||
print(f"✓ Tracking pixel injected: {pixel_url}")
|
||||
else:
|
||||
print(f"✗ Tracking pixel NOT found")
|
||||
return False
|
||||
|
||||
# Step 4: Replace article links
|
||||
print("\n" + "-"*70)
|
||||
print("Step 4: Replace article links with tracking URLs")
|
||||
print("-"*70)
|
||||
|
||||
html = replace_article_links(
|
||||
html,
|
||||
tracking_data['link_tracking_map'],
|
||||
api_url
|
||||
)
|
||||
|
||||
# Verify all article links were replaced
|
||||
success = True
|
||||
for article in articles:
|
||||
original_url = article['link']
|
||||
tracking_id = tracking_data['link_tracking_map'].get(original_url)
|
||||
|
||||
if tracking_id:
|
||||
tracking_url = f"{api_url}/api/track/click/{tracking_id}"
|
||||
if tracking_url in html:
|
||||
print(f"✓ Link replaced: {original_url}")
|
||||
print(f" → {tracking_url}")
|
||||
else:
|
||||
print(f"✗ Link NOT replaced: {original_url}")
|
||||
success = False
|
||||
|
||||
# Verify original URL is NOT in the HTML (should be replaced)
|
||||
if f'href="{original_url}"' in html:
|
||||
print(f"✗ Original URL still present: {original_url}")
|
||||
success = False
|
||||
|
||||
# Step 5: Verify privacy notice
|
||||
print("\n" + "-"*70)
|
||||
print("Step 5: Verify privacy notice")
|
||||
print("-"*70)
|
||||
|
||||
if "This email contains tracking to measure engagement" in html:
|
||||
print("✓ Privacy notice present in footer")
|
||||
else:
|
||||
print("✗ Privacy notice NOT found")
|
||||
success = False
|
||||
|
||||
# Step 6: Save output for inspection
|
||||
print("\n" + "-"*70)
|
||||
print("Step 6: Save test output")
|
||||
print("-"*70)
|
||||
|
||||
output_file = 'test_newsletter_with_tracking.html'
|
||||
with open(output_file, 'w', encoding='utf-8') as f:
|
||||
f.write(html)
|
||||
|
||||
print(f"✓ Test newsletter saved to: {output_file}")
|
||||
print(f" Open it in your browser to inspect the tracking integration")
|
||||
|
||||
return success
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("\n" + "="*70)
|
||||
print("TESTING NEWSLETTER WITH TRACKING")
|
||||
print("="*70)
|
||||
|
||||
success = test_newsletter_with_tracking()
|
||||
|
||||
print("\n" + "="*70)
|
||||
if success:
|
||||
print("✓ ALL TESTS PASSED")
|
||||
print("="*70 + "\n")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("✗ SOME TESTS FAILED")
|
||||
print("="*70 + "\n")
|
||||
sys.exit(1)
|
||||
179
tests/sender/test_newsletter_with_tracking.html
Normal file
179
tests/sender/test_newsletter_with_tracking.html
Normal file
@@ -0,0 +1,179 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<meta content="IE=edge" http-equiv="X-UA-Compatible"/>
|
||||
<title>Munich News Daily</title>
|
||||
<!--[if mso]>
|
||||
<style type="text/css">
|
||||
body, table, td {font-family: Arial, Helvetica, sans-serif !important;}
|
||||
</style>
|
||||
<![endif]-->
|
||||
</head>
|
||||
<body style="margin: 0; padding: 0; background-color: #f4f4f4; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;">
|
||||
<!-- Wrapper Table -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color: #f4f4f4;" width="100%">
|
||||
<tr>
|
||||
<td align="center" style="padding: 20px 0;">
|
||||
<!-- Main Container -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color: #ffffff; max-width: 600px;" width="600">
|
||||
<!-- Header -->
|
||||
<tr>
|
||||
<td style="background-color: #1a1a1a; padding: 30px 40px; text-align: center;">
|
||||
<h1 style="margin: 0 0 8px 0; font-size: 28px; font-weight: 700; color: #ffffff; letter-spacing: -0.5px;">
|
||||
Munich News Daily
|
||||
</h1>
|
||||
<p style="margin: 0; font-size: 14px; color: #999999; letter-spacing: 0.5px;">
|
||||
Tuesday, November 11, 2025
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Greeting -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px 20px 40px;">
|
||||
<p style="margin: 0; font-size: 16px; line-height: 1.5; color: #333333;">
|
||||
Good morning ☀️
|
||||
</p>
|
||||
<p style="margin: 15px 0 0 0; font-size: 15px; line-height: 1.6; color: #666666;">
|
||||
Here's what's happening in Munich today. We've summarized 2 stories using AI so you can stay informed in under 5 minutes.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="height: 1px; background-color: #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Articles -->
|
||||
<tr>
|
||||
<td style="padding: 25px 40px;">
|
||||
<!-- Article Number Badge -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<span style="display: inline-block; background-color: #000000; color: #ffffff; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600;">
|
||||
1
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Article Title -->
|
||||
<h2 style="margin: 12px 0 8px 0; font-size: 19px; font-weight: 700; line-height: 1.3; color: #1a1a1a;">
|
||||
Munich Tech Summit Announces 2025 Dates
|
||||
</h2>
|
||||
<!-- Article Meta -->
|
||||
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999;">
|
||||
<span style="color: #000000; font-weight: 600;">Munich Tech News</span>
|
||||
<span> • Tech Reporter</span>
|
||||
</p>
|
||||
<!-- Article Summary -->
|
||||
<p style="margin: 0 0 15px 0; font-size: 15px; line-height: 1.6; color: #333333;">
|
||||
The annual Munich Tech Summit will return in 2025 with exciting new features.
|
||||
</p>
|
||||
<!-- Read More Link -->
|
||||
<a href="http://localhost:5001/api/track/click/mock-link-0" style="display: inline-block; color: #000000; text-decoration: none; font-size: 14px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px;">
|
||||
Read more →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Article Divider -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<div style="height: 1px; background-color: #f0f0f0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 25px 40px;">
|
||||
<!-- Article Number Badge -->
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" width="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<span style="display: inline-block; background-color: #000000; color: #ffffff; width: 24px; height: 24px; line-height: 24px; text-align: center; border-radius: 50%; font-size: 12px; font-weight: 600;">
|
||||
2
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- Article Title -->
|
||||
<h2 style="margin: 12px 0 8px 0; font-size: 19px; font-weight: 700; line-height: 1.3; color: #1a1a1a;">
|
||||
New Public Transport Routes Launched
|
||||
</h2>
|
||||
<!-- Article Meta -->
|
||||
<p style="margin: 0 0 12px 0; font-size: 13px; color: #999999;">
|
||||
<span style="color: #000000; font-weight: 600;">Munich Transport</span>
|
||||
<span> • Transport Desk</span>
|
||||
</p>
|
||||
<!-- Article Summary -->
|
||||
<p style="margin: 0 0 15px 0; font-size: 15px; line-height: 1.6; color: #333333;">
|
||||
MVG announces three new bus routes connecting suburban areas.
|
||||
</p>
|
||||
<!-- Read More Link -->
|
||||
<a href="http://localhost:5001/api/track/click/mock-link-1" style="display: inline-block; color: #000000; text-decoration: none; font-size: 14px; font-weight: 600; border-bottom: 2px solid #000000; padding-bottom: 2px;">
|
||||
Read more →
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Article Divider -->
|
||||
<!-- Bottom Divider -->
|
||||
<tr>
|
||||
<td style="padding: 25px 40px 0 40px;">
|
||||
<div style="height: 1px; background-color: #e0e0e0;"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Summary Box -->
|
||||
<tr>
|
||||
<td style="padding: 30px 40px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="background-color: #f8f8f8; border-radius: 8px;" width="100%">
|
||||
<tr>
|
||||
<td style="padding: 25px; text-align: center;">
|
||||
<p style="margin: 0 0 8px 0; font-size: 13px; color: #666666; text-transform: uppercase; letter-spacing: 1px; font-weight: 600;">
|
||||
Today's Digest
|
||||
</p>
|
||||
<p style="margin: 0; font-size: 36px; font-weight: 700; color: #000000;">
|
||||
2
|
||||
</p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 14px; color: #666666;">
|
||||
stories • AI-summarized • 5 min read
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="background-color: #1a1a1a; padding: 30px 40px; text-align: center;">
|
||||
<p style="margin: 0 0 15px 0; font-size: 14px; color: #ffffff; font-weight: 600;">
|
||||
Munich News Daily
|
||||
</p>
|
||||
<p style="margin: 0 0 20px 0; font-size: 13px; color: #999999; line-height: 1.5;">
|
||||
AI-powered news summaries for busy people.<br/>
|
||||
Delivered daily to your inbox.
|
||||
</p>
|
||||
<!-- Footer Links -->
|
||||
<p style="margin: 0; font-size: 12px; color: #666666;">
|
||||
<a href="http://localhost:3000" style="color: #999999; text-decoration: none;">Visit Website</a>
|
||||
<span style="color: #444444;"> • </span>
|
||||
<a href="http://localhost:3000/unsubscribe" style="color: #999999; text-decoration: none;">Unsubscribe</a>
|
||||
</p>
|
||||
<!-- Privacy Notice -->
|
||||
<p style="margin: 20px 0 0 0; font-size: 11px; color: #666666; line-height: 1.4;">
|
||||
This email contains tracking to measure engagement and improve our content.<br/>
|
||||
We respect your privacy and anonymize data after 90 days.
|
||||
</p>
|
||||
<p style="margin: 20px 0 0 0; font-size: 11px; color: #666666;">
|
||||
© 2025 Munich News Daily. All rights reserved.
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End Main Container -->
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- End Wrapper Table -->
|
||||
<img alt="" height="1" src="http://localhost:5001/api/track/pixel/mock-pixel-123" style="display:block;" width="1"/></body>
|
||||
</html>
|
||||
187
tests/sender/test_tracking_integration.py
Normal file
187
tests/sender/test_tracking_integration.py
Normal file
@@ -0,0 +1,187 @@
|
||||
#!/usr/bin/env python
|
||||
"""
|
||||
Test script for tracking integration in newsletter sender.
|
||||
Tests tracking pixel injection and link replacement.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add backend directory to path
|
||||
backend_dir = Path(__file__).parent.parent / 'backend'
|
||||
sys.path.insert(0, str(backend_dir))
|
||||
|
||||
from tracking_integration import inject_tracking_pixel, replace_article_links
|
||||
|
||||
|
||||
def test_inject_tracking_pixel():
|
||||
"""Test that tracking pixel is correctly injected into HTML"""
|
||||
print("\n" + "="*70)
|
||||
print("TEST 1: Inject Tracking Pixel")
|
||||
print("="*70)
|
||||
|
||||
# Test HTML
|
||||
html = """<html>
|
||||
<body>
|
||||
<p>Newsletter content</p>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
tracking_id = "test-tracking-123"
|
||||
api_url = "http://localhost:5001"
|
||||
|
||||
# Inject pixel
|
||||
result = inject_tracking_pixel(html, tracking_id, api_url)
|
||||
|
||||
# Verify pixel is present
|
||||
expected_pixel = f'<img src="{api_url}/api/track/pixel/{tracking_id}" width="1" height="1" alt="" style="display:block;" />'
|
||||
|
||||
if expected_pixel in result:
|
||||
print("✓ Tracking pixel correctly injected")
|
||||
print(f" Pixel URL: {api_url}/api/track/pixel/{tracking_id}")
|
||||
return True
|
||||
else:
|
||||
print("✗ Tracking pixel NOT found in HTML")
|
||||
print(f" Expected: {expected_pixel}")
|
||||
print(f" Result: {result}")
|
||||
return False
|
||||
|
||||
|
||||
def test_replace_article_links():
|
||||
"""Test that article links are correctly replaced with tracking URLs"""
|
||||
print("\n" + "="*70)
|
||||
print("TEST 2: Replace Article Links")
|
||||
print("="*70)
|
||||
|
||||
# Test HTML with article links
|
||||
html = """<html>
|
||||
<body>
|
||||
<a href="https://example.com/article1">Article 1</a>
|
||||
<a href="https://example.com/article2">Article 2</a>
|
||||
<a href="https://example.com/untracked">Untracked Link</a>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
# Tracking map
|
||||
link_tracking_map = {
|
||||
"https://example.com/article1": "track-id-1",
|
||||
"https://example.com/article2": "track-id-2"
|
||||
}
|
||||
|
||||
api_url = "http://localhost:5001"
|
||||
|
||||
# Replace links
|
||||
result = replace_article_links(html, link_tracking_map, api_url)
|
||||
|
||||
# Verify replacements
|
||||
success = True
|
||||
|
||||
# Check article 1 link
|
||||
expected_url_1 = f"{api_url}/api/track/click/track-id-1"
|
||||
if expected_url_1 in result:
|
||||
print(f"✓ Article 1 link replaced: {expected_url_1}")
|
||||
else:
|
||||
print(f"✗ Article 1 link NOT replaced")
|
||||
success = False
|
||||
|
||||
# Check article 2 link
|
||||
expected_url_2 = f"{api_url}/api/track/click/track-id-2"
|
||||
if expected_url_2 in result:
|
||||
print(f"✓ Article 2 link replaced: {expected_url_2}")
|
||||
else:
|
||||
print(f"✗ Article 2 link NOT replaced")
|
||||
success = False
|
||||
|
||||
# Check untracked link remains unchanged
|
||||
if "https://example.com/untracked" in result:
|
||||
print(f"✓ Untracked link preserved: https://example.com/untracked")
|
||||
else:
|
||||
print(f"✗ Untracked link was modified (should remain unchanged)")
|
||||
success = False
|
||||
|
||||
return success
|
||||
|
||||
|
||||
def test_full_integration():
|
||||
"""Test full integration: pixel + link replacement"""
|
||||
print("\n" + "="*70)
|
||||
print("TEST 3: Full Integration (Pixel + Links)")
|
||||
print("="*70)
|
||||
|
||||
# Test HTML
|
||||
html = """<html>
|
||||
<body>
|
||||
<h1>Newsletter</h1>
|
||||
<a href="https://example.com/article">Read Article</a>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
api_url = "http://localhost:5001"
|
||||
pixel_tracking_id = "pixel-123"
|
||||
link_tracking_map = {
|
||||
"https://example.com/article": "link-456"
|
||||
}
|
||||
|
||||
# First inject pixel
|
||||
html = inject_tracking_pixel(html, pixel_tracking_id, api_url)
|
||||
|
||||
# Then replace links
|
||||
html = replace_article_links(html, link_tracking_map, api_url)
|
||||
|
||||
# Verify both are present
|
||||
success = True
|
||||
|
||||
pixel_url = f"{api_url}/api/track/pixel/{pixel_tracking_id}"
|
||||
if pixel_url in html:
|
||||
print(f"✓ Tracking pixel present: {pixel_url}")
|
||||
else:
|
||||
print(f"✗ Tracking pixel NOT found")
|
||||
success = False
|
||||
|
||||
link_url = f"{api_url}/api/track/click/link-456"
|
||||
if link_url in html:
|
||||
print(f"✓ Tracking link present: {link_url}")
|
||||
else:
|
||||
print(f"✗ Tracking link NOT found")
|
||||
success = False
|
||||
|
||||
if success:
|
||||
print("\n✓ Full integration successful!")
|
||||
print("\nFinal HTML:")
|
||||
print("-" * 70)
|
||||
print(html)
|
||||
print("-" * 70)
|
||||
|
||||
return success
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("\n" + "="*70)
|
||||
print("TRACKING INTEGRATION TEST SUITE")
|
||||
print("="*70)
|
||||
|
||||
results = []
|
||||
|
||||
# Run tests
|
||||
results.append(("Inject Tracking Pixel", test_inject_tracking_pixel()))
|
||||
results.append(("Replace Article Links", test_replace_article_links()))
|
||||
results.append(("Full Integration", test_full_integration()))
|
||||
|
||||
# Summary
|
||||
print("\n" + "="*70)
|
||||
print("TEST SUMMARY")
|
||||
print("="*70)
|
||||
|
||||
passed = sum(1 for _, result in results if result)
|
||||
total = len(results)
|
||||
|
||||
for test_name, result in results:
|
||||
status = "✓ PASS" if result else "✗ FAIL"
|
||||
print(f"{status}: {test_name}")
|
||||
|
||||
print("-" * 70)
|
||||
print(f"Results: {passed}/{total} tests passed")
|
||||
print("="*70 + "\n")
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if passed == total else 1)
|
||||
Reference in New Issue
Block a user