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'))