#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Standalone property-based test for mapping respect during generation. This test can run without the full Odoo environment by importing the generator service directly from the local module structure. Feature: survey-custom-certificate-template, Property 14: Mapping respect during generation Validates: Requirements 5.3 """ import sys import os import re from io import BytesIO # Add parent directory to path to import services sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) try: from hypothesis import given, settings, HealthCheck HYPOTHESIS_AVAILABLE = True except ImportError: print("ERROR: Hypothesis is not installed. Install with: pip install hypothesis") sys.exit(1) try: from docx import Document DOCX_AVAILABLE = True except ImportError: print("ERROR: python-docx is not installed. Install with: pip install python-docx") sys.exit(1) # Import the generator service directly from services.certificate_generator import CertificateGenerator # Import our custom strategies from hypothesis_strategies import docx_with_mappings_and_data def extract_all_text(document): """ Extract all text from a document including paragraphs, tables, headers, and footers. Args: document: python-docx Document object Returns: str: All text content concatenated """ text_parts = [] # Extract from paragraphs for paragraph in document.paragraphs: text_parts.append(paragraph.text) # Extract from tables for table in document.tables: for row in table.rows: for cell in row.cells: for paragraph in cell.paragraphs: text_parts.append(paragraph.text) # Extract from headers and footers for section in document.sections: # Header for paragraph in section.header.paragraphs: text_parts.append(paragraph.text) # Footer for paragraph in section.footer.paragraphs: text_parts.append(paragraph.text) return ' '.join(text_parts) def test_property_14_mapping_respect_during_generation(): """ Feature: survey-custom-certificate-template, Property 14: Mapping respect during generation For any set of configured mappings, the Certificate Generator should use those exact mappings when replacing placeholders during certificate generation. Validates: Requirements 5.3 This property test verifies that: 1. The generator respects the value_type specified in each mapping 2. Custom text is used when value_type is 'custom_text' 3. Dynamic data from the correct field is used when value_type is 'survey_field' or 'user_field' 4. The generator does not use incorrect fields or swap mappings 5. Each placeholder is replaced according to its specific mapping configuration """ print("\nTesting Property 14: Mapping respect during generation") print("=" * 60) generator = CertificateGenerator() test_count = 0 placeholder_pattern = r'\{key\.[a-zA-Z0-9_]+\}' @given(test_data=docx_with_mappings_and_data()) @settings( max_examples=100, deadline=None, suppress_health_check=[HealthCheck.function_scoped_fixture] ) def check_mapping_respect(test_data): nonlocal test_count test_count += 1 docx_binary, mappings, data = test_data # Load the original template to get the placeholders original_doc = Document(BytesIO(docx_binary)) original_text = extract_all_text(original_doc) # Extract placeholders from original document original_placeholders = set(re.findall(placeholder_pattern, original_text)) # Skip if no placeholders (edge case) if not original_placeholders: return # Replace placeholders using the generator result_doc = generator.replace_placeholders( Document(BytesIO(docx_binary)), mappings, data ) # Extract text from result document result_text = extract_all_text(result_doc) # Property 1: For each mapping, verify the correct value was used for mapping in mappings.get('placeholders', []): placeholder_key = mapping.get('key', '') # Skip if this placeholder wasn't in the original document if placeholder_key not in original_placeholders: continue value_type = mapping.get('value_type', '') # Determine what the expected replacement should be based on the mapping if value_type == 'custom_text': # For custom_text, the exact custom_text value should be used expected_value = mapping.get('custom_text', '') # Verify custom text appears in result (if not empty) if expected_value: if expected_value not in result_text: raise AssertionError( f"Custom text '{expected_value}' for placeholder '{placeholder_key}' " f"not found in result. The generator did not respect the custom_text mapping.\n" f"Mapping: {mapping}\n" f"Result text excerpt: {result_text[:500]}" ) # Verify that data from value_field was NOT used instead # (This checks that the generator didn't ignore value_type) value_field = mapping.get('value_field', '') if value_field and value_field in data: field_value = str(data[value_field]) # If field value is different from custom text and appears in result, # that's a violation (unless it's coincidentally the same) if field_value != expected_value and field_value and field_value in result_text: # Check if this field value was used for a different placeholder # by checking if any other mapping uses this field other_mapping_uses_field = any( m.get('value_field') == value_field and m.get('key') != placeholder_key for m in mappings.get('placeholders', []) ) if not other_mapping_uses_field: raise AssertionError( f"Generator used field value '{field_value}' instead of custom text " f"'{expected_value}' for placeholder '{placeholder_key}'. " f"The mapping specified value_type='custom_text' but the generator " f"did not respect this.\n" f"Mapping: {mapping}\n" f"Result text excerpt: {result_text[:500]}" ) elif value_type in ('survey_field', 'user_field'): # For dynamic fields, the value from the specified field should be used value_field = mapping.get('value_field', '') if value_field: expected_value = data.get(value_field, '') expected_value = str(expected_value) if expected_value else '' # Verify the field value appears in result (if not empty) if expected_value and expected_value not in result_text: raise AssertionError( f"Field value '{expected_value}' from '{value_field}' for placeholder " f"'{placeholder_key}' not found in result. The generator did not respect " f"the field mapping.\n" f"Mapping: {mapping}\n" f"Data: {data}\n" f"Result text excerpt: {result_text[:500]}" ) # Verify that custom_text was NOT used instead # (This checks that the generator didn't ignore value_type) custom_text = mapping.get('custom_text', '') if custom_text and custom_text != expected_value and custom_text in result_text: # Check if this custom text was used for a different placeholder other_mapping_uses_custom = any( m.get('custom_text') == custom_text and m.get('key') != placeholder_key for m in mappings.get('placeholders', []) ) if not other_mapping_uses_custom: raise AssertionError( f"Generator used custom text '{custom_text}' instead of field value " f"'{expected_value}' for placeholder '{placeholder_key}'. " f"The mapping specified value_type='{value_type}' with field " f"'{value_field}' but the generator did not respect this.\n" f"Mapping: {mapping}\n" f"Result text excerpt: {result_text[:500]}" ) # Property 2: Verify no placeholder was replaced with a value from the wrong mapping # Build a map of what each placeholder should have been replaced with expected_replacements = {} for mapping in mappings.get('placeholders', []): placeholder_key = mapping.get('key', '') if placeholder_key not in original_placeholders: continue if mapping.get('value_type') == 'custom_text': expected_replacements[placeholder_key] = mapping.get('custom_text', '') else: value_field = mapping.get('value_field', '') if value_field: expected_replacements[placeholder_key] = str(data.get(value_field, '')) else: expected_replacements[placeholder_key] = '' # Verify the original placeholder doesn't appear in result for placeholder in original_placeholders: if placeholder in result_text: raise AssertionError( f"Original placeholder '{placeholder}' still present in result. " f"The generator did not replace it according to the mapping.\n" f"Result text excerpt: {result_text[:500]}" ) try: check_mapping_respect() print(f"✓ Property 14 verified across {test_count} test cases") print(" All mappings were correctly respected during generation") print(" Custom text was used when specified") print(" Dynamic field data was used when specified") print(" No incorrect field swapping occurred") return True except Exception as e: print(f"✗ Property 14 FAILED after {test_count} test cases") print(f" Error: {e}") import traceback traceback.print_exc() return False def main(): """Run the property test.""" print("=" * 60) print("Property-Based Test: Mapping Respect During Generation") print("=" * 60) success = test_property_14_mapping_respect_during_generation() print("\n" + "=" * 60) if success: print("✓ Property test PASSED") print("=" * 60) return 0 else: print("✗ Property test FAILED") print("=" * 60) return 1 if __name__ == '__main__': sys.exit(main())