Skip to content

Commit aa950cb

Browse files
authored
Add CI action to test endpoints from whitelisted chains and providers (#5427)
* Add CI action to test endpoints from whitelisted chains and providers * Improve output * Test all chains, only for providers that work everywhere. * Output improvement * Added warnings on chains processing * Set schedule * Rename CI to more meaningful name * Move test folder inside github workflows * Rename workflow file * Add testing endpoint info to the readme * Test all providers * Raise workers to 24 (6 jobs per core)
1 parent f2b6dab commit aa950cb

File tree

6 files changed

+186
-1
lines changed

6 files changed

+186
-1
lines changed

.github/workflows/test_endpoints.yml

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Test RPC and LCD endpoints
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * *' # Runs daily at 00:00 UTC
6+
workflow_dispatch:
7+
8+
jobs:
9+
test-endpoints:
10+
runs-on: ubuntu-latest
11+
12+
steps:
13+
- uses: actions/checkout@v2
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v2
17+
with:
18+
python-version: '3.x'
19+
20+
- name: Install dependencies
21+
run: |
22+
python -m pip install --upgrade pip
23+
pip install -r .github/workflows/tests/requirements.txt
24+
25+
- name: Run tests
26+
run: |
27+
pytest --no-header --tb=no -n 64 .github/workflows/tests

.github/workflows/tests/apis.py

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# -*- coding: utf-8 -*-
2+
import requests
3+
import pytest
4+
from collections import namedtuple
5+
import glob
6+
import os
7+
import json
8+
import logging
9+
import re
10+
import warnings
11+
12+
# Setup basic configuration for logging
13+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
14+
15+
EndpointTest = namedtuple('EndpointTest', ['chain', 'endpoint', 'provider', 'address'])
16+
17+
# Set this to False to ignore the whitelist and process all providers
18+
use_whitelist = True
19+
20+
# Whitelist for specific chains and providers
21+
whitelist = {
22+
# If chains whitelist empty, all chains will be evaluated.
23+
"chains": [
24+
# "axelar",
25+
# "celestia",
26+
# "composable",
27+
# "cosmoshub",
28+
# "dydx",
29+
# "dymension",
30+
# "evmos",
31+
# "injective",
32+
# "neutron",
33+
# "noble",
34+
# "osmosis",
35+
# "stargaze",
36+
# "stride"
37+
],
38+
"providers": [
39+
# "Osmosis Foundation",
40+
# "Polkachu",
41+
# "CryptoCrew",
42+
# # "forbole",
43+
# "Imperator.co",
44+
# # "Lavender.Five Nodes 🐝",
45+
# # "WhisperNode 🤐",
46+
# "chainlayer",
47+
# "Numia",
48+
# # "Enigma",
49+
# # "kjnodes",
50+
# # "Stake&Relax 🦥",
51+
# "Allnodes ⚡️ Nodes & Staking",
52+
# "Lava",
53+
# "Golden Ratio Staking",
54+
# "Stargaze Foundation",
55+
]
56+
} if use_whitelist else {'chains': [], 'providers': []}
57+
58+
def generate_endpoint_tests():
59+
test_cases = []
60+
files_found = glob.glob('*/chain.json', recursive=True)
61+
62+
if not files_found:
63+
warnings.warn("No chain.json files found in the current directory or its subdirectories.")
64+
65+
for filename in files_found:
66+
try:
67+
with open(filename) as f:
68+
data = json.load(f)
69+
chain_name = data.get('chain_name', 'unknown')
70+
if 'apis' in data:
71+
if not isinstance(data['apis'], dict):
72+
warnings.warn(f"Invalid 'apis' format in file '{filename}'. Expected a dictionary.")
73+
continue
74+
for api_type in ['rpc', 'rest']:
75+
if api_type not in data['apis']:
76+
warnings.warn(f"Missing '{api_type}' key in 'apis' of file '{filename}'.")
77+
continue
78+
if not isinstance(data['apis'][api_type], list):
79+
warnings.warn(f"Invalid '{api_type}' format in 'apis' of file '{filename}'. Expected a list.")
80+
continue
81+
for api in data['apis'].get(api_type, []):
82+
if 'provider' not in api:
83+
warnings.warn(f"Missing 'provider' key in '{api_type}' of file '{filename}'.")
84+
continue
85+
if not isinstance(api['provider'], str):
86+
warnings.warn(f"Invalid 'provider' format in '{api_type}' of file '{filename}'. Expected a string.")
87+
continue
88+
if (
89+
not use_whitelist or
90+
(not whitelist['chains'] or chain_name in whitelist['chains']) and
91+
(not whitelist['providers'] or api['provider'] in whitelist['providers'])
92+
):
93+
address = api.get('address')
94+
if not address:
95+
warnings.warn(f"Missing 'address' key in '{api_type}' of file '{filename}'.")
96+
continue
97+
if api_type == 'rpc':
98+
address += '/status'
99+
elif api_type == 'rest':
100+
address += '/cosmos/base/tendermint/v1beta1/syncing'
101+
test_cases.append(EndpointTest(chain=chain_name, endpoint=api_type, provider=api['provider'], address=address))
102+
else:
103+
warnings.warn(f"Missing 'apis' key in file '{filename}'.")
104+
except json.JSONDecodeError as e:
105+
warnings.warn(f"Failed to decode JSON file '{filename}': {str(e)}")
106+
except Exception as e:
107+
warnings.warn(f"An error occurred while processing file '{filename}': {str(e)}")
108+
109+
return test_cases
110+
111+
test_cases = generate_endpoint_tests()
112+
113+
def generate_test_function(test_case):
114+
def test(self):
115+
try:
116+
response = requests.get(test_case.address, timeout=2)
117+
assert response.status_code == 200, f"{test_case.chain.upper()}-{test_case.endpoint.upper()}-{test_case.provider} endpoint not reachable"
118+
except requests.exceptions.Timeout:
119+
logging.error(f"{test_case.chain.upper()}-{test_case.endpoint.upper()}-{test_case.provider} endpoint timed out after 2 seconds")
120+
pytest.fail(f"{test_case.chain.upper()}-{test_case.endpoint.upper()}-{test_case.provider} endpoint timed out after 2 seconds")
121+
return test
122+
123+
class Test:
124+
pass
125+
126+
for test_case in test_cases:
127+
test_name = f"chain: {test_case.chain.capitalize()[:15].ljust(15)}{test_case.endpoint.upper()[:4].ljust(4)}{re.sub(r'[^a-zA-Z0-9]+', ' ', test_case.provider)[:30].ljust(30)}"
128+
generate_tests = generate_test_function(test_case)
129+
setattr(Test, test_name, generate_tests)
130+
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[tool.pytest.ini_options]
2+
log_cli = false
3+
log_cli_level = "INFO"
4+
log_cli_format = "%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s)"
5+
log_cli_date_format = "%Y-%m-%d %H:%M:%S"
6+
7+
python_files = "apis.py"
8+
python_classes = "Test"
9+
python_functions = "chain*"
10+
11+
md_report = true
12+
md_report_verbose = 1
13+
md_report_color = "auto"
+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pytest
2+
pytest-xdist
3+
pytest-md-report
4+
requests

.gitignore

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@
22
.DS_Store
33
.github/workflows/utility/__pycache__
44

5-
node_modules/
5+
node_modules/
6+
7+
__pycache__/

README.md

+9
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ We accept pull requests to add data to an existing assetlist.json or chain.json
3838

3939
Please give Pull Requests a title that somewhat describes the change more precisely than the default title given to a Commit. PRs titled 'Update chain.json' are insufficient, and would be difficult to navigate when searching through the backlog of Pull Requests. Some recommended details would be: the affected Chain Name, API types, or Provider to give some more detail; e.g., "Add Cosmos Hub APIs for Acme Validator".
4040

41+
### Endpoints reachability
42+
43+
The endpoints added here are being tested via CI daily at 00:00 UTC. It is expected that your endpoints return an HTTP 200 in the following paths:
44+
- rest: `/status`
45+
- rpc: `/cosmos/base/tendermint/v1beta1/syncing`
46+
- grpc: not tested
47+
48+
Providers ready to be tested daily should be whitelisted here: `.github/workflows/tests/apis.py`
49+
4150
# chain.json
4251

4352
## Sample

0 commit comments

Comments
 (0)