Architecture Decision Records (ADRs)¶
This document records the key architectural decisions made in the Optics Framework, including the rationale, alternatives considered, and trade-offs. ADRs help understand why the framework is designed the way it is and provide context for future changes.
What are ADRs?¶
Architecture Decision Records (ADRs) are documents that capture important architectural decisions made during the development of the framework. Each ADR describes:
- Context: The situation and requirements
- Decision: What was decided
- Rationale: Why this decision was made
- Alternatives: Other options considered
- Consequences: Impact and trade-offs
ADR Format¶
Each ADR follows this structure:
## ADR-XXX: [Title]
**Status:** [Proposed | Accepted | Deprecated | Superseded]
**Context:**
[The situation and requirements]
**Decision:**
[What was decided]
**Rationale:**
[Why this decision was made]
**Alternatives Considered:**
[Other options that were evaluated]
**Consequences:**
[Positive and negative impacts]
Key Architectural Decisions¶
ADR-001: Linked List Structure for Test Execution Hierarchy¶
Status: Accepted
Context: The framework needs to represent test execution hierarchy (TestSuite → TestCase → Module → Keyword) in a way that supports:
- Sequential execution
- Dynamic modification
- State tracking per node
- Memory efficiency
Decision: Use a linked list structure with node classes (TestCaseNode, ModuleNode, KeywordNode) instead of nested lists or trees.
Rationale:
- Sequential Execution: Linked lists naturally represent sequential execution flow
- Memory Efficiency: No array overhead, only stores necessary links
- Dynamic Structure: Easy to add/remove nodes during execution
- State Tracking: Each node can track its own execution state independently
- Traversal Simplicity: Simple forward traversal matches execution flow
Alternatives Considered:
-
Nested Lists/Dictionaries:
-
Pros: Simple structure, easy to serialize
- Cons: Less memory efficient, harder to modify during execution
-
Rejected: Doesn't support dynamic modification well
-
Tree Structure:
-
Pros: Hierarchical representation
- Cons: More complex, overhead for parent/child relationships
-
Rejected: Overkill for sequential execution
-
Array/List with Indices:
-
Pros: Simple indexing
- Cons: Reallocation overhead, less flexible
- Rejected: Doesn't support dynamic structure well
Consequences:
- ✅ Efficient sequential execution
- ✅ Low memory overhead
- ✅ Easy to modify structure
- ✅ Natural state tracking
- ⚠️ No random access (must traverse)
- ⚠️ More complex serialization if needed
Implementation:
class TestCaseNode(Node):
modules_head: Optional[ModuleNode] = None
next: Optional['TestCaseNode'] = None
ADR-002: Factory Pattern with Dynamic Discovery¶
Status: Accepted
Context: The framework needs to support multiple drivers, element sources, and vision models that can be added without modifying core code. Components should be discoverable and instantiable based on configuration.
Decision: Use a factory pattern with dynamic module discovery and automatic registration. Factories scan package directories, discover implementations, and instantiate them based on configuration.
Rationale:
- Extensibility: New engines can be added without core code changes
- Automatic Discovery: No manual registration required
- Interface-Based: Components discovered by interface implementation
- Configuration-Driven: Selection based on configuration, not code
- Lazy Loading: Modules loaded only when needed
Alternatives Considered:
-
Manual Registration:
-
Pros: Explicit control, no discovery overhead
- Cons: Requires code changes for new engines
-
Rejected: Less extensible
-
Plugin System:
-
Pros: Standard plugin architecture
- Cons: More complex, requires plugin infrastructure
-
Rejected: Overkill for current needs
-
Dependency Injection Container:
-
Pros: Standard DI pattern
- Cons: More complex, requires container setup
- Rejected: Too heavyweight
Consequences:
- ✅ Highly extensible
- ✅ No code changes for new engines
- ✅ Automatic discovery
- ✅ Interface-based selection
- ⚠️ Discovery overhead on first use
- ⚠️ Requires consistent naming conventions
Implementation:
class GenericFactory:
@classmethod
def register_package(cls, package: str) -> None:
# Recursively discover modules
# Register module paths
ADR-003: Strategy Pattern for Element Location¶
Status: Accepted
Context: Elements can be located using multiple methods (XPath, text, OCR, image matching). The framework needs to try multiple strategies automatically until one succeeds (self-healing).
Decision: Use the Strategy pattern with a StrategyManager that tries multiple location strategies in priority order until one succeeds.
Rationale:
- Self-Healing: Automatic fallback to alternative methods
- Flexibility: Easy to add new location strategies
- Priority-Based: Fastest strategies tried first
- Separation of Concerns: Each strategy is independent
- Extensibility: New strategies can be added easily
Alternatives Considered:
-
Single Method with Internal Fallback:
-
Pros: Simpler implementation
- Cons: Harder to extend, less flexible
-
Rejected: Not extensible enough
-
Chain of Responsibility:
-
Pros: Standard pattern for fallback
- Cons: More complex, less explicit
-
Rejected: Strategy pattern is clearer
-
Template Method:
-
Pros: Code reuse
- Cons: Less flexible, harder to extend
- Rejected: Doesn't support multiple independent strategies well
Consequences:
- ✅ Self-healing test automation
- ✅ Easy to add new strategies
- ✅ Clear priority ordering
- ✅ Independent strategy implementations
- ⚠️ Multiple strategy attempts may be slower
- ⚠️ Requires strategy coordination
Implementation:
class StrategyManager:
def locate(self, element: str, index: int = 0):
for strategy in self.locator_strategies:
try:
result = strategy.locate(element, index)
if result is not None:
yield LocateResult(result, strategy)
except Exception:
continue
ADR-004: Fallback Parameter System¶
Status: Accepted
Context: Keywords need to support multiple fallback values for parameters (e.g., try multiple element identifiers). This should be automatic and transparent to users.
Decision: Implement a @fallback_params decorator that automatically tries all combinations of fallback parameter values until one succeeds.
Rationale:
- User-Friendly: Simple API, automatic fallback
- Flexible: Supports multiple fallback parameters
- Transparent: Works automatically without user intervention
- Error Aggregation: Collects errors from all attempts
- Type-Safe: Uses type hints for detection
Alternatives Considered:
-
Manual Fallback in Keywords:
-
Pros: Explicit control
- Cons: Code duplication, error-prone
-
Rejected: Too much boilerplate
-
Separate Fallback Keyword:
-
Pros: Explicit fallback
- Cons: More verbose, less intuitive
-
Rejected: Not user-friendly
-
Configuration-Based Fallback:
-
Pros: Centralized configuration
- Cons: Less flexible, harder to use
- Rejected: Too rigid
Consequences:
- ✅ Simple API for users
- ✅ Automatic fallback handling
- ✅ Supports multiple fallback parameters
- ✅ Error aggregation for debugging
- ⚠️ Exponential growth with multiple fallback params
- ⚠️ May try many combinations
Implementation:
@fallback_params
def press_element(self, element: fallback_str, ...):
# Automatically tries all combinations
ADR-005: Queue-Based Logging System¶
Status: Accepted
Context: Logging needs to be thread-safe, non-blocking, and support multiple logger instances (internal vs execution). Logging should not slow down test execution.
Decision: Use queue-based logging with QueueHandler and background listeners. Separate loggers for internal operations and execution events.
Rationale:
- Thread-Safe: Queues are thread-safe by design
- Non-Blocking: Log writes don't block execution
- Background Processing: Logs processed asynchronously
- Separation of Concerns: Different loggers for different purposes
- Performance: Doesn't impact execution speed
Alternatives Considered:
-
Direct Logging:
-
Pros: Simple, immediate
- Cons: Blocking, may slow execution
-
Rejected: Performance impact
-
File-Based Only:
-
Pros: Simple
- Cons: No console output, harder to debug
-
Rejected: Need console output for development
-
Single Logger:
-
Pros: Simpler
- Cons: Can't separate internal vs execution logs
- Rejected: Need separation for clarity
Consequences:
- ✅ Non-blocking execution
- ✅ Thread-safe logging
- ✅ Better performance
- ✅ Separated log streams
- ⚠️ More complex implementation
- ⚠️ Queue management overhead
Implementation:
execution_queue_handler = QueueHandler(self.execution_log_queue)
self.execution_logger.addHandler(self.execution_queue_handler)
ADR-006: Instance Caching in Factories¶
Status: Accepted
Context: Factory instantiation can be expensive. The same components may be requested multiple times. Need to balance performance with memory usage.
Decision: Cache factory instances by module name. Return cached instance if available, otherwise create and cache new instance.
Rationale:
- Performance: Reduces instantiation overhead
- Singleton Behavior: Ensures one instance per module name
- Memory Efficiency: Reuses instances instead of creating duplicates
- Simple: Easy to implement and understand
Alternatives Considered:
-
No Caching:
-
Pros: Simple, always fresh instances
- Cons: Performance overhead, potential duplicates
-
Rejected: Too slow for repeated access
-
Weak Reference Caching:
-
Pros: Automatic cleanup
- Cons: More complex, instances may be garbage collected
-
Rejected: Too complex for current needs
-
LRU Cache:
-
Pros: Bounded memory usage
- Cons: More complex, may evict needed instances
- Rejected: Overkill, instances should persist
Consequences:
- ✅ Faster instantiation after first use
- ✅ Singleton behavior per module
- ✅ Reduced memory allocation
- ⚠️ Instances persist in memory
- ⚠️ Manual cache clearing needed
Implementation:
ADR-007: Context Variables for Test Context¶
Status: Accepted
Context: Components need access to current test case name without explicit parameter passing. This should work across async operations and threads.
Decision: Use Python's contextvars module to provide thread-local and async-safe test context.
Rationale:
- Async-Safe: Automatically propagated to async tasks
- Thread-Safe: Each thread has its own context
- No Parameter Passing: Access context without explicit parameters
- Standard Library: Uses standard Python feature
- Isolation: Context isolated per execution context
Alternatives Considered:
-
Thread-Local Storage:
-
Pros: Simple
- Cons: Not async-safe, doesn't propagate to async tasks
-
Rejected: Doesn't work with async
-
Explicit Parameter Passing:
-
Pros: Explicit, clear
- Cons: Verbose, pollutes method signatures
-
Rejected: Too verbose
-
Global Variable:
-
Pros: Simple access
- Cons: Not thread-safe, race conditions
- Rejected: Not safe for concurrent execution
Consequences:
- ✅ Async-safe context propagation
- ✅ Thread-safe
- ✅ Clean API (no parameter passing)
- ✅ Standard library solution
- ⚠️ Requires Python 3.7+
- ⚠️ Context must be set explicitly
Implementation:
from contextvars import ContextVar
current_test_case: ContextVar[str] = ContextVar("current_test_case", default=None)
ADR-008: Self-Healing Decorator Pattern¶
Status: Accepted
Context: Action keywords need automatic element location with fallback strategies. This should be transparent to keyword implementations and handle errors gracefully.
Decision: Use a @with_self_healing decorator that wraps action methods to provide automatic element location, strategy fallback, and error handling.
Rationale:
- Separation of Concerns: Location logic separated from action logic
- Reusability: Same decorator for all action methods
- Transparency: Keyword implementations don't need location logic
- Error Handling: Centralized error handling and aggregation
- Screenshot Management: Automatic screenshot capture and saving
Alternatives Considered:
-
Location in Each Keyword:
-
Pros: Explicit control
- Cons: Code duplication, error-prone
-
Rejected: Too much duplication
-
Base Class with Location:
-
Pros: Code reuse
- Cons: Inheritance complexity, less flexible
-
Rejected: Decorator is more flexible
-
Separate Location Service:
-
Pros: Explicit service
- Cons: More verbose, requires manual calls
- Rejected: Decorator is cleaner
Consequences:
- ✅ Clean keyword implementations
- ✅ Reusable location logic
- ✅ Automatic error handling
- ✅ Screenshot management
- ⚠️ Decorator complexity
- ⚠️ May hide location failures
Implementation:
@with_self_healing
def press_element(self, element: str, ..., *, located: Any = None):
# located parameter provided by decorator
ADR-009: Percentage-Based AOI Coordinates¶
Status: Accepted
Context: Area of Interest (AOI) needs to work across different screen sizes and resolutions. Absolute pixel coordinates won't work for different devices.
Decision: Use percentage-based coordinates (0-100) for AOI parameters. Convert to pixel coordinates based on screenshot dimensions.
Rationale:
- Screen Size Agnostic: Works on any screen size
- Device Independent: Same percentages work on different devices
- Intuitive: Easy to understand (50% = half screen)
- Flexible: Supports any screen resolution
- Portable: Test cases work across devices
Alternatives Considered:
-
Absolute Pixel Coordinates:
-
Pros: Precise control
- Cons: Device-specific, doesn't scale
-
Rejected: Not portable
-
Normalized Coordinates (0-1):
-
Pros: Standard normalization
- Cons: Less intuitive than percentages
-
Rejected: Percentages are more intuitive
-
Relative Coordinates:
-
Pros: Relative to element
- Cons: More complex, requires reference element
- Rejected: Too complex
Consequences:
- ✅ Works on any screen size
- ✅ Device-independent
- ✅ Intuitive percentage values
- ✅ Portable tests
- ⚠️ Less precise than pixels
- ⚠️ Conversion overhead
Implementation:
def calculate_aoi_bounds(screenshot_shape, aoi_x, aoi_y, aoi_width, aoi_height):
# Convert percentages to pixels
x1 = int(width * (aoi_x / 100))
y1 = int(height * (aoi_y / 100))
ADR-010: Screenshot Streaming with Deduplication¶
Status: Accepted
Context: Timeout-based element location requires continuous screenshot capture. Need to avoid processing duplicate frames and manage memory efficiently.
Decision: Use queue-based screenshot streaming with SSIM-based deduplication. Capture screenshots in background thread, deduplicate using structural similarity, store in filtered queue.
Rationale:
- Non-Blocking: Capture doesn't block execution
- Deduplication: Reduces processing of similar frames
- Memory Management: Bounded queues prevent memory issues
- Efficiency: Only process unique frames
- Background Processing: Doesn't slow down execution
Alternatives Considered:
-
No Deduplication:
-
Pros: Simple
- Cons: Processes many duplicate frames
-
Rejected: Too inefficient
-
Hash-Based Deduplication:
-
Pros: Fast comparison
- Cons: Doesn't handle minor variations
-
Rejected: SSIM is more robust
-
Fixed Interval Capture:
-
Pros: Predictable
- Cons: May miss changes, inefficient
- Rejected: Less efficient than streaming
Consequences: - ✅ Efficient frame processing - ✅ Non-blocking capture - ✅ Memory bounded - ✅ Handles screen changes - ⚠️ SSIM computation overhead - ⚠️ Queue management complexity
Implementation:
class ScreenshotStream:
def process_screenshot_queue(self):
similarity = ssim(gray_last_frame, gray_frame)
if similarity >= 0.75:
# Skip duplicate
ADR-011: Multiple Logger Instances¶
Status: Accepted
Context: Framework needs to separate internal debugging logs from execution event logs. Different log levels and formats are needed for different purposes.
Decision: Use two separate logger instances: internal_logger for framework operations and execution_logger for test execution events.
Rationale:
- Separation of Concerns: Different logs for different purposes
- Different Formats: Internal logs can be more verbose
- Different Levels: Can set different log levels
- User Experience: Execution logs are user-facing
- Debugging: Internal logs help with framework debugging
Alternatives Considered:
-
Single Logger:
-
Pros: Simpler
- Cons: Can't separate concerns, mixed output
-
Rejected: Need separation
-
Logger Hierarchy:
-
Pros: Standard logging hierarchy
- Cons: More complex, propagation issues
-
Rejected: Two loggers are sufficient
-
Custom Logging System:
-
Pros: Full control
- Cons: More complex, reinventing wheel
- Rejected: Standard logging is better
Consequences:
- ✅ Clear separation of logs
- ✅ Different formats and levels
- ✅ Better user experience
- ✅ Easier debugging
- ⚠️ More complex configuration
- ⚠️ Two loggers to manage
Implementation:
internal_logger = logging.getLogger("optics.internal")
execution_logger = logging.getLogger("optics.execution")
ADR-012: InstanceFallback Wrapper¶
Status: Accepted
Context: When multiple drivers or element sources are configured, the framework should automatically try each one until one succeeds. This provides resilience and fallback capabilities.
Decision: Wrap multiple instances in an InstanceFallback class that automatically tries each instance on method calls until one succeeds.
Rationale:
- Automatic Fallback: No manual fallback logic needed
- Transparent: Works like a single instance
- Resilient: Continues working if one instance fails
- Simple API: Users don't need to handle fallback
- Flexible: Supports any number of instances
Alternatives Considered:
-
Manual Fallback in Code:
-
Pros: Explicit control
- Cons: Code duplication, error-prone
-
Rejected: Too much boilerplate
-
Proxy Pattern:
-
Pros: Standard pattern
- Cons: More complex, less transparent
-
Rejected: InstanceFallback is simpler
-
Configuration-Based Selection:
-
Pros: Explicit selection
- Cons: No automatic fallback
- Rejected: Need automatic fallback
Consequences:
- ✅ Automatic fallback
- ✅ Transparent usage
- ✅ Resilient to failures
- ✅ Simple API
- ⚠️ May hide failures
- ⚠️ Performance impact if many instances
Implementation:
class InstanceFallback:
def __getattr__(self, attr):
for instance in self.instances:
try:
return getattr(instance, attr)(*args, **kwargs)
except Exception:
continue
ADR-013: YAML and CSV Dual Format Support¶
Status: Accepted
Context: Users have different preferences for test data format. Some prefer CSV (spreadsheet-friendly), others prefer YAML (more structured). Framework should support both.
Decision: Support both CSV and YAML formats for test cases, modules, and elements. Use content-based file discovery to identify file types.
Rationale:
- User Choice: Supports different user preferences
- Flexibility: Users can choose best format for their needs
- Content-Based: Files identified by content, not just extension
- Merging: Multiple files of same type are merged
- Backward Compatible: Supports existing CSV-based projects
Alternatives Considered:
-
CSV Only:
-
Pros: Simpler, one format
- Cons: Less flexible, harder for complex data
-
Rejected: Too limiting
-
YAML Only:
-
Pros: More structured, better for complex data
- Cons: Less spreadsheet-friendly
-
Rejected: CSV is important for many users
-
JSON Support:
-
Pros: Standard format
- Cons: Less human-readable, another format to support
- Rejected: YAML is more readable
Consequences:
- ✅ Flexible format choice
- ✅ Content-based discovery
- ✅ File merging support
- ✅ Backward compatible
- ⚠️ More complex file reading
- ⚠️ Two formats to maintain
Implementation:
class CSVDataReader(DataReader):
def read_test_cases(self, source: str) -> TestCases:
# Read CSV
class YAMLDataReader(DataReader):
def read_test_cases(self, source: str) -> TestCases:
# Read YAML
ADR-014: Session-Based Architecture¶
Status: Accepted
Context: Framework needs to support multiple concurrent test executions, each with its own configuration, state, and resources. Need isolation between executions.
Decision: Use session-based architecture where each test execution has its own session with isolated configuration, drivers, and state.
Rationale:
- Isolation: Each session is independent
- Concurrency: Supports multiple concurrent executions
- Resource Management: Clear lifecycle for resources
- Configuration: Per-session configuration
- State Management: Session-scoped state
Alternatives Considered:
-
Global State:
-
Pros: Simpler
- Cons: No isolation, race conditions
-
Rejected: Not safe for concurrent execution
-
Thread-Local Storage:
-
Pros: Automatic isolation
- Cons: Not async-safe, less explicit
-
Rejected: Sessions are more explicit
-
Context Manager:
-
Pros: Automatic cleanup
- Cons: Less flexible, harder to share
- Rejected: Sessions need more flexibility
Consequences:
- ✅ Session isolation
- ✅ Concurrent execution support
- ✅ Clear resource lifecycle
- ✅ Per-session configuration
- ⚠️ Session management overhead
- ⚠️ Need to pass session_id
Implementation:
class SessionManager:
def create_session(self, config, ...) -> str:
session_id = str(uuid4())
session = Session(session_id, config, ...)
self.sessions[session_id] = session
return session_id
ADR-015: Builder Pattern for Component Construction¶
Status: Accepted
Context: Complex component hierarchies need to be constructed with proper dependency injection. Components have dependencies on each other (e.g., element sources need drivers).
Decision: Use Builder pattern (OpticsBuilder) to construct component hierarchies with automatic dependency injection.
Rationale:
- Complex Construction: Handles complex component setup
- Dependency Injection: Automatically injects dependencies
- Fluent API: Method chaining for readability
- Validation: Can validate configuration during construction
- Flexibility: Supports different component combinations
Alternatives Considered:
-
Direct Instantiation:
-
Pros: Simple, explicit
- Cons: Manual dependency management, error-prone
-
Rejected: Too error-prone
-
Factory Methods:
-
Pros: Encapsulates creation
- Cons: Less flexible, harder to extend
-
Rejected: Builder is more flexible
-
Dependency Injection Container:
-
Pros: Standard DI pattern
- Cons: More complex, requires container
- Rejected: Builder is simpler for this use case
Consequences:
- ✅ Handles complex construction
- ✅ Automatic dependency injection
- ✅ Fluent API
- ✅ Configuration validation
- ⚠️ More complex than direct instantiation
- ⚠️ Builder state management
Implementation:
builder = OpticsBuilder(session)
builder.add_driver(config)
builder.add_element_source(config)
driver = builder.get_driver()
Historical Context¶
Framework Evolution¶
The Optics Framework has evolved through several key phases:
Phase 1: Initial Design (Early Development)
- Focus on basic automation capabilities
- Single driver support (Appium)
- Simple element location (XPath only)
- CSV-based test cases
Phase 2: Vision Integration
- Added OCR and image matching capabilities
- Multiple location strategies
- Self-healing mechanism
- Template-based image location
Phase 3: Extensibility
- Factory pattern for dynamic discovery
- Multiple driver support
- Plugin architecture for engines
- Strategy pattern for location
Phase 4: API and CLI
- REST API layer
- CLI interface
- Session management
- Event system
Phase 5: Advanced Features
- Fallback parameters
- AOI support
- Screenshot streaming
- Performance optimizations
Design Principles Evolution¶
Early Principles:
- Simplicity
- Ease of use
- CSV-based (no code)
Current Principles:
- Modularity
- Extensibility
- Resilience (self-healing)
- a bit more focus on Performance
- Separation of concerns
Key Design Influences¶
- Robot Framework: Keyword-based approach, library pattern
- Selenium/Appium: Driver abstraction, element location
- Factory Pattern: Dynamic component creation
- Strategy Pattern: Multiple location methods
- Builder Pattern: Complex object construction
Breaking Changes and Migrations¶
None documented yet - Framework is still in active development.
Future ADRs should document:
- Breaking changes
- Migration guides
- Deprecation notices
- Version compatibility
Contributing ADRs¶
When making significant architectural decisions:
- Create ADR: Document the decision using the ADR format
- Number Sequentially: Use ADR-XXX format
- Update This Document: Add to the list above
- Review: Get team review before implementation
- Status Tracking: Update status as decision evolves
ADR Status Values¶
- Proposed: Decision under consideration
- Accepted: Decision made and implemented
- Deprecated: Decision replaced by newer ADR
- Superseded: Decision replaced by ADR-XXX
Related Documentation¶
- Architecture Overview - High-level architecture
- Components - Component implementations
- Strategies - Strategy pattern implementation
- Execution - Execution architecture
- Extending - Extension guidelines