Skip to content

Commit 840e7be

Browse files
committed
✨ Add the stats command
1 parent 43564a6 commit 840e7be

File tree

4 files changed

+177
-2
lines changed

4 files changed

+177
-2
lines changed

README.md

+6-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
## 🧑🏽‍🔬 Usage
1010

11-
To enable [ccache][ccache] for `glibc`, run:
11+
To enable [ccache][ccache] for `sys-libs/glibc`, run:
1212
```shell
1313
gcm enable glibc
1414
```
@@ -18,6 +18,11 @@ To disable it, run:
1818
gcm disable glibc
1919
```
2020

21+
To explore all available commands, run:
22+
```shell
23+
gcm --help
24+
```
25+
2126
[ci-badge]: https://img.shields.io/github/actions/workflow/status/Jamim/gentoo-cache-manager/ci.yml.svg
2227
[ci]: https://github.com/Jamim/gentoo-cache-manager/actions/workflows/ci.yml
2328
[cov-badge]: https://codecov.io/github/Jamim/gentoo-cache-manager/graph/badge.svg

src/gcm/commands/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from .disable import Disable
22
from .enable import Enable
3+
from .stats import Stats
34

4-
COMMANDS = [Enable(), Disable()]
5+
COMMANDS = [Enable(), Disable(), Stats()]

src/gcm/commands/stats.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import io
2+
import re
3+
import subprocess
4+
5+
import click
6+
7+
from .base import (
8+
CCACHE_DIR,
9+
PACKAGE_NAME,
10+
Command,
11+
)
12+
13+
COLORS = {
14+
'Cacheable calls': ('yellow', 'magenta'),
15+
'Hits': ('green', 'yellow'),
16+
'Direct': ('bright_green', 'green'),
17+
'Preprocessed': ('bright_green', 'green'),
18+
'Misses': ('blue', 'yellow'),
19+
'Uncacheable calls': ('red', 'magenta'),
20+
'Local storage': ('white', ''),
21+
'Cache size (GiB)': ('cyan', 'magenta'),
22+
'Cache size (GB)': ('cyan', 'magenta'),
23+
'Cleanups': ('bright_black', ''),
24+
}
25+
STAT_REGEX = re.compile(r'(?P<indent> *)(?P<title>.*):(?P<data>.*)?\n')
26+
DATA_REGEX = re.compile(
27+
r'(?P<spacing> +)(?P<value>[\d.]+) / '
28+
r'(?P<total> *[\d.]+) \((?P<percent>.*)\)'
29+
)
30+
STAT_TEMPLATE = ' {indent}{title}:{data}\n'
31+
DATA_TEMPLATE = '{spacing}{value} / {total} ({percent})'
32+
33+
34+
def colorize_data(data: str, stat_color: str, total_color: str) -> str:
35+
match = DATA_REGEX.match(data)
36+
if not match:
37+
return click.style(data, stat_color)
38+
39+
values = match.groupdict()
40+
41+
def colorize(key: str, color: str) -> None:
42+
values[key] = click.style(values[key], color)
43+
44+
colorize('value', stat_color)
45+
colorize('total', total_color)
46+
colorize('percent', stat_color)
47+
48+
return DATA_TEMPLATE.format(**values)
49+
50+
51+
def show_stats(package: str) -> None:
52+
stdout: io.TextIOWrapper = subprocess.Popen(
53+
['ccache', '-s'],
54+
env={'CCACHE_DIR': CCACHE_DIR / package},
55+
stdout=subprocess.PIPE,
56+
text=True,
57+
).stdout # type: ignore[assignment]
58+
stats = stdout.readlines()
59+
for line in stats:
60+
match = STAT_REGEX.match(line)
61+
if match:
62+
indent, title, data = match.groups()
63+
colors = COLORS.get(title)
64+
if colors:
65+
stat_color, total_color = colors
66+
title = click.style(title, stat_color)
67+
if data:
68+
data = colorize_data(data, stat_color, total_color)
69+
line = STAT_TEMPLATE.format(indent=indent, title=title, data=data)
70+
else:
71+
line = f' {line}'
72+
click.echo(line, nl=False)
73+
74+
75+
class Stats(Command):
76+
"""Show ccache stats for a package."""
77+
78+
INVOKE_MESSAGE = f'Showing ccache stats for {PACKAGE_NAME}'
79+
80+
@staticmethod
81+
def callback(package: str) -> None: # type: ignore[override]
82+
show_stats(package)

tests/commands/test_stats.py

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
from click.testing import CliRunner
5+
6+
from gcm.commands.stats import Stats
7+
8+
MODERN_STATS = """Cacheable calls: 101 / 235 (42.98%)
9+
Hits: 8 / 101 ( 7.92%)
10+
Direct: 6 / 8 (75.00%)
11+
Preprocessed: 2 / 8 (25.00%)
12+
Misses: 93 / 101 (92.08%)
13+
Uncacheable calls: 134 / 235 (57.02%)
14+
Local storage:
15+
Cache size (GiB): 0.1 / 2.0 ( 0.00%)
16+
Cleanups: 16
17+
Hits: 8 / 101 ( 7.92%)
18+
Misses: 93 / 101 (92.08%)"""
19+
20+
LEGACY_STATS = """Summary:
21+
Hits: 8 / 101 (7.92 %)
22+
Direct: 6 / 135 (4.44 %)
23+
Preprocessed: 2 / 119 (1.68 %)
24+
Misses: 93
25+
Direct: 129
26+
Preprocessed: 117
27+
Uncacheable: 134
28+
Primary storage:
29+
Hits: 37 / 260 (14.23 %)
30+
Misses: 223
31+
Cache size (GB): 0.10 / 2.00 (0.00 %)
32+
Cleanups: 16
33+
34+
Use the -v/--verbose option for more details."""
35+
36+
MODERN_OUTPUT = """Showing ccache stats for \x1b[32m\x1b[1mapp-misc/foo\x1b[0m
37+
\x1b[33mCacheable calls\x1b[0m: \x1b[33m101\x1b[0m / \x1b[35m235\x1b[0m (\x1b[33m42.98%\x1b[0m)
38+
\x1b[32mHits\x1b[0m: \x1b[32m8\x1b[0m / \x1b[33m101\x1b[0m (\x1b[32m 7.92%\x1b[0m)
39+
\x1b[92mDirect\x1b[0m: \x1b[92m6\x1b[0m / \x1b[32m 8\x1b[0m (\x1b[92m75.00%\x1b[0m)
40+
\x1b[92mPreprocessed\x1b[0m: \x1b[92m2\x1b[0m / \x1b[32m 8\x1b[0m (\x1b[92m25.00%\x1b[0m)
41+
\x1b[34mMisses\x1b[0m: \x1b[34m93\x1b[0m / \x1b[33m101\x1b[0m (\x1b[34m92.08%\x1b[0m)
42+
\x1b[31mUncacheable calls\x1b[0m: \x1b[31m134\x1b[0m / \x1b[35m235\x1b[0m (\x1b[31m57.02%\x1b[0m)
43+
\x1b[37mLocal storage\x1b[0m:
44+
\x1b[36mCache size (GiB)\x1b[0m: \x1b[36m0.1\x1b[0m / \x1b[35m2.0\x1b[0m (\x1b[36m 0.00%\x1b[0m)
45+
\x1b[90mCleanups\x1b[0m:\x1b[90m 16\x1b[0m
46+
\x1b[32mHits\x1b[0m: \x1b[32m8\x1b[0m / \x1b[33m101\x1b[0m (\x1b[32m 7.92%\x1b[0m)
47+
\x1b[34mMisses\x1b[0m: \x1b[34m93\x1b[0m / \x1b[33m101\x1b[0m (\x1b[34m92.08%\x1b[0m)
48+
\x1b[32mDone :-)\x1b[0m
49+
""" # noqa: E501
50+
51+
LEGACY_OUTPUT = """Showing ccache stats for \x1b[32m\x1b[1mapp-misc/foo\x1b[0m
52+
Summary:
53+
\x1b[32mHits\x1b[0m: \x1b[32m8\x1b[0m / \x1b[33m 101\x1b[0m (\x1b[32m7.92 %\x1b[0m)
54+
\x1b[92mDirect\x1b[0m: \x1b[92m6\x1b[0m / \x1b[32m 135\x1b[0m (\x1b[92m4.44 %\x1b[0m)
55+
\x1b[92mPreprocessed\x1b[0m: \x1b[92m2\x1b[0m / \x1b[32m 119\x1b[0m (\x1b[92m1.68 %\x1b[0m)
56+
\x1b[34mMisses\x1b[0m:\x1b[34m 93\x1b[0m
57+
\x1b[92mDirect\x1b[0m:\x1b[92m 129\x1b[0m
58+
\x1b[92mPreprocessed\x1b[0m:\x1b[92m 117\x1b[0m
59+
Uncacheable: 134
60+
Primary storage:
61+
\x1b[32mHits\x1b[0m: \x1b[32m37\x1b[0m / \x1b[33m 260\x1b[0m (\x1b[32m14.23 %\x1b[0m)
62+
\x1b[34mMisses\x1b[0m:\x1b[34m 223\x1b[0m
63+
\x1b[36mCache size (GB)\x1b[0m: \x1b[36m0.10\x1b[0m / \x1b[35m2.00\x1b[0m (\x1b[36m0.00 %\x1b[0m)
64+
\x1b[90mCleanups\x1b[0m:\x1b[90m 16\x1b[0m
65+
66+
Use the -v/--verbose option for more details.
67+
\x1b[32mDone :-)\x1b[0m
68+
""" # noqa: E501
69+
70+
71+
@pytest.mark.parametrize(
72+
'stats,output',
73+
(
74+
(MODERN_STATS, MODERN_OUTPUT),
75+
(LEGACY_STATS, LEGACY_OUTPUT),
76+
),
77+
)
78+
@patch('subprocess.Popen')
79+
def test_stats(popen, stats, output):
80+
popen.return_value.stdout.readlines.return_value = [
81+
f'{line}\n' for line in stats.split('\n')
82+
]
83+
84+
result = CliRunner().invoke(Stats(), ['foo'], color=True)
85+
86+
assert result.exit_code == 0
87+
assert result.output == output

0 commit comments

Comments
 (0)