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 # Timeout and verbosity timeout: int = 300 # seconds verbose: bool = False
[docs] def __post_init__(self): """Run initial validation (non-raising).""" self._validation_errors: List[str] = [] try: self._validate() except ConfigurationError as e: self._validation_errors = str(e).split(': ', 1)[1].split('; ') if ': ' in str(e) else [str(e)]
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 """ return len(self._validation_errors) == 0
@property def validation_errors(self) -> List[str]: """Get list of validation errors. Returns: List of validation error messages """ return list(self._validation_errors) @property def sensors(self) -> List[str]: """Get list of enabled sensor types.""" sensor_map = { 'acceleration': 'accelerometer', 'gyroscope': 'gyroscope', 'light': 'light', 'pressure': 'pressure', } return [name for attr, name in sensor_map.items() if getattr(self, attr, 0) > 0] @property def network_states(self) -> List[str]: """Get list of enabled network state types.""" states = [] if self.wifi is not None and self.wifi: states.append('wifi') if self.data is not None and self.data: states.append('data') if self.bluetooth is not None and self.bluetooth: states.append('bluetooth') return states @property def has_sensor_manipulation(self) -> bool: """Check if any sensor manipulation is configured.""" return len(self.sensors) > 0 @property def has_network_manipulation(self) -> bool: """Check if any network manipulation is configured.""" return any(x is not None for x in [self.wifi, self.data, self.bluetooth])
[docs] def to_dict(self) -> Dict[str, Any]: """Convert configuration to dictionary. Returns: Dictionary representation of configuration """ from dataclasses import MISSING 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 default = field_def.default if default is MISSING and field_def.default_factory is not MISSING: default = field_def.default_factory() if default is not MISSING and value != default: 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] def to_yaml_file(self, file_path: str) -> None: """Save configuration to YAML file. Args: file_path: Path to save YAML file """ self.to_yaml(file_path=file_path)
[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())