Skip to content

Commit f85f898

Browse files
authored
test: add network option and search by address for stability tests (#639)
1 parent 41e3725 commit f85f898

File tree

3 files changed

+138
-40
lines changed

3 files changed

+138
-40
lines changed

load-tests/README.MD

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ usage: stability_test.py [-h] [--url BASE_URL] [--csv CSV_FILE]
7878
[--concurrency CONCURRENCIES] [--duration TEST_DURATION]
7979
[--sla SLA_THRESHOLD] [--error-threshold ERROR_THRESHOLD]
8080
[--skip-header] [-v] [--cooldown COOLDOWN]
81+
[--network NETWORK]
8182
[--endpoints SELECTED_ENDPOINTS] [--list-endpoints]
8283
8384
Cardano Rosetta API Stability Testing Tool
@@ -102,6 +103,7 @@ options:
102103
--skip-header Skip the header row in the CSV file (default: False)
103104
-v, --verbose Enable verbose output (default: False)
104105
--cooldown COOLDOWN Cooldown period in seconds between endpoint tests (default: 60)
106+
--network NETWORK Network identifier for API requests: mainnet or preprod (default: mainnet)
105107
--endpoints SELECTED_ENDPOINTS
106108
Comma-separated list of endpoint names or paths to test (e.g. "Network Status,Block"
107109
or "/account/balance,/block"). If not specified, all endpoints will be tested.
@@ -140,10 +142,22 @@ Test only specific endpoints by path:
140142
./load-tests/stability_test.py --endpoints "/network/status,/block,/account/balance"
141143
```
142144

143-
Test only search/transactions endpoint with stake address data:
145+
Test search/transactions by hash lookup:
144146

145147
```bash
146-
./load-tests/stability_test.py --endpoints "/search/transactions" --csv load-tests/data/mainnet-data-stake-address.csv
148+
./load-tests/stability_test.py --endpoints "Search Transactions by Hash"
149+
```
150+
151+
Test search/transactions by address (more resource-intensive):
152+
153+
```bash
154+
./load-tests/stability_test.py --endpoints "Search Transactions by Address" --csv load-tests/data/mainnet-data.csv
155+
```
156+
157+
Test on preprod network:
158+
159+
```bash
160+
./load-tests/stability_test.py --network preprod --url http://127.0.0.1:8082 --csv data/preprod-data.csv
147161
```
148162

149163
List all available endpoints without running tests:
@@ -164,6 +178,62 @@ Test with custom SLA and error thresholds:
164178
./load-tests/stability_test.py --sla 500 --error-threshold 0.5
165179
```
166180

181+
## Test Data
182+
183+
### CSV Format Requirements
184+
185+
Each CSV row must have 6 fields with specific associations:
186+
187+
```
188+
address,block_index,block_hash,transaction_size,relative_ttl,transaction_hash
189+
```
190+
191+
**Critical Data Associations (Implicit Rules):**
192+
193+
1. **Block Consistency**: The `block_hash` MUST be the hash of the block at `block_index`
194+
2. **Transaction in Block**: The `transaction_hash` MUST exist in the specified block (`block_hash`)
195+
3. **Address in Transaction**: The `address` MUST be involved in the transaction (appear in operations as input/output)
196+
4. **Transaction Size**: The `transaction_size` MUST match the actual size of the transaction in bytes
197+
5. **Valid Address**: The `address` MUST have a balance and UTXO history (for account endpoints)
198+
6. **TTL Value**: The `relative_ttl` is used by construction/metadata endpoint (1000 is standard)
199+
200+
These associations ensure all 8 endpoints can successfully use the same data row:
201+
- Network Status: No specific data needed
202+
- Account Balance/Coins: Requires valid address with balance
203+
- Block: Requires valid block_index and block_hash
204+
- Block Transaction: Requires transaction in specified block
205+
- Search by Hash: Requires valid transaction_hash
206+
- Search by Address: Requires address involved in transactions
207+
- Construction Metadata: Requires transaction_size and relative_ttl
208+
209+
### Available Data Files
210+
211+
The `data/` directory contains pre-validated CSV files for different networks:
212+
213+
### Mainnet Data (`mainnet-data.csv`)
214+
- **Block**: 11573705
215+
- **Transaction**: 3a954835b69ca01ff9cf3b30ce385d5d9ef0cea502bd0f2ad156684dfbaf325a
216+
- **Address**: addr1qxw5ly68dml8ceg7eawa7we8pjw8j8hn74n2djt2upmnq9th42p6lrke4yj3e0xqg3sdqm6lzksa53wd2550vrpkedks4fttnm
217+
218+
### Preprod Data (`preprod-data.csv`)
219+
- **Block**: 4070700
220+
- **Transaction**: bf540a825d5d40af7435801ce6adcac010f3f9f29ae102aee8cff8007f68c3d4
221+
- **Address**: addr_test1wzn5ee2qaqvly3hx7e0nk3vhm240n5muq3plhjcnvx9ppjgf62u6a
222+
223+
All data has been validated to work with all 8 stability test endpoints, with proper associations between blocks, transactions, and addresses.
224+
225+
## Endpoint Details
226+
227+
### Search Transactions Endpoints
228+
229+
The stability test includes two variants of the `/search/transactions` endpoint:
230+
231+
1. **Search Transactions by Hash**: Queries transactions using `transaction_identifier`. This is a fast, direct lookup by transaction hash.
232+
233+
2. **Search Transactions by Address**: Queries transactions using `account_identifier` with an address. This is more resource-intensive as it requires scanning transaction operations to find all transactions involving the specified address.
234+
235+
Both endpoints use the same API path (`/search/transactions`) but with different query parameters, allowing independent performance testing of each query pattern.
236+
167237
## Output
168238

169239
The script creates a timestamped directory containing:

load-tests/data/preprod-data.csv

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
address,block_index,block_hash,transaction_size,relative_ttl,transaction_hash
2+
addr_test1wzn5ee2qaqvly3hx7e0nk3vhm240n5muq3plhjcnvx9ppjgf62u6a,4070700,6b1b29d0533a86443140a88d3758f26fa9d4a8954363e78818b3235126ba933b,683,1000,bf540a825d5d40af7435801ce6adcac010f3f9f29ae102aee8cff8007f68c3d4

load-tests/stability_test.py

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ def parse_args():
7373
help='Cooldown period in seconds between endpoint tests')
7474
parser.add_argument('--max-retries', dest='max_retries', type=int, default=2,
7575
help='Maximum number of retries when an ab command fails')
76-
76+
parser.add_argument('--network', dest='network', default="mainnet",
77+
choices=['mainnet', 'preprod'],
78+
help='Network identifier for API requests')
79+
7780
# Endpoint selection
7881
parser.add_argument('--endpoints', dest='selected_endpoints', type=str,
7982
help='Comma-separated list of endpoint names or paths to test (e.g. "Network Status,Block" or "/account/balance,/block"). If not specified, all endpoints will be tested.')
@@ -104,6 +107,7 @@ def parse_args():
104107
VERBOSE = args.verbose
105108
COOLDOWN_PERIOD = args.cooldown
106109
MAX_RETRIES = args.max_retries
110+
NETWORK_ID = args.network
107111

