survey_custom_certificate_t.../tests/test_property_mapping_respect_standalone.py
2025-11-29 08:46:04 +07:00

285 lines
12 KiB
Python

#!/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())