"""
Test that DSL rendering is TRULY dynamic using only the ModuleWrapper.
This test proves that:
1. All symbols come from the wrapper (not hardcoded)
2. All component relationships come from the wrapper (not hardcoded)
3. Random valid structures can be generated and rendered
4. NO hardcoded component lists are needed
"""
import random
from typing import Any, Dict, List, Optional, Set
import pytest
def get_all_symbols_from_wrapper() -> Dict[str, str]:
"""Get ALL symbols dynamically from wrapper - no hardcoding."""
from gchat.card_framework_wrapper import get_gchat_symbols
symbols = get_gchat_symbols()
print(f"\nπ Loaded {len(symbols)} symbols from wrapper")
return symbols
def get_all_relationships_from_wrapper() -> Dict[str, List[str]]:
"""Get ALL parent->children relationships dynamically from wrapper."""
from gchat.card_framework_wrapper import get_component_relationships_for_dsl
relationships = get_component_relationships_for_dsl()
print(f"π Loaded {len(relationships)} parent->children relationships")
return relationships
def get_widget_components_from_wrapper() -> Set[str]:
"""Get all widget components (things that can be in a Section) from wrapper."""
relationships = get_all_relationships_from_wrapper()
# Section's children are the widget components
section_children = set(relationships.get("Section", []))
print(f"π Found {len(section_children)} widget components under Section")
return section_children
def get_component_class_from_wrapper(component_name: str):
"""Dynamically get a component class from the wrapper."""
from gchat.card_framework_wrapper import get_card_framework_wrapper
wrapper = get_card_framework_wrapper()
# Try multiple path patterns (component paths vary)
paths = [
f"card_framework.v2.widgets.{component_name.lower()}.{component_name}",
f"card_framework.v2.widgets.{component_name}",
f"card_framework.v2.{component_name.lower()}.{component_name}",
f"card_framework.v2.{component_name}",
f"card_framework.{component_name}",
]
for path in paths:
comp_class = wrapper.get_component_by_path(path)
if comp_class:
return comp_class
# Also try searching wrapper.components directly
for comp_path, comp in wrapper.components.items():
if comp_path.endswith(f".{component_name}") or comp_path.endswith(
f".{component_name.lower()}.{component_name}"
):
if hasattr(comp, "obj") and comp.obj:
return comp.obj
return None
def get_required_fields_from_class(comp_class) -> Dict[str, Any]:
"""Inspect a component class to find required fields."""
import dataclasses
import inspect
if not comp_class:
return {}
required = {}
# For dataclasses, check field defaults
if dataclasses.is_dataclass(comp_class):
for field in dataclasses.fields(comp_class):
# Fields without defaults are required
if (
field.default is dataclasses.MISSING
and field.default_factory is dataclasses.MISSING
):
# Generate a sensible default based on field name/type
if "name" in field.name.lower():
required[field.name] = f"auto_{field.name}"
elif "label" in field.name.lower():
required[field.name] = f"Auto Label"
elif "text" in field.name.lower():
required[field.name] = "Auto generated text"
elif "url" in field.name.lower():
required[field.name] = "https://example.com"
return required
def render_component_dynamically(component_name: str, index: int = 0) -> Optional[Dict]:
"""
Render ANY component dynamically using ONLY the wrapper.
No hardcoded defaults - inspects the class to find required fields.
"""
from gchat.card_framework_wrapper import get_card_framework_wrapper
wrapper = get_card_framework_wrapper()
comp_class = get_component_class_from_wrapper(component_name)
if not comp_class:
print(f" β οΈ No class found for {component_name}")
return None
# Get required fields by inspecting the class
required_params = get_required_fields_from_class(comp_class)
# Add index to name fields for uniqueness
for key in required_params:
if "name" in key.lower():
required_params[key] = f"{required_params[key]}_{index}"
try:
# Use wrapper's create_card_component
instance = wrapper.create_card_component(comp_class, required_params)
if instance and hasattr(instance, "render"):
rendered = instance.render()
print(f" β
{component_name} rendered successfully")
return rendered
elif instance and hasattr(instance, "to_dict"):
key = component_name[0].lower() + component_name[1:]
rendered = {key: instance.to_dict()}
print(f" β
{component_name} rendered via to_dict()")
return rendered
else:
print(f" β οΈ {component_name} instance has no render/to_dict")
return None
except Exception as e:
print(f" β {component_name} failed: {e}")
return None
def generate_random_valid_structure(max_widgets: int = 5) -> List[str]:
"""
Generate a random but VALID component structure using ONLY wrapper data.
Uses relationships to ensure parent-child validity.
"""
relationships = get_all_relationships_from_wrapper()
# Start with Section (standard card structure)
structure = ["Section"]
# Get valid children for Section
valid_widgets = relationships.get("Section", [])
if not valid_widgets:
print("β οΈ No valid widgets found for Section")
return structure
# Randomly pick some widgets
num_widgets = random.randint(1, min(max_widgets, len(valid_widgets)))
chosen = random.sample(valid_widgets, num_widgets)
for widget in chosen:
structure.append(widget)
# If this widget has children, maybe add some
widget_children = relationships.get(widget, [])
if widget_children and random.random() > 0.5:
# Add 1-3 children
num_children = random.randint(1, min(3, len(widget_children)))
for child in random.sample(widget_children, num_children):
structure.append(child)
return structure
def build_dsl_from_structure(structure: List[str]) -> str:
"""Build DSL notation from a component structure using wrapper symbols."""
symbols = get_all_symbols_from_wrapper()
# Simple flat DSL for now
dsl_parts = []
for comp in structure:
symbol = symbols.get(comp, comp[0]) # Fallback to first letter
dsl_parts.append(symbol)
# Wrap in Section if not already
if structure and structure[0] == "Section":
section_sym = symbols.get("Section", "Β§")
inner = ", ".join(dsl_parts[1:])
return f"{section_sym}[{inner}]"
return ", ".join(dsl_parts)
# =============================================================================
# TESTS
# =============================================================================
def test_wrapper_provides_all_symbols():
"""Verify wrapper provides symbol mappings without hardcoding."""
symbols = get_all_symbols_from_wrapper()
assert len(symbols) > 50, f"Expected 50+ symbols, got {len(symbols)}"
# Check some expected components exist (but don't hardcode the symbols!)
expected_components = ["Button", "Section", "DecoratedText", "Grid", "TextInput"]
for comp in expected_components:
assert comp in symbols, f"Missing symbol for {comp}"
print(f" {comp} -> {symbols[comp]}")
def test_wrapper_provides_relationships():
"""Verify wrapper provides parent->child relationships."""
relationships = get_all_relationships_from_wrapper()
assert (
len(relationships) > 5
), f"Expected 5+ relationships, got {len(relationships)}"
# Section should have children
assert "Section" in relationships, "Section not in relationships"
section_children = relationships["Section"]
assert (
len(section_children) > 5
), f"Section should have 5+ children, got {len(section_children)}"
print(f"\nSection children: {section_children}")
def test_can_get_component_classes_dynamically():
"""Verify we can get component classes from wrapper for widget symbols."""
symbols = get_all_symbols_from_wrapper()
# Focus on actual widget component names (not enums, not internal classes)
widget_names = [
"Button",
"ButtonList",
"DecoratedText",
"TextParagraph",
"Grid",
"Image",
"Divider",
"TextInput",
"DateTimePicker",
"SelectionInput",
"ChipList",
"Columns",
"Section",
]
found = 0
failed = []
for comp_name in widget_names:
if comp_name not in symbols:
continue
symbol = symbols[comp_name]
comp_class = get_component_class_from_wrapper(comp_name)
if comp_class:
found += 1
print(f" β
{symbol} -> {comp_name} -> {comp_class.__name__}")
else:
failed.append(comp_name)
print(f"\nπ Found {found}/{len(widget_names)} widget component classes")
if failed:
print(f"β οΈ Failed to find: {failed}")
assert found >= 10, f"Should find at least 10 widget component classes, got {found}"
def test_render_random_widget_components():
"""Test rendering random widget components using ONLY wrapper."""
widget_components = get_widget_components_from_wrapper()
# Try to render 10 random widgets
to_test = random.sample(list(widget_components), min(10, len(widget_components)))
print(f"\nπ² Testing {len(to_test)} random widgets:")
rendered = 0
for i, comp_name in enumerate(to_test):
result = render_component_dynamically(comp_name, i)
if result:
rendered += 1
print(f"\nπ Successfully rendered {rendered}/{len(to_test)} components")
# At least half should render
assert (
rendered >= len(to_test) // 2
), f"Too many render failures: {rendered}/{len(to_test)}"
def test_generate_and_render_random_structure():
"""Generate random valid structures and render them."""
print("\nπ² Generating 5 random card structures:")
successful = 0
for i in range(5):
structure = generate_random_valid_structure(max_widgets=4)
dsl = build_dsl_from_structure(structure)
print(f"\n Structure {i+1}: {structure}")
print(f" DSL: {dsl}")
# Try to render each widget in the structure
widgets_rendered = 0
for j, comp in enumerate(structure):
if comp == "Section":
continue
result = render_component_dynamically(comp, j)
if result:
widgets_rendered += 1
if widgets_rendered > 0:
successful += 1
print(f" β
Rendered {widgets_rendered} widgets")
else:
print(f" β No widgets rendered")
assert successful >= 3, f"At least 3/5 structures should render, got {successful}"
def test_prepared_pattern_with_dynamic_structure():
"""Test PreparedPattern with dynamically generated structure."""
from gchat.card_builder import PreparedPattern
from gchat.card_framework_wrapper import get_card_framework_wrapper
wrapper = get_card_framework_wrapper()
# Generate random structure
structure = generate_random_valid_structure(max_widgets=3)
print(f"\nπ² Random structure: {structure}")
# Create PreparedPattern
prepared = PreparedPattern(structure, wrapper)
# Set some generic params
prepared.set_params(
text="Dynamically generated content",
title="Dynamic Card",
)
# Get instances (without rendering)
instances = prepared.get_instances()
print(f"π Got {len(instances)} instances")
for inst in instances:
status = "β
" if inst["instance"] else "β οΈ"
print(f" {status} {inst['name']}: class={inst['class'] is not None}")
# Render
card = prepared.render()
assert "sections" in card, "Card should have sections"
print(f"\nβ
Card rendered with {len(card.get('sections', []))} sections")
def test_all_section_children_can_render():
"""Test that ALL components valid under Section can render."""
relationships = get_all_relationships_from_wrapper()
section_children = relationships.get("Section", [])
print(f"\nπ§ͺ Testing ALL {len(section_children)} Section children:")
results = {"success": [], "failed": []}
for i, comp_name in enumerate(section_children):
result = render_component_dynamically(comp_name, i)
if result:
results["success"].append(comp_name)
else:
results["failed"].append(comp_name)
print(f"\nπ Results:")
print(f" β
Success: {len(results['success'])} - {results['success']}")
print(f" β Failed: {len(results['failed'])} - {results['failed']}")
# Most should succeed
success_rate = (
len(results["success"]) / len(section_children) if section_children else 0
)
assert success_rate >= 0.5, f"At least 50% should render, got {success_rate:.0%}"
if __name__ == "__main__":
print("=" * 60)
print("DYNAMIC DSL RENDERING TESTS")
print("=" * 60)
test_wrapper_provides_all_symbols()
test_wrapper_provides_relationships()
test_can_get_component_classes_dynamically()
test_render_random_widget_components()
test_generate_and_render_random_structure()
test_prepared_pattern_with_dynamic_structure()
test_all_section_children_can_render()
print("\n" + "=" * 60)
print("β
ALL DYNAMIC TESTS PASSED")
print("=" * 60)