Skip to content

Commit 28c5a0a

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

7 files changed

Lines changed: 406 additions & 1 deletion

File tree

.github/workflows/test.yml

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

tests/test_magic_commands.py

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import pytest
2+
from jumper_extension.perfmonitor_magics import perfmonitorMagics, load_ipython_extension, unload_ipython_extension
3+
from unittest.mock import Mock, patch
4+
import pandas as pd
5+
6+
def test_initialization_and_basic_operations(ipython, mock_cpu_only):
7+
"""Test initialization, start/stop, and basic operations"""
8+
magics = perfmonitorMagics(ipython)
9+
assert magics.monitor is None
10+
assert not magics.print_perfreports
11+
12+
# Test start/stop cycle with valid interval parsing
13+
magics.perfmonitor_start("0.5")
14+
assert magics.monitor.interval == 0.5
15+
magics.perfmonitor_stop("")
16+
17+
# Test already running
18+
magics.perfmonitor_start("")
19+
magics.perfmonitor_start("") # Already running
20+
magics.perfmonitor_stop("")
21+
22+
# Test invalid interval (no monitor running)
23+
magics.perfmonitor_start("invalid") # Invalid interval
24+
25+
def test_no_monitor_error_cases(ipython, mock_cpu_only):
26+
"""Test all commands that require active monitor"""
27+
magics = perfmonitorMagics(ipython)
28+
magics.perfmonitor_stop("")
29+
magics.perfmonitor_resources("")
30+
magics.perfmonitor_plot("")
31+
magics.perfmonitor_perfreport("")
32+
magics.perfmonitor_export_perfdata("")
33+
34+
def test_resources_and_gpu(ipython, mock_cpu_gpu):
35+
"""Test resources display with GPU"""
36+
magics = perfmonitorMagics(ipython)
37+
magics.perfmonitor_start("")
38+
magics.perfmonitor_resources("")
39+
magics.perfmonitor_stop("")
40+
41+
def test_cell_operations(ipython, mock_cpu_only):
42+
"""Test cell history and reports"""
43+
magics = perfmonitorMagics(ipython)
44+
45+
# Test cell history tracking and command
46+
cell_info = type('Info', (), {'raw_cell': 'test'})()
47+
magics.pre_run_cell(cell_info)
48+
result = type('Result', (), {'result': None})()
49+
magics.post_run_cell(result)
50+
51+
with patch.object(magics.cell_history, 'print'):
52+
perfmonitorMagics.cell_history(magics, "")
53+
54+
# Test auto-reports
55+
magics.perfmonitor_start("")
56+
magics.perfmonitor_enable_perfreports("")
57+
with patch.object(magics, '_perfreport'):
58+
magics.post_run_cell(result)
59+
magics.perfmonitor_disable_perfreports("")
60+
magics.perfmonitor_stop("")
61+
62+
def test_parse_cell_number(ipython, mock_cpu_only):
63+
"""Test all cell number parsing scenarios"""
64+
magics = perfmonitorMagics(ipython)
65+
assert magics._parse_cell_number("") is None
66+
assert magics._parse_cell_number("invalid") is False
67+
assert magics._parse_cell_number("999") is False
68+
69+
# Add valid cell for testing
70+
cell_info = type('Info', (), {'raw_cell': 'test'})()
71+
magics.pre_run_cell(cell_info)
72+
result = type('Result', (), {'result': None})()
73+
magics.post_run_cell(result)
74+
assert magics._parse_cell_number("0") is not None
75+
76+
def test_plot_scenarios(ipython, mock_cpu_only):
77+
"""Test plotting with various data scenarios"""
78+
magics = perfmonitorMagics(ipython)
79+
magics.perfmonitor_start("")
80+
81+
# Test invalid cell
82+
magics.perfmonitor_plot("invalid")
83+
84+
# Test empty data
85+
with patch.object(magics.monitor.data, 'to_dataframe', return_value=pd.DataFrame(columns=['time'])):
86+
magics.perfmonitor_plot("")
87+
88+
# Test with data
89+
df = pd.DataFrame({'time': [1.0, 2.0], 'cpu_util_avg': [50.0, 60.0]})
90+
with patch.object(magics.monitor.data, 'to_dataframe', return_value=df), \
91+
patch.object(magics.visualizer, 'plot'), \
92+
patch.object(magics.monitor, 'start_time', 0.0):
93+
magics.perfmonitor_plot("")
94+
95+
# Test with cell filter
96+
with patch('time.time', side_effect=[1.0, 2.0]):
97+
cell_info = type('Info', (), {'raw_cell': 'test'})()
98+
magics.pre_run_cell(cell_info)
99+
magics.post_run_cell(type('Result', (), {'result': None})())
100+
101+
with patch.object(magics.monitor.data, 'to_dataframe', return_value=df), \
102+
patch.object(magics.visualizer, 'plot'), \
103+
patch.object(magics.monitor, 'start_time', 0.0):
104+
magics.perfmonitor_plot("0")
105+
106+
magics.perfmonitor_stop("")
107+
108+
def test_perfreport_scenarios(ipython, mock_cpu_only):
109+
"""Test performance reporting scenarios"""
110+
magics = perfmonitorMagics(ipython)
111+
112+
# Test no monitor
113+
magics._perfreport()
114+
115+
magics.perfmonitor_start("")
116+
117+
# Test invalid cell for perfreport command
118+
magics.perfmonitor_perfreport("invalid")
119+
120+
# Add cell to history
121+
with patch('time.time', side_effect=[1.0, 2.0]):
122+
cell_info = type('Info', (), {'raw_cell': 'test'})()
123+
magics.pre_run_cell(cell_info)
124+
magics.post_run_cell(type('Result', (), {'result': None})())
125+
126+
# Test empty data
127+
with patch.object(magics.monitor.data, 'to_dataframe', return_value=pd.DataFrame(columns=['time'])):
128+
magics._perfreport()
129+
130+
# Test with full data
131+
df = pd.DataFrame({
132+
'time': [1.0, 2.0], 'cpu_util_avg': [50.0, 60.0], 'memory_usage_gb': [4.0, 4.5],
133+
'gpu_util_avg': [30.0, 40.0], 'gpu_mem_avg': [2.0, 2.5]
134+
})
135+
with patch.object(magics.monitor.data, 'to_dataframe', return_value=df):
136+
magics._perfreport()
137+
magics._perfreport((1.0, 2.0)) # Custom cell marks
138+
magics.perfmonitor_perfreport("0") # Via command
139+
140+
# Test with missing columns
141+
df_partial = pd.DataFrame({'time': [1.0, 2.0], 'cpu_util_avg': [50.0, 60.0], 'memory_usage_gb': [4.0, 4.5]})
142+
with patch.object(magics.monitor.data, 'to_dataframe', return_value=df_partial):
143+
magics._perfreport()
144+
145+
magics.perfmonitor_stop("")
146+
147+
def test_export_and_help(ipython, mock_cpu_only):
148+
"""Test export functions and help"""
149+
magics = perfmonitorMagics(ipython)
150+
151+
# Test exports without monitor
152+
magics.perfmonitor_export_perfdata("")
153+
154+
# Test exports with monitor
155+
magics.perfmonitor_start("")
156+
with patch.object(magics.monitor.data, 'export'):
157+
magics.perfmonitor_export_perfdata("")
158+
magics.perfmonitor_export_perfdata("custom.csv")
159+
magics.perfmonitor_stop("")
160+
161+
# Test cell history export
162+
with patch.object(magics.cell_history, 'export'):
163+
magics.perfmonitor_export_cell_history("")
164+
magics.perfmonitor_export_cell_history("custom.json")
165+
166+
# Test help
167+
magics.perfmonitor_help("")
168+
169+
def test_extension_lifecycle(ipython, mock_cpu_only):
170+
"""Test IPython extension load/unload"""
171+
with patch.object(ipython.events, 'register'), patch.object(ipython, 'register_magics'):
172+
load_ipython_extension(ipython)
173+
174+
# Test unload with monitor
175+
from jumper_extension.perfmonitor_magics import _perfmonitor_magics
176+
_perfmonitor_magics.perfmonitor_start("")
177+
with patch.object(ipython.events, 'unregister'):
178+
unload_ipython_extension(ipython)
179+
180+
# Test unload without magics
181+
from jumper_extension import perfmonitor_magics
182+
perfmonitor_magics._perfmonitor_magics = None
183+
unload_ipython_extension(ipython)

0 commit comments

Comments
 (0)