nano-dev-utils 0.4.4__tar.gz → 0.5.2__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.
- nano_dev_utils-0.5.2/.idea/misc.xml +4 -0
- nano_dev_utils-0.5.2/.idea/workspace.xml +47 -0
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/PKG-INFO +4 -1
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/pyproject.toml +12 -5
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/src/nano_dev_utils/__init__.py +8 -5
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/src/nano_dev_utils/dynamic_importer.py +0 -3
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/src/nano_dev_utils/release_ports.py +22 -18
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/src/nano_dev_utils/timers.py +6 -7
- nano_dev_utils-0.5.2/tests/__init__.py +10 -0
- nano_dev_utils-0.5.2/tests/test_dynamic_importer.py +62 -0
- nano_dev_utils-0.5.2/tests/test_release_ports.py +307 -0
- nano_dev_utils-0.5.2/tests/test_timer.py +159 -0
- nano_dev_utils-0.4.4/.idea/misc.xml +0 -7
- nano_dev_utils-0.4.4/.idea/workspace.xml +0 -327
- nano_dev_utils-0.4.4/tests/__init__.py +0 -16
- nano_dev_utils-0.4.4/tests/testing_dynamic_importer.py +0 -55
- nano_dev_utils-0.4.4/tests/testing_release_ports.py +0 -276
- nano_dev_utils-0.4.4/tests/testing_timer.py +0 -157
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/.gitignore +0 -0
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/.idea/.gitignore +0 -0
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/.idea/inspectionProfiles/profiles_settings.xml +0 -0
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/.idea/modules.xml +0 -0
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/.idea/nano_dev_utils.iml +0 -0
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/.idea/vcs.xml +0 -0
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/LICENSE.md +0 -0
- {nano_dev_utils-0.4.4 → nano_dev_utils-0.5.2}/README.md +0 -0
|
@@ -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.
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "nano_dev_utils"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.2"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
'Importer',
|
|
14
|
+
'Timer',
|
|
15
|
+
'PortsRelease',
|
|
16
|
+
'PROXY_SERVER',
|
|
17
|
+
'INSPECTOR_CLIENT',
|
|
15
18
|
]
|
|
@@ -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] =
|
|
15
|
-
|
|
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(
|
|
35
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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(
|
|
73
|
-
|
|
74
|
-
|
|
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 ==
|
|
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 ==
|
|
94
|
+
elif system == 'Linux':
|
|
91
95
|
for part in parts:
|
|
92
|
-
if
|
|
96
|
+
if 'pid=' in part:
|
|
93
97
|
try:
|
|
94
|
-
return int(part.split(
|
|
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 ==
|
|
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(),
|
|
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(
|
|
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,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)
|