Skip to content

Commit aec62d5

Browse files
authored
test: Initial k6 smoke test coverage (#1876)
- Add a k6 smote test dir under tooling - Add a test files to cover BN API smote tests to confirm serverStatus, getBlock and subscribeBlockItems work - Add a data.json file to allow for customization - Add a README to explain the logic and how it's intended to be used Signed-off-by: Nana Essilfie-Conduah <nana@swirldslabs.com>
1 parent 9ba57d1 commit aec62d5

File tree

5 files changed

+269
-0
lines changed

5 files changed

+269
-0
lines changed

tools-and-tests/k6/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# K6 Performance Tests
2+
3+
This directory contains performance tests for the application using [k6](https://k6.io/), a modern load testing tool.
4+
5+
## Prerequisites
6+
7+
- Ensure you have [k6](https://grafana.com/docs/k6/latest/set-up/install-k6/) installed on your machine.
8+
- Make sure the application you want to test is running and accessible.
9+
10+
## Setup
11+
12+
The tests utilize protobuf files specific to the Block Node (BN) application for validation purposes and config settings
13+
located in the `./tools-and-tests/k6/data.json` file.
14+
15+
The BN protobuf files should be downloaded from the desired [BN Release](https://github.com/hiero-ledger/hiero-block-node/releases) and placed in the `./k6` directory before
16+
running the tests e.g. `tools-and-tests/k6/block-node-protobuf-0.23.1`.
17+
18+
The `data.json` file should be updated to reflect the paths to these
19+
protobuf files.
20+
- `configs.blockNodeUrl`: URL of the Block Node instance to be tested.
21+
- `configs.protobufPath`: Path to the directory containing the Block Node protobuf files.
22+
23+
- Example:
24+
25+
```json
26+
{
27+
"configs": [{
28+
"blockNodeUrl": "localhost:40840",
29+
"protobufPath": "./../block-node-protobuf-0.23.1",
30+
...
31+
}]
32+
}
33+
```
34+
35+
## Test Types
36+
37+
The k6 setup will be used to run different [test types](https://grafana.com/docs/k6/latest/testing-guides/test-types/) located in subdirectories.
38+
Each subdirectory contains its own k6 test scripts and configuration files.
39+
40+
### Average Load Tests
41+
42+
The `./tools-and-tests/k6/average-load` directory contains test scripts designed to simulate average load conditions on
43+
the application.
44+
45+
### Smoke Tests
46+
47+
The `./tools-and-tests/k6/smoke` directory contains basic smoke test scripts to verify the application's core functionality.
48+
No additional load is applied during these tests but a test runner can configure virtual users if needed.
49+
50+
The `data.json` file may be updated to change the following settings
51+
protobuf files.
52+
- `configs.smokeTestConfigs.numOfBlocksToStream`: Number of blocks to stream as a subscriber during the smoke test.
53+
54+
- Example:
55+
56+
```json
57+
{
58+
"configs": [{
59+
...,
60+
"smokeTestConfigs": {
61+
"numOfBlocksToStream": 10
62+
}
63+
}]
64+
}
65+
```
66+
67+
## Running the Tests
68+
69+
1. Navigate to the desired directory `tools-and-tests/k6/<testTypeDir>`:
70+
71+
```bash
72+
cd tools-and-tests/k6/<testTypeDir>
73+
```
74+
2. Run the desired k6 test script using the following command:
75+
76+
```bash
77+
k6 run <script-name>.js
78+
```
79+
80+
Replace `<script-name>.js` with the k6 test script you want to execute.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Client, StatusOK } from 'k6/net/grpc';
2+
import { check, sleep } from 'k6';
3+
import { SharedArray } from 'k6/data';
4+
5+
// Configure k6 VUs scheduling and iterations
6+
export const options = {
7+
stages: [
8+
{ duration: '2m', target: 40 }, // traffic ramp-up from 1 to 40 users (all CNs and a shadow MN) over 5 minutes.
9+
{ duration: '3m', target: 100 }, // stay at 100 users for 3 minutes
10+
{ duration: '1m', target: 0 }, // ramp-down to 0 users
11+
],
12+
};
13+
14+
// load test configuration data
15+
const data = new SharedArray('BN Test Configs', function () {
16+
return JSON.parse(open('./../data.json')).configs;
17+
})[0];
18+
19+
const client = new Client();
20+
client.load([data.protobufPath], 'block-node/api/node_service.proto');
21+
22+
export default () => {
23+
client.connect(data.blockNodeUrl, {
24+
plaintext: true
25+
});
26+
27+
const response = client.invoke('org.hiero.block.api.BlockNodeService/serverStatus', {});
28+
29+
check(response, {
30+
'status is OK': (r) => r && r.status === StatusOK,
31+
});
32+
33+
console.log(JSON.stringify(response.message));
34+
35+
client.close();
36+
sleep(1);
37+
};

tools-and-tests/k6/data.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"configs": [{
3+
"blockNodeUrl": "localhost:40840",
4+
"protobufPath": "./../block-node-protobuf-0.23.1",
5+
"smokeTestConfigs": {
6+
"numOfBlocksToStream": 10
7+
}
8+
}]
9+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Client, StatusOK } from 'k6/net/grpc';
2+
import { check, sleep } from 'k6';
3+
import { SharedArray } from 'k6/data';
4+
5+
// load test configuration data
6+
const data = new SharedArray('BN Test Configs', function () {
7+
return JSON.parse(open('./../data.json')).configs;
8+
})[0];
9+
10+
const client = new Client();
11+
client.load([data.protobufPath],
12+
'block-node/api/node_service.proto',
13+
'block-node/api/block_access_service.proto',
14+
'block-node/api/block_stream_subscribe_service.proto');
15+
16+
export default () => {
17+
client.connect(data.blockNodeUrl, {
18+
plaintext: true
19+
});
20+
21+
const response = client.invoke('org.hiero.block.api.BlockNodeService/serverStatus', {});
22+
23+
check(response, {
24+
'status is OK': (r) => r && r.status === StatusOK,
25+
});
26+
27+
const firstAvailableBlock = response.message.firstAvailableBlock;
28+
const lastAvailableBlock = response.message.lastAvailableBlock;
29+
30+
console.log(`First Available Block: ${firstAvailableBlock}, Latest Block: ${lastAvailableBlock}`);
31+
console.log(JSON.stringify(response.message));
32+
33+
if (firstAvailableBlock === '18446744073709551615') {
34+
console.log(`No blocks to fetch, exiting test.`);
35+
client.close();
36+
return;
37+
} else {
38+
const firstAvailableBlockResponse = client.invoke('org.hiero.block.api.BlockAccessService/getBlock', {
39+
block_number: firstAvailableBlock,
40+
});
41+
42+
check(firstAvailableBlockResponse, {
43+
'block fetch status is OK': (r) => r && r.status === StatusOK,
44+
'fetched block number is correct': (r) => r && r.message.block.items[0].blockHeader.number === firstAvailableBlock,
45+
});
46+
47+
console.log(`Fetched Block '${firstAvailableBlock}' with size '${firstAvailableBlockResponse.message.block.items.length}' items`);
48+
49+
const lastAvailableBlockResponse = client.invoke('org.hiero.block.api.BlockAccessService/getBlock', {
50+
block_number: lastAvailableBlock,
51+
});
52+
53+
check(lastAvailableBlockResponse, {
54+
'block fetch status is OK': (r) => r && r.status === StatusOK,
55+
'fetched block number is correct': (r) => r && r.message.block.items[0].blockHeader.number === lastAvailableBlock,
56+
});
57+
58+
console.log(`Fetched Block '${lastAvailableBlock}' with size '${lastAvailableBlockResponse.message.block.items.length}' items`);
59+
}
60+
61+
client.close();
62+
sleep(1);
63+
};
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Client, StatusOK, Stream } from 'k6/net/grpc';
2+
import { check, sleep } from 'k6';
3+
import { SharedArray } from 'k6/data';
4+
5+
// load test configuration data
6+
const data = new SharedArray('BN Test Configs', function () {
7+
return JSON.parse(open('./../data.json')).configs;
8+
})[0];
9+
10+
const client = new Client();
11+
client.load([data.protobufPath],
12+
'block-node/api/node_service.proto',
13+
'block-node/api/block_access_service.proto',
14+
'block-node/api/block_stream_subscribe_service.proto');
15+
16+
export default () => {
17+
client.connect(data.blockNodeUrl, {
18+
plaintext: true
19+
});
20+
21+
const response = client.invoke('org.hiero.block.api.BlockNodeService/serverStatus', {});
22+
23+
check(response, {
24+
'status is OK': (r) => r && r.status === StatusOK,
25+
});
26+
27+
const firstAvailableBlock = BigInt(response.message.firstAvailableBlock);
28+
const lastAvailableBlock = BigInt(response.message.lastAvailableBlock);
29+
30+
console.log(`First Available Block: ${firstAvailableBlock}, Latest Block: ${lastAvailableBlock}`);
31+
console.log(JSON.stringify(response.message));
32+
33+
// decide how many blocks to stream based on availability
34+
let blockDelta = 0n;
35+
if (response.message.firstAvailableBlock === '18446744073709551615') {
36+
console.log(`No blocks to stream, exiting test.`);
37+
client.close();
38+
return;
39+
}
40+
else if (firstAvailableBlock === lastAvailableBlock) {
41+
console.log(`Block Node only has one block.`);
42+
} else if ((lastAvailableBlock - firstAvailableBlock) < data.smokeTestConfigs.numOfBlocksToStream) {
43+
blockDelta = lastAvailableBlock - firstAvailableBlock;
44+
console.log(`Block Node has only ${blockDelta + 1n} blocks to stream.`);
45+
} else {
46+
blockDelta = BigInt(data.smokeTestConfigs.numOfBlocksToStream);
47+
console.log(`Block Node has sufficient blocks to stream ${data.smokeTestConfigs.numOfBlocksToStream} blocks.`);
48+
}
49+
50+
// stream block from subscribe API
51+
const stream = new Stream(client, 'org.hiero.block.api.BlockStreamSubscribeService/subscribeBlockStream');
52+
stream.on('data', (subscribeStreamResponse) => {
53+
54+
if (subscribeStreamResponse.blockItems) {
55+
console.log(`Stream Response: BlockHeader for Block ${JSON.stringify(subscribeStreamResponse.blockItems.blockItems[0].blockHeader.number)}`);
56+
} else if (subscribeStreamResponse.endOfBlock) {
57+
console.log(`Stream Response: endOfBlock for Block ${JSON.stringify(subscribeStreamResponse.endOfBlock.blockNumber)}`);
58+
} else {
59+
console.log(`Unknown Stream Response: , ${JSON.stringify(subscribeStreamResponse)}`);
60+
}
61+
});
62+
63+
stream.on('error', (err) => {
64+
console.log('Stream Error: ' + JSON.stringify(err));
65+
});
66+
67+
stream.on('end', () => {
68+
client.close();
69+
console.log('Stream ended.');
70+
});
71+
72+
stream.write({
73+
"start_block_number": firstAvailableBlock.toString(),
74+
"end_block_number": (firstAvailableBlock + blockDelta).toString(),
75+
});
76+
77+
stream.end();
78+
sleep(1);
79+
client.close();
80+
};

0 commit comments

Comments
 (0)