Skip to content

Commit 3a3b1d9

Browse files
authored
Merge pull request #1 from grafana/integration-tests
2 parents f3be636 + 7ccd845 commit 3a3b1d9

14 files changed

+369
-5
lines changed

.github/workflows/cloud.yml

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Cloud tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
uv-example:
11+
name: python
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Install uv
18+
uses: astral-sh/setup-uv@v5
19+
with:
20+
# Install a specific version of uv.
21+
version: "0.5.13"
22+
23+
- name: "Set up Python"
24+
uses: actions/setup-python@v5
25+
with:
26+
python-version-file: "pyproject.toml"
27+
28+
- name: Install the project
29+
run: uv sync --all-extras --all-groups
30+
31+
- name: Run ruff
32+
run: uv run ruff check .
33+
34+
- name: Run tests
35+
run: uv run pytest tests --cloud
36+
env:
37+
# E.g. https://mcptests.grafana-dev.net.
38+
GRAFANA_URL: ${{ vars.CLOUD_GRAFANA_URL }}
39+
GRAFANA_API_KEY: ${{ secrets.CLOUD_GRAFANA_API_KEY }}

.github/workflows/python.yml

+9-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@ jobs:
1414
steps:
1515
- uses: actions/checkout@v4
1616

17+
# Start the Grafana server.
18+
# Do this early so that it can start up in time for the tests to run.
19+
# We may need to add a wait here.
20+
- name: Start docker-compose services
21+
uses: hoverkraft-tech/[email protected]
22+
with:
23+
compose-file: "docker-compose.yaml"
24+
1725
- name: Install uv
1826
uses: astral-sh/setup-uv@v5
1927
with:
@@ -32,4 +40,4 @@ jobs:
3240
run: uv run ruff check .
3341

3442
- name: Run tests
35-
run: uv run pytest tests
43+
run: uv run pytest tests --integration

docker-compose.yaml

+2-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@ services:
99
ports:
1010
- 3000:3000/tcp
1111
volumes:
12-
# - ./provisioning:/etc/grafana/provisioning
13-
# persist data in /var/lib/grafana so that jobs still work between re-ups
14-
- "var-lib-grafana:/var/lib/grafana"
12+
- ./tests/provisioning:/etc/grafana/provisioning
13+
- ./tests/dashboards:/var/lib/grafana/dashboards
1514

1615
volumes:
1716
var-lib-grafana:

pyproject.toml

