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

521 lines
17 KiB
Python

# -*- coding: utf-8 -*-
"""
Hypothesis strategies for property-based testing of the Survey Custom Certificate Template module.
This module provides custom Hypothesis strategies for generating test data including:
- DOCX files with placeholders
- Placeholder mappings
- Participant data
- Valid complete mapping structures
These strategies are used by property-based tests to verify correctness properties
across a wide range of randomly generated inputs.
"""
import io
import logging
from datetime import datetime, timedelta
from hypothesis import strategies as st
from hypothesis.strategies import composite
try:
from docx import Document
DOCX_AVAILABLE = True
except ImportError:
DOCX_AVAILABLE = False
Document = None
_logger = logging.getLogger(__name__)
# ============================================================================
# Basic Building Blocks
# ============================================================================
@composite
def placeholder_keys(draw):
"""
Generate valid placeholder keys in the format {key.field_name}.
Field names can contain ASCII letters, numbers, and underscores.
Returns:
str: A valid placeholder key like "{key.name}" or "{key.field_123}"
"""
# Generate field name with ASCII letters, numbers, and underscores
# Use ASCII range to avoid unicode characters
field_name = draw(st.text(
alphabet='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_',
min_size=1,
max_size=50
))
# Ensure it starts with a letter (not a number or underscore)
if not field_name[0].isalpha():
field_name = 'field_' + field_name
return f'{{key.{field_name}}}'
@composite
def text_with_placeholders(draw, min_placeholders=0, max_placeholders=5):
"""
Generate text content containing placeholders.
Args:
min_placeholders: Minimum number of placeholders to include
max_placeholders: Maximum number of placeholders to include
Returns:
tuple: (text_content, list_of_placeholders)
"""
num_placeholders = draw(st.integers(min_value=min_placeholders, max_value=max_placeholders))
# Generate unique placeholders
placeholders = []
for _ in range(num_placeholders):
placeholder = draw(placeholder_keys())
if placeholder not in placeholders:
placeholders.append(placeholder)
# Generate text segments (XML-safe characters only)
# Exclude control characters and use printable ASCII + common unicode
text_segments = []
for _ in range(len(placeholders) + 1):
segment = draw(st.text(
alphabet=st.characters(
min_codepoint=32, # Space
max_codepoint=126, # Tilde (printable ASCII)
blacklist_characters='{}<>' # Exclude these for safety
),
min_size=0,
max_size=50
))
text_segments.append(segment)
# Interleave text segments and placeholders
text_content = text_segments[0]
for i, placeholder in enumerate(placeholders):
text_content += placeholder
if i + 1 < len(text_segments):
text_content += text_segments[i + 1]
return text_content, placeholders
# ============================================================================
# DOCX Generation Strategies
# ============================================================================
@composite
def docx_with_placeholders(draw, min_placeholders=1, max_placeholders=10):
"""
Generate a DOCX file with known placeholders.
This strategy creates a valid DOCX document containing paragraphs with
placeholders in the format {key.field_name}. The document structure
includes paragraphs, tables, headers, and footers.
Args:
min_placeholders: Minimum number of placeholders to include
max_placeholders: Maximum number of placeholders to include
Returns:
tuple: (docx_binary, list_of_unique_placeholders)
- docx_binary: Binary content of the DOCX file
- list_of_unique_placeholders: Sorted list of unique placeholder strings
Raises:
ImportError: If python-docx is not available
"""
if not DOCX_AVAILABLE:
raise ImportError("python-docx is required for this strategy")
doc = Document()
all_placeholders = set()
# Determine number of placeholders
num_placeholders = draw(st.integers(min_value=min_placeholders, max_value=max_placeholders))
# Add paragraphs with placeholders
num_paragraphs = draw(st.integers(min_value=1, max_value=5))
for _ in range(num_paragraphs):
text, placeholders = draw(text_with_placeholders(
min_placeholders=0,
max_placeholders=max(1, num_placeholders // num_paragraphs + 1)
))
if text.strip(): # Only add non-empty paragraphs
doc.add_paragraph(text)
all_placeholders.update(placeholders)
# Optionally add a table with placeholders
add_table = draw(st.booleans())
if add_table and len(all_placeholders) < num_placeholders:
rows = draw(st.integers(min_value=1, max_value=3))
cols = draw(st.integers(min_value=1, max_value=3))
table = doc.add_table(rows=rows, cols=cols)
for row in table.rows:
for cell in row.cells:
text, placeholders = draw(text_with_placeholders(
min_placeholders=0,
max_placeholders=2
))
if text.strip():
cell.text = text
all_placeholders.update(placeholders)
# Optionally add header/footer with placeholders
add_header_footer = draw(st.booleans())
if add_header_footer and len(all_placeholders) < num_placeholders:
section = doc.sections[0]
# Add header
text, placeholders = draw(text_with_placeholders(
min_placeholders=0,
max_placeholders=1
))
if text.strip():
section.header.paragraphs[0].text = text
all_placeholders.update(placeholders)
# Add footer
text, placeholders = draw(text_with_placeholders(
min_placeholders=0,
max_placeholders=1
))
if text.strip():
section.footer.paragraphs[0].text = text
all_placeholders.update(placeholders)
# Ensure we have at least min_placeholders
while len(all_placeholders) < min_placeholders:
placeholder = draw(placeholder_keys())
doc.add_paragraph(f"Additional content: {placeholder}")
all_placeholders.add(placeholder)
# Save to bytes
doc_stream = io.BytesIO()
doc.save(doc_stream)
doc_stream.seek(0)
docx_binary = doc_stream.read()
# Return sorted list for consistency
return docx_binary, sorted(list(all_placeholders))
# ============================================================================
# Mapping Strategies
# ============================================================================
@composite
def placeholder_mappings(draw):
"""
Generate a single placeholder mapping dictionary.
A placeholder mapping defines how a placeholder should be replaced with data.
It includes the placeholder key, value type, and associated field or custom text.
Returns:
dict: A placeholder mapping with keys:
- key: The placeholder string (e.g., "{key.name}")
- value_type: One of 'survey_field', 'user_field', or 'custom_text'
- value_field: Field name if value_type is not 'custom_text'
- custom_text: Custom text if value_type is 'custom_text'
"""
placeholder_key = draw(placeholder_keys())
value_type = draw(st.sampled_from(['survey_field', 'user_field', 'custom_text']))
mapping = {
'key': placeholder_key,
'value_type': value_type,
}
if value_type == 'custom_text':
# Generate custom text (can be empty or contain any text)
# Use printable characters only to avoid XML issues
custom_text = draw(st.text(
alphabet=st.characters(
min_codepoint=32, # Space
max_codepoint=126, # Tilde (printable ASCII)
blacklist_characters='<>&' # Exclude XML special chars
),
max_size=200
))
mapping['value_field'] = ''
mapping['custom_text'] = custom_text
else:
# Generate a field name
if value_type == 'survey_field':
# Common survey fields
value_field = draw(st.sampled_from([
'survey_title',
'survey_description',
]))
else: # user_field
# Common user/participant fields
value_field = draw(st.sampled_from([
'partner_name',
'partner_email',
'email',
'create_date',
'completion_date',
'scoring_percentage',
'scoring_total',
]))
mapping['value_field'] = value_field
mapping['custom_text'] = ''
return mapping
@composite
def valid_mappings(draw, min_placeholders=1, max_placeholders=10):
"""
Generate a complete valid mappings structure.
This strategy creates a full mappings dictionary as stored in the database,
with a list of placeholder mappings.
Args:
min_placeholders: Minimum number of placeholder mappings
max_placeholders: Maximum number of placeholder mappings
Returns:
dict: A complete mappings structure with:
- placeholders: List of placeholder mapping dictionaries
"""
num_placeholders = draw(st.integers(min_value=min_placeholders, max_value=max_placeholders))
# Generate unique placeholder mappings
placeholders = []
used_keys = set()
for _ in range(num_placeholders):
mapping = draw(placeholder_mappings())
# Ensure unique keys
if mapping['key'] not in used_keys:
placeholders.append(mapping)
used_keys.add(mapping['key'])
return {
'placeholders': placeholders
}
# ============================================================================
# Participant Data Strategies
# ============================================================================
@composite
def participant_data(draw):
"""
Generate random participant data for certificate generation.
This strategy creates a dictionary containing all possible fields that
might be used in certificate generation, including survey data and
participant information.
Returns:
dict: Participant data dictionary with keys:
- survey_title: Survey/course title
- survey_description: Survey/course description
- partner_name: Participant's full name
- partner_email: Participant's email address
- email: Alternative email field
- create_date: Creation/submission date
- completion_date: Completion date
- scoring_percentage: Score as percentage
- scoring_total: Total score points
"""
# Generate realistic names
first_names = ['John', 'Jane', 'Alice', 'Bob', 'Charlie', 'Diana', 'Eve', 'Frank']
last_names = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis']
first_name = draw(st.sampled_from(first_names))
last_name = draw(st.sampled_from(last_names))
full_name = f"{first_name} {last_name}"
# Generate email from name
email = f"{first_name.lower()}.{last_name.lower()}@example.com"
# Generate dates
base_date = datetime.now()
days_ago = draw(st.integers(min_value=0, max_value=365))
completion_date = base_date - timedelta(days=days_ago)
# Generate scores
scoring_percentage = draw(st.floats(min_value=0.0, max_value=100.0))
scoring_total = draw(st.integers(min_value=0, max_value=100))
# Generate survey info
survey_titles = [
'Introduction to Python Programming',
'Advanced Web Development',
'Data Science Fundamentals',
'Machine Learning Basics',
'Cybersecurity Essentials',
'Cloud Computing Overview',
]
survey_title = draw(st.sampled_from(survey_titles))
survey_description = draw(st.text(
alphabet=st.characters(whitelist_categories=('Lu', 'Ll', 'Nd', 'Zs')),
min_size=10,
max_size=200
))
return {
'survey_title': survey_title,
'survey_description': survey_description,
'partner_name': full_name,
'partner_email': email,
'email': email,
'create_date': completion_date.strftime('%Y-%m-%d'),
'completion_date': completion_date.strftime('%Y-%m-%d'),
'scoring_percentage': f'{scoring_percentage:.1f}',
'scoring_total': str(scoring_total),
}
# ============================================================================
# Combined Strategies
# ============================================================================
@composite
def docx_with_mappings_and_data(draw):
"""
Generate a complete test case: DOCX with placeholders, mappings, and data.
This strategy creates a coordinated set of test inputs where:
- The DOCX contains specific placeholders
- The mappings define how to replace those placeholders
- The data contains values for all mapped fields
Returns:
tuple: (docx_binary, mappings, data)
- docx_binary: Binary content of DOCX file
- mappings: Complete mappings dictionary
- data: Participant data dictionary
"""
# Generate DOCX with placeholders
docx_binary, placeholders = draw(docx_with_placeholders(min_placeholders=1, max_placeholders=8))
# Generate mappings for these specific placeholders
mappings_list = []
for placeholder in placeholders:
value_type = draw(st.sampled_from(['survey_field', 'user_field', 'custom_text']))
mapping = {
'key': placeholder,
'value_type': value_type,
}
if value_type == 'custom_text':
# Use printable characters only to avoid XML issues
custom_text = draw(st.text(
alphabet=st.characters(
min_codepoint=32, # Space
max_codepoint=126, # Tilde (printable ASCII)
blacklist_characters='<>&' # Exclude XML special chars
),
max_size=100
))
mapping['value_field'] = ''
mapping['custom_text'] = custom_text
else:
if value_type == 'survey_field':
value_field = draw(st.sampled_from(['survey_title', 'survey_description']))
else:
value_field = draw(st.sampled_from([
'partner_name', 'partner_email', 'completion_date',
'scoring_percentage', 'scoring_total'
]))
mapping['value_field'] = value_field
mapping['custom_text'] = ''
mappings_list.append(mapping)
mappings = {'placeholders': mappings_list}
# Generate participant data
data = draw(participant_data())
return docx_binary, mappings, data
# ============================================================================
# Edge Case Strategies
# ============================================================================
@composite
def empty_docx(draw):
"""
Generate an empty DOCX file with no placeholders.
Returns:
bytes: Binary content of an empty DOCX file
"""
if not DOCX_AVAILABLE:
raise ImportError("python-docx is required for this strategy")
doc = Document()
doc.add_paragraph("This is a static certificate with no placeholders.")
doc_stream = io.BytesIO()
doc.save(doc_stream)
doc_stream.seek(0)
return doc_stream.read()
@composite
def docx_with_duplicate_placeholders(draw):
"""
Generate a DOCX file where the same placeholder appears multiple times.
Returns:
tuple: (docx_binary, list_of_unique_placeholders)
"""
if not DOCX_AVAILABLE:
raise ImportError("python-docx is required for this strategy")
doc = Document()
# Generate a few unique placeholders
num_unique = draw(st.integers(min_value=1, max_value=5))
unique_placeholders = [draw(placeholder_keys()) for _ in range(num_unique)]
# Add paragraphs with repeated placeholders
num_paragraphs = draw(st.integers(min_value=2, max_value=6))
for _ in range(num_paragraphs):
# Pick a random placeholder to repeat
placeholder = draw(st.sampled_from(unique_placeholders))
text = draw(st.text(max_size=50))
doc.add_paragraph(f"{text} {placeholder}")
doc_stream = io.BytesIO()
doc.save(doc_stream)
doc_stream.seek(0)
docx_binary = doc_stream.read()
return docx_binary, sorted(list(set(unique_placeholders)))
# ============================================================================
# Export all strategies
# ============================================================================
__all__ = [
'placeholder_keys',
'text_with_placeholders',
'docx_with_placeholders',
'placeholder_mappings',
'valid_mappings',
'participant_data',
'docx_with_mappings_and_data',
'empty_docx',
'docx_with_duplicate_placeholders',
]