Source code for trigdroid.api.config

"""Configuration classes for TrigDroid API."""

from typing import Optional, List, Dict, Any, Union
from dataclasses import dataclass, field
from pathlib import Path
import yaml

from ..core.enums import LogLevel
from ..exceptions import ConfigurationError


[docs] @dataclass class TestConfiguration: """Test configuration for TrigDroid API. This class provides a clean, type-safe interface for configuring TrigDroid tests programmatically. All parameters have sensible defaults for quick setup. Examples: # Minimal configuration config = TestConfiguration(package='com.example.app') # Full configuration config = TestConfiguration( package='com.example.app', device_id='emulator-5554', acceleration=5, gyroscope=3, battery_rotation=4, min_runtime=5, log_level=LogLevel.DEBUG, frida_hooks=True, install_dummy_apps=['com.dummy.app1', 'com.dummy.app2'] ) # Load from file config = TestConfiguration.from_yaml_file('config.yaml') # Convert to dictionary config_dict = config.to_dict() """ # Required parameters package: str = "" # Device configuration device_id: Optional[str] = None # Test duration min_runtime: int = 1 # minutes background_time: int = 0 # seconds # Sensor configuration acceleration: int = 0 # 0-10 elaborateness level gyroscope: int = 0 # 0-10 elaborateness level light: int = 0 # 0-10 elaborateness level pressure: int = 0 # 0-10 elaborateness level # Battery configuration battery_rotation: int = 0 # 0-4 elaborateness level # Network configuration wifi: Optional[bool] = None data: Optional[bool] = None bluetooth: Optional[bool] = None bluetooth_mac: Optional[str] = None # Application management install_dummy_apps: List[str] = field(default_factory=list) uninstall_apps: List[str] = field(default_factory=list) grant_permissions: List[str] = field(default_factory=list) revoke_permissions: List[str] = field(default_factory=list) # Frida configuration frida_hooks: bool = False frida_constants: Optional[str] = None adb_enabled: Optional[bool] = None uptime_offset: int = 0 # minutes to add to uptime # Geolocation and language geolocation: Optional[str] = None language: Optional[str] = None # System properties baseband: Optional[str] = None build_properties: Dict[str, str] = field(default_factory=dict) # Logging configuration log_level: LogLevel = LogLevel.INFO log_file: Optional[str] = None suppress_console_logs: bool = False extended_log_format: bool = False log_filter_include: List[str] = field(default_factory=list) log_filter_exclude: List[str] = field(default_factory=list) # Changelog configuration disable_changelog: bool = False changelog_file: str = "changelog.txt" # Advanced options interaction: bool = False # Enable UI interaction simulation no_unroot: bool = False
[docs] def __post_init__(self): """Validate configuration after initialization.""" self._validate()
def _validate(self) -> None: """Validate configuration values.""" errors = [] # Package name validation if not self.package: errors.append("Package name is required") elif self.package != "no_package": # Basic package name validation if not self.package.replace('.', '').replace('_', '').isalnum(): errors.append(f"Invalid package name format: {self.package}") # Range validations if not (0 <= self.min_runtime <= 1440): # Max 24 hours errors.append("min_runtime must be between 0 and 1440 minutes") if not (0 <= self.background_time <= 300): # Max 5 minutes errors.append("background_time must be between 0 and 300 seconds") # Sensor level validations for sensor in ['acceleration', 'gyroscope', 'light', 'pressure']: value = getattr(self, sensor) if not (0 <= value <= 10): errors.append(f"{sensor} must be between 0 and 10") # Battery rotation validation if not (0 <= self.battery_rotation <= 4): errors.append("battery_rotation must be between 0 and 4") # File path validations if self.log_file and not Path(self.log_file).parent.exists(): errors.append(f"Log file directory does not exist: {Path(self.log_file).parent}") if errors: raise ConfigurationError(f"Configuration validation failed: {'; '.join(errors)}")
[docs] def is_valid(self) -> bool: """Check if configuration is valid. Returns: True if valid, False otherwise """ try: self._validate() return True except ConfigurationError: return False
@property def validation_errors(self) -> List[str]: """Get list of validation errors. Returns: List of validation error messages """ try: self._validate() return [] except ConfigurationError as e: return str(e).split(': ', 1)[1].split('; ')
[docs] def to_dict(self) -> Dict[str, Any]: """Convert configuration to dictionary. Returns: Dictionary representation of configuration """ result = {} for field_name, field_def in self.__dataclass_fields__.items(): value = getattr(self, field_name) # Convert enums to their values if hasattr(value, 'value'): value = value.value # Skip default values for cleaner output if value != field_def.default and value != field_def.default_factory(): result[field_name] = value return result
[docs] def to_yaml(self, file_path: Optional[str] = None) -> str: """Convert configuration to YAML format. Args: file_path: Optional path to save YAML file Returns: YAML string representation """ yaml_str = yaml.safe_dump(self.to_dict(), default_flow_style=False, sort_keys=True) if file_path: Path(file_path).write_text(yaml_str) return yaml_str
[docs] @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'TestConfiguration': """Create configuration from dictionary. Args: data: Dictionary containing configuration values Returns: TestConfiguration instance """ # Convert string log levels to enum if 'log_level' in data and isinstance(data['log_level'], str): data['log_level'] = LogLevel(data['log_level']) # Handle missing fields with defaults valid_fields = {f.name for f in cls.__dataclass_fields__.values()} filtered_data = {k: v for k, v in data.items() if k in valid_fields} return cls(**filtered_data)
[docs] @classmethod def from_yaml_file(cls, file_path: str) -> 'TestConfiguration': """Load configuration from YAML file. Args: file_path: Path to YAML configuration file Returns: TestConfiguration instance Raises: ConfigurationError: If file cannot be loaded or parsed """ try: file_path_obj = Path(file_path) if not file_path_obj.exists(): raise ConfigurationError(f"Configuration file not found: {file_path}") with open(file_path_obj, 'r', encoding='utf-8') as f: data = yaml.safe_load(f) if not isinstance(data, dict): raise ConfigurationError(f"Invalid YAML format in {file_path}") return cls.from_dict(data) except yaml.YAMLError as e: raise ConfigurationError(f"Failed to parse YAML file {file_path}: {e}") except Exception as e: raise ConfigurationError(f"Failed to load configuration from {file_path}: {e}")
[docs] @classmethod def from_command_line(cls, args: List[str]) -> 'TestConfiguration': """Create configuration from command line arguments. Args: args: List of command line arguments Returns: TestConfiguration instance """ # This would integrate with the existing command line parser # For now, return a basic configuration # TODO: Implement full command line parsing return cls(package=args[0] if args else "")
[docs] def merge_with(self, other: 'TestConfiguration') -> 'TestConfiguration': """Merge this configuration with another, with other taking precedence. Args: other: Configuration to merge with Returns: New TestConfiguration instance with merged values """ merged_data = self.to_dict() other_data = other.to_dict() merged_data.update(other_data) return self.from_dict(merged_data)
[docs] def copy(self) -> 'TestConfiguration': """Create a copy of this configuration. Returns: New TestConfiguration instance """ return self.from_dict(self.to_dict())