108112
# Global logger variable
109113
logger = None
@@ -200,7 +204,6 @@ def parse_ab_output(ab_stdout: str):
200204
requests_per_sec = 0.0
201205
mean_time = 0.0
202206
non_2xx_responses = 0
203-
failed_requests = 0
204207

205208
# Parse each metric
206209
for line in ab_stdout.splitlines():
@@ -234,13 +237,8 @@ def parse_ab_output(ab_stdout: str):
234237
parts = line.split()
235238
if len(parts) >= 3:
236239
non_2xx_responses = int(parts[2])
237-
# Parse Failed requests
238-
elif "Failed requests:" in line:
239-
parts = line.split()
240-
if len(parts) >= 3:
241-
failed_requests = int(parts[2])
242240

243-
return p95, p99, complete_requests, requests_per_sec, mean_time, non_2xx_responses, failed_requests
241+
return p95, p99, complete_requests, requests_per_sec, mean_time, non_2xx_responses
244242

245243
###############################################################################
246244
# PAYLOAD GENERATORS
@@ -252,14 +250,14 @@ def payload_network_status(*_):
252250
"""
253251
/network/status does not really need CSV data.
254252
"""
255-
return dedent("""\
256-
{
257-
"network_identifier": {
253+
return dedent(f"""\
254+
{{
255+
"network_identifier": {{
258256
"blockchain": "cardano",
259-
"network": "mainnet"
260-
},
261-
"metadata": {}
262-
}
257+
"network": "{NETWORK_ID}"
258+
}},
259+
"metadata": {{}}
260+
}}
263261
""")
264262

265263
def payload_account_balance(address, *_):
@@ -270,7 +268,7 @@ def payload_account_balance(address, *_):
270268
{{
271269
"network_identifier": {{
272270
"blockchain": "cardano",
273-
"network": "mainnet"
271+
"network": "{NETWORK_ID}"
274272
}},
275273
"account_identifier": {{
276274
"address": "{address}"
@@ -286,7 +284,7 @@ def payload_account_coins(address, *_):
286284
{{
287285
"network_identifier": {{
288286
"blockchain": "cardano",
289-
"network": "mainnet"
287+
"network": "{NETWORK_ID}"
290288
}},
291289
"account_identifier": {{
292290
"address": "{address}"
@@ -303,7 +301,7 @@ def payload_block(_addr, block_index, block_hash, *_):
303301
{{
304302
"network_identifier": {{
305303
"blockchain": "cardano",
306-
"network": "mainnet"
304+
"network": "{NETWORK_ID}"
307305
}},
308306
"block_identifier": {{
309307
"index": {block_index},
@@ -320,7 +318,7 @@ def payload_block_transaction(_addr, block_index, block_hash, _tx_size, _ttl, tr
320318
{{
321319
"network_identifier": {{
322320
"blockchain": "cardano",
323-
"network": "mainnet"
321+
"network": "{NETWORK_ID}"
324322
}},
325323
"block_identifier": {{
326324
"index": {block_index},
@@ -332,22 +330,38 @@ def payload_block_transaction(_addr, block_index, block_hash, _tx_size, _ttl, tr
332330
}}
333331
""")
334332

