Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.13"]

steps:
- uses: actions/checkout@v4

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}

- name: Install Poetry
run: pip install poetry

- name: Install dependencies
run: poetry install --with dev

- name: Run tests
run: poetry run pytest -v --tb=short
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ This tool is useful when you need system metrics but can't or don't want to run

Basically, if you can run `vmstat`, you can get charts. No agents, no daemons, no network dependencies.

## How does it differ from top / htop?

`top` and `htop` are live, interactive monitors — they show what's happening *right now* and the data is gone when you close the terminal. `vmstat-visualizer` is a post-hoc analysis tool for recorded `vmstat` logs.

| | `top` / `htop` | `vmstat-visualizer` |
|---|---|---|
| **When** | Live, interactive | Offline, after-the-fact |
| **Input** | Reads `/proc` directly | Reads saved `vmstat` log files |
| **Output** | Terminal UI | PNG/SVG plot images |
| **Granularity** | Per-process | System-wide aggregates |
| **Comparison** | No | Compare two captures side-by-side |
| **Shareability** | Screenshot or nothing | Generates artifacts for reports/tickets |
| **Persistence** | Ephemeral | Works on logs captured hours/days ago |

Think of it as: `vmstat` is the data recorder, `top`/`htop` are the live dashboards, and `vmstat-visualizer` is the flight data recorder playback tool.

## Requirements

- Python 3.7+
Expand Down
73 changes: 69 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ pyparsing = "3.2.5"
python-dateutil = "2.9.0.post0"
six = "1.17.0"

[tool.poetry.group.dev.dependencies]
pytest = ">=8.0"

[tool.pytest.ini_options]
testpaths = ["tests"]
markers = [
"integration: end-to-end CLI tests that produce file output",
]

[tool.setuptools_scm]
write_to = "vmstat_visualizer/cli/__version__.py"

Empty file added tests/__init__.py
Empty file.
36 changes: 36 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import os
import pytest

FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "fixtures")


@pytest.fixture
def active_log():
return os.path.join(FIXTURES_DIR, "vmstat_active.log")


@pytest.fixture
def standard_log():
return os.path.join(FIXTURES_DIR, "vmstat_standard.log")


@pytest.fixture
def no_header_log():
return os.path.join(FIXTURES_DIR, "vmstat_no_header.log")


@pytest.fixture
def empty_log():
return os.path.join(FIXTURES_DIR, "vmstat_empty.log")


@pytest.fixture
def header_only_log():
return os.path.join(FIXTURES_DIR, "vmstat_header_only.log")


@pytest.fixture
def example_log():
return os.path.join(
os.path.dirname(__file__), "..", "examples", "example.log"
)
7 changes: 7 additions & 0 deletions tests/fixtures/vmstat_active.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
r b swpd free inact active si so bi bo in cs us sy id wa st gu
3 0 0 523 128 1059 0 0 4 1 71 111 0 0 99 0 0 0 2025-08-01 10:06:32
1 0 0 523 128 1059 0 0 0 0 1815 3452 4 1 96 0 0 0 2025-08-01 10:06:33
1 0 0 523 128 1059 0 0 0 16 2530 4711 4 3 93 0 0 0 2025-08-01 10:06:34
1 0 0 523 128 1059 0 0 0 0 2061 3776 4 2 94 0 0 0 2025-08-01 10:06:35
1 0 0 523 128 1059 0 0 0 0 2181 4079 4 3 94 0 0 0 2025-08-01 10:06:36
Empty file added tests/fixtures/vmstat_empty.log
Empty file.
2 changes: 2 additions & 0 deletions tests/fixtures/vmstat_header_only.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
r b swpd free inact active si so bi bo in cs us sy id wa st gu
3 changes: 3 additions & 0 deletions tests/fixtures/vmstat_no_header.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
3 0 0 523 128 1059 0 0 4 1 71 111 0 0 99 0 0 0 2025-08-01 10:06:32
1 0 0 523 128 1059 0 0 0 0 1815 3452 4 1 96 0 0 0 2025-08-01 10:06:33
1 0 0 523 128 1059 0 0 0 16 2530 4711 4 3 93 0 0 0 2025-08-01 10:06:34
7 changes: 7 additions & 0 deletions tests/fixtures/vmstat_standard.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st gu
2 0 0 15234 512 8192 0 0 12 8 200 450 5 2 92 1 0 0 2025-09-15 14:00:01
1 0 0 15100 512 8200 0 0 0 4 180 400 3 1 95 1 0 0 2025-09-15 14:00:02
1 0 0 14900 514 8210 0 0 4 12 220 480 8 3 88 1 0 0 2025-09-15 14:00:03
3 1 256 14500 514 8220 2 4 20 40 350 600 12 5 80 3 0 0 2025-09-15 14:00:04
1 0 256 14700 516 8230 0 0 0 8 190 420 4 2 93 1 0 0 2025-09-15 14:00:05
51 changes: 51 additions & 0 deletions tests/test_check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from unittest.mock import patch, MagicMock
import subprocess
import pytest
from vmstat_visualizer.checks.check import check_vmstat_columns


class TestCheckVmstatColumns:
VMSTAT_HEADER_WITH_ST_GU = (
"procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------\n"
" r b swpd free inact active si so bi bo in cs us sy id wa st gu\n"
" 1 0 0 15234 128 1059 0 0 4 1 71 111 0 0 99 0 0 0\n"
)
VMSTAT_HEADER_WITHOUT_GU = (
"procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----\n"
" r b swpd free inact active si so bi bo in cs us sy id wa st\n"
" 1 0 0 15234 128 1059 0 0 4 1 71 111 0 0 99 0 0\n"
)

@patch("vmstat_visualizer.checks.check.subprocess.run")
def test_returns_true_true_when_st_and_gu_present(self, mock_run):
mock_run.return_value = MagicMock(stdout=self.VMSTAT_HEADER_WITH_ST_GU)
has_st, has_gu = check_vmstat_columns()
assert has_st is True
assert has_gu is True

@patch("vmstat_visualizer.checks.check.subprocess.run")
def test_returns_true_false_when_gu_absent(self, mock_run):
mock_run.return_value = MagicMock(stdout=self.VMSTAT_HEADER_WITHOUT_GU)
has_st, has_gu = check_vmstat_columns()
assert has_st is True
assert has_gu is False

@patch("vmstat_visualizer.checks.check.subprocess.run")
def test_returns_false_false_on_short_output(self, mock_run):
mock_run.return_value = MagicMock(stdout="only one line")
has_st, has_gu = check_vmstat_columns()
assert has_st is False
assert has_gu is False

@patch("vmstat_visualizer.checks.check.subprocess.run", side_effect=FileNotFoundError)
def test_returns_false_false_on_missing_vmstat(self, mock_run):
has_st, has_gu = check_vmstat_columns()
assert has_st is False
assert has_gu is False

@patch("vmstat_visualizer.checks.check.subprocess.run",
side_effect=subprocess.CalledProcessError(1, "vmstat", stderr="fail"))
def test_returns_false_false_on_process_error(self, mock_run):
has_st, has_gu = check_vmstat_columns()
assert has_st is False
assert has_gu is False
Loading
Loading