diff --git a/CHANGELOG.md b/CHANGELOG.md index 58b62a3..c5d2884 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [1.2.0] 2022-06-17 + +### Features +- watcher service: + - allowing processes to be ignored through the mapping file: **watch.ignore** (must be located in `~/.config/guapow` or `/etc/guapow`) + - it follows the same patterns as the `watch.map` file, but the profile name is not required (as it makes no sense). e.g: + ``` + my_app_name + my_app_name* + /bin/my_proc + r:/bin/.+/xpto + ``` + - this feature is useful if you have general mappings that cover a lot of processes in `watch.map` (e.g: `/usr/bin/*`), but want to ignore specific ones + - new config property `ignored.cache` to cache all mapped patterns to memory after the first read and skip next I/O calls (default: `false`) + + ## [1.1.1] 2022-06-08 ### Fixes diff --git a/README.md b/README.md index 3bc9c93..595a896 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ - [Watcher service](#watcher) - [Mapping patterns](#watch_patterns) - [Built-in patterns (steam)](#watch_builtin) + - [Ignoring processes](#watch_ignore) - [Settings](#watch_settings) - [CLI](#cli) 7. [Improving optimizations timing](#improve_opt) @@ -517,13 +518,24 @@ makepkg -si - **Built-in patterns:** - Pre-defined words following the pattern `__word__` that are evaulated as a regex. - `__steam__`: defines a regex for any game launched through **Steam** (native and Proton). Example [here](#tutorial_steam). - + +- Ignoring processes: it is possible to define patterns to ignore specific processes through the file `~/.config/guapow/watch.ignore` (user) (or `/etc/guapow/watch.ignore` (system)). Preference: user > system (if running as **root**, system is the only option). + - this file follows the same mapping rules as `watch.map`, but you don't need to provide the profile names (as it makes no sense). e.g: + ``` + my_app_name + my_app_name* + /bin/my_proc + r:/bin/.+/xpto + ``` + - this feature is useful if you have general mappings that cover a lot of processes in `watch.map` (e.g: `/usr/bin/*`), but want to ignore specific ones + - Settings - Defined at the file `~/.config/guapow/watch.conf` (user) or `/etc/guapow/watch.conf` (system). Preference: user > system (if running as **root**, system is the only option). ``` interval = 1 (in seconds to check for new-born applications and request optimizations) regex.cache = true (caches pre-compiled regex mapping patterns in memory) - mapping.cache = false (if 'true', caches the all mapping in memory to skip I/O calls. Changes to the mapping file won't take effect. + mapping.cache = false (if 'true', caches the all mapping in memory to skip I/O calls. Changes to watch.map won't have effect until the service is restarted. + ignored.cache = false (if 'true', caches the all ignored patterns in memory to skip I/O calls. Changes to watch.ignore won't have effect until the service is restarted. ``` - Logs: diff --git a/guapow/__init__.py b/guapow/__init__.py index 1a1d9bf..80a1a44 100644 --- a/guapow/__init__.py +++ b/guapow/__init__.py @@ -2,4 +2,4 @@ ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) __app_name__ = 'guapow' -__version__ = '1.1.1' +__version__ = '1.2.0' diff --git a/guapow/service/watcher/config.py b/guapow/service/watcher/config.py index 80ca55b..524e82d 100644 --- a/guapow/service/watcher/config.py +++ b/guapow/service/watcher/config.py @@ -13,12 +13,15 @@ class ProcessWatcherConfig(FileModel): FILE_NAME = 'watch.conf' FILE_MAPPING = {'interval': ('check_interval', float, None), 'regex.cache': ('regex_cache', bool, True), - 'mapping.cache': ('mapping_cache', bool, True)} + 'mapping.cache': ('mapping_cache', bool, True), + 'ignored.cache': ('ignored_cache', bool, True)} - def __init__(self, regex_cache: Optional[bool], check_interval: Optional[float], mapping_cache: Optional[bool]): + def __init__(self, regex_cache: Optional[bool], check_interval: Optional[float], mapping_cache: Optional[bool], + ignored_cache: Optional[bool]): self.check_interval = check_interval self.regex_cache = regex_cache self.mapping_cache = mapping_cache + self.ignored_cache = ignored_cache def get_output_name(self) -> str: pass @@ -29,7 +32,8 @@ def get_file_mapping(self) -> Dict[str, Tuple[str, type, Optional[object]]]: def is_valid(self) -> bool: return all((self.is_check_interval_valid(), self.regex_cache is not None, - self.mapping_cache is not None)) + self.mapping_cache is not None, + self.ignored_cache is not None)) def is_check_interval_valid(self): return self.check_interval is not None and self.check_interval > 0 @@ -44,12 +48,15 @@ def setup_valid_properties(self): if self.mapping_cache is None: self.mapping_cache = False + if self.ignored_cache is None: + self.ignored_cache = False + def get_file_root_node_name(self) -> Optional[str]: pass @classmethod def empty(cls) -> "ProcessWatcherConfig": - return cls(None, None, None) + return cls(None, None, None, None) @classmethod def default(cls) -> "ProcessWatcherConfig": diff --git a/guapow/service/watcher/core.py b/guapow/service/watcher/core.py index 4f8e6c9..fe5d78c 100644 --- a/guapow/service/watcher/core.py +++ b/guapow/service/watcher/core.py @@ -1,15 +1,16 @@ import asyncio import re import time +from asyncio import create_task from logging import Logger -from typing import Dict, Optional, Tuple +from typing import Dict, Optional, Tuple, Set, Any, Union from guapow.common import network from guapow.common.config import OptimizerConfig from guapow.common.dto import OptimizationRequest -from guapow.service.watcher import mapping +from guapow.service.watcher import mapping, ignored from guapow.service.watcher.config import ProcessWatcherConfig -from guapow.service.watcher.mapping import RegexMapper +from guapow.service.watcher.patterns import RegexMapper, RegexType from guapow.service.watcher.util import map_processes @@ -17,7 +18,8 @@ class ProcessWatcherContext: def __init__(self, user_id: int, user_name: str, user_env: Dict[str, str], logger: Logger, optimized: Dict[int, str], opt_config: OptimizerConfig, watch_config: ProcessWatcherConfig, - mapping_file_path: str, machine_id: Optional[str]): + mapping_file_path: str, machine_id: Optional[str], ignored_file_path: str, + ignored_procs: Dict[Union[str, re.Pattern], Set[str]]): self.user_id = user_id self.user_name = user_name self.user_env = user_env @@ -27,6 +29,8 @@ def __init__(self, user_id: int, user_name: str, user_env: Dict[str, str], logge self.watch_config = watch_config self.mapping_file_path = mapping_file_path self.machine_id = machine_id + self.ignored_file_path = ignored_file_path + self.ignored_procs = ignored_procs class ProcessWatcher: @@ -41,17 +45,57 @@ def __init__(self, regex_mapper: RegexMapper, context: ProcessWatcherContext): self._mappings: Optional[Dict[str, str]] = None self._cmd_patterns: Optional[Dict[re.Pattern, str]] = None self._comm_patterns: Optional[Dict[re.Pattern, str]] = None - self._last_file_found_log: Optional[bool] = None # controls repetitive file found logs + + self._last_mapping_file_found: Optional[bool] = None # controls repetitive file found logs + self._last_ignored_file_found: Optional[bool] = None # controls repetitive file found logs + + self._ignored_cached = False + self._ignored_exact_strs: Optional[Set[str]] = None + self._ignored_cmd_patterns: Optional[Set[re.Pattern]] = None + self._ignored_comm_patterns: Optional[Set[re.Pattern]] = None + + async def _read_ignored(self) -> Optional[Tuple[Set[str], Optional[Set[re.Pattern]], Optional[Set[re.Pattern]]]]: + """ + return a tuple with command patterns (cmd) and name patterns (comm) + """ + if self._ignored_cached: + if self._ignored_exact_strs: + return self._ignored_exact_strs, self._ignored_cmd_patterns, self._ignored_comm_patterns + else: + file_found, ignored_strs = await ignored.read(file_path=self._context.ignored_file_path, logger=self._log, + last_file_found_log=self._last_ignored_file_found) + self._last_ignored_file_found = file_found + + if not self._ignored_cached and self._context.watch_config.ignored_cache: + self._log.debug("Caching ignored patterns to memory") + self._ignored_cached = True # pre-saving the caching state (if enabled) + + if ignored_strs: + if self._context.watch_config.ignored_cache: # caching to memory (if enabled) + self._ignored_exact_strs = ignored_strs + + patterns = self._regex_mapper.map_collection(ignored_strs) + + cmd_patterns, comm_patterns = None, None + + if patterns: + cmd_patterns, comm_patterns = patterns.get(RegexType.CMD), patterns.get(RegexType.COMM) + + if self._context.watch_config.ignored_cache: # caching to memory (if enabled) + self._ignored_cmd_patterns = cmd_patterns + self._ignored_comm_patterns = comm_patterns + + return ignored_strs, cmd_patterns, comm_patterns async def _read_mappings(self) -> Optional[Tuple[Dict[str, str], Optional[Dict[re.Pattern, str]], Optional[Dict[re.Pattern, str]]]]: if self._mapping_cached: if self._mappings: return self._mappings, self._cmd_patterns, self._comm_patterns else: - file_found, mappings = await mapping.read(file_path=self._context.mapping_file_path, logger=self._log, last_file_found_log=self._last_file_found_log) - self._last_file_found_log = file_found + file_found, mappings = await mapping.read(file_path=self._context.mapping_file_path, logger=self._log, last_file_found_log=self._last_mapping_file_found) + self._last_mapping_file_found = file_found - pattern_mappings = self._regex_mapper.map(mappings) + pattern_mappings = self._regex_mapper.map_for_profiles(mappings) cmd_patterns, comm_patterns = (pattern_mappings[0], pattern_mappings[1]) if pattern_mappings else (None, None) if self._context.watch_config.mapping_cache: @@ -60,6 +104,67 @@ async def _read_mappings(self) -> Optional[Tuple[Dict[str, str], Optional[Dict[r return mappings, cmd_patterns, comm_patterns + def _map_ignored_id(self, pid: int, comm: str) -> str: + return f'{pid}:{comm}' + + def _is_ignored(self, ignored_id: str) -> bool: + if self._context.ignored_procs: + for ignored_ids in self._context.ignored_procs.values(): + if ignored_id in ignored_ids: + return True + + return False + + def _clean_old_ignore_patterns(self, ignored_exact: Set[str], current_cmd_patterns: Optional[Set[re.Pattern]], + current_comm_patterns: Optional[Set[re.Pattern]]): + if self._context.ignored_procs: + all_patterns = (ignored_exact, current_cmd_patterns, current_comm_patterns) + to_remove = set() + for pattern in self._context.ignored_procs: + found = False + for patterns in all_patterns: + if patterns and pattern in patterns: + found = True + break + + if not found: + to_remove.add(pattern) + + if to_remove: + self._log.debug(f"Cleaning old ignored patterns from context: {', '.join(str(p) for p in to_remove)}") + for pattern in to_remove: + del self._context.ignored_procs[pattern] + + def _matches_ignored(self, cmd_com: Tuple[str, str], ignored_exact: Set[str], + cmd_patterns: Optional[Set[re.Pattern]], comm_patterns: Optional[Set[re.Pattern]], + ignored_id: str) -> bool: + + if ignored_exact or cmd_patterns or comm_patterns: + for idx, cmd in enumerate(cmd_com): # 0: cmd, 1: comm + matched_pattern = None + if ignored_exact and cmd in ignored_exact: # exact matches have higher priority than patterns + matched_pattern = cmd + else: + regex_ignored = cmd_patterns if idx == 0 else comm_patterns + + if regex_ignored: + for pattern in regex_ignored: + if pattern.match(cmd): + matched_pattern = pattern + break + + if matched_pattern: + ignored_ids = self._context.ignored_procs.get(matched_pattern) + + if ignored_ids is None: + ignored_ids = set() + self._context.ignored_procs[matched_pattern] = ignored_ids + + ignored_ids.add(ignored_id) + return True + + return False + async def check_mappings(self): mapping_tuple = await self._read_mappings() @@ -67,21 +172,40 @@ async def check_mappings(self): return mappings, cmd_patterns, comm_patterns = mapping_tuple[0], mapping_tuple[1], mapping_tuple[2] - - procs = await map_processes() + + task_map_procs, task_ignored = create_task(map_processes()), create_task(self._read_ignored()) + + procs = await task_map_procs if not procs: self._log.warning('No processes alive') self._context.optimized.clear() return + ignored_exact, ignored_cmd, ignored_comm = None, None, None + ignored_patterns = await task_ignored + + if ignored_patterns: + ignored_exact, ignored_cmd, ignored_comm = ignored_patterns + + self._clean_old_ignore_patterns(ignored_exact, ignored_cmd, ignored_comm) + tasks = [] for pid, cmd_comm in procs.items(): + ignored_id = self._map_ignored_id(pid, cmd_comm[1]) + + if self._is_ignored(ignored_id): + continue + previously_optimized_cmd = self._context.optimized.get(pid) if previously_optimized_cmd and (previously_optimized_cmd == cmd_comm[0] or previously_optimized_cmd == cmd_comm[1]): continue + if self._matches_ignored(cmd_comm, ignored_exact, ignored_cmd, ignored_comm, ignored_id): + self._log.info(f"Ignoring process (pid: {pid}, name: {cmd_comm[1]})") + continue + for idx, cmd in enumerate(cmd_comm): # 0: cmd, 1: comm profile = mappings.get(cmd) # exact matches have higher priority than patterns if profile: @@ -104,12 +228,35 @@ async def check_mappings(self): if tasks: await asyncio.gather(*tasks) - if self._context.optimized: # removing dead processes from the context - pids_alive = procs.keys() + self._clean_dead_processes_from_context(procs) + + def _clean_dead_processes_from_context(self, current_processes: Dict[int, Any]): + if self._context.optimized: + pids_alive = current_processes.keys() for pid in {*self._context.optimized.keys()}: if pid not in pids_alive: del self._context.optimized[pid] + if self._context.ignored_procs: + pids_alive = current_processes.keys() + patterns_to_remove = set() + for pattern, ignored_ids in self._context.ignored_procs.items(): + to_remove = set() + for id_ in ignored_ids: + if int(id_.split(':')[0]) not in pids_alive: + to_remove.add(id_) + + if to_remove: + self._log.debug(f"Removing dead pids from ignored context: {', '.join(f'{p}' for p in to_remove)}") + ignored_ids.difference_update(to_remove) + + if not ignored_ids: + patterns_to_remove.add(pattern) + + if patterns_to_remove: + for pattern in patterns_to_remove: + del self._context.ignored_procs[pattern] + async def send_request(self, pid: int, command: str, match: str, profile: str): request = OptimizationRequest(pid=pid, command=command, created_at=time.time(), user_name=self._context.user_name, user_env=self._context.user_env, diff --git a/guapow/service/watcher/ignored.py b/guapow/service/watcher/ignored.py new file mode 100644 index 0000000..56d48d2 --- /dev/null +++ b/guapow/service/watcher/ignored.py @@ -0,0 +1,67 @@ +import os +from logging import Logger +from typing import Optional, Set, Tuple + +import aiofiles + +from guapow import __app_name__ +from guapow.common.users import is_root_user + +FILE_NAME = 'watch.ignore' + + +def get_file_path(user_id: int, user_name: str) -> Optional[str]: + if is_root_user(user_id): + return f'/etc/{__app_name__}/{FILE_NAME}' + else: + return f'/home/{user_name}/.config/{__app_name__}/{FILE_NAME}' + + +def get_existing_file_path(user_id: int, user_name: str, logger: Logger) -> Optional[str]: + file_path = get_file_path(user_id, user_name) + if os.path.isfile(file_path): + logger.info(f"Ignored file '{file_path}' found") + return file_path + else: + logger.warning(f"Ignored file '{file_path}' not found") + return get_existing_file_path(0, 'root', logger) if not is_root_user(user_id) else None + + +def get_default_file_path(user_id: int, user_name: str, logger: Logger) -> str: + file_path = get_existing_file_path(user_id, user_name, logger) + + if file_path: + return file_path + + default_path = get_file_path(user_id, user_name) + logger.info(f'Considering the default ignored file path for current user: {default_path}') + return default_path + + +async def read(file_path: str, logger: Logger, last_file_found_log: Optional[bool]) -> Tuple[bool, Optional[Set[str]]]: + try: + async with aiofiles.open(file_path) as f: + ignored_str = (await f.read()).strip() + + if not last_file_found_log: + logger.info(f"Ignore file '{file_path}' found") + + except FileNotFoundError: + if last_file_found_log is None or last_file_found_log is True: + logger.debug(f"Ignore file '{file_path}' not found") + + return False, None + + ignored = set() + + for string in ignored_str.split('\n'): + clean_string = string.strip() + + if clean_string: + sharp_less = clean_string.split('#') + final_str = sharp_less[0].strip() + + if final_str: + ignored.add(final_str) + + return True, ignored if ignored else None diff --git a/guapow/service/watcher/main.py b/guapow/service/watcher/main.py index 8910ad8..75d59c1 100644 --- a/guapow/service/watcher/main.py +++ b/guapow/service/watcher/main.py @@ -8,10 +8,10 @@ from guapow.common.config import read_optimizer_config from guapow.common.log import new_logger, get_log_level from guapow.common.model_util import FileModelFiller -from guapow.service.watcher import mapping +from guapow.service.watcher import mapping, ignored from guapow.service.watcher.config import ProcessWatcherConfig, ProcessWatcherConfigReader from guapow.service.watcher.core import ProcessWatcherContext, ProcessWatcher -from guapow.service.watcher.mapping import RegexMapper +from guapow.service.watcher.patterns import RegexMapper def is_log_enabled() -> bool: @@ -58,13 +58,17 @@ async def watch(): context = ProcessWatcherContext(user_id=os.getuid(), user_name=getpass.getuser(), user_env=user_env, logger=logger, optimized={}, opt_config=opt_config, watch_config=watch_config, - mapping_file_path=mapping.get_default_file_path(user_id, user_name, logger), machine_id=machine_id) + mapping_file_path=mapping.get_default_file_path(user_id, user_name, logger), + machine_id=machine_id, + ignored_file_path=ignored.get_default_file_path(user_id, user_name, logger), + ignored_procs=dict()) regex_mapper = RegexMapper(cache=watch_config.regex_cache, logger=logger) watcher = ProcessWatcher(regex_mapper, context) logger.info(f'Requests encryption: {str(machine_id is not None).lower()}') logger.info(f'Regex cache: {str(watch_config.regex_cache).lower()}') logger.info(f'Mapping cache: {str(watch_config.mapping_cache).lower()}') + logger.info(f'Ignored cache: {str(watch_config.ignored_cache).lower()}') logger.info(f'Checking processes every {watch_config.check_interval} second(s)') tf = time.time() diff --git a/guapow/service/watcher/mapping.py b/guapow/service/watcher/mapping.py index 7e14f11..d2bf8fb 100644 --- a/guapow/service/watcher/mapping.py +++ b/guapow/service/watcher/mapping.py @@ -1,18 +1,14 @@ import os -import re from logging import Logger -from typing import Optional, Dict, Pattern, Tuple, Set +from typing import Optional, Dict, Tuple import aiofiles from guapow import __app_name__ -from guapow.common import steam from guapow.common.profile import get_default_profile_name from guapow.common.users import is_root_user -from guapow.common.util import map_only_any_regex, has_any_regex FILE_NAME = 'watch.map' -RE_PYTHON_REGEX = re.compile(r'^r:(.+)$') def get_root_file_path() -> str: @@ -94,81 +90,3 @@ def map_string(string: str) -> Optional[Dict[str, str]]: if mappings: return mappings - - -class RegexMapper: - - RE_TYPE_CMD, RE_TYPE_COMM = 0, 1 - - BUILTIN_RE = {'__steam__': (steam.RE_STEAM_CMD, RE_TYPE_CMD)} - - def __init__(self, cache: bool, logger: Logger): - self._pattern_cache: Optional[dict] = None - self._no_pattern_cache: Optional[Set] = None - - if cache: - self._pattern_cache, self._no_pattern_cache = {}, set() - - self._log = logger - - def get_cached_pattern(self, str_pattern: str) -> Optional[re.Pattern]: - if self._pattern_cache is not None: - return self._pattern_cache.get(str_pattern) - - def is_no_pattern_string_cached(self, string: str) -> bool: - return string in self._no_pattern_cache if self._no_pattern_cache is not None else False - - def map(self, mapping: Dict[str, str]) -> Optional[Tuple[Dict[Pattern, str], Dict[Pattern, str]]]: - """ - return: a tuple with two dictionaries: first with cmd patterns and second with comm patterns. - """ - if mapping: - cmd, comm = {}, {} - - for string, prof in mapping.items(): - builtin_pattern = self.BUILTIN_RE.get(string) - - if builtin_pattern: - if builtin_pattern[1] == self.RE_TYPE_CMD: - cmd[builtin_pattern[0]] = prof - elif builtin_pattern[1] == self.RE_TYPE_COMM: - comm[builtin_pattern[0]] = prof - else: - self._log.error(f"Unknown type of built-in pattern '{string}'. It will be ignored.") - - continue - - if self.is_no_pattern_string_cached(string): - continue - - pattern = self.get_cached_pattern(string) - cached = bool(pattern) - - if not cached: - python_regex = RE_PYTHON_REGEX.findall(string) - - if python_regex: - try: - pattern = re.compile('^{}$'.format(python_regex[0])) - except re.error: - self._log.warning(f'Invalid Python regex mapping: {string}') - continue - - elif has_any_regex(string): - pattern = map_only_any_regex(string) - else: - pattern = None - - if pattern: - if pattern.pattern[1] == '/': - cmd[pattern] = prof - else: - comm[pattern] = prof - - if not cached and self._pattern_cache is not None: - self._pattern_cache[string] = pattern - - elif self._no_pattern_cache is not None: - self._no_pattern_cache.add(string) - - return cmd, comm diff --git a/guapow/service/watcher/patterns.py b/guapow/service/watcher/patterns.py new file mode 100644 index 0000000..28803c2 --- /dev/null +++ b/guapow/service/watcher/patterns.py @@ -0,0 +1,128 @@ +import re +from enum import Enum +from logging import Logger +from re import Pattern +from typing import Optional, Set, Tuple, Dict, Collection + +from guapow.common import steam +from guapow.common.util import map_only_any_regex, has_any_regex + +RE_PYTHON_REGEX = re.compile(r'^r:(.+)$') + + +class RegexType(Enum): + CMD = 0 + COMM = 1 + + +class RegexMapper: + + BUILTIN_RE = {'__steam__': (steam.RE_STEAM_CMD, RegexType.CMD)} + + def __init__(self, cache: bool, logger: Logger): + self._pattern_cache: Optional[dict] = None + self._string_no_pattern_cache: Optional[Set[str]] = None + + if cache: + self._pattern_cache, self._string_no_pattern_cache = {}, set() + + self._log = logger + + def get_cached_pattern(self, str_pattern: str) -> Optional[Pattern]: + if self._pattern_cache is not None: + return self._pattern_cache.get(str_pattern) + + def is_cached_as_no_pattern(self, string: str) -> bool: + return string in self._string_no_pattern_cache if self._string_no_pattern_cache is not None else False + + def map(self, string: str) -> Optional[Tuple[Pattern, RegexType]]: + builtin_pattern = self.BUILTIN_RE.get(string) + + if builtin_pattern: + if builtin_pattern[1] == RegexType.CMD: + return builtin_pattern[0], RegexType.CMD + + if builtin_pattern[1] == RegexType.COMM: + return builtin_pattern[0], RegexType.COMM + + self._log.error(f"Unknown type of built-in pattern '{string}'. It will be ignored.") + return + + if self.is_cached_as_no_pattern(string): + return + + pattern = self.get_cached_pattern(string) + cached = bool(pattern) + + if not cached: + python_regex = RE_PYTHON_REGEX.findall(string) + + if python_regex: + try: + pattern = re.compile('^{}$'.format(python_regex[0])) + except re.error: + self._log.warning(f'Invalid Python regex mapping: {string}') + return + + elif has_any_regex(string): + pattern = map_only_any_regex(string) + else: + pattern = None + + if pattern: + type_ = RegexType.CMD if pattern.pattern[1] == '/' else RegexType.COMM + + if not cached and self._pattern_cache is not None: + self._pattern_cache[string] = pattern + + return pattern, type_ + + elif self._string_no_pattern_cache is not None: + self._string_no_pattern_cache.add(string) + + def map_for_profiles(self, mapping: Dict[str, str]) -> Optional[Tuple[Dict[Pattern, str], Dict[Pattern, str]]]: + """ + return: a tuple with two dictionaries: first with cmd patterns and second with comm patterns. + """ + if mapping: + cmd, comm = {}, {} + + for string, prof in mapping.items(): + string_mapping = self.map(string) + + if string_mapping: + pattern, type_ = string_mapping + + if type_ == RegexType.CMD: + cmd[pattern] = prof + elif type_ == RegexType.COMM: + comm[pattern] = prof + elif type_: + self._log.error(f"Could not assign pattern ({string}) to profile ({prof}). " + f"Unsupported {RegexType.__class__.__name__} ({type_})") + else: + self._log.error(f"No {RegexType.__class__.__name__} returned when mapping pattern ({string})" + f" to profile ({prof})") + + return cmd, comm + + def map_collection(self, strings: Collection[str]) -> Optional[Dict[RegexType, Set[Pattern]]]: + if strings: + res: Dict[RegexType, Set[Pattern]] = {type_: None for type_ in RegexType} + for string in strings: + string_mapping = self.map(string) + if string_mapping: + pattern, type_ = string_mapping + + if type_: + patterns = res.get(type_) + + if patterns is None: + patterns = set() + res[type_] = patterns + + patterns.add(pattern) + else: + self._log.error(f"No {RegexType.__class__.__name__} returned when mapping pattern ({string})") + + return res diff --git a/tests/resources/empty.ignore b/tests/resources/empty.ignore new file mode 100644 index 0000000..e69de29 diff --git a/tests/resources/watch_ignored_cache.conf b/tests/resources/watch_ignored_cache.conf new file mode 100644 index 0000000..365b181 --- /dev/null +++ b/tests/resources/watch_ignored_cache.conf @@ -0,0 +1 @@ +ignored.cache=1 diff --git a/tests/resources/with_comments.ignore b/tests/resources/with_comments.ignore new file mode 100644 index 0000000..1a9101f --- /dev/null +++ b/tests/resources/with_comments.ignore @@ -0,0 +1,4 @@ +#xpto +abc#=tralala +ijk + # vlc#media diff --git a/tests/service/watcher/test_config.py b/tests/service/watcher/test_config.py index 204568d..5747b06 100644 --- a/tests/service/watcher/test_config.py +++ b/tests/service/watcher/test_config.py @@ -57,6 +57,16 @@ def test_is_valid__true_when_mapping_cache_is_not_none(self): instance.mapping_cache = False self.assertTrue(instance.is_valid()) + def test_is_valid__false_when_ignored_cache_is_none(self): + instance = ProcessWatcherConfig.default() + instance.ignored_cache = None + self.assertFalse(instance.is_valid()) + + def test_is_valid__true_when_ignored_cache_is_not_none(self): + instance = ProcessWatcherConfig.default() + instance.ignored_cache = False + self.assertTrue(instance.is_valid()) + def test_setup_valid_properties__must_set_check_interval_to_1_when_invalid(self): instance = ProcessWatcherConfig.empty() instance.check_interval = -1 @@ -75,6 +85,12 @@ def test_setup_valid_properties__must_set_mapping_cache_to_false_when_invalid(se instance.setup_valid_properties() self.assertEqual(False, instance.mapping_cache) + def test_setup_valid_properties__must_set_ignored_cache_to_false_when_invalid(self): + instance = ProcessWatcherConfig.empty() + self.assertIsNone(instance.ignored_cache) + instance.setup_valid_properties() + self.assertEqual(False, instance.ignored_cache) + @patch(f'{__app_name__}.service.watcher.config.os.path.isfile', return_value=True) def test_get_by_path_by_user__return_etc_path_for_root_user_when_exists(self, isfile: Mock): exp_path = f'/etc/{__app_name__}/watch.conf' @@ -142,6 +158,11 @@ def test_read_valid__must_return_an_instance_with_valid_mapping_cache_defined(se self.assertIsInstance(instance, ProcessWatcherConfig) self.assertTrue(instance.mapping_cache) + def test_read_valid__must_return_an_instance_with_valid_ignored_cache_defined(self): + instance = self.reader.read_valid(f'{RESOURCES_DIR}/watch_ignored_cache.conf') + self.assertIsInstance(instance, ProcessWatcherConfig) + self.assertTrue(instance.ignored_cache) + def test_read_valid__must_return_default_config_when_no_path_is_defined(self): instance = self.reader.read_valid(None) self.assertIsInstance(instance, ProcessWatcherConfig) diff --git a/tests/service/watcher/test_core.py b/tests/service/watcher/test_core.py index db0dc8d..8fb2244 100644 --- a/tests/service/watcher/test_core.py +++ b/tests/service/watcher/test_core.py @@ -7,7 +7,7 @@ from guapow.common.dto import OptimizationRequest from guapow.service.watcher.config import ProcessWatcherConfig from guapow.service.watcher.core import ProcessWatcher, ProcessWatcherContext -from guapow.service.watcher.mapping import RegexMapper +from guapow.service.watcher.patterns import RegexMapper class ProcessWatcherTest(IsolatedAsyncioTestCase): @@ -15,45 +15,54 @@ class ProcessWatcherTest(IsolatedAsyncioTestCase): def setUp(self): self.context = ProcessWatcherContext(user_id=1, user_name='xpto', user_env={'a': '1'}, logger=Mock(), mapping_file_path='test.map', optimized={}, opt_config=OptimizerConfig.default(), - watch_config=ProcessWatcherConfig.default(), machine_id='abc126517ha') + watch_config=ProcessWatcherConfig.default(), machine_id='abc126517ha', + ignored_procs=dict(), ignored_file_path='test.ignore') self.watcher = ProcessWatcher(regex_mapper=RegexMapper(cache=False, logger=Mock()), context=self.context) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(False, None)) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes') - async def test_check_mappings__must_not_map_processes_when_no_mapping_is_found(self, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_not_map_processes_when_no_mapping_is_found(self, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_not_called() map_processes.assert_not_called() @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'abc': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={}) - async def test_check_mappings__must_clear_the_optimized_context_when_no_process_could_be_retrieved(self, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_clear_the_optimized_context_when_no_process_could_be_retrieved(self, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): self.context.optimized.update({1: 'abc', 2: 'def'}) await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() self.assertEqual({}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'abc': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={123: ('/bin/def', 'def')}) @patch(f'{__app_name__}.service.watcher.core.network.send') - async def test_check_mappings__must_no_perform_any_request_in_case_of_no_mapping_matches(self, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_no_perform_any_request_in_case_of_no_mapping_matches(self, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() send_async.assert_not_called() self.assertEqual({}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a': 'default', 'b': 'prof_1', '/bin/c': 'prof_2'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/bin/a', 'a'), 2: ('/bin/b', 'b'), 3: ('/bin/c', 'c'), 4: ('/bin/d', 'd')}) # no profile for this process @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__must_trigger_requests_for_mapped_processes(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_trigger_requests_for_mapped_processes(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -77,13 +86,15 @@ async def test_check_mappings__must_trigger_requests_for_mapped_processes(self, self.assertIsNone(self.watcher._comm_patterns) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a': 'prof_1', '/bin/a': 'prof_2'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/bin/a', 'a'), 2: ('/bin/b', 'b')}) # no profile for this process @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__cmd_mapping_must_have_higher_priority_than_comm(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__cmd_mapping_must_have_higher_priority_than_comm(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -95,50 +106,59 @@ async def test_check_mappings__cmd_mapping_must_have_higher_priority_than_comm(s self.assertEqual({1: '/bin/a'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/bin/a', 'a')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=False) - async def test_check_mappings__must_add_process_to_the_optimized_context_when_request_fail(self, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_add_process_to_the_optimized_context_when_request_fail(self, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() send_async.assert_called_once() self.assertEqual({1: 'a'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/bin/a', 'a')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) - async def test_check_mappings__must_remove_an_optimized_process_from_the_context_when_it_is_not_alive(self, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_remove_an_optimized_process_from_the_context_when_it_is_not_alive(self, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): self.context.optimized.update({2: 'b'}) # 'b' was previously optimized, but will not me mapped as a process alive await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() send_async.assert_called_once() self.assertEqual({1: 'a'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/bin/a', 'a')}) @patch(f'{__app_name__}.service.watcher.core.network.send') - async def test_check_mappings__must_not_send_a_new_request_for_a_previously_optimized_process(self, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_not_send_a_new_request_for_a_previously_optimized_process(self, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): self.context.optimized.update({1: 'a'}) # 'a' was previously optimized and still alive await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() send_async.assert_not_called() self.assertEqual({1: 'a'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/bin/a', 'a'), 2: ('/bin/a', 'a')}) @patch(f'{__app_name__}.service.watcher.core.network.send') @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__must_send_a_new_request_for_a_previously_optimized_command_with_a_different_pid(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_send_a_new_request_for_a_previously_optimized_command_with_a_different_pid(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): self.context.optimized.update({1: 'a'}) # 'a' (1) was previously optimized and still alive await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() + time.assert_called() exp_req = OptimizationRequest(pid=2, command='/bin/a', user_name=self.context.user_name, user_env=self.context.user_env, profile='default', created_at=12345) @@ -147,14 +167,16 @@ async def test_check_mappings__must_send_a_new_request_for_a_previously_optimize self.assertEqual({1: 'a', 2: 'a'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a*': 'default', '/bin/*': 'prof_1', '/local/d': 'prof_2'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/local/bin/abacaxi', 'abacaxi'), 2: ('/bin/b', 'b'), 3: ('/bin/c', 'c')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__must_trigger_requests_for_mapped_using_regex(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_trigger_requests_for_mapped_using_regex(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -177,13 +199,15 @@ async def test_check_mappings__must_trigger_requests_for_mapped_using_regex(self self.assertIsNone(self.watcher._comm_patterns) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'abacax': 'default', 'a*': 'prof1'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/local/bin/abacaxi', 'abacaxi'), 2: ('/bin/b', 'b')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__a_request_must_be_sent_when_an_exact_comm_match_fails_but_a_pattern_works(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__a_request_must_be_sent_when_an_exact_comm_match_fails_but_a_pattern_works(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -195,13 +219,15 @@ async def test_check_mappings__a_request_must_be_sent_when_an_exact_comm_match_f self.assertEqual({1: 'abacaxi'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'abacaxi': 'default', 'a**': 'prof1'})) # both matches + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/local/bin/abacaxi', 'abacaxi'), 2: ('/bin/b', 'b')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__comm_exact_match_must_prevail_over_a_regex_match(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__comm_exact_match_must_prevail_over_a_regex_match(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -213,13 +239,15 @@ async def test_check_mappings__comm_exact_match_must_prevail_over_a_regex_match( self.assertEqual({1: 'abacaxi'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'/local/bin/abacaxi': 'default', '/local/*': 'prof1'})) # both matches + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/local/bin/abacaxi', 'abacaxi'), 2: ('/bin/b', 'b')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__cmd_exact_match_must_prevail_over_a_regex_match(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__cmd_exact_match_must_prevail_over_a_regex_match(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignored_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -231,15 +259,18 @@ async def test_check_mappings__cmd_exact_match_must_prevail_over_a_regex_match(s self.assertEqual({1: '/local/bin/abacaxi'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'abacaxi': 'default', '/local/*': 'prof1'})) # both matches + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/local/bin/abacaxi', 'abacaxi'), 2: ('/bin/b', 'b')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) async def test_check_mappings__cmd_regex_match_must_prevail_over_a_comm_exact_match(self, time: Mock, send_async: Mock, map_processes: Mock, + ignore_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignore_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -252,15 +283,18 @@ async def test_check_mappings__cmd_regex_match_must_prevail_over_a_comm_exact_ma self.assertEqual({1: '/local/bin/abacaxi'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'/local/*': 'default', '/local/*/a*': 'prof1'})) # both matches + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/local/bin/abacaxi', 'abacaxi'), 2: ('/bin/b', 'b')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) async def test_check_mappings__more_specific_cmd_match_must_prevail_over_others(self, time: Mock, send_async: Mock, map_processes: Mock, + ignore_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignore_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -272,15 +306,18 @@ async def test_check_mappings__more_specific_cmd_match_must_prevail_over_others( self.assertEqual({1: '/local/bin/abacaxi'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'ab*': 'default', 'aba*': 'prof1'})) # both matches + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={1: ('/local/bin/abacaxi', 'abacaxi'), 2: ('/bin/b', 'b')}) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) async def test_check_mappings__more_specific_comm_match_must_prevail_over_others(self, time: Mock, send_async: Mock, map_processes: Mock, + ignore_read: Mock, mapping_read: Mock): await self.watcher.check_mappings() mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=None) + ignore_read.assert_called_once() map_processes.assert_called_once() time.assert_called() @@ -292,13 +329,14 @@ async def test_check_mappings__more_specific_comm_match_must_prevail_over_others self.assertEqual({1: 'abacaxi'}, self.context.optimized) @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a*': 'default', '/bin/*': 'prof_1', '/local/d': 'prof_2'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', side_effect=[{1: ('/local/bin/abacaxi', 'abacaxi'), 2: ('/bin/b', 'b'), 3: ('/bin/c', 'c')}, {4: ('/xpto', 'xpto')}]) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__must_cache_mapping_when_defined_on_config(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_cache_mapping_when_defined_on_config(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): self.assertIsNone(self.watcher._mappings) self.assertIsNone(self.watcher._cmd_patterns) self.assertIsNone(self.watcher._comm_patterns) @@ -330,15 +368,17 @@ async def test_check_mappings__must_cache_mapping_when_defined_on_config(self, t await self.watcher.check_mappings() mapping_read.assert_called_once() + self.assertEqual(2, ignored_read.call_count) self.assertEqual(2, map_processes.call_count) time.assert_called() @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'a*': 'default', '/bin/*': 'prof_1', '/local/d': 'prof_2'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes', side_effect=[{1: ('/local/bin/abacaxi', 'abacaxi')}, {4: ('/bin/b', 'b')}]) @patch(f'{__app_name__}.service.watcher.core.network.send', return_value=True) @patch(f'{__app_name__}.service.watcher.core.time.time', return_value=12345) - async def test_check_mappings__must_always_read_mapping_from_disk_when_cache_is_off(self, time: Mock, send_async: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_always_read_mapping_from_disk_when_cache_is_off(self, time: Mock, send_async: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): self.assertIsNone(self.watcher._mappings) self.assertIsNone(self.watcher._cmd_patterns) self.assertIsNone(self.watcher._comm_patterns) @@ -368,13 +408,15 @@ async def test_check_mappings__must_always_read_mapping_from_disk_when_cache_is_ self.assertEqual({4: '/bin/b'}, self.context.optimized) self.assertEqual(2, mapping_read.call_count) + self.assertEqual(2, ignored_read.call_count) self.assertEqual(2, map_processes.call_count) time.assert_called() @patch(f'{__app_name__}.service.watcher.core.mapping.read', side_effect=[(True, None), (False, None), (False, None), (True, None), (False, None)]) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(False, None)) @patch(f'{__app_name__}.service.watcher.core.map_processes') @patch(f'{__app_name__}.service.watcher.core.network.send') - async def test_check_mappings__must_always_call_read_with_the_last_file_found_result(self, send: Mock, map_processes: Mock, mapping_read: Mock): + async def test_check_mappings__must_always_call_read_with_the_last_file_found_result(self, send: Mock, map_processes: Mock, ignored_read: Mock, mapping_read: Mock): for _ in range(5): await self.watcher.check_mappings() @@ -385,4 +427,177 @@ async def test_check_mappings__must_always_call_read_with_the_last_file_found_re call(file_path=self.context.mapping_file_path, logger=self.context.logger, last_file_found_log=True)]) send.assert_not_called() + ignored_read.assert_not_called() map_processes.assert_not_called() + + @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'def': 'default', + 'abc': 'default', + 'ghi': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(True, {'/bin/a*', 'de*', 'ghi'})) + @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={123: ('/bin/def', 'def'), + 456: ('/bin/abc', 'abc'), + 789: ('/bin/ghi', 'ghi')}) + @patch(f'{__app_name__}.service.watcher.core.network.send') + async def test_check_mappings__must_not_perform_any_request_if_ignored_matches(self, *mocks: Mock): + send_async, map_processes, ignored_read, mapping_read = mocks + + await self.watcher.check_mappings() + mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, + logger=self.context.logger, + last_file_found_log=None) + ignored_read.assert_called_once() + map_processes.assert_called_once() + send_async.assert_not_called() + self.assertEqual({}, self.context.optimized) + self.assertEqual({re.compile(r'^de.+$'): {'123:def'}, + re.compile(r'^/bin/a.+$'): {'456:abc'}, + 'ghi': {'789:ghi'}}, self.context.ignored_procs) + self.assertFalse(self.watcher._ignored_cached) # ensuring nothing was cached (when disabled) + + @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'def': 'default', + 'abc': 'default', + 'ghi': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(True, {'def', 'ghi'})) + @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={123: ('/bin/def', 'def'), + 789: ('/bin/ghi', 'ghi')}) + @patch(f'{__app_name__}.service.watcher.core.network.send') + async def test_check_mappings__must_not_perform_any_request_if_only_exact_ignored_matches(self, *mocks: Mock): + send_async, map_processes, ignored_read, mapping_read = mocks + + await self.watcher.check_mappings() + mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, + logger=self.context.logger, + last_file_found_log=None) + ignored_read.assert_called_once() + map_processes.assert_called_once() + send_async.assert_not_called() + self.assertEqual({}, self.context.optimized) + self.assertEqual({'def': {'123:def'}, + 'ghi': {'789:ghi'}}, self.context.ignored_procs) + self.assertFalse(self.watcher._ignored_cached) # ensuring nothing was cached (when disabled) + + @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'def': 'default', + 'abc': 'default', + 'ghi': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(True, {'de*'})) + @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={123: ('/bin/def', 'def'), + 456: ('/bin/abc', 'abc'), + 789: ('/bin/ghi', 'ghi')}) + @patch(f'{__app_name__}.service.watcher.core.network.send') + async def test_check_mappings__must_clean_ignored_context_when_pattern_is_no_long_returned(self, *mocks: Mock): + send_async, map_processes, ignored_read, mapping_read = mocks + + self.context.ignored_procs.update({re.compile(r'^de.+$'): {'123:def'}, + re.compile(r'^/bin/a.+$'): {'456:abc'}, + 'ghi': {'789:ghi'}}) + await self.watcher.check_mappings() + mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, + logger=self.context.logger, + last_file_found_log=None) + ignored_read.assert_called_once() + map_processes.assert_called_once() + self.assertEqual(2, send_async.call_count) + self.assertEqual(2, len(self.context.optimized)) + self.assertEqual({re.compile(r'^de.+$'): {'123:def'}}, self.context.ignored_procs) + + @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'def': 'default', + 'abc': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(True, {'/bin/a*', 'de*', 'ghi'})) + @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={123: ('/bin/def', 'def')}) + @patch(f'{__app_name__}.service.watcher.core.network.send') + async def test_check_mappings__must_clean_ignored_context_when_ignored_proc_stops(self, *mocks: Mock): + send_async, map_processes, ignored_read, mapping_read = mocks + + self.context.ignored_procs.update({r'^de.+$': {'123:def'}, + r'^/bin/a.+$': {'456:abc'}, + 'ghi': {'789:ghi'}}) + await self.watcher.check_mappings() + mapping_read.assert_called_once_with(file_path=self.context.mapping_file_path, + logger=self.context.logger, + last_file_found_log=None) + ignored_read.assert_called_once() + map_processes.assert_called_once() + self.assertEqual(0, send_async.call_count) + self.assertEqual(0, len(self.context.optimized)) + self.assertEqual({re.compile(r'^de.+$'): {'123:def'}}, self.context.ignored_procs) + + @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'def': 'default', + 'abc': 'default', + 'ghi': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(True, {'/bin/a*', 'de*', 'ghi'})) + @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={123: ('/bin/def', 'def'), + 456: ('/bin/abc', 'abc'), + 789: ('/bin/ghi', 'ghi')}) + @patch(f'{__app_name__}.service.watcher.core.network.send') + async def test_check_mappings__must_cache_all_types_of_ignored_mappings_if_enabled(self, *mocks: Mock): + send_async, map_processes, ignored_read, mapping_read = mocks + + self.context.watch_config.ignored_cache = True # enabling ignored caching + + self.assertFalse(self.watcher._ignored_cached) + self.assertIsNone(self.watcher._ignored_exact_strs) + self.assertIsNone(self.watcher._ignored_cmd_patterns) + self.assertIsNone(self.watcher._ignored_comm_patterns) + + await self.watcher.check_mappings() # first call + + self.assertTrue(self.watcher._ignored_cached) + self.assertEqual({'ghi', 'de*', '/bin/a*'}, self.watcher._ignored_exact_strs) + self.assertEqual({re.compile(r'^/bin/a.+$')}, self.watcher._ignored_cmd_patterns) + self.assertEqual({re.compile(r'^de.+$')}, self.watcher._ignored_comm_patterns) + + await self.watcher.check_mappings() # second call + + self.assertTrue(self.watcher._ignored_cached) + self.assertEqual({'ghi', 'de*', '/bin/a*'}, self.watcher._ignored_exact_strs) + self.assertEqual({re.compile(r'^/bin/a.+$')}, self.watcher._ignored_cmd_patterns) + self.assertEqual({re.compile(r'^de.+$')}, self.watcher._ignored_comm_patterns) + + ignored_read.assert_called_once() + self.assertEqual(2, mapping_read.call_count) + self.assertEqual(2, map_processes.call_count) + send_async.assert_not_called() + + self.assertEqual({}, self.context.optimized) + self.assertEqual({re.compile(r'^de.+$'): {'123:def'}, + re.compile(r'^/bin/a.+$'): {'456:abc'}, + 'ghi': {'789:ghi'}}, self.context.ignored_procs) + + @patch(f'{__app_name__}.service.watcher.core.mapping.read', return_value=(True, {'def': 'default', + 'ghi': 'default'})) + @patch(f'{__app_name__}.service.watcher.core.ignored.read', return_value=(True, {'def', 'ghi'})) + @patch(f'{__app_name__}.service.watcher.core.map_processes', return_value={123: ('/bin/def', 'def'), + 789: ('/bin/ghi', 'ghi')}) + @patch(f'{__app_name__}.service.watcher.core.network.send') + async def test_check_mappings__must_cache_only_exact_ignored_mappings_available_if_enabled(self, *mocks: Mock): + send_async, map_processes, ignored_read, mapping_read = mocks + + self.context.watch_config.ignored_cache = True # enabling ignored caching + + self.assertFalse(self.watcher._ignored_cached) + self.assertIsNone(self.watcher._ignored_exact_strs) + self.assertIsNone(self.watcher._ignored_cmd_patterns) + self.assertIsNone(self.watcher._ignored_comm_patterns) + + await self.watcher.check_mappings() # first call + + self.assertTrue(self.watcher._ignored_cached) + self.assertEqual({'ghi', 'def'}, self.watcher._ignored_exact_strs) + self.assertIsNone(self.watcher._ignored_cmd_patterns) + self.assertIsNone(self.watcher._ignored_comm_patterns) + + await self.watcher.check_mappings() # second call + + self.assertTrue(self.watcher._ignored_cached) + self.assertEqual({'ghi', 'def'}, self.watcher._ignored_exact_strs) + self.assertIsNone(self.watcher._ignored_cmd_patterns) + self.assertIsNone(self.watcher._ignored_comm_patterns) + + ignored_read.assert_called_once() + self.assertEqual(2, mapping_read.call_count) + self.assertEqual(2, map_processes.call_count) + send_async.assert_not_called() + + self.assertEqual({}, self.context.optimized) + self.assertEqual({'def': {'123:def'}, + 'ghi': {'789:ghi'}}, self.context.ignored_procs) diff --git a/tests/service/watcher/test_ignored.py b/tests/service/watcher/test_ignored.py new file mode 100644 index 0000000..d38c915 --- /dev/null +++ b/tests/service/watcher/test_ignored.py @@ -0,0 +1,119 @@ +from unittest import TestCase, IsolatedAsyncioTestCase +from unittest.mock import Mock, patch, call + +from guapow import __app_name__ +from guapow.service.watcher import ignored +from tests import RESOURCES_DIR + + +class GetFilePathTest(TestCase): + + def test__must_return_etc_path_for_root_user(self): + file_path = ignored.get_file_path(0, 'root') + self.assertEqual(f'/etc/{__app_name__}/watch.ignore', file_path) + + def test__must_return_user_home_path_for_noon_root_user(self): + file_path = ignored.get_file_path(1, 'abc') + self.assertEqual(f'/home/abc/.config/{__app_name__}/watch.ignore', file_path) + + +class GetExistingFilePathTest(TestCase): + + @patch(f'{__app_name__}.common.config.os.path.isfile', return_value=True) + def test__return_etc_file_when_root_user(self, isfile: Mock): + exp_path = f'/etc/{__app_name__}/watch.ignore' + path = ignored.get_existing_file_path(0, 'root', Mock()) + self.assertEqual(exp_path, path) + isfile.assert_called_once_with(exp_path) + + @patch(f'{__app_name__}.common.config.os.path.isfile', return_value=True) + def test__return_home_file_when_normal_user(self, isfile: Mock): + exp_path = f'/home/test/.config/{__app_name__}/watch.ignore' + path = ignored.get_existing_file_path(123, 'test', Mock()) + self.assertEqual(exp_path, path) + isfile.assert_called_once_with(exp_path) + + @patch(f'{__app_name__}.common.config.os.path.isfile', side_effect=[False, True]) + def test__return_etc_file_when_no_home_file_for_user(self, isfile: Mock): + exp_user_path = f'/home/test/.config/{__app_name__}/watch.ignore' + exp_root_path = f'/etc/{__app_name__}/watch.ignore' + + path = ignored.get_existing_file_path(123, 'test', Mock()) + self.assertEqual(exp_root_path, path) + + isfile.assert_has_calls([call(exp_user_path), call(exp_root_path)]) + + @patch(f'{__app_name__}.common.config.os.path.isfile', side_effect=[False, False]) + def test__return_none_when_neither_etc_nor_home_file_exist_for_user(self, isfile: Mock): + exp_user_path = f'/home/test/.config/{__app_name__}/watch.ignore' + exp_root_path = f'/etc/{__app_name__}/watch.ignore' + + path = ignored.get_existing_file_path(123, 'test', Mock()) + self.assertIsNone(path) + + isfile.assert_has_calls([call(exp_user_path), call(exp_root_path)]) + + +class GetDefaultFilePathTest(TestCase): + + @patch(f'{__app_name__}.common.config.os.path.isfile', return_value=True) + def test__return_existing_etc_file_when_root_user(self, isfile: Mock): + exp_path = f'/etc/{__app_name__}/watch.ignore' + path = ignored.get_default_file_path(0, 'root', Mock()) + self.assertEqual(exp_path, path) + isfile.assert_called_once_with(exp_path) + + @patch(f'{__app_name__}.common.config.os.path.isfile', return_value=True) + def test__return_existing_home_file_when_normal_user(self, isfile: Mock): + exp_path = f'/home/test/.config/{__app_name__}/watch.ignore' + path = ignored.get_default_file_path(123, 'test', Mock()) + self.assertEqual(exp_path, path) + isfile.assert_called_once_with(exp_path) + + @patch(f'{__app_name__}.common.config.os.path.isfile', side_effect=[False, True]) + def test__return_existing_etc_file_when_no_home_file_for_user(self, isfile: Mock): + exp_user_path = f'/home/test/.config/{__app_name__}/watch.ignore' + exp_root_path = f'/etc/{__app_name__}/watch.ignore' + + path = ignored.get_default_file_path(123, 'test', Mock()) + self.assertEqual(exp_root_path, path) + + isfile.assert_has_calls([call(exp_user_path), call(exp_root_path)]) + + @patch(f'{__app_name__}.common.config.os.path.isfile', side_effect=[False, False]) + def test__return_home_file_path_when_neither_etc_nor_home_file_exist_for_user(self, isfile: Mock): + exp_user_path = f'/home/test/.config/{__app_name__}/watch.ignore' + exp_root_path = f'/etc/{__app_name__}/watch.ignore' + + path = ignored.get_default_file_path(123, 'test', Mock()) + self.assertEqual(exp_user_path, path) + + isfile.assert_has_calls([call(exp_user_path), call(exp_root_path)]) + + @patch(f'{__app_name__}.common.config.os.path.isfile', side_effect=[False, False]) + def test__return_etc_path_even_when_it_does_not_exist_for_root_user(self, isfile: Mock): + exp_root_path = f'/etc/{__app_name__}/watch.ignore' + + path = ignored.get_default_file_path(0, 'root', Mock()) + self.assertEqual(path, exp_root_path) + + isfile.assert_called_once_with(exp_root_path) + + +class ReadTest(IsolatedAsyncioTestCase): + + async def test__must_return_none_when_file_not_found(self): + file_found, instance = await ignored.read(f'{RESOURCES_DIR}/xpto_1234.ignore', Mock(), None) + self.assertFalse(file_found) + self.assertIsNone(instance) + + async def test__must_return_none_when_no_valid_mapping_is_defined(self): + file_found, instance = await ignored.read(f'{RESOURCES_DIR}/empty.ignore', Mock(), None) + self.assertTrue(file_found) + self.assertIsNone(instance) + + async def test__must_ignore_lines_and_content_starting_with_sharp(self): + file_found, instance = await ignored.read(f'{RESOURCES_DIR}/with_comments.ignore', Mock(), None) + self.assertEqual(True, file_found) + self.assertIsInstance(instance, set) + self.assertEqual({'abc', 'ijk'}, instance) diff --git a/tests/service/watcher/test_mapping.py b/tests/service/watcher/test_mapping.py index 0dad3bd..afc39cc 100644 --- a/tests/service/watcher/test_mapping.py +++ b/tests/service/watcher/test_mapping.py @@ -1,12 +1,9 @@ -import re from unittest import TestCase, IsolatedAsyncioTestCase from unittest.mock import Mock, patch, call from guapow import __app_name__ from guapow.common.profile import get_default_profile_name -from guapow.common.steam import RE_STEAM_CMD from guapow.service.watcher import mapping -from guapow.service.watcher.mapping import RegexMapper from tests import RESOURCES_DIR @@ -99,7 +96,7 @@ def test__return_etc_path_even_when_it_does_not_exist_for_root_user(self, isfile exp_root_path = f'/etc/{__app_name__}/watch.map' path = mapping.get_default_file_path(0, 'root', Mock()) - self.assertEqual(exp_root_path, exp_root_path) + self.assertEqual(path, exp_root_path) isfile.assert_called_once_with(exp_root_path) @@ -137,78 +134,3 @@ async def test__must_return_python_regex_mapping_using_equal_sign(self): self.assertTrue(file_found) self.assertIsInstance(instance, dict) self.assertEqual({'r:/.+\s+SteamLaunch\s+AppId=\d+\s+--\s+/.+': 'steam'}, instance) - - -class RegexMapperTest(TestCase): - - def setUp(self): - self.mapper = RegexMapper(cache=False, logger=Mock()) - - def test_map__must_return_pattern_keys_only_for_strings_with_asterisk(self): - cmd_profs = {'abc': 'default', '/*/xpto': 'prof', 'def*abc*': 'prof2'} - pattern_mappings = self.mapper.map(cmd_profs) - self.assertIsInstance(pattern_mappings, tuple) - self.assertEqual({re.compile(r'^/.+/xpto$'): 'prof'}, pattern_mappings[0]) # cmd - self.assertEqual({re.compile(r'^def.+abc.+$'): 'prof2'}, pattern_mappings[1]) # comm - - def test_map__must_return_pattern_keys_when_key_starts_with_python_regex_pattern(self): - cmd_profs = {'abc': 'default', 'r:/.+/xpto': 'prof', 'r:def.+abc\d+': 'prof2'} - pattern_mappings = self.mapper.map(cmd_profs) - self.assertIsInstance(pattern_mappings, tuple) - self.assertEqual({re.compile(r'^/.+/xpto$'): 'prof'}, pattern_mappings[0]) # cmd - self.assertEqual({re.compile(r'^def.+abc\d+$'): 'prof2'}, pattern_mappings[1]) # comm - - def test_map__must_cache_a_valid_pattern_when_cache_is_true(self): - self.mapper = RegexMapper(cache=True, logger=Mock()) - cmd_profs = {'abc': 'default', 'r:/.+/xpto': 'prof', 'def*ihk*': 'prof2'} - - self.assertFalse(self.mapper.is_no_pattern_string_cached('abc')) - self.assertIsNone(self.mapper.get_cached_pattern('r:/.+/xpto')) - self.assertIsNone(self.mapper.get_cached_pattern('def*ihk*')) - self.assertIsNone(self.mapper.get_cached_pattern('abc')) - - pattern_mappings = self.mapper.map(cmd_profs) - self.assertIsInstance(pattern_mappings, tuple) - - self.assertIsNone(self.mapper.get_cached_pattern('abc')) - self.assertEqual(re.compile(r'^/.+/xpto$'), self.mapper.get_cached_pattern('r:/.+/xpto')) - self.assertEqual(re.compile(r'^def.+ihk.+$'), self.mapper.get_cached_pattern('def*ihk*')) - - self.assertTrue(self.mapper.is_no_pattern_string_cached('abc')) - - def test_map__must_not_cache_a_valid_pattern_when_cache_is_false(self): - cmd_profs = {'abc': 'default', 'r:/.+/xpto': 'prof', 'def*ihk*': 'prof2'} - - self.assertFalse(self.mapper.is_no_pattern_string_cached('abc')) - self.assertIsNone(self.mapper.get_cached_pattern('r:/.+/xpto')) - self.assertIsNone(self.mapper.get_cached_pattern('def*ihk*')) - - pattern_mappings = self.mapper.map(cmd_profs) - self.assertIsInstance(pattern_mappings, tuple) - - self.assertFalse(self.mapper.is_no_pattern_string_cached('abc')) - self.assertIsNone(self.mapper.get_cached_pattern('r:/.+/xpto')) - self.assertIsNone(self.mapper.get_cached_pattern('def*ihk*')) - - def test_map__must_return_steam_cmd_pattern_when_steam_keyword_is_informed(self): - cmd_profs = {'__steam__': 'default'} - - pattern_mappings = self.mapper.map(cmd_profs) - self.assertIsInstance(pattern_mappings, tuple) - - self.assertEqual({RE_STEAM_CMD: 'default'}, pattern_mappings[0]) - self.assertEqual({}, pattern_mappings[1]) - - def test_map__default_patterns_must_not_be_cached(self): - self.mapper = RegexMapper(cache=True, logger=Mock()) - - cmd_profs = {'__steam__': 'default'} - - pattern_mappings = self.mapper.map(cmd_profs) - self.assertIsInstance(pattern_mappings, tuple) - - self.assertTrue(pattern_mappings[0]) - self.assertFalse(pattern_mappings[1]) - - self.assertIsNone(self.mapper.get_cached_pattern('__steam__')) - self.assertIsNone(self.mapper.get_cached_pattern(RE_STEAM_CMD.pattern)) diff --git a/tests/service/watcher/test_patterns.py b/tests/service/watcher/test_patterns.py new file mode 100644 index 0000000..9d8bc1f --- /dev/null +++ b/tests/service/watcher/test_patterns.py @@ -0,0 +1,86 @@ +import re +from unittest import TestCase +from unittest.mock import Mock + +from guapow.common.steam import RE_STEAM_CMD +from guapow.service.watcher.patterns import RegexMapper + + +class RegexMapperTest(TestCase): + + def setUp(self): + self.mapper = RegexMapper(cache=False, logger=Mock()) + + def test_map_for_profiles__must_return_pattern_keys_only_for_strings_with_asterisk(self): + cmd_profs = {'abc': 'default', '/*/xpto': 'prof', 'def*abc*': 'prof2'} + pattern_mappings = self.mapper.map_for_profiles(cmd_profs) + self.assertIsInstance(pattern_mappings, tuple) + self.assertEqual({re.compile(r'^/.+/xpto$'): 'prof'}, pattern_mappings[0]) # cmd + self.assertEqual({re.compile(r'^def.+abc.+$'): 'prof2'}, pattern_mappings[1]) # comm + + def test_map_for_profiles__must_return_pattern_keys_when_key_starts_with_python_regex_pattern(self): + cmd_profs = {'abc': 'default', 'r:/.+/xpto': 'prof', 'r:def.+abc\d+': 'prof2'} + pattern_mappings = self.mapper.map_for_profiles(cmd_profs) + self.assertIsInstance(pattern_mappings, tuple) + self.assertEqual({re.compile(r'^/.+/xpto$'): 'prof'}, pattern_mappings[0]) # cmd + self.assertEqual({re.compile(r'^def.+abc\d+$'): 'prof2'}, pattern_mappings[1]) # comm + + def test_map_for_profiles__must_cache_a_valid_pattern_when_cache_is_true(self): + self.mapper = RegexMapper(cache=True, logger=Mock()) + cmd_profs = {'abc': 'default', 'r:/.+/xpto': 'prof', 'def*ihk*': 'prof2'} + + self.assertFalse(self.mapper.is_cached_as_no_pattern('abc')) + self.assertIsNone(self.mapper.get_cached_pattern('r:/.+/xpto')) + self.assertIsNone(self.mapper.get_cached_pattern('def*ihk*')) + self.assertIsNone(self.mapper.get_cached_pattern('abc')) + + pattern_mappings = self.mapper.map_for_profiles(cmd_profs) + self.assertIsInstance(pattern_mappings, tuple) + + self.assertIsNone(self.mapper.get_cached_pattern('abc')) + self.assertEqual(re.compile(r'^/.+/xpto$'), self.mapper.get_cached_pattern('r:/.+/xpto')) + self.assertEqual(re.compile(r'^def.+ihk.+$'), self.mapper.get_cached_pattern('def*ihk*')) + + self.assertTrue(self.mapper.is_cached_as_no_pattern('abc')) + + def test_map_for_profiles__must_not_cache_a_valid_pattern_when_cache_is_false(self): + cmd_profs = {'abc': 'default', 'r:/.+/xpto': 'prof', 'def*ihk*': 'prof2'} + + self.assertFalse(self.mapper.is_cached_as_no_pattern('abc')) + self.assertIsNone(self.mapper.get_cached_pattern('r:/.+/xpto')) + self.assertIsNone(self.mapper.get_cached_pattern('def*ihk*')) + + pattern_mappings = self.mapper.map_for_profiles(cmd_profs) + self.assertIsInstance(pattern_mappings, tuple) + + self.assertFalse(self.mapper.is_cached_as_no_pattern('abc')) + self.assertIsNone(self.mapper.get_cached_pattern('r:/.+/xpto')) + self.assertIsNone(self.mapper.get_cached_pattern('def*ihk*')) + + def test_map_for_profiles__must_return_steam_cmd_pattern_when_steam_keyword_is_informed(self): + cmd_profs = {'__steam__': 'default'} + + pattern_mappings = self.mapper.map_for_profiles(cmd_profs) + self.assertIsInstance(pattern_mappings, tuple) + + self.assertEqual({RE_STEAM_CMD: 'default'}, pattern_mappings[0]) + self.assertEqual({}, pattern_mappings[1]) + + def test_map_for_profiles__default_patterns_must_not_be_cached(self): + self.mapper = RegexMapper(cache=True, logger=Mock()) + + cmd_profs = {'__steam__': 'default'} + + pattern_mappings = self.mapper.map_for_profiles(cmd_profs) + self.assertIsInstance(pattern_mappings, tuple) + + self.assertTrue(pattern_mappings[0]) + self.assertFalse(pattern_mappings[1]) + + self.assertIsNone(self.mapper.get_cached_pattern('__steam__')) + self.assertIsNone(self.mapper.get_cached_pattern(RE_STEAM_CMD.pattern)) + + def test_map__must_return_none_when_string_with_no_pattern_associated_is_already_cached(self): + self.mapper = RegexMapper(cache=True, logger=Mock()) + self.mapper._string_no_pattern_cache.add('abc') + self.assertIsNone(self.mapper.map('abc'))