335-
def payload_search_transactions(_addr, _block_index, _block_hash, _tx_size, _ttl, transaction_hash):
333+
def payload_search_transactions_by_hash(_addr, _block_index, _block_hash, _tx_size, _ttl, transaction_hash):
336334
"""
337335
/search/transactions requires transaction_hash.
338336
"""
339337
return dedent(f"""\
340338
{{
341339
"network_identifier": {{
342340
"blockchain": "cardano",
343-
"network": "mainnet"
341+
"network": "{NETWORK_ID}"
344342
}},
345343
"transaction_identifier": {{
346344
"hash": "{transaction_hash}"
347345
}}
348346
}}
349347
""")
350348

349+
def payload_search_transactions_by_address(address, *_):
350+
"""
351+
/search/transactions with account_identifier (address-based query).
352+
"""
353+
return dedent(f"""\
354+
{{
355+
"network_identifier": {{
356+
"blockchain": "cardano",
357+
"network": "{NETWORK_ID}"
358+
}},
359+
"account_identifier": {{
360+
"address": "{address}"
361+
}}
362+
}}
363+
""")
364+
351365
def payload_construction_metadata(_addr, _block_index, _block_hash, transaction_size, relative_ttl, _tx_hash):
352366
"""
353367
/construction/metadata requires transaction_size, relative_ttl
@@ -356,7 +370,7 @@ def payload_construction_metadata(_addr, _block_index, _block_hash, transaction_
356370
{{
357371
"network_identifier": {{
358372
"blockchain": "cardano",
359-
"network": "mainnet"
373+
"network": "{NETWORK_ID}"
360374
}},
361375
"options": {{
362376
"transaction_size": {transaction_size},
@@ -368,15 +382,16 @@ def payload_construction_metadata(_addr, _block_index, _block_hash, transaction_
368382
###############################################################################
369383
# ENDPOINT DEFINITION
370384
###############################################################################
371-
# We'll define 7 endpoints with: (Name, Path, Payload Generator Function)
385+
# We'll define 8 endpoints with: (Name, Path, Payload Generator Function)
372386
ENDPOINTS = [
373-
("Network Status", "/network/status", payload_network_status),
374-
("Account Balance", "/account/balance", payload_account_balance),
375-
("Account Coins", "/account/coins", payload_account_coins),
376-
("Block", "/block", payload_block),
377-
("Block Transaction", "/block/transaction", payload_block_transaction),
378-
("Search Transactions", "/search/transactions", payload_search_transactions),
379-
("Construction Metadata","/construction/metadata", payload_construction_metadata),
387+
("Network Status", "/network/status", payload_network_status),
388+
("Account Balance", "/account/balance", payload_account_balance),
389+
("Account Coins", "/account/coins", payload_account_coins),
390+
("Block", "/block", payload_block),
391+
("Block Transaction", "/block/transaction", payload_block_transaction),
392+
("Search Transactions by Hash", "/search/transactions", payload_search_transactions_by_hash),
393+
("Search Transactions by Address", "/search/transactions", payload_search_transactions_by_address),
394+
("Construction Metadata", "/construction/metadata", payload_construction_metadata),
380395
]
381396

382397
###############################################################################
@@ -493,7 +508,13 @@ def test_endpoint(endpoint_name, endpoint_path, payload_func, csv_row):
493508
# Example CSV columns:
494509
# address, block_index, block_hash, transaction_size, relative_ttl, transaction_hash
495510
#
496-
# Adjust if your CSV has different columns or order.
511+
# Validate CSV structure
512+
if len(csv_row) != 6:
513+
logger.error(f"Invalid CSV format for endpoint {endpoint_name}.")
514+
logger.error(f"Expected 6 columns (address, block_index, block_hash, transaction_size, relative_ttl, transaction_hash)")
515+
logger.error(f"Got {len(csv_row)} columns: {csv_row}")
516+
sys.exit(1)
517+
497518
address, block_index, block_hash, transaction_size, relative_ttl, transaction_hash = csv_row
498519

499520
# Generate JSON payload
@@ -546,9 +567,13 @@ def test_endpoint(endpoint_name, endpoint_path, payload_func, csv_row):
546567
if VERBOSE:
547568
# Format each line with box borders
548569
if line_stripped:
549-
# Fixed width approach
570+
# Truncate long lines to fit box width
571+
max_content_width = box_width - 4 # 2 for borders, 2 for padding
572+
if len(line_stripped) > max_content_width:
573+
line_stripped = line_stripped[:max_content_width - 3] + "..."
550574
content = "│ " + line_stripped
551-
logger.debug(content + " " * (box_width - len(content) - 1) + "│")
575+
padding = " " * (box_width - len(content) - 1)
576+
logger.debug(content + padding + "│")
552577
else:
553578
logger.debug("│" + " " * (box_width - 2) + "│")
554579
proc.stdout.close()
@@ -615,7 +640,7 @@ def test_endpoint(endpoint_name, endpoint_path, payload_func, csv_row):
615640
break
616641

617642
# Parse p95, p99 and additional metrics from the captured stdout
618-
p95, p99, complete_requests, requests_per_sec, mean_time, non_2xx_responses, failed_requests = parse_ab_output(ab_output)
643+
p95, p99, complete_requests, requests_per_sec, mean_time, non_2xx_responses = parse_ab_output(ab_output)
619644

620645
# Calculate error rate as a percentage
621646
error_rate = 0.0
@@ -748,8 +773,9 @@ def main():
748773
logger.debug(f"Data row: {', '.join(rows[0]) if rows else 'No data available'}")
749774
logger.debug(f"{'-' * 80}")
750775

751-
# For demonstration, pick the *first* row only.
752-
# If you want to test multiple rows, you can loop here or adapt logic.
776+
# Use only the first CSV row for consistency across all endpoints and concurrency levels.
777+
# This ensures that performance comparisons are based on the same data characteristics.
778+
# All endpoints will be tested with identical input data to measure their relative performance.
753779
if not rows:
754780
# Use logger.error and exit
755781
logger.error("No CSV data after skipping header.")
@@ -911,8 +937,8 @@ def main():
911937
logger.warning("=" * 80)
912938

913939
current_endpoint = None
914-
if 'ep_name' in locals() and 'c' in locals():
915-
current_endpoint = f"{ep_name} at concurrency level {c}"
940+
if 'ep_name' in locals():
941+
current_endpoint = ep_name
916942

917943
if current_endpoint:
918944
logger.warning(f"Test was interrupted while testing: {current_endpoint}")

0 commit comments

Comments
 (0)