psinfo 0.1.0__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.
- psinfo-0.1.0/LICENSE +21 -0
- psinfo-0.1.0/PKG-INFO +103 -0
- psinfo-0.1.0/README.md +72 -0
- psinfo-0.1.0/psinfo.egg-info/PKG-INFO +103 -0
- psinfo-0.1.0/psinfo.egg-info/SOURCES.txt +12 -0
- psinfo-0.1.0/psinfo.egg-info/dependency_links.txt +1 -0
- psinfo-0.1.0/psinfo.egg-info/entry_points.txt +2 -0
- psinfo-0.1.0/psinfo.egg-info/requires.txt +2 -0
- psinfo-0.1.0/psinfo.egg-info/top_level.txt +1 -0
- psinfo-0.1.0/pyproject.toml +44 -0
- psinfo-0.1.0/setup.cfg +4 -0
- psinfo-0.1.0/tests/test_cli.py +99 -0
- psinfo-0.1.0/tests/test_query.py +261 -0
- psinfo-0.1.0/tests/test_render.py +177 -0
psinfo-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dan Blore
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
psinfo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: psinfo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rich terminal process viewer
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/dblore/psinfo
|
|
7
|
+
Project-URL: Repository, https://github.com/dblore/psinfo
|
|
8
|
+
Project-URL: Issues, https://github.com/dblore/psinfo/issues
|
|
9
|
+
Keywords: process,ps,terminal,cli,rich
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Monitoring
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: psutil>=5.9
|
|
29
|
+
Requires-Dist: rich>=13.0
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# psinfo
|
|
33
|
+
|
|
34
|
+
A rich terminal process viewer. Search, filter, and watch running processes with colour-coded output.
|
|
35
|
+
|
|
36
|
+
psinfo is for when you have a specific question — "what's on port 8080", "how much memory is Chrome using across all its helpers", "is nginx running and what's it doing" — and want a clean answer without launching a full process manager or constructing a `ps aux | grep | awk` pipeline.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e .
|
|
42
|
+
# or, to install as an isolated CLI tool:
|
|
43
|
+
pipx install .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`pipx` is recommended for CLI tools — it installs into an isolated environment and puts `psinfo` on your PATH. Install it with `brew install pipx` or `pip install pipx`.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
psinfo <name> search by process name or command
|
|
52
|
+
psinfo -u <username> all processes owned by a user
|
|
53
|
+
psinfo -p <port> process listening on a port
|
|
54
|
+
psinfo -P <pid> single process by PID
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Flags
|
|
58
|
+
|
|
59
|
+
| Flag | Short | Description |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `--watch [N]` | `-w` | Live refresh every N seconds (default: 2) |
|
|
62
|
+
| `--sort cpu\|mem` | `-s` | Sort order (default: cpu) |
|
|
63
|
+
| `--tree` | `-t` | Group results by process tree hierarchy |
|
|
64
|
+
|
|
65
|
+
Flags compose freely:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
psinfo nginx -w
|
|
69
|
+
psinfo python -w 5 -s mem
|
|
70
|
+
psinfo slack -t -w
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Output
|
|
74
|
+
|
|
75
|
+
### Card view (default)
|
|
76
|
+
|
|
77
|
+
Each matching process is shown as a panel. The highest-CPU process gets a green border. Metrics are colour-coded: green (< 10%), orange (≥ 10%), red (≥ 50%).
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
### Tree view (`-t`)
|
|
82
|
+
|
|
83
|
+
Results are grouped by parent/child relationships. Parent nodes show their own CPU plus a `∑` total across the whole subtree.
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Python 3.9+
|
|
90
|
+
- psutil >= 5.9
|
|
91
|
+
- rich >= 13.0
|
|
92
|
+
|
|
93
|
+
## Platform support
|
|
94
|
+
|
|
95
|
+
| Platform | Status | Notes |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| macOS | ✓ | `--port` requires `sudo` |
|
|
98
|
+
| Linux | ✓ | |
|
|
99
|
+
| Windows | ✓ | Shows handles instead of file descriptors |
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT — see [LICENSE](LICENSE).
|
psinfo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# psinfo
|
|
2
|
+
|
|
3
|
+
A rich terminal process viewer. Search, filter, and watch running processes with colour-coded output.
|
|
4
|
+
|
|
5
|
+
psinfo is for when you have a specific question — "what's on port 8080", "how much memory is Chrome using across all its helpers", "is nginx running and what's it doing" — and want a clean answer without launching a full process manager or constructing a `ps aux | grep | awk` pipeline.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e .
|
|
11
|
+
# or, to install as an isolated CLI tool:
|
|
12
|
+
pipx install .
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`pipx` is recommended for CLI tools — it installs into an isolated environment and puts `psinfo` on your PATH. Install it with `brew install pipx` or `pip install pipx`.
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
psinfo <name> search by process name or command
|
|
21
|
+
psinfo -u <username> all processes owned by a user
|
|
22
|
+
psinfo -p <port> process listening on a port
|
|
23
|
+
psinfo -P <pid> single process by PID
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Flags
|
|
27
|
+
|
|
28
|
+
| Flag | Short | Description |
|
|
29
|
+
|---|---|---|
|
|
30
|
+
| `--watch [N]` | `-w` | Live refresh every N seconds (default: 2) |
|
|
31
|
+
| `--sort cpu\|mem` | `-s` | Sort order (default: cpu) |
|
|
32
|
+
| `--tree` | `-t` | Group results by process tree hierarchy |
|
|
33
|
+
|
|
34
|
+
Flags compose freely:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
psinfo nginx -w
|
|
38
|
+
psinfo python -w 5 -s mem
|
|
39
|
+
psinfo slack -t -w
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Output
|
|
43
|
+
|
|
44
|
+
### Card view (default)
|
|
45
|
+
|
|
46
|
+
Each matching process is shown as a panel. The highest-CPU process gets a green border. Metrics are colour-coded: green (< 10%), orange (≥ 10%), red (≥ 50%).
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
### Tree view (`-t`)
|
|
51
|
+
|
|
52
|
+
Results are grouped by parent/child relationships. Parent nodes show their own CPU plus a `∑` total across the whole subtree.
|
|
53
|
+
|
|
54
|
+

