Spaces:
Sleeping
Sleeping
| """ | |
| Property-Based Tests for Analytics and Reporting Invariants | |
| Tests critical analytics and reporting business logic: | |
| - Metrics collection and aggregation | |
| - Report generation | |
| - Data filtering and segmentation | |
| - Time-based analytics | |
| - Performance analytics | |
| - User behavior analytics | |
| - Analytics export | |
| - Analytics retention | |
| """ | |
| import pytest | |
| from datetime import datetime, timedelta | |
| from hypothesis import given, settings, assume | |
| from hypothesis import strategies as st | |
| from typing import Dict, List, Set, Optional | |
| import uuid | |
| class TestMetricsCollectionInvariants: | |
| """Tests for metrics collection invariants""" | |
| def test_metric_collection_creates_valid_metric(self, metric_name, metric_value, timestamp, tags): | |
| """Test that metric collection creates a valid metric""" | |
| # Collect metric | |
| metric = { | |
| 'id': str(uuid.uuid4()), | |
| 'name': metric_name, | |
| 'value': metric_value, | |
| 'timestamp': timestamp, | |
| 'tags': tags | |
| } | |
| # Verify metric | |
| assert metric['id'] is not None, "Metric ID must be set" | |
| assert len(metric['name']) >= 1, "Metric name must not be empty" | |
| assert metric['timestamp'] is not None, "Timestamp must be set" | |
| def test_metric_aggregation(self, values): | |
| """Test that metrics are aggregated correctly""" | |
| if len(values) == 0: | |
| assert True # No data | |
| else: | |
| # Calculate sum, avg, min, max, count | |
| total = sum(values) | |
| average = sum(values) / len(values) | |
| minimum = min(values) | |
| maximum = max(values) | |
| count = len(values) | |
| # Verify aggregations | |
| assert total >= 0, "Non-negative sum" | |
| # Use epsilon for floating-point comparison | |
| epsilon = 1e-9 | |
| assert minimum - epsilon <= average <= maximum + epsilon, \ | |
| f"Average {average} should be between min {minimum} and max {maximum}" | |
| assert count >= 1, "Count must be >= 1" | |
| def test_time_window_aggregation(self, metric_name, time_window_seconds, timestamp): | |
| """Test that time window aggregation is correct""" | |
| # Define time window | |
| window_start = timestamp - timedelta(seconds=time_window_seconds) | |
| window_end = timestamp | |
| # Verify window | |
| assert window_end >= window_start, "Window end must be after window start" | |
| assert (window_end - window_start).total_seconds() == time_window_seconds, "Window duration must match" | |
| def test_metric_grouping(self, metrics, group_by): | |
| """Test that metrics can be grouped by tags""" | |
| # Group metrics by tags | |
| grouped = {} | |
| for metric in metrics: | |
| # Create group key from tags | |
| group_key = tuple(metric.get(tag, '') for tag in group_by) | |
| if group_key not in grouped: | |
| grouped[group_key] = [] | |
| grouped[group_key].append(metric) | |
| # Verify grouping | |
| for group_key, group_metrics in grouped.items(): | |
| assert len(group_key) == len(group_by), "Group key must match group_by fields" | |
| assert len(group_metrics) >= 1, "Group must have at least one metric" | |
| class TestReportGenerationInvariants: | |
| """Tests for report generation invariants""" | |
| def test_report_generation_creates_valid_report(self, report_id, report_type, user_id, created_at): | |
| """Test that report generation creates a valid report""" | |
| # Generate report | |
| report = { | |
| 'id': str(report_id), | |
| 'type': report_type, | |
| 'user_id': str(user_id), | |
| 'created_at': created_at, | |
| 'status': 'GENERATING' | |
| } | |
| # Verify report | |
| assert report['id'] is not None, "Report ID must be set" | |
| assert report['type'] in ['usage', 'performance', 'financial', 'user_activity', 'errors', 'custom'], "Valid report type" | |
| assert report['user_id'] is not None, "User ID must be set" | |
| assert report['created_at'] is not None, "Created at must be set" | |
| assert report['status'] in ['GENERATING', 'COMPLETED', 'FAILED'], "Valid status" | |
| def test_report_date_range_validation(self, start_date, end_date): | |
| """Test that report date range is valid""" | |
| assume(end_date >= start_date) | |
| # Create date range | |
| date_range = { | |
| 'start': start_date, | |
| 'end': end_date | |
| } | |
| # Verify date range | |
| assert date_range['end'] >= date_range['start'], "End date must be after start date" | |
| def test_report_format_limits(self, report_format, data_row_count, max_rows): | |
| """Test that report format enforces row limits""" | |
| # Check if within limits | |
| within_limits = data_row_count <= max_rows | |
| # Verify limits | |
| assert data_row_count >= 0, "Row count must be non-negative" | |
| if data_row_count <= max_rows: | |
| assert within_limits is True, "Within row limit" | |
| else: | |
| assert within_limits is False, "Exceeds row limit" | |
| def test_report_completion(self, report_id, status_and_size, generated_at): | |
| """Test that report completion is recorded correctly""" | |
| status, file_size_bytes = status_and_size | |
| # Complete report | |
| report = { | |
| 'id': str(report_id), | |
| 'status': status, | |
| 'file_size_bytes': file_size_bytes, | |
| 'generated_at': generated_at | |
| } | |
| # Verify completion | |
| assert report['status'] in ['GENERATING', 'COMPLETED', 'FAILED'], "Valid status" | |
| assert report['file_size_bytes'] >= 0, "File size must be non-negative" | |
| if report['status'] == 'COMPLETED': | |
| assert report['generated_at'] is not None, "Generated at must be set" | |
| assert report['file_size_bytes'] > 0, "Completed report must have file size" | |
| class TestDataFilteringInvariants: | |
| """Tests for data filtering and segmentation invariants""" | |
| def test_data_filtering(self, data_points, filter_action): | |
| """Test that data filtering works correctly""" | |
| # Filter data points by action | |
| if filter_action is not None: | |
| filtered = [dp for dp in data_points if dp['action'] == filter_action] | |
| else: | |
| filtered = data_points | |
| # Verify filtering | |
| assert len(filtered) >= 0, "Filtered count must be non-negative" | |
| assert len(filtered) <= len(data_points), "Filtered count must be <= total count" | |
| if filter_action is not None: | |
| for dp in filtered: | |
| assert dp['action'] == filter_action, f"Filtered data must match action {filter_action}" | |
| def test_user_segmentation(self, user_id, event_count, segment_threshold): | |
| """Test that user segmentation is correct""" | |
| # Segment users by activity | |
| if event_count >= segment_threshold: | |
| segment = 'high_value' | |
| elif event_count >= segment_threshold // 2: | |
| segment = 'medium_value' | |
| else: | |
| segment = 'low_value' | |
| # Verify segmentation | |
| assert segment in ['high_value', 'medium_value', 'low_value'], "Valid segment" | |
| if event_count >= segment_threshold: | |
| assert segment == 'high_value', "High value users meet threshold" | |
| elif event_count >= segment_threshold // 2: | |
| assert segment == 'medium_value', "Medium value users meet half threshold" | |
| def test_percentile_calculation(self, metrics, percentile): | |
| """Test that percentile calculation is correct""" | |
| if len(metrics) == 0: | |
| assert True # No data | |
| else: | |
| # Calculate percentile | |
| sorted_metrics = sorted(metrics) | |
| index = int(len(sorted_metrics) * percentile / 100) | |
| index = min(index, len(sorted_metrics) - 1) | |
| percentile_value = sorted_metrics[index] | |
| # Verify percentile | |
| assert 0 <= percentile <= 100, "Percentile must be between 0 and 100" | |
| assert 0 <= percentile_value <= max(metrics), "Percentile value must be within range" | |
| def test_categorical_aggregation(self, data_points): | |
| """Test that categorical aggregation is correct""" | |
| # Aggregate by category | |
| aggregated = {} | |
| for dp in data_points: | |
| category = dp['category'] | |
| if category not in aggregated: | |
| aggregated[category] = {'count': 0, 'total': 0} | |
| aggregated[category]['count'] += 1 | |
| aggregated[category]['total'] += dp['value'] | |
| # Verify aggregation | |
| for category, stats in aggregated.items(): | |
| assert stats['count'] >= 1, f"Category {category} must have at least one data point" | |
| assert stats['total'] >= 0, f"Category {category} total must be non-negative" | |
| class TestTimeBasedAnalyticsInvariants: | |
| """Tests for time-based analytics invariants""" | |
| def test_time_series_aggregation(self, events, interval_minutes): | |
| """Test that time series aggregation is correct""" | |
| if len(events) == 0: | |
| assert True # No data | |
| else: | |
| # Aggregate by time interval | |
| interval = timedelta(minutes=interval_minutes) | |
| aggregated = {} | |
| for event in events: | |
| # Round timestamp to interval | |
| timestamp = event['timestamp'] | |
| interval_start = timestamp.replace(second=0, microsecond=0) | |
| interval_start = interval_start.replace(minute=(interval_start.minute // interval_minutes) * interval_minutes) | |
| key = interval_start | |
| if key not in aggregated: | |
| aggregated[key] = {'count': 0, 'total': 0} | |
| aggregated[key]['count'] += 1 | |
| aggregated[key]['total'] += event['value'] | |
| # Verify aggregation | |
| for key, stats in aggregated.items(): | |
| assert stats['count'] >= 1, "Interval must have at least one event" | |
| assert stats['total'] >= 0, "Interval total must be non-negative" | |
| def test_period_over_period_comparison(self, current_value, previous_value): | |
| """Test that period-over-period comparison is correct""" | |
| # Calculate growth rate | |
| if previous_value == 0: | |
| growth_rate = None if current_value == 0 else float('inf') | |
| else: | |
| growth_rate = (current_value - previous_value) / previous_value | |
| # Verify calculation | |
| if previous_value > 0: | |
| assert -1.0 <= growth_rate <= float('inf'), "Growth rate must be >= -100%" | |
| def test_moving_average_calculation(self, values, window_size): | |
| """Test that moving average calculation is correct""" | |
| if len(values) == 0: | |
| assert True # No data | |
| else: | |
| # Calculate moving average | |
| moving_averages = [] | |
| for i in range(len(values)): | |
| window = values[max(0, i - window_size + 1):i + 1] | |
| avg = sum(window) / len(window) | |
| moving_averages.append(avg) | |
| # Verify calculation | |
| assert len(moving_averages) == len(values), "Moving averages count must match values count" | |
| for avg in moving_averages: | |
| assert 0 <= avg <= max(values), "Moving average must be within value range" | |
| def test_time_period_inclusion(self, timestamp, period_start, period_end): | |
| """Test that time period inclusion is checked correctly""" | |
| assume(period_end >= period_start) | |
| # Check if timestamp is in period | |
| in_period = period_start <= timestamp <= period_end | |
| # Verify inclusion | |
| if period_start <= timestamp <= period_end: | |
| assert in_period is True, "Timestamp is in period" | |
| else: | |
| assert in_period is False, "Timestamp is not in period" | |
| class TestPerformanceAnalyticsInvariants: | |
| """Tests for performance analytics invariants""" | |
| def test_response_time_percentiles(self, response_times_ms): | |
| """Test that response time percentiles are calculated correctly""" | |
| if len(response_times_ms) == 0: | |
| assert True # No data | |
| else: | |
| # Calculate percentiles | |
| sorted_times = sorted(response_times_ms) | |
| p50 = sorted_times[int(len(sorted_times) * 0.5)] | |
| p95 = sorted_times[int(len(sorted_times) * 0.95)] | |
| p99 = sorted_times[int(len(sorted_times) * 0.99)] | |
| # Verify percentiles | |
| assert p50 <= p95 <= p99 or len(sorted_times) < 100, "Percentiles must be ordered" | |
| assert p50 >= 0, "P50 must be non-negative" | |
| assert p99 <= max(response_times_ms), "P99 must be within max" | |
| def test_success_rate_calculation(self, total_requests, successful_requests): | |
| """Test that success rate is calculated correctly""" | |
| # Ensure successful_requests <= total_requests | |
| assume(successful_requests <= total_requests) | |
| # Calculate success rate | |
| if total_requests == 0: | |
| success_rate = 1.0 # No failures if no requests | |
| else: | |
| success_rate = successful_requests / total_requests | |
| # Verify calculation | |
| assert 0.0 <= success_rate <= 1.0, "Success rate must be between 0 and 1" | |
| def test_uptime_percentage_validation(self, uptime_percentage): | |
| """Test that uptime percentage is valid""" | |
| # Validate uptime percentage | |
| assert 0.0 <= uptime_percentage <= 100.0, "Uptime must be between 0 and 100" | |
| # Determine SLA compliance | |
| sla_compliant = uptime_percentage >= 99.9 | |
| # Verify compliance | |
| if uptime_percentage >= 99.9: | |
| assert sla_compliant is True, "Meets SLA" | |
| else: | |
| assert sla_compliant is False, "Does not meet SLA" | |
| def test_throughput_calculation(self, execution_times_ms): | |
| """Test that throughput is calculated correctly""" | |
| if len(execution_times_ms) == 0: | |
| assert True # No data | |
| else: | |
| # Calculate throughput (requests per second) | |
| total_time_seconds = sum(execution_times_ms) / 1000 | |
| if total_time_seconds > 0: | |
| throughput = len(execution_times_ms) / total_time_seconds | |
| else: | |
| throughput = 0 | |
| # Verify calculation | |
| assert throughput >= 0, "Throughput must be non-negative" | |
| class TestUserBehaviorAnalyticsInvariants: | |
| """Tests for user behavior analytics invariants""" | |
| def test_session_duration_tracking(self, user_sessions): | |
| """Test that session duration is tracked correctly""" | |
| # Calculate session durations | |
| durations = [] | |
| for session in user_sessions: | |
| if session['session_end'] >= session['session_start']: | |
| duration = (session['session_end'] - session['session_start']).total_seconds() | |
| durations.append(duration) | |
| # Verify tracking | |
| for duration in durations: | |
| assert duration >= 0, "Duration must be non-negative" | |
| def test_engagement_metrics(self, page_views, unique_visitors): | |
| """Test that engagement metrics are calculated correctly""" | |
| # Calculate pages per visitor | |
| if unique_visitors > 0: | |
| pages_per_visitor = page_views / unique_visitors | |
| else: | |
| pages_per_visitor = 0 | |
| # Verify metrics | |
| assert page_views >= 0, "Page views must be non-negative" | |
| assert unique_visitors >= 0, "Unique visitors must be non-negative" | |
| assert pages_per_visitor >= 0, "Pages per visitor must be non-negative" | |
| def test_funnel_analysis(self, events, action_type): | |
| """Test that funnel analysis is correct""" | |
| # Filter events by action type | |
| filtered_events = [e for e in events if e['action'] == action_type] | |
| # Count unique users | |
| unique_users = len(set(e['user_id'] for e in filtered_events)) | |
| # Verify analysis | |
| assert unique_users >= 0, "Unique users must be non-negative" | |
| assert unique_users <= len(filtered_events), "Unique users must be <= total events" | |
| def test_user_cohort_analysis(self, user1_events, user2_events): | |
| """Test that cohort analysis groups users correctly""" | |
| # Group users by activity level | |
| if user1_events >= 100: | |
| cohort1 = 'high_activity' | |
| elif user1_events >= 10: | |
| cohort1 = 'medium_activity' | |
| else: | |
| cohort1 = 'low_activity' | |
| if user2_events >= 100: | |
| cohort2 = 'high_activity' | |
| elif user2_events >= 10: | |
| cohort2 = 'medium_activity' | |
| else: | |
| cohort2 = 'low_activity' | |
| # Verify cohorts | |
| assert cohort1 in ['high_activity', 'medium_activity', 'low_activity'], "Valid cohort" | |
| assert cohort2 in ['high_activity', 'medium_activity', 'low_activity'], "Valid cohort" | |
| class TestAnalyticsExportInvariants: | |
| """Tests for analytics export invariants""" | |
| def test_export_creation(self, export_id, export_format, data_point_count, created_at): | |
| """Test that export creation creates a valid export""" | |
| # Create export | |
| export = { | |
| 'id': str(export_id), | |
| 'format': export_format, | |
| 'data_point_count': data_point_count, | |
| 'created_at': created_at, | |
| 'status': 'PENDING' | |
| } | |
| # Verify export | |
| assert export['id'] is not None, "Export ID must be set" | |
| assert export['format'] in ['csv', 'json', 'xlsx', 'pdf'], "Valid export format" | |
| assert export['data_point_count'] >= 0, "Data point count must be non-negative" | |
| assert export['status'] in ['PENDING', 'COMPLETED', 'FAILED'], "Valid status" | |
| def test_export_size_limit_enforcement(self, data_point_count, max_export_size_mb): | |
| """Test that export size limits are enforced""" | |
| # Estimate export size (rough estimate: 100 bytes per data point) | |
| estimated_size_bytes = data_point_count * 100 | |
| estimated_size_mb = estimated_size_bytes / (1024 * 1024) | |
| # Check if within limits | |
| within_limits = estimated_size_mb <= max_export_size_mb | |
| # Verify enforcement | |
| if estimated_size_mb <= max_export_size_mb: | |
| assert within_limits is True, "Within size limit" | |
| else: | |
| assert within_limits is False, "Exceeds size limit" | |
| def test_export_completion(self, export_id, file_path, file_size_bytes, completed_at): | |
| """Test that export completion is recorded correctly""" | |
| # Complete export | |
| export = { | |
| 'id': str(export_id), | |
| 'file_path': file_path, | |
| 'file_size_bytes': file_size_bytes, | |
| 'completed_at': completed_at, | |
| 'status': 'COMPLETED' | |
| } | |
| # Verify completion | |
| assert export['file_path'] is not None, "File path must be set" | |
| assert export['file_size_bytes'] >= 0, "File size must be non-negative" | |
| assert export['completed_at'] is not None, "Completed at must be set" | |
| assert export['status'] == 'COMPLETED', "Status must be COMPLETED" | |
| def test_export_retention_policy(self, export_id, created_at, retention_days, current_time): | |
| """Test that export retention policy is enforced""" | |
| # Calculate export age | |
| export_age_days = (current_time - created_at).days | |
| # Check if should delete | |
| should_delete = export_age_days > retention_days | |
| # Verify retention policy | |
| if export_age_days > retention_days: | |
| assert should_delete is True, "Export should be deleted" | |
| else: | |
| assert should_delete is False, "Export should be kept" | |
| class TestAnalyticsRetentionInvariants: | |
| """Tests for analytics data retention invariants""" | |
| def test_data_retention_policy(self, data_point_id, created_at, retention_days, current_time): | |
| """Test that data retention policy is enforced""" | |
| # Calculate data age | |
| data_age_days = (current_time - created_at).days | |
| # Check if should delete | |
| should_delete = data_age_days > retention_days | |
| # Verify retention policy | |
| if data_age_days > retention_days: | |
| assert should_delete is True, "Data should be deleted" | |
| else: | |
| assert should_delete is False, "Data should be kept" | |
| def test_data_type_retention_policies(self, data_type): | |
| """Test that different data types have appropriate retention policies""" | |
| # Define minimum retention periods by data type | |
| min_retention_days = { | |
| 'raw_events': 30, # 30 days minimum | |
| 'aggregated_metrics': 365, # 1 year minimum | |
| 'reports': 365, # 1 year minimum | |
| 'exports': 90 # 90 days minimum | |
| } | |
| # Define maximum retention periods by data type | |
| max_retention_days = { | |
| 'raw_events': 90, # 90 days maximum | |
| 'aggregated_metrics': 3650, # 10 years maximum | |
| 'reports': 3650, # 10 years maximum | |
| 'exports': 365 # 1 year maximum | |
| } | |
| # Verify retention policy exists for data type | |
| assert data_type in min_retention_days, f"Minimum retention defined for {data_type}" | |
| assert data_type in max_retention_days, f"Maximum retention defined for {data_type}" | |
| assert min_retention_days[data_type] <= max_retention_days[data_type], f"Valid retention range for {data_type}" | |
| def test_batch_deletion(self, data_count, deletion_batch_size): | |
| """Test that batch deletion is performed correctly""" | |
| # Calculate number of batches | |
| batches_needed = (data_count + deletion_batch_size - 1) // deletion_batch_size if data_count > 0 else 0 | |
| # Verify batch calculation | |
| assert batches_needed >= 0, "Batches needed must be non-negative" | |
| if data_count > 0: | |
| assert batches_needed >= 1, "At least one batch needed for data" | |