Skip to content

Commit c2c6e79

Browse files
author
user
committed
Add test suite
1 parent a72b793 commit c2c6e79

9 files changed

Lines changed: 443 additions & 1 deletion

File tree

.github/workflows/formatter.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
name: Formatter
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
formatter:
7+
runs-on: ubuntu-latest
8+
steps:
9+
- uses: actions/checkout@v4
10+
- uses: rickstaa/action-black@v1
11+
with:
12+
black_args: "-l 79 ."

.github/workflows/linter.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Linter
2+
3+
on:
4+
workflow_run:
5+
workflows: [Formatter]
6+
types: [completed]
7+
8+
jobs:
9+
linter:
10+
runs-on: ubuntu-latest
11+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
12+
steps:
13+
- name: Check out source repository
14+
uses: actions/checkout@v4
15+
- name: Set up Python environment
16+
uses: actions/setup-python@v4
17+
with:
18+
python-version: "3.11"
19+
- name: flake8 Lint
20+
uses: py-actions/flake8@v2
21+
with:
22+
path: "src"

.github/workflows/test.yml

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ "main" ]
6+
pull_request:
7+
branches: [ "main" ]
8+
# Running workflow manually
9+
workflow_dispatch:
10+
11+
jobs:
12+
test:
13+
runs-on: ubuntu-22.04
14+
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Set up Python 3.11
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: "3.11"
22+
cache: 'pip'
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e .
28+
pip install pytest pytest-mock pytest-cov ipython
29+
30+
- name: Run tests with coverage
31+
run: |
32+
pytest --cov=jumper_extension --cov-report=xml tests/
33+
34+
- name: Generate coverage badge
35+
uses: tj-actions/coverage-badge-py@v2
36+
with:
37+
output: coverage.svg
38+
39+
- name: Commit coverage badge
40+
run: |
41+
git config --local user.email "action@github.com"
42+
git config --local user.name "GitHub Action"
43+
git add coverage.svg
44+
git diff --staged --quiet || git commit -m "Update coverage badge"
45+
git push

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
__pycache__/
1+
__pycache__/
2+
*.egg-info/
3+
.coverage
4+
htmlcov/
5+
.pytest_cache/
6+
coverage.xml

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# JUmPER Extension
22