|
|
55
|
+
|
|
56
|
+
## Requirements
|
|
57
|
+
|
|
58
|
+
- Python 3.9+
|
|
59
|
+
- psutil >= 5.9
|
|
60
|
+
- rich >= 13.0
|
|
61
|
+
|
|
62
|
+
## Platform support
|
|
63
|
+
|
|
64
|
+
| Platform | Status | Notes |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| macOS | ✓ | `--port` requires `sudo` |
|
|
67
|
+
| Linux | ✓ | |
|
|
68
|
+
| Windows | ✓ | Shows handles instead of file descriptors |
|
|
69
|
+
|
|
70
|
+
## License
|
|
71
|
+
|
|
72
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: psinfo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Rich terminal process viewer
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/dblore/psinfo
|
|
7
|
+
Project-URL: Repository, https://github.com/dblore/psinfo
|
|
8
|
+
Project-URL: Issues, https://github.com/dblore/psinfo/issues
|
|
9
|
+
Keywords: process,ps,terminal,cli,rich
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Topic :: System :: Monitoring
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Requires-Python: >=3.9
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
License-File: LICENSE
|
|
28
|
+
Requires-Dist: psutil>=5.9
|
|
29
|
+
Requires-Dist: rich>=13.0
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# psinfo
|
|
33
|
+
|
|
34
|
+
A rich terminal process viewer. Search, filter, and watch running processes with colour-coded output.
|
|
35
|
+
|
|
36
|
+
psinfo is for when you have a specific question — "what's on port 8080", "how much memory is Chrome using across all its helpers", "is nginx running and what's it doing" — and want a clean answer without launching a full process manager or constructing a `ps aux | grep | awk` pipeline.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install -e .
|
|
42
|
+
# or, to install as an isolated CLI tool:
|
|
43
|
+
pipx install .
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`pipx` is recommended for CLI tools — it installs into an isolated environment and puts `psinfo` on your PATH. Install it with `brew install pipx` or `pip install pipx`.
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
psinfo <name> search by process name or command
|
|
52
|
+
psinfo -u <username> all processes owned by a user
|
|
53
|
+
psinfo -p <port> process listening on a port
|
|
54
|
+
psinfo -P <pid> single process by PID
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Flags
|
|
58
|
+
|
|
59
|
+
| Flag | Short | Description |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| `--watch [N]` | `-w` | Live refresh every N seconds (default: 2) |
|
|
62
|
+
| `--sort cpu\|mem` | `-s` | Sort order (default: cpu) |
|
|
63
|
+
| `--tree` | `-t` | Group results by process tree hierarchy |
|
|
64
|
+
|
|
65
|
+
Flags compose freely:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
psinfo nginx -w
|
|
69
|
+
psinfo python -w 5 -s mem
|
|
70
|
+
psinfo slack -t -w
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Output
|
|
74
|
+
|
|
75
|
+
### Card view (default)
|
|
76
|
+
|
|
77
|
+
Each matching process is shown as a panel. The highest-CPU process gets a green border. Metrics are colour-coded: green (< 10%), orange (≥ 10%), red (≥ 50%).
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
### Tree view (`-t`)
|
|
82
|
+
|
|
83
|
+
Results are grouped by parent/child relationships. Parent nodes show their own CPU plus a `∑` total across the whole subtree.
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Python 3.9+
|
|
90
|
+
- psutil >= 5.9
|
|
91
|
+
- rich >= 13.0
|
|
92
|
+
|
|
93
|
+
## Platform support
|
|
94
|
+
|
|
95
|
+
| Platform | Status | Notes |
|
|
96
|
+
|---|---|---|
|
|
97
|
+
| macOS | ✓ | `--port` requires `sudo` |
|
|
98
|
+
| Linux | ✓ | |
|
|
99
|
+
| Windows | ✓ | Shows handles instead of file descriptors |
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
psinfo.egg-info/PKG-INFO
|
|
5
|
+
psinfo.egg-info/SOURCES.txt
|
|
6
|
+
psinfo.egg-info/dependency_links.txt
|
|
7
|
+
psinfo.egg-info/entry_points.txt
|
|
8
|
+
psinfo.egg-info/requires.txt
|
|
9
|
+
psinfo.egg-info/top_level.txt
|
|
10
|
+
tests/test_cli.py
|
|
11
|
+
tests/test_query.py
|
|
12
|
+
tests/test_render.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
assets
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "psinfo"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Rich terminal process viewer"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
keywords = ["process", "ps", "terminal", "cli", "rich"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: System Administrators",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: MacOS",
|
|
20
|
+
"Operating System :: POSIX :: Linux",
|
|
21
|
+
"Operating System :: Microsoft :: Windows",
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.9",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Topic :: System :: Monitoring",
|
|
28
|
+
"Topic :: Utilities",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"psutil>=5.9",
|
|
32
|
+
"rich>=13.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/dblore/psinfo"
|
|
37
|
+
Repository = "https://github.com/dblore/psinfo"
|
|
38
|
+
Issues = "https://github.com/dblore/psinfo/issues"
|
|
39
|
+
|
|
40
|
+
[project.scripts]
|
|
41
|
+
psinfo = "psinfo:main"
|
|
42
|
+
|
|
43
|
+
[tool.pytest.ini_options]
|
|
44
|
+
testpaths = ["tests"]
|
psinfo-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import pytest
|
|
3
|
+
from unittest.mock import patch, MagicMock
|
|
4
|
+
from psinfo import ProcessInfo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_parser_accepts_name_positional():
|
|
8
|
+
from psinfo import build_parser
|
|
9
|
+
args = build_parser().parse_args(["mysql"])
|
|
10
|
+
assert args.name == "mysql"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_parser_accepts_user_flag():
|
|
14
|
+
from psinfo import build_parser
|
|
15
|
+
args = build_parser().parse_args(["--user", "root"])
|
|
16
|
+
assert args.user == "root"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_parser_accepts_port_flag():
|
|
20
|
+
from psinfo import build_parser
|
|
21
|
+
args = build_parser().parse_args(["--port", "3306"])
|
|
22
|
+
assert args.port == 3306
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_parser_accepts_pid_flag():
|
|
26
|
+
from psinfo import build_parser
|
|
27
|
+
args = build_parser().parse_args(["--pid", "1234"])
|
|
28
|
+
assert args.pid == 1234
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_parser_watch_defaults_to_2():
|
|
32
|
+
from psinfo import build_parser
|
|
33
|
+
args = build_parser().parse_args(["mysql", "--watch"])
|
|
34
|
+
assert args.watch == 2.0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_parser_watch_accepts_custom_interval():
|
|
38
|
+
from psinfo import build_parser
|
|
39
|
+
args = build_parser().parse_args(["mysql", "--watch", "5"])
|
|
40
|
+
assert args.watch == 5.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_parser_sort_defaults_to_cpu():
|
|
44
|
+
from psinfo import build_parser
|
|
45
|
+
args = build_parser().parse_args(["mysql"])
|
|
46
|
+
assert args.sort == "cpu"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_parser_sort_accepts_mem():
|
|
50
|
+
from psinfo import build_parser
|
|
51
|
+
args = build_parser().parse_args(["mysql", "--sort", "mem"])
|
|
52
|
+
assert args.sort == "mem"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_main_calls_find_by_name(mocker):
|
|
56
|
+
from psinfo import main
|
|
57
|
+
mock_find = mocker.patch("psinfo.find_by_name", return_value=[])
|
|
58
|
+
mocker.patch("psutil.net_connections", return_value=[])
|
|
59
|
+
with patch("sys.argv", ["psinfo", "mysql"]):
|
|
60
|
+
main()
|
|
61
|
+
mock_find.assert_called_once_with("mysql")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_main_calls_find_by_user(mocker):
|
|
65
|
+
from psinfo import main
|
|
66
|
+
mock_find = mocker.patch("psinfo.find_by_user", return_value=[])
|
|
67
|
+
with patch("sys.argv", ["psinfo", "--user", "root"]):
|
|
68
|
+
main()
|
|
69
|
+
mock_find.assert_called_once_with("root")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_main_calls_find_by_pid(mocker):
|
|
73
|
+
from psinfo import main
|
|
74
|
+
proc = ProcessInfo(
|
|
75
|
+
pid=1234, name="test", cmdline=[], username="user",
|
|
76
|
+
cpu_percent=0.0, mem_percent=0.0, rss_mb=0.0,
|
|
77
|
+
num_threads=1, create_time=0.0, num_fds=0
|
|
78
|
+
)
|
|
79
|
+
mock_find = mocker.patch("psinfo.find_by_pid", return_value=proc)
|
|
80
|
+
with patch("sys.argv", ["psinfo", "--pid", "1234"]):
|
|
81
|
+
main()
|
|
82
|
+
mock_find.assert_called_once_with(1234)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_main_calls_find_by_port(mocker):
|
|
86
|
+
from psinfo import main
|
|
87
|
+
mock_find = mocker.patch("psinfo.find_by_port", return_value=None)
|
|
88
|
+
with patch("sys.argv", ["psinfo", "--port", "3306"]):
|
|
89
|
+
main()
|
|
90
|
+
mock_find.assert_called_once_with(3306)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_main_exits_1_when_pid_not_found(mocker):
|
|
94
|
+
from psinfo import main
|
|
95
|
+
mocker.patch("psinfo.find_by_pid", return_value=None)
|
|
96
|
+
with patch("sys.argv", ["psinfo", "--pid", "9999"]):
|
|
97
|
+
with pytest.raises(SystemExit) as exc:
|
|
98
|
+
main()
|
|
99
|
+
assert exc.value.code == 1
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
from unittest.mock import MagicMock
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def make_mock_proc(
|
|
5
|
+
pid=1234,
|
|
6
|
+
name="mysqld",
|
|
7
|
+
cmdline=None,
|
|
8
|
+
username="mysql",
|
|
9
|
+
cpu_percent=0.5,
|
|
10
|
+
memory_percent=2.1,
|
|
11
|
+
rss=176160768, # bytes → 168 MB
|
|
12
|
+
num_threads=32,
|
|
13
|
+
create_time=1000000.0,
|
|
14
|
+
num_fds=48,
|
|
15
|
+
):
|
|
16
|
+
if cmdline is None:
|
|
17
|
+
cmdline = ["/usr/sbin/mysqld", "--datadir=/var/lib/mysql"]
|
|
18
|
+
proc = MagicMock()
|
|
19
|
+
mem_info = MagicMock()
|
|
20
|
+
mem_info.rss = rss
|
|
21
|
+
proc.info = {
|
|
22
|
+
"pid": pid,
|
|
23
|
+
"name": name,
|
|
24
|
+
"cmdline": cmdline,
|
|
25
|
+
"username": username,
|
|
26
|
+
"cpu_percent": cpu_percent,
|
|
27
|
+
"memory_percent": memory_percent,
|
|
28
|
+
"memory_info": mem_info,
|
|
29
|
+
"num_threads": num_threads,
|
|
30
|
+
"create_time": create_time,
|
|
31
|
+
"num_fds": num_fds,
|
|
32
|
+
}
|
|
33
|
+
proc.as_dict.return_value = dict(proc.info)
|
|
34
|
+
return proc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_process_info_fields():
|
|
38
|
+
from psinfo import ProcessInfo
|
|
39
|
+
p = ProcessInfo(
|
|
40
|
+
pid=1234,
|
|
41
|
+
name="mysqld",
|
|
42
|
+
cmdline=["/usr/sbin/mysqld", "--datadir=/var/lib/mysql"],
|
|
43
|
+
username="mysql",
|
|
44
|
+
cpu_percent=0.5,
|
|
45
|
+
mem_percent=2.1,
|
|
46
|
+
rss_mb=168.0,
|
|
47
|
+
num_threads=32,
|
|
48
|
+
create_time=1000000.0,
|
|
49
|
+
num_fds=48,
|
|
50
|
+
)
|
|
51
|
+
assert p.pid == 1234
|
|
52
|
+
assert p.name == "mysqld"
|
|
53
|
+
assert p.rss_mb == 168.0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_command_joins_cmdline():
|
|
57
|
+
from psinfo import ProcessInfo
|
|
58
|
+
p = ProcessInfo(
|
|
59
|
+
pid=1, name="mysqld",
|
|
60
|
+
cmdline=["/usr/sbin/mysqld", "--datadir=/var/lib/mysql"],
|
|
61
|
+
username="mysql", cpu_percent=0.0, mem_percent=0.0,
|
|
62
|
+
rss_mb=0.0, num_threads=1, create_time=1000000.0, num_fds=0,
|
|
63
|
+
)
|
|
64
|
+
assert p.command == "/usr/sbin/mysqld --datadir=/var/lib/mysql"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_command_falls_back_to_name_when_no_cmdline():
|
|
68
|
+
from psinfo import ProcessInfo
|
|
69
|
+
p = ProcessInfo(
|
|
70
|
+
pid=1, name="kernel_task", cmdline=[],
|
|
71
|
+
username="root", cpu_percent=0.0, mem_percent=0.0,
|
|
72
|
+
rss_mb=0.0, num_threads=1, create_time=1000000.0, num_fds=0,
|
|
73
|
+
)
|
|
74
|
+
assert p.command == "kernel_task"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_proc_to_info_converts_mock():
|
|
78
|
+
from psinfo import _proc_to_info
|
|
79
|
+
mock = make_mock_proc()
|
|
80
|
+
result = _proc_to_info(mock)
|
|
81
|
+
assert result is not None
|
|
82
|
+
assert result.pid == 1234
|
|
83
|
+
assert result.name == "mysqld"
|
|
84
|
+
assert abs(result.rss_mb - 168.0) < 1.0
|
|
85
|
+
assert result.num_threads == 32
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_proc_to_info_returns_none_on_no_such_process():
|
|
89
|
+
import psutil
|
|
90
|
+
from psinfo import _proc_to_info
|
|
91
|
+
mock = MagicMock()
|
|
92
|
+
mock.as_dict.side_effect = psutil.NoSuchProcess(pid=9999)
|
|
93
|
+
assert _proc_to_info(mock) is None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_proc_to_info_returns_none_on_access_denied():
|
|
97
|
+
import psutil
|
|
98
|
+
from psinfo import _proc_to_info
|
|
99
|
+
mock = MagicMock()
|
|
100
|
+
mock.as_dict.side_effect = psutil.AccessDenied(pid=1)
|
|
101
|
+
assert _proc_to_info(mock) is None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_find_by_name_matches_name_field(mocker):
|
|
105
|
+
from psinfo import find_by_name
|
|
106
|
+
mock_mysql = make_mock_proc(name="mysqld", cmdline=["/usr/sbin/mysqld"])
|
|
107
|
+
mock_other = make_mock_proc(pid=9999, name="nginx", cmdline=["/usr/sbin/nginx"])
|
|
108
|
+
mocker.patch("psutil.process_iter", return_value=[mock_mysql, mock_other])
|
|
109
|
+
results = find_by_name("mysql")
|
|
110
|
+
assert len(results) == 1
|
|
111
|
+
assert results[0].name == "mysqld"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_find_by_name_matches_cmdline(mocker):
|
|
115
|
+
from psinfo import find_by_name
|
|
116
|
+
mock = make_mock_proc(name="python3", cmdline=["python3", "manage.py", "runserver"])
|
|
117
|
+
mocker.patch("psutil.process_iter", return_value=[mock])
|
|
118
|
+
results = find_by_name("manage")
|
|
119
|
+
assert len(results) == 1
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_find_by_name_is_case_insensitive(mocker):
|
|
123
|
+
from psinfo import find_by_name
|
|
124
|
+
mock = make_mock_proc(name="MySQLd")
|
|
125
|
+
mocker.patch("psutil.process_iter", return_value=[mock])
|
|
126
|
+
results = find_by_name("mysql")
|
|
127
|
+
assert len(results) == 1
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_find_by_name_returns_empty_when_no_match(mocker):
|
|
131
|
+
from psinfo import find_by_name
|
|
132
|
+
mock = make_mock_proc(name="nginx", cmdline=["/usr/sbin/nginx"])
|
|
133
|
+
mocker.patch("psutil.process_iter", return_value=[mock])
|
|
134
|
+
results = find_by_name("mysql")
|
|
135
|
+
assert results == []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_find_by_name_skips_inaccessible_processes(mocker):
|
|
139
|
+
import psutil
|
|
140
|
+
from psinfo import find_by_name
|
|
141
|
+
bad_proc = MagicMock()
|
|
142
|
+
bad_proc.info = MagicMock()
|
|
143
|
+
bad_proc.info.get = MagicMock(side_effect=psutil.AccessDenied(pid=1))
|
|
144
|
+
good = make_mock_proc(name="mysqld")
|
|
145
|
+
mocker.patch("psutil.process_iter", return_value=[bad_proc, good])
|
|
146
|
+
results = find_by_name("mysql")
|
|
147
|
+
assert len(results) == 1
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_find_by_user_filters_by_username(mocker):
|
|
151
|
+
from psinfo import find_by_user
|
|
152
|
+
mysql_proc = make_mock_proc(pid=1234, name="mysqld", username="mysql")
|
|
153
|
+
root_proc = make_mock_proc(pid=1235, name="sshd", username="root")
|
|
154
|
+
mocker.patch("psutil.process_iter", return_value=[mysql_proc, root_proc])
|
|
155
|
+
results = find_by_user("mysql")
|
|
156
|
+
assert len(results) == 1
|
|
157
|
+
assert results[0].username == "mysql"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_find_by_user_is_case_insensitive(mocker):
|
|
161
|
+
from psinfo import find_by_user
|
|
162
|
+
proc = make_mock_proc(username="MySQL")
|
|
163
|
+
mocker.patch("psutil.process_iter", return_value=[proc])
|
|
164
|
+
results = find_by_user("mysql")
|
|
165
|
+
assert len(results) == 1
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def test_find_by_pid_returns_process(mocker):
|
|
169
|
+
from psinfo import find_by_pid
|
|
170
|
+
mock = make_mock_proc(pid=1234)
|
|
171
|
+
mocker.patch("psutil.Process", return_value=mock)
|
|
172
|
+
result = find_by_pid(1234)
|
|
173
|
+
assert result is not None
|
|
174
|
+
assert result.pid == 1234
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_find_by_pid_returns_none_when_not_found(mocker):
|
|
178
|
+
import psutil
|
|
179
|
+
from psinfo import find_by_pid
|
|
180
|
+
mocker.patch("psutil.Process", side_effect=psutil.NoSuchProcess(pid=9999))
|
|
181
|
+
assert find_by_pid(9999) is None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_find_by_port_returns_process_owning_port(mocker):
|
|
185
|
+
from psinfo import find_by_port
|
|
186
|
+
conn = MagicMock()
|
|
187
|
+
conn.laddr.port = 3306
|
|
188
|
+
conn.pid = 1234
|
|
189
|
+
mocker.patch("psutil.net_connections", return_value=[conn])
|
|
190
|
+
mock_proc = make_mock_proc(pid=1234, name="mysqld")
|
|
191
|
+
mocker.patch("psutil.Process", return_value=mock_proc)
|
|
192
|
+
result = find_by_port(3306)
|
|
193
|
+
assert result is not None
|
|
194
|
+
assert result.pid == 1234
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def test_find_by_port_returns_none_when_port_not_in_use(mocker):
|
|
198
|
+
from psinfo import find_by_port
|
|
199
|
+
conn = MagicMock()
|
|
200
|
+
conn.laddr.port = 8080
|
|
201
|
+
conn.pid = 999
|
|
202
|
+
mocker.patch("psutil.net_connections", return_value=[conn])
|
|
203
|
+
result = find_by_port(3306)
|
|
204
|
+
assert result is None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def test_find_by_port_returns_none_on_access_denied(mocker):
|
|
208
|
+
import psutil
|
|
209
|
+
from psinfo import find_by_port
|
|
210
|
+
mocker.patch("psutil.net_connections", side_effect=psutil.AccessDenied(pid=0))
|
|
211
|
+
assert find_by_port(3306) is None
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_find_by_pid_returns_none_on_access_denied(mocker):
|
|
215
|
+
import psutil
|
|
216
|
+
from psinfo import find_by_pid
|
|
217
|
+
mocker.patch("psutil.Process", side_effect=psutil.AccessDenied(pid=1))
|
|
218
|
+
assert find_by_pid(1) is None
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_find_by_port_prefers_listen_state(mocker):
|
|
222
|
+
from psinfo import find_by_port
|
|
223
|
+
listen_conn = MagicMock()
|
|
224
|
+
listen_conn.laddr.port = 3306
|
|
225
|
+
listen_conn.pid = 1234
|
|
226
|
+
listen_conn.status = "LISTEN"
|
|
227
|
+
estab_conn = MagicMock()
|
|
228
|
+
estab_conn.laddr.port = 3306
|
|
229
|
+
estab_conn.pid = 5678
|
|
230
|
+
estab_conn.status = "ESTABLISHED"
|
|
231
|
+
mocker.patch("psutil.net_connections", return_value=[estab_conn, listen_conn])
|
|
232
|
+
mock_proc = make_mock_proc(pid=1234, name="mysqld")
|
|
233
|
+
mocker.patch("psutil.Process", return_value=mock_proc)
|
|
234
|
+
result = find_by_port(3306)
|
|
235
|
+
assert result is not None
|
|
236
|
+
assert result.pid == 1234 # LISTEN connection preferred over ESTABLISHED
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def test_find_by_name_excludes_self(mocker):
|
|
240
|
+
import os
|
|
241
|
+
from psinfo import find_by_name
|
|
242
|
+
self_proc = make_mock_proc(pid=os.getpid(), name="python3",
|
|
243
|
+
cmdline=["python3", "psinfo", "mysql"])
|
|
244
|
+
other_proc = make_mock_proc(pid=9999, name="mysqld", cmdline=["/usr/sbin/mysqld"])
|
|
245
|
+
mocker.patch("psutil.process_iter", return_value=[self_proc, other_proc])
|
|
246
|
+
results = find_by_name("mysql")
|
|
247
|
+
assert all(r.pid != os.getpid() for r in results)
|
|
248
|
+
assert len(results) == 1
|
|
249
|
+
assert results[0].pid == 9999
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_find_by_user_excludes_self(mocker):
|
|
253
|
+
import os
|
|
254
|
+
from psinfo import find_by_user
|
|
255
|
+
self_proc = make_mock_proc(pid=os.getpid(), name="python3", username="dan")
|
|
256
|
+
other_proc = make_mock_proc(pid=9999, name="bash", username="dan")
|
|
257
|
+
mocker.patch("psutil.process_iter", return_value=[self_proc, other_proc])
|
|
258
|
+
results = find_by_user("dan")
|
|
259
|
+
assert all(r.pid != os.getpid() for r in results)
|
|
260
|
+
assert len(results) == 1
|
|
261
|
+
assert results[0].pid == 9999
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import pytest
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.panel import Panel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def make_proc(cpu=0.5, mem=2.1, rss_mb=168.0, name="mysqld", pid=1234,
|
|
8
|
+
cmdline=None, username="mysql", num_threads=32, num_fds=48,
|
|
9
|
+
create_time=1000000.0):
|
|
10
|
+
if cmdline is None:
|
|
11
|
+
cmdline = ["/usr/sbin/mysqld", "--datadir=/var/lib/mysql"]
|
|
12
|
+
from psinfo import ProcessInfo
|
|
13
|
+
return ProcessInfo(
|
|
14
|
+
pid=pid, name=name, cmdline=cmdline, username=username,
|
|
15
|
+
cpu_percent=cpu, mem_percent=mem, rss_mb=rss_mb,
|
|
16
|
+
num_threads=num_threads, create_time=create_time, num_fds=num_fds,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def render_to_str(renderable) -> str:
|
|
21
|
+
buf = io.StringIO()
|
|
22
|
+
console = Console(file=buf, width=100, highlight=False, markup=False)
|
|
23
|
+
console.print(renderable)
|
|
24
|
+
return buf.getvalue()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_render_card_returns_panel():
|
|
28
|
+
from psinfo import render_card
|
|
29
|
+
proc = make_proc()
|
|
30
|
+
card = render_card(proc, highlight=False)
|
|
31
|
+
assert isinstance(card, Panel)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_render_card_contains_pid():
|
|
35
|
+
from psinfo import render_card
|
|
36
|
+
proc = make_proc(pid=1234)
|
|
37
|
+
output = render_to_str(render_card(proc))
|
|
38
|
+
assert "1234" in output
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_render_card_contains_process_name():
|
|
42
|
+
from psinfo import render_card
|
|
43
|
+
proc = make_proc(name="mysqld")
|
|
44
|
+
output = render_to_str(render_card(proc))
|
|
45
|
+
assert "mysqld" in output
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_render_card_contains_username():
|
|
49
|
+
from psinfo import render_card
|
|
50
|
+
proc = make_proc(username="mysql")
|
|
51
|
+
output = render_to_str(render_card(proc))
|
|
52
|
+
assert "mysql" in output
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_render_card_contains_rss():
|
|
56
|
+
from psinfo import render_card
|
|
57
|
+
proc = make_proc(rss_mb=168.0)
|
|
58
|
+
output = render_to_str(render_card(proc))
|
|
59
|
+
assert "168" in output
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_render_card_contains_thread_count():
|
|
63
|
+
from psinfo import render_card
|
|
64
|
+
proc = make_proc(num_threads=32)
|
|
65
|
+
output = render_to_str(render_card(proc))
|
|
66
|
+
assert "32" in output
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_metric_color_green_below_10():
|
|
70
|
+
from psinfo import _metric_color
|
|
71
|
+
assert _metric_color(5.0) == "green"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_metric_color_orange_between_10_and_50():
|
|
75
|
+
from psinfo import _metric_color
|
|
76
|
+
assert _metric_color(25.0) == "dark_orange"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_metric_color_red_above_50():
|
|
80
|
+
from psinfo import _metric_color
|
|
81
|
+
assert _metric_color(75.0) == "red"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_render_card_contains_cpu():
|
|
85
|
+
from psinfo import render_card
|
|
86
|
+
proc = make_proc(cpu=42.5)
|
|
87
|
+
output = render_to_str(render_card(proc))
|
|
88
|
+
assert "42.5" in output
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_render_card_contains_mem():
|
|
92
|
+
from psinfo import render_card
|
|
93
|
+
proc = make_proc(mem=3.7)
|
|
94
|
+
output = render_to_str(render_card(proc))
|
|
95
|
+
assert "3.7" in output
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_render_card_contains_fd_count():
|
|
99
|
+
from psinfo import render_card
|
|
100
|
+
proc = make_proc(num_fds=99)
|
|
101
|
+
output = render_to_str(render_card(proc))
|
|
102
|
+
assert "99" in output
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_render_card_contains_command():
|
|
106
|
+
from psinfo import render_card
|
|
107
|
+
proc = make_proc(cmdline=["/usr/sbin/mysqld", "--datadir=/var/lib/mysql"])
|
|
108
|
+
output = render_to_str(render_card(proc))
|
|
109
|
+
assert "/usr/sbin/mysqld" in output
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_render_card_highlight_uses_green_title():
|
|
113
|
+
from psinfo import render_card
|
|
114
|
+
from rich.panel import Panel
|
|
115
|
+
from rich.text import Text
|
|
116
|
+
proc = make_proc(name="mysqld")
|
|
117
|
+
highlighted_card = render_card(proc, highlight=True)
|
|
118
|
+
normal_card = render_card(proc, highlight=False)
|
|
119
|
+
# Highlighted card has green border, normal has dim
|
|
120
|
+
assert highlighted_card.border_style == "green"
|
|
121
|
+
assert normal_card.border_style == "dim"
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_metric_color_at_warn_boundary():
|
|
125
|
+
from psinfo import _metric_color
|
|
126
|
+
assert _metric_color(10.0) == "dark_orange" # exactly at warn threshold
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def test_metric_color_at_critical_boundary():
|
|
130
|
+
from psinfo import _metric_color
|
|
131
|
+
assert _metric_color(50.0) == "red" # exactly at critical threshold
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_render_results_shows_match_count():
|
|
135
|
+
from psinfo import render_results
|
|
136
|
+
procs = [make_proc(pid=1), make_proc(pid=2, name="mysqld_safe")]
|
|
137
|
+
output = render_to_str(render_results(procs, "mysql"))
|
|
138
|
+
assert "2" in output
|
|
139
|
+
assert "mysql" in output
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_render_results_shows_no_processes_when_empty():
|
|
143
|
+
from psinfo import render_results
|
|
144
|
+
output = render_to_str(render_results([], "mysql"))
|
|
145
|
+
assert "No processes found" in output
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_sort_procs_by_cpu_descending():
|
|
149
|
+
from psinfo import sort_procs
|
|
150
|
+
procs = [make_proc(cpu=1.0, pid=1), make_proc(cpu=5.0, pid=2), make_proc(cpu=0.5, pid=3)]
|
|
151
|
+
result = sort_procs(procs, "cpu")
|
|
152
|
+
assert result[0].cpu_percent == 5.0
|
|
153
|
+
assert result[-1].cpu_percent == 0.5
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_sort_procs_by_mem_descending():
|
|
157
|
+
from psinfo import sort_procs
|
|
158
|
+
procs = [make_proc(mem=1.0, pid=1), make_proc(mem=5.0, pid=2)]
|
|
159
|
+
result = sort_procs(procs, "mem")
|
|
160
|
+
assert result[0].mem_percent == 5.0
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def test_sort_procs_by_pid_ascending():
|
|
164
|
+
from psinfo import sort_procs
|
|
165
|
+
procs = [make_proc(pid=300), make_proc(pid=100), make_proc(pid=200)]
|
|
166
|
+
result = sort_procs(procs, "pid")
|
|
167
|
+
assert result[0].pid == 100
|
|
168
|
+
assert result[-1].pid == 300
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_render_results_highlights_highest_cpu():
|
|
172
|
+
from psinfo import render_results
|
|
173
|
+
high = make_proc(pid=1, cpu=50.0, name="high_cpu")
|
|
174
|
+
low = make_proc(pid=2, cpu=1.0, name="low_cpu")
|
|
175
|
+
output = render_to_str(render_results([high, low], "test"))
|
|
176
|
+
assert "high_cpu" in output
|
|
177
|
+
assert "low_cpu" in output
|