nano-dev-utils 0.4.4__tar.gz → 0.5.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of nano-dev-utils might be problematic. Click here for more details.

Files changed (26) hide show
  1. nano_dev_utils-0.5.3/.idea/misc.xml +4 -0
  2. nano_dev_utils-0.5.3/.idea/workspace.xml +47 -0
  3. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/PKG-INFO +5 -2
  4. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/README.md +1 -1
  5. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/pyproject.toml +12 -5
  6. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/src/nano_dev_utils/__init__.py +8 -5
  7. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/src/nano_dev_utils/dynamic_importer.py +0 -3
  8. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/src/nano_dev_utils/release_ports.py +22 -18
  9. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/src/nano_dev_utils/timers.py +6 -7
  10. nano_dev_utils-0.5.3/tests/__init__.py +10 -0
  11. nano_dev_utils-0.5.3/tests/test_dynamic_importer.py +62 -0
  12. nano_dev_utils-0.5.3/tests/test_release_ports.py +307 -0
  13. nano_dev_utils-0.5.3/tests/test_timer.py +159 -0
  14. nano_dev_utils-0.4.4/.idea/misc.xml +0 -7
  15. nano_dev_utils-0.4.4/.idea/workspace.xml +0 -327
  16. nano_dev_utils-0.4.4/tests/__init__.py +0 -16
  17. nano_dev_utils-0.4.4/tests/testing_dynamic_importer.py +0 -55
  18. nano_dev_utils-0.4.4/tests/testing_release_ports.py +0 -276
  19. nano_dev_utils-0.4.4/tests/testing_timer.py +0 -157
  20. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/.gitignore +0 -0
  21. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/.idea/.gitignore +0 -0
  22. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
  23. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/.idea/modules.xml +0 -0
  24. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/.idea/nano_dev_utils.iml +0 -0
  25. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/.idea/vcs.xml +0 -0
  26. {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.3}/LICENSE.md +0 -0
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10 (datagov-mcp)" project-jdk-type="Python SDK" />
4
+ </project>
@@ -0,0 +1,47 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ChangeListManager">
4
+ <list default="true" id="1859e23b-7665-4b92-98cc-65e07a208923" name="Changes" comment="" />
5
+ <option name="SHOW_DIALOG" value="false" />
6
+ <option name="HIGHLIGHT_CONFLICTS" value="true" />
7
+ <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
8
+ <option name="LAST_RESOLUTION" value="IGNORE" />
9
+ </component>
10
+ <component name="Git.Settings">
11
+ <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
12
+ </component>
13
+ <component name="ProjectColorInfo"><![CDATA[{
14
+ "customColor": "",
15
+ "associatedIndex": 5
16
+ }]]></component>
17
+ <component name="ProjectId" id="2wVV0HQbcggEgX5PEayC1Mcx9Gd" />
18
+ <component name="ProjectViewState">
19
+ <option name="hideEmptyMiddlePackages" value="true" />
20
+ <option name="showLibraryContents" value="true" />
21
+ <option name="showMembers" value="true" />
22
+ </component>
23
+ <component name="PropertiesComponent"><![CDATA[{
24
+ "keyToString": {
25
+ "RunOnceActivity.ShowReadmeOnStart": "true",
26
+ "git-widget-placeholder": "master"
27
+ }
28
+ }]]></component>
29
+ <component name="SharedIndexes">
30
+ <attachedChunks>
31
+ <set>
32
+ <option value="bundled-python-sdk-5b207ade9991-746f403e7f0c-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-241.17890.14" />
33
+ </set>
34
+ </attachedChunks>
35
+ </component>
36
+ <component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
37
+ <component name="TaskManager">
38
+ <task active="true" id="Default" summary="Default task">
39
+ <changelist id="1859e23b-7665-4b92-98cc-65e07a208923" name="Changes" comment="" />
40
+ <created>1746126505743</created>
41
+ <option name="number" value="Default" />
42
+ <option name="presentableId" value="Default" />
43
+ <updated>1746126505743</updated>
44
+ </task>
45
+ <servers />
46
+ </component>
47
+ </project>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nano_dev_utils
3
- Version: 0.4.4
3
+ Version: 0.5.3
4
4
  Summary: A collection of small Python utilities for developers.
5
5
  Project-URL: Homepage, https://github.com/yaronday/nano_utils
6
6
  Project-URL: Issues, https://github.com/yaronday/nano_utils/issues
@@ -10,6 +10,9 @@ License-File: LICENSE.md
10
10
  Classifier: Operating System :: OS Independent
11
11
  Classifier: Programming Language :: Python :: 3
12
12
  Requires-Python: >=3.10
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest-mock>=3.14.0; extra == 'test'
15
+ Requires-Dist: pytest>=8.2.0; extra == 'test'
13
16
  Description-Content-Type: text/markdown
14
17
 
15
18
  # nano_dev_utils
@@ -142,4 +145,4 @@ port_releaser.release_all()
142
145
 
143
146
  ## License
144
147
  This project is licensed under the MIT License.
145
- See [LICENSE](https://github.com/yaronday/nano_dev_utils/blob/master/LICENSE.md) for details.
148
+ See [LICENSE](LICENSE.md) for details.
@@ -128,4 +128,4 @@ port_releaser.release_all()
128
128
 
129
129
  ## License
130
130
  This project is licensed under the MIT License.
131
- See [LICENSE](https://github.com/yaronday/nano_dev_utils/blob/master/LICENSE.md) for details.
131
+ See [LICENSE](LICENSE.md) for details.
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nano_dev_utils"
3
- version = "0.4.4"
3
+ version = "0.5.3"
4
4
 
5
5
  authors = [
6
6
  { name="Yaron Dayan", email="yaronday77@gmail.com" },
@@ -13,12 +13,19 @@ classifiers = [
13
13
  "Operating System :: OS Independent",
14
14
  ]
15
15
  license = { text = "MIT" }
16
- license-files = ["LICENSE.md"]
16
+
17
+ [project.optional-dependencies]
18
+ test = [
19
+ "pytest>=8.2.0",
20
+ "pytest-mock>=3.14.0",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/yaronday/nano_utils"
25
+ Issues = "https://github.com/yaronday/nano_utils/issues"
17
26
 
18
27
  [build-system]
19
28
  requires = ["hatchling >= 1.26"]
20
29
  build-backend = "hatchling.build"
21
30
 
22
- [project.urls]
23
- Homepage = "https://github.com/yaronday/nano_utils" # PyPI
24
- Issues = "https://github.com/yaronday/nano_utils/issues"
31
+
@@ -5,11 +5,14 @@ Copyright (c) 2025 Yaron Dayan
5
5
  from .dynamic_importer import Importer
6
6
  from .timers import Timer
7
7
  from .release_ports import PortsRelease, PROXY_SERVER, INSPECTOR_CLIENT
8
+ from importlib.metadata import version
9
+
10
+ __version__ = version('nano-dev-utils')
8
11
 
9
12
  __all__ = [
10
- "Importer",
11
- "Timer",
12
- "PortsRelease",
13
- "PROXY_SERVER",
14
- "INSPECTOR_CLIENT",
13
+ 'Importer',
14
+ 'Timer',
15
+ 'PortsRelease',
16
+ 'PROXY_SERVER',
17
+ 'INSPECTOR_CLIENT',
15
18
  ]
@@ -25,6 +25,3 @@ class Importer:
25
25
  return module
26
26
  except ModuleNotFoundError as e:
27
27
  raise ImportError(f'Could not import {lib_mod}') from e
28
-
29
-
30
-
@@ -11,8 +11,11 @@ INSPECTOR_CLIENT = 6274
11
11
 
12
12
  class PortsRelease:
13
13
  def __init__(self, default_ports: list[int] | None = None):
14
- self.default_ports: list[int] = default_ports \
15
- if default_ports is not None else [PROXY_SERVER, INSPECTOR_CLIENT]
14
+ self.default_ports: list[int] = (
15
+ default_ports
16
+ if default_ports is not None
17
+ else [PROXY_SERVER, INSPECTOR_CLIENT]
18
+ )
16
19
 
17
20
  @staticmethod
18
21
  def _log_process_found(port: int, pid: int) -> str:
@@ -31,8 +34,9 @@ class PortsRelease:
31
34
  return f'Invalid port number: {port}. Skipping.'
32
35
 
33
36
  @staticmethod
34
- def _log_terminate_failed(pid: int, port: int | None = None,
35
- error: str | None = None) -> str:
37
+ def _log_terminate_failed(
38
+ pid: int, port: int | None = None, error: str | None = None
39
+ ) -> str:
36
40
  base_msg = f'Failed to terminate process {pid}'
37
41
  if port:
38
42
  base_msg += f' (on port {port})'
@@ -61,17 +65,17 @@ class PortsRelease:
61
65
  system = platform.system()
62
66
  try:
63
67
  cmd: str = {
64
- "Windows": f"netstat -ano | findstr :{port}",
65
- "Linux": f"ss -lntp | grep :{port}",
66
- "Darwin": f"lsof -i :{port}",
67
- }.get(system, "")
68
+ 'Windows': f'netstat -ano | findstr :{port}',
69
+ 'Linux': f'ss -lntp | grep :{port}',
70
+ 'Darwin': f'lsof -i :{port}',
71
+ }.get(system, '')
68
72
  if not cmd:
69
73
  lgr.error(self._log_unsupported_os())
70
74
  return None
71
75
 
72
- process = subprocess.Popen(cmd, shell=True,
73
- stdout=subprocess.PIPE,
74
- stderr=subprocess.PIPE)
76
+ process = subprocess.Popen(
77
+ cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
78
+ )
75
79
  output, error = process.communicate()
76
80
  if error:
77
81
  lgr.error(self._log_cmd_error(error))
@@ -81,21 +85,21 @@ class PortsRelease:
81
85
  for line in lines:
82
86
  if str(port) in line:
83
87
  parts: list[str] = line.split()
84
- if system == "Windows" and len(parts) > 4:
88
+ if system == 'Windows' and len(parts) > 4:
85
89
  try:
86
90
  return int(parts[4])
87
91
  except ValueError:
88
92
  lgr.error(self._log_line_parse_failed(line))
89
93
  return None
90
- elif system == "Linux":
94
+ elif system == 'Linux':
91
95
  for part in parts:
92
- if "pid=" in part:
96
+ if 'pid=' in part:
93
97
  try:
94
- return int(part.split("=")[1])
98
+ return int(part.split('=')[1])
95
99
  except ValueError:
96
100
  lgr.error(self._log_line_parse_failed(line))
97
101
  return None
98
- elif system == "Darwin" and len(parts) > 1:
102
+ elif system == 'Darwin' and len(parts) > 1:
99
103
  try:
100
104
  return int(parts[1])
101
105
  except ValueError:
@@ -113,7 +117,7 @@ class PortsRelease:
113
117
  'Windows': f'taskkill /F /PID {pid}',
114
118
  'Linux': f'kill -9 {pid}',
115
119
  'Darwin': f'kill -9 {pid}',
116
- }.get(platform.system(), "") # fallback to empty string
120
+ }.get(platform.system(), '') # fallback to empty string
117
121
  if not cmd:
118
122
  lgr.error(self._log_unsupported_os())
119
123
  return False
@@ -148,4 +152,4 @@ class PortsRelease:
148
152
  else:
149
153
  lgr.error(self._log_terminate_failed(pid=pid, port=port))
150
154
  except Exception as e:
151
- lgr.error(self._log_unexpected_error(e))
155
+ lgr.error(self._log_unexpected_error(e))
@@ -1,6 +1,7 @@
1
1
  from functools import wraps
2
2
  import time
3
3
  from typing import Callable, ParamSpec, TypeVar
4
+
4
5
  P = ParamSpec('P')
5
6
  R = TypeVar('R')
6
7
 
@@ -9,15 +10,11 @@ class Timer:
9
10
  def __init__(self, precision=4, verbose=False):
10
11
  self.precision = precision
11
12
  self.verbose = verbose
12
- self.units = [
13
- (1e9, 's'),
14
- (1e6, 'ms'),
15
- (1e3, 'μs'),
16
- (1.0, 'ns')
17
- ]
13
+ self.units = [(1e9, 's'), (1e6, 'ms'), (1e3, 'μs'), (1.0, 'ns')]
18
14
 
19
15
  def timeit(self, func: Callable[P, R]) -> Callable[P, R]:
20
16
  """Decorator that times function execution with automatic unit scaling."""
17
+
21
18
  @wraps(func)
22
19
  def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
23
20
  start = time.perf_counter_ns()
@@ -33,7 +30,9 @@ class Timer:
33
30
  break
34
31
 
35
32
  extra_info = f'{args} {kwargs} ' if self.verbose else ''
36
- print(f'{func.__name__} {extra_info}took {value:.{self.precision}f} [{unit}]')
33
+ print(
34
+ f'{func.__name__} {extra_info}took {value:.{self.precision}f} [{unit}]'
35
+ )
37
36
  return result
38
37
 
39
38
  return wrapper
@@ -0,0 +1,10 @@
1
+ """nano-dev-utils - A collection of small Python utilities for developers.
2
+ Copyright (c) 2025 Yaron Dayan
3
+ """
4
+
5
+ from .test_release_ports import PROXY_SERVER, CLIENT_PORT
6
+
7
+ __all__ = [
8
+ 'PROXY_SERVER',
9
+ 'CLIENT_PORT',
10
+ ]
@@ -0,0 +1,62 @@
1
+ import pytest
2
+ from types import ModuleType
3
+ from src.nano_dev_utils.dynamic_importer import Importer
4
+
5
+
6
+ @pytest.fixture
7
+ def importer():
8
+ return Importer()
9
+
10
+
11
+ @pytest.fixture
12
+ def mock_module():
13
+ module = ModuleType('mock_module')
14
+ setattr(module, 'attribute', 'test_value')
15
+ return module
16
+
17
+
18
+ def test_import_mod_from_lib_success_new_import(importer, mock_module, mocker):
19
+ mock_import = mocker.patch('importlib.import_module', return_value=mock_module)
20
+ module = importer.import_mod_from_lib('test_library', 'test_module')
21
+
22
+ assert module == mock_module
23
+ assert 'test_module' in importer.imported_modules
24
+ assert importer.imported_modules['test_module'] == mock_module
25
+ mock_import.assert_called_once_with('test_library.test_module')
26
+
27
+
28
+ def test_import_mod_from_lib_success_cached_import(importer, mock_module, mocker):
29
+ importer.imported_modules['cached_module'] = mock_module
30
+ mock_import = mocker.patch('importlib.import_module')
31
+
32
+ module = importer.import_mod_from_lib('another_library', 'cached_module')
33
+
34
+ assert module == mock_module
35
+ mock_import.assert_not_called()
36
+
37
+
38
+ def test_import_mod_from_lib_failure(importer, mocker):
39
+ mocker.patch(
40
+ 'importlib.import_module',
41
+ side_effect=ModuleNotFoundError(
42
+ "No module named 'nonexistent_library.missing_module'"
43
+ ),
44
+ )
45
+
46
+ with pytest.raises(
47
+ ImportError, match='Could not import nonexistent_library.missing_module'
48
+ ):
49
+ importer.import_mod_from_lib('nonexistent_library', 'missing_module')
50
+
51
+ assert 'missing_module' not in importer.imported_modules
52
+
53
+
54
+ def test_import_mod_from_lib_returns_any(importer, mocker):
55
+ mock_object = 'not a module'
56
+ mocker.patch.dict('sys.modules', {'yet_another_library.some_object': mock_object})
57
+
58
+ result = importer.import_mod_from_lib('yet_another_library', 'some_object')
59
+
60
+ assert result == mock_object
61
+ assert 'some_object' in importer.imported_modules
62
+ assert importer.imported_modules['some_object'] == mock_object
@@ -0,0 +1,307 @@
1
+ import logging
2
+ import pytest
3
+ from src.nano_dev_utils import release_ports as rp
4
+
5
+ PROXY_SERVER = rp.PROXY_SERVER
6
+ CLIENT_PORT = rp.INSPECTOR_CLIENT
7
+
8
+
9
+ @pytest.fixture
10
+ def ports_release():
11
+ return rp.PortsRelease()
12
+
13
+
14
+ @pytest.fixture
15
+ def mock_logger(mocker):
16
+ logger = mocker.MagicMock(spec=logging.Logger)
17
+ mocker.patch.object(rp, 'lgr', logger)
18
+ return logger
19
+
20
+
21
+ def encode_dict(input_dict: dict) -> bytes:
22
+ return b' '.join(str(v).encode() for v in input_dict.values())
23
+
24
+
25
+ def remove_file_handlers():
26
+ """Temporarily remove any file handlers from the root logger"""
27
+ root_logger = logging.getLogger()
28
+ existing_file_handlers = [
29
+ handler
30
+ for handler in root_logger.handlers
31
+ if isinstance(handler, logging.FileHandler)
32
+ ]
33
+ for handler in existing_file_handlers:
34
+ root_logger.removeHandler(handler)
35
+ handler.close()
36
+
37
+
38
+ @pytest.fixture(autouse=True)
39
+ def cleanup():
40
+ remove_file_handlers()
41
+ yield
42
+
43
+
44
+ def mock_pid_retrieval(ports_release, mocker, entry, port):
45
+ mock_process = mocker.MagicMock()
46
+ encoded_entry = encode_dict(entry)
47
+ mock_process.communicate.return_value = (encoded_entry, '')
48
+ mocker.patch('subprocess.Popen', return_value=mock_process)
49
+ pid = ports_release.get_pid_by_port(port)
50
+ return pid, encoded_entry
51
+
52
+
53
+ def test_get_pid_by_port_linux_success(ports_release, mock_logger, mocker):
54
+ mock_popen = mocker.patch('subprocess.Popen')
55
+ mocker.patch('platform.system', return_value='Linux')
56
+ # ss - lntp command response structure
57
+ port = 8080 # local port
58
+ peer_port = '*'
59
+ local_addr = '::' # :: - listening to all available interfaces
60
+ peer_addr = '::'
61
+ _pid = 1234
62
+ fd = 4 # file descriptor
63
+ ss_entry = {
64
+ 'netid': 'tcp6',
65
+ 'rx_q': 0,
66
+ 'tx_q': 0,
67
+ 'local_addr_port': f'{local_addr}:{port}',
68
+ 'peer_addr_port': f'{peer_addr}:{peer_port}',
69
+ 'process_name': 'users:python3',
70
+ 'pid': f'pid={_pid}',
71
+ 'fd': f'fd={fd}',
72
+ }
73
+
74
+ mock_process = mocker.MagicMock()
75
+ mock_process.communicate.return_value = (encode_dict(ss_entry), '')
76
+ mock_popen.return_value = mock_process
77
+
78
+ pid = ports_release.get_pid_by_port(port)
79
+ assert pid == 1234
80
+ mock_popen.assert_called_once_with(
81
+ f'ss -lntp | grep :{port}',
82
+ shell=True,
83
+ stdout=-1,
84
+ stderr=-1,
85
+ )
86
+
87
+
88
+ def test_get_pid_by_port_windows_success(ports_release, mock_logger, mocker):
89
+ mock_popen = mocker.patch('subprocess.Popen')
90
+ mocker.patch('platform.system', return_value='Windows')
91
+
92
+ # netstat -ano command response structure (Windows)
93
+ port = 9000
94
+ _pid = 5678
95
+ netstat_entry = {
96
+ 'protocol': 'TCP',
97
+ 'local_addr_and_port ': f'0.0.0.0:{port}',
98
+ 'remote_addr_and_port': '0.0.0.0:0',
99
+ 'state_and_pid': f'LISTENING {_pid}\n',
100
+ }
101
+
102
+ mock_process = mocker.MagicMock()
103
+ mock_process.communicate.return_value = (encode_dict(netstat_entry), '')
104
+ mock_popen.return_value = mock_process
105
+
106
+ pid = ports_release.get_pid_by_port(port)
107
+ assert pid == 5678
108
+ mock_popen.assert_called_once_with(
109
+ f'netstat -ano | findstr :{port}',
110
+ shell=True,
111
+ stdout=-1,
112
+ stderr=-1,
113
+ )
114
+
115
+
116
+ def test_get_pid_by_port_darwin_success(ports_release, mock_logger, mocker):
117
+ mock_popen = mocker.patch('subprocess.Popen')
118
+ mocker.patch('platform.system', return_value='Darwin')
119
+ # lsof -i command response structure (MacOS)
120
+ port = 7000
121
+ _pid = 1111
122
+ lsof_entry = {
123
+ 'command': 'python3', # Process name
124
+ 'pid': f'{_pid}', # Process ID (integer)
125
+ 'user': 'user', # User running the process
126
+ 'fd': '10u', # File descriptor (read/write)
127
+ 'type': 'IPv4', # Network connection type (IPv4/IPv6)
128
+ 'device': '0xabcdef0123456789', # Kernel device identifier
129
+ 'size_off': '0t0', # Size/offset (0 for sockets)
130
+ 'protocol': 'TCP', # Protocol (TCP/UDP)
131
+ 'name': f'*:{port} (LISTEN)', # Combined address & state (optional)
132
+ }
133
+
134
+ mock_process = mocker.MagicMock()
135
+ mock_process.communicate.return_value = (encode_dict(lsof_entry), '')
136
+ mock_popen.return_value = mock_process
137
+
138
+ pid = ports_release.get_pid_by_port(port)
139
+ assert pid == 1111
140
+ mock_popen.assert_called_once_with(
141
+ f'lsof -i :{port}',
142
+ shell=True,
143
+ stdout=-1,
144
+ stderr=-1,
145
+ )
146
+
147
+
148
+ def test_get_pid_by_port_unsupported_os(ports_release, mock_logger, mocker):
149
+ mocker.patch('platform.system', return_value='UnsupportedOS')
150
+ pid = ports_release.get_pid_by_port(1234)
151
+ assert pid is None
152
+ mock_logger.error.assert_called_once_with(ports_release._log_unsupported_os())
153
+
154
+
155
+ def test_get_pid_by_port_no_process(ports_release, mock_logger, mocker):
156
+ port = 9999
157
+ mocker.patch('platform.system', return_value='Linux')
158
+ mock_process = mocker.MagicMock()
159
+ mock_process.communicate.return_value = (b'', b'')
160
+ mocker.patch('subprocess.Popen', return_value=mock_process)
161
+
162
+ pid = ports_release.get_pid_by_port(port)
163
+ assert pid is None
164
+ mock_logger.error.assert_not_called()
165
+
166
+
167
+ def test_get_pid_by_port_command_error(ports_release, mock_logger, mocker):
168
+ mock_popen = mocker.patch('subprocess.Popen')
169
+ mocker.patch('platform.system', return_value='Linux')
170
+ port = 80
171
+ err = 'Error occurred'
172
+ mock_process = mocker.MagicMock()
173
+ mock_process.communicate.return_value = (b'', err.encode())
174
+ mock_popen.return_value = mock_process
175
+ pid = ports_release.get_pid_by_port(port)
176
+ assert pid is None
177
+ mock_logger.error.assert_called_once_with(f'Error running command: {err}')
178
+
179
+
180
+ def test_get_pid_by_port_parse_error(ports_release, mock_logger, mocker):
181
+ mocker.patch('platform.system', return_value='Linux')
182
+ port = 8080
183
+ peer_port = '*'
184
+ local_addr = '::'
185
+ peer_addr = '::'
186
+ _pid = 'invalid'
187
+ fd = 4
188
+ ss_entry = {
189
+ 'netid': 'tcp6',
190
+ 'rx_q': 0,
191
+ 'tx_q': 0,
192
+ 'local_addr_port': f'{local_addr}:{port}',
193
+ 'peer_addr_port': f'{peer_addr}:{peer_port}',
194
+ 'process_name': 'users:python3',
195
+ 'pid': f'pid={_pid}',
196
+ 'fd': f'fd={fd}',
197
+ }
198
+
199
+ pid, enc_entry = mock_pid_retrieval(ports_release, mocker, ss_entry, port)
200
+ assert pid is None
201
+ mock_logger.error.assert_called_once_with(
202
+ f'Could not parse PID from line: {enc_entry.decode()}'
203
+ )
204
+
205
+
206
+ def test_get_pid_by_port_unexpected_exception(ports_release, mock_logger, mocker):
207
+ mocker.patch('platform.system', return_value='Linux')
208
+ err = Exception('Unexpected')
209
+ port = 1234
210
+ mocker.patch('subprocess.Popen', side_effect=err)
211
+ pid = ports_release.get_pid_by_port(port)
212
+ assert pid is None
213
+ mock_logger.error.assert_called_once_with(f'An unexpected error occurred: {err}')
214
+
215
+
216
+ def test_kill_process_success(ports_release, mock_logger, mocker):
217
+ mocker.patch('platform.system', return_value='Linux')
218
+ mock_popen = mocker.patch('subprocess.Popen')
219
+
220
+ port = 5678
221
+ mock_process = mocker.MagicMock()
222
+ mock_process.returncode = 0
223
+ mock_process.communicate.return_value = (b'', b'')
224
+ mock_popen.return_value = mock_process
225
+ result = ports_release.kill_process(port)
226
+ assert result is True
227
+ mock_popen.assert_called_once_with(f'kill -9 {port}', shell=True, stderr=-1)
228
+
229
+
230
+ def test_kill_process_fail(ports_release, mock_logger, mocker):
231
+ pid = 1234
232
+ err = 'Access denied'
233
+ mocker.patch('platform.system', return_value='Windows')
234
+ mock_popen = mocker.patch('subprocess.Popen')
235
+ mock_process = mocker.MagicMock()
236
+ mock_process.returncode = 1
237
+ mock_process.communicate.return_value = (b'', err.encode())
238
+ mock_popen.return_value = mock_process
239
+ result = ports_release.kill_process(pid)
240
+ assert result is False
241
+ mock_logger.error.assert_called_once_with(
242
+ ports_release._log_terminate_failed(pid=pid, error=err)
243
+ )
244
+
245
+
246
+ def test_kill_process_unsupported_os(ports_release, mock_logger, mocker):
247
+ mocker.patch('platform.system', return_value='UnsupportedOS')
248
+ pid = 9999
249
+ result = ports_release.kill_process(pid)
250
+ assert result is False
251
+ mock_logger.error.assert_called_once_with(ports_release._log_unsupported_os())
252
+
253
+
254
+ def test_kill_process_unexpected_exception(ports_release, mock_logger, mocker):
255
+ err = Exception('Another error')
256
+ mocker.patch('platform.system', return_value='Linux')
257
+ mocker.patch('subprocess.Popen', side_effect=err)
258
+
259
+ pid = 4321
260
+ result = ports_release.kill_process(pid)
261
+ assert result is False
262
+ mock_logger.error.assert_called_once_with(ports_release._log_unexpected_error(err))
263
+
264
+
265
+ def test_release_all_default_ports_success(ports_release, mock_logger, mocker):
266
+ mock_get_pid = mocker.patch.object(ports_release, 'get_pid_by_port')
267
+ mock_kill = mocker.patch.object(ports_release, 'kill_process')
268
+
269
+ pid1, pid2 = 1111, 2222
270
+ mock_get_pid.side_effect = [pid1, pid2]
271
+ mock_kill.side_effect = [True, True]
272
+ ports_release.release_all()
273
+
274
+ assert mock_get_pid.call_args_list == [
275
+ mocker.call(PROXY_SERVER),
276
+ mocker.call(CLIENT_PORT),
277
+ ]
278
+ assert mock_kill.call_args_list == [mocker.call(pid1), mocker.call(pid2)]
279
+ mock_logger.info.assert_has_calls(
280
+ [
281
+ mocker.call(ports_release._log_process_found(PROXY_SERVER, pid1)),
282
+ mocker.call(ports_release._log_process_terminated(pid1, PROXY_SERVER)),
283
+ mocker.call(ports_release._log_process_found(CLIENT_PORT, pid2)),
284
+ mocker.call(ports_release._log_process_terminated(pid2, CLIENT_PORT)),
285
+ ]
286
+ )
287
+
288
+
289
+ def test_release_all_invalid_port(ports_release, mock_logger, mocker):
290
+ mock_get_pid = mocker.patch.object(ports_release, 'get_pid_by_port')
291
+ mock_kill = mocker.patch.object(ports_release, 'kill_process')
292
+ ports = ['invalid', 1234, 5678]
293
+ mock_get_pid.side_effect = [None, None]
294
+ ports_release.release_all(ports=ports)
295
+ assert mock_get_pid.call_args_list == [mocker.call(ports[1]), mocker.call(ports[2])]
296
+
297
+ mock_kill.assert_not_called()
298
+ mock_logger.error.assert_called_once_with(ports_release._log_invalid_port(ports[0]))
299
+
300
+
301
+ def test_release_all_unexpected_exception(ports_release, mock_logger, mocker):
302
+ err = Exception('Release all error')
303
+ mocker.patch.object(ports_release, 'get_pid_by_port', side_effect=err)
304
+ port = 9010
305
+ ports_release.release_all(ports=[port])
306
+ mock_logger.error.assert_called_once_with(ports_release._log_unexpected_error(err))
307
+ ports_release.get_pid_by_port.assert_called_once_with(port)