285 lines
12 KiB
Python
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())
|