3+
![Coverage](./coverage.svg)
4+
35
This is JUmPER IPython extension for real-time performance monitoring in IPython environments and Jupyter notebooks. It allows you to gather performance data on CPU usage, memory consumption, GPU utilization, and I/O operations for individual cells and present it in the notebook/IPython session either as text report or as a plot. The extension can be naturally integrated with [JUmPER Jupyter kernel](https://github.com/score-p/scorep_jupyter_kernel_python/) for most comprehensive analysis of notebook.
46

57
## Installation

tests/conftest.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import pytest
2+
import os
3+
import tempfile
4+
from unittest.mock import Mock, patch
5+
from IPython.testing.globalipapp import get_ipython
6+
7+
@pytest.fixture
8+
def ipython():
9+
# Try to get actual IPython instance, fallback to mock if not available
10+
ip = get_ipython()
11+
if ip is not None:
12+
return ip
13+
14+
# Create a mock IPython instance with required attributes
15+
from IPython.testing import decorators
16+
from IPython import InteractiveShell
17+
18+
# Create a basic InteractiveShell instance for testing
19+
shell = InteractiveShell.instance()
20+
shell.events = Mock()
21+
shell.register_magics = Mock()
22+
shell.events.register = Mock()
23+
shell.events.unregister = Mock()
24+
25+
return shell
26+
27+
@pytest.fixture
28+
def temp_dir():
29+
with tempfile.TemporaryDirectory() as tmpdir:
30+
yield tmpdir
31+
32+
@pytest.fixture
33+
def mock_cpu_only():
34+
"""Mock system with 1 CPU (4 cores) and no GPU"""
35+
with patch('psutil.cpu_count', return_value=4), \
36+
patch('psutil.cpu_percent', return_value=[25.0, 30.0, 20.0, 35.0]), \
37+
patch('psutil.virtual_memory') as mock_mem, \
38+
patch('psutil.Process') as mock_proc, \
39+
patch('jumper_extension.performance_monitor.PYNVML_AVAILABLE', False):
40+
mock_mem.return_value.total = 8 * 1024**3
41+
mock_mem.return_value.available = 4 * 1024**3
42+
mock_proc.return_value.cpu_affinity.return_value = [0, 1, 2, 3]
43+
mock_proc.return_value.io_counters.return_value = Mock(read_count=100, write_count=50, read_bytes=1024, write_bytes=512)
44+
yield
45+
46+
@pytest.fixture
47+
def mock_cpu_gpu(mock_cpu_only):
48+
"""Mock system with 1 CPU (4 cores) and 1 GPU"""
49+
with patch('jumper_extension.performance_monitor.PYNVML_AVAILABLE', True), \
50+
patch('pynvml.nvmlInit'), \
51+
patch('pynvml.nvmlDeviceGetCount', return_value=1), \
52+
patch('pynvml.nvmlDeviceGetHandleByIndex', return_value=Mock()), \
53+
patch('pynvml.nvmlDeviceGetName', return_value=b'NVIDIA GeForce RTX 3080'), \
54+
patch('pynvml.nvmlDeviceGetMemoryInfo') as mock_mem, \
55+
patch('pynvml.nvmlDeviceGetUtilizationRates') as mock_util, \
56+
patch('pynvml.nvmlDeviceGetTemperature', return_value=65):
57+
mock_mem.return_value = Mock(total=10*1024**3, used=2*1024**3, free=8*1024**3)
58+
mock_util.return_value = Mock(gpu=75, memory=20)
59+
yield

tests/test_data_handling.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import os
2+
import pandas as pd
3+
from jumper_extension.performance_data import PerformanceData
4+
from jumper_extension.cell_history import CellHistory
5+
6+
def test_performance_data(temp_dir):
7+
"""Test all PerformanceData functionality"""
8+
# Test initialization and empty dataframe
9+
data = PerformanceData(num_cpus=2, num_gpus=0)
10+
assert data.num_cpus == 2 and data.num_gpus == 0 and len(data.data) == 0
11+
assert len(data.to_dataframe()) == 0
12+
13+
# Test add_sample and to_dataframe
14+
data.add_sample(1234567890, [25.0, 30.0], 4.0, [], [], [], [100, 50, 1024, 512])
15+
assert len(data.data) == 1
16+
df = data.to_dataframe()
17+
assert len(df) == 1 and df['cpu_util_avg'].iloc[0] == 27.5
18+
19+
# Test CSV export
20+
csv_file = os.path.join(temp_dir, "test.csv")
21+
data.export(csv_file)
22+
assert os.path.exists(csv_file) and len(pd.read_csv(csv_file)) == 1
23+
24+
def test_performance_data_gpu():
25+
"""Test GPU functionality and slicing"""
26+
data = PerformanceData(num_cpus=2, num_gpus=1)
27+
data.add_sample(1234567890, [25.0, 30.0], 4.0, [75.0], [20.0], [60.0], [100, 50, 1024, 512])
28+
data.add_sample(1234567891, [35.0, 40.0], 5.0, [80.0], [25.0], [65.0], [200, 60, 2048, 1024])
29+
30+
df = data.to_dataframe()
31+
assert len(df) == 2 and all(col in df.columns for col in ['gpu_util_avg', 'gpu_band_avg', 'gpu_mem_avg'])
32+
assert len(data.to_dataframe(slice_=(0, 0))) == 1
33+
34+
def test_cell_history(capsys, temp_dir):
35+
"""Test all CellHistory functionality"""
36+
history = CellHistory()
37+
38+
# Test start/end cell
39+
history.start_cell("print('hello')")
40+
assert history.current_cell['number'] == 0
41+
history.end_cell(None)
42+
assert len(history.cells) == 1 and len(history.cell_timestamps) == 1
43+
44+
# Test print method
45+
history.print()
46+
assert "Cell #0" in capsys.readouterr().out
47+
48+
# Test export
49+
json_file = os.path.join(temp_dir, "history.json")
50+
history.export(json_file)
51+
assert os.path.exists(json_file)

0 commit comments

Comments
 (0)