+4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ mcp-grafana = "mcp_grafana.cli:app"
2222
[dependency-groups]
2323
dev = [
2424
"pytest>=8.3.4",
25+
"pytest-asyncio>=0.25.2",
2526
]
2627
lint = [
2728
"ruff>=0.8.5",
@@ -30,3 +31,6 @@ lint = [
3031
[build-system]
3132
requires = ["hatchling"]
3233
build-backend = "hatchling.build"
34+
35+
[tool.pytest.ini_options]
36+
asyncio_default_fixture_loop_scope = "session"

tests/conftest.py

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
def pytest_addoption(parser):
2+
"""
3+
Add command line options for integration and cloud tests.
4+
5+
This is used as an alternative to relying on custom markers,
6+
which do not stop the tests running by default.
7+
8+
To run integration tests, use:
9+
10+
pytest --integration
11+
12+
To run cloud tests, use:
13+
14+
pytest --integration --cloud
15+
16+
These two can be combined.
17+
"""
18+
parser.addoption(
19+
"--integration",
20+
action="store_true",
21+
dest="integration",
22+
default=False,
23+
help="enable integration tests",
24+
)
25+
parser.addoption(
26+
"--cloud",
27+
action="store_true",
28+
dest="cloud",
29+
default=False,
30+
help="enable cloud integration tests",
31+
)

tests/dashboards/demo.json

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
{
2+
"annotations": {
3+
"list": [
4+
{
5+
"builtIn": 1,
6+
"datasource": {
7+
"type": "grafana",
8+
"uid": "-- Grafana --"
9+
},
10+
"enable": true,
11+
"hide": true,
12+
"iconColor": "rgba(0, 211, 255, 1)",
13+
"name": "Annotations & Alerts",
14+
"type": "dashboard"
15+
}
16+
]
17+
},
18+
"editable": true,
19+
"fiscalYearStartMonth": 0,
20+
"graphTooltip": 0,
21+
"id": 1,
22+
"isStarred": true,
23+
"links": [],
24+
"panels": [
25+
{
26+
"datasource": {
27+
"default": true,
28+
"type": "prometheus",
29+
"uid": "robustperception"
30+
},
31+
"fieldConfig": {
32+
"defaults": {
33+
"color": {
34+
"mode": "palette-classic"
35+
},
36+
"custom": {
37+
"axisBorderShow": false,
38+
"axisCenteredZero": false,
39+
"axisColorMode": "text",
40+
"axisLabel": "",
41+
"axisPlacement": "auto",
42+
"barAlignment": 0,
43+
"barWidthFactor": 0.6,
44+
"drawStyle": "line",
45+
"fillOpacity": 0,
46+
"gradientMode": "none",
47+
"hideFrom": {
48+
"legend": false,
49+
"tooltip": false,
50+
"viz": false
51+
},
52+
"insertNulls": false,
53+
"lineInterpolation": "linear",
54+
"lineWidth": 1,
55+
"pointSize": 5,
56+
"scaleDistribution": {
57+
"type": "linear"
58+
},
59+
"showPoints": "auto",
60+
"spanNulls": false,
61+
"stacking": {
62+
"group": "A",
63+
"mode": "none"
64+
},
65+
"thresholdsStyle": {
66+
"mode": "off"
67+
}
68+
},
69+
"mappings": [],
70+
"thresholds": {
71+
"mode": "absolute",
72+
"steps": [
73+
{
74+
"color": "green",
75+
"value": null
76+
},
77+
{
78+
"color": "red",
79+
"value": 80
80+
}
81+
]
82+
}
83+
},
84+
"overrides": []
85+
},
86+
"gridPos": {
87+
"h": 8,
88+
"w": 12,
89+
"x": 0,
90+
"y": 0
91+
},
92+
"id": 1,
93+
"options": {
94+
"legend": {
95+
"calcs": [],
96+
"displayMode": "list",
97+
"placement": "bottom",
98+
"showLegend": true
99+
},
100+
"tooltip": {
101+
"mode": "single",
102+
"sort": "none"
103+
}
104+
},
105+
"targets": [
106+
{
107+
"datasource": {
108+
"type": "prometheus",
109+
"uid": "robustperception"
110+
},
111+
"editorMode": "code",
112+
"expr": "node_load1",
113+
"instant": false,
114+
"legendFormat": "__auto",
115+
"range": true,
116+
"refId": "A"
117+
}
118+
],
119+
"title": "Node Load",
120+
"type": "timeseries"
121+
}
122+
],
123+
"schemaVersion": 39,
124+
"tags": [
125+
"demo"
126+
],
127+
"templating": {
128+
"list": []
129+
},
130+
"time": {
131+
"from": "now-6h",
132+
"to": "now"
133+
},
134+
"timepicker": {},
135+
"timezone": "browser",
136+
"title": "Demo",
137+
"uid": "fe9gm6guyzi0wd",
138+
"version": 2,
139+
"weekStart": ""
140+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: 1
2+
3+
providers:
4+
- name: 'docker-compose'
5+
orgId: 1
6+
folder: ''
7+
folderUid: ''
8+
type: file
9+
disableDeletion: true
10+
updateIntervalSeconds: 60
11+
allowUiUpdates: false
12+
options:
13+
# <string, required> path to dashboard files on disk. Required when using the 'file' type
14+
path: /var/lib/grafana/dashboards
15+
# <bool> use folder names from filesystem to create folders in Grafana
16+
foldersFromFilesStructure: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
apiVersion: 1
2+
3+
datasources:
4+
- name: Robust Perception
5+
id: 1
6+
uid: robustperception
7+
type: prometheus
8+
access: proxy
9+
url: http://demo.robustperception.io:9090
10+
isDefault: true

tests/settings_test.py

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from mcp_grafana.settings import GrafanaSettings
55

66

7+
@patch.dict(os.environ, clear=True)
78
def test_settings():
89
# Test we can instantiate settings with defaults.
910
settings = GrafanaSettings(url="http://localhost:3000")

tests/tools/__init__.py

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import pytest
2+
3+
mark_integration = [
4+
# Without marking the loop scope as session, the event loop
5+
# is closed after each test runs, which causes subsequent tests
6+
# to fail.
7+
pytest.mark.asyncio(loop_scope="session"),
8+
pytest.mark.skipif("not config.getoption('integration')"),
9+
]
10+
11+
mark_cloud = [
12+
# Without marking the loop scope as session, the event loop
13+
# is closed after each test runs, which causes subsequent tests
14+
# to fail.
15+
pytest.mark.asyncio(loop_scope="session"),
16+
pytest.mark.skipif("not config.getoption('cloud')"),
17+
]

tests/tools/datasources_test.py

+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import json
2+
3+
from mcp_grafana.tools.datasources import (
4+
list_datasources,
5+
get_datasource_by_uid,
6+
get_datasource_by_name,
7+
)
8+
9+
from . import mark_integration
10+
11+
# All tests in this file require a running Grafana instance and
12+
# an asynchronous event loop.
13+
pytestmark = mark_integration
14+
15+
16+
async def test_list_datasources():
17+
datasources_bytes = await list_datasources()
18+
datasources = json.loads(datasources_bytes)
19+
assert len(datasources) == 1
20+
assert datasources[0]["name"] == "Robust Perception"
21+
assert datasources[0]["id"] == 1
22+
assert datasources[0]["uid"] == "robustperception"
23+
24+
25+
async def test_get_datasource_by_uid():
26+
datasource_bytes = await get_datasource_by_uid("robustperception")
27+
datasource = json.loads(datasource_bytes)
28+
assert datasource["name"] == "Robust Perception"
29+
assert datasource["id"] == 1
30+
assert datasource["uid"] == "robustperception"
31+
32+
33+
async def test_get_datasource_by_name():
34+
datasource_bytes = await get_datasource_by_name("Robust Perception")
35+
datasource = json.loads(datasource_bytes)
36+
assert datasource["name"] == "Robust Perception"
37+
assert datasource["id"] == 1
38+
assert datasource["uid"] == "robustperception"

tests/tools/incident_test.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import json
2+
3+
from mcp_grafana.tools.incident import list_incidents, ListIncidentsArguments
4+
5+
from . import mark_cloud
6+
7+
# All tests in this file require a cloud Grafana instance and
8+
# an asynchronous event loop.
9+
pytestmark = mark_cloud
10+
11+
12+
async def test_list_incidents():
13+
arguments = ListIncidentsArguments(
14+
limit=2,
15+
drill=False,
16+
status="active",
17+
)
18+
incidents_bytes = await list_incidents(arguments)
19+
incidents = json.loads(incidents_bytes)
20+
previews = incidents["incidentPreviews"]
21+
assert len(previews) <= 2
22+
for preview in previews:
23+
assert preview["status"] == "active"
24+
assert preview["isDrill"] is False

0 commit comments

Comments
 (0)