Skip to content
This repository has been archived by the owner on Apr 4, 2024. It is now read-only.

fullMethodField Drop - down box edit auto-complement #98

Merged
merged 5 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
[![Javadocs](https://www.javadoc.io/badge/org.apache.jmeter/ApacheJMeter_core.svg)](https://www.javadoc.io/doc/org.apache.jmeter/ApacheJMeter_core)
[![Stack Overflow](https://img.shields.io/:stack%20overflow-jmeter-brightgreen.svg)](https://stackoverflow.com/questions/tagged/jmeter)

[简体中文](./README.zh-CN.md) | English

## What is it

This is a simpler of JMeter used to test for any gRPC server, it is not necessary to generate gRPC classes or to compile the protos binary for the service. Just a very simple for input:
Expand Down Expand Up @@ -68,12 +70,13 @@ Run test:
| 1 | Server Name or IP | Domain/IP for gRPC server |
| 2 | Port Number | Port for gRPC server (80/ 443) |
| 3 | SSL/TLS | SSL/TLS to authenticate the server |
| 4 | Proto Root Directory | Root directory contains proto files |
| 5 | Library Directory (Optional) | Using a different underlying library (googleapis) |
| 6 | Full Method | Full Method to test |
| 7 | Metadata | Metadata can be use for Store token, authentication method, etc. <br/>Two Ways to use metadata, <br/><br/> &nbsp;<b>1. Comma separated Key:Value : </b><br/>&nbsp; - key1:value1,key2:value2<br/>&nbsp; - Value should url encode with utf-8 <br/><br/>&nbsp;2.<b> Json String : </b><br/>&nbsp;&nbsp; - {"key1":"Value1", "key2":"value2"} <br/><br/> <b>Note: <i>In gRPC Metadata value is (Key, value) both in format of (String, String), in case of nested Json Objects values, will go to request as a JsonString. </i></b> |
| 8 | Deadline | How long gRPC clients are willing to wait for an RPC to complete |
| 9 | Send JSON Format With the Request | Data request with JSON format |
| 4 | Disable SSL/TLS Cert Verification | Disable SSL/TLS certificate verification (enable this function when using self-signed certificates) |
| 5 | Proto Root Directory | Root directory contains proto files |
| 6 | Library Directory (Optional) | Using a different underlying library (googleapis) |
| 7 | Full Method | Full Method to test |
| 8 | Metadata | Metadata can be use for Store token, authentication method, etc. <br/>Two Ways to use metadata, <br/><br/> &nbsp;<b>1. Comma separated Key:Value : </b><br/>&nbsp; - key1:value1,key2:value2<br/>&nbsp; - Value should url encode with utf-8 <br/><br/>&nbsp;2.<b> Json String : </b><br/>&nbsp;&nbsp; - {"key1":"Value1", "key2":"value2"} <br/><br/> <b>Note: <i>In gRPC Metadata value is (Key, value) both in format of (String, String), in case of nested Json Objects values, will go to request as a JsonString. </i></b> |
| 9 | Deadline | How long gRPC clients are willing to wait for an RPC to complete |
| 10 | Send JSON Format With the Request | Data request with JSON format |

## Running the examples

Expand Down
96 changes: 96 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# jmeter-grpc-request
<p align="center"><img src="./dist/asset/jmeter-and-grpc.png" width="600px" alt="Apache JMeter and gRPC logo" /></p>

<h4 align="center">这个JMeter采样器允许您向服务器发送一个gRPC请求</h4>
<h4 align="center">它和HTTP请求一样简单</h4>

<p align="center">
<a target="_blank" href="https://www.javadoc.io/doc/org.apache.jmeter/ApacheJMeter_core">
<img src="https://www.javadoc.io/badge/org.apache.jmeter/ApacheJMeter_core.svg" alt="Javadocs">
</a>
<a target="_blank" href="https://stackoverflow.com/questions/tagged/jmeter">
<img src="https://img.shields.io/:stack%20overflow-jmeter-brightgreen.svg" alt="Stack Overflow">
</a>
</p>

[English](./README.md) | 简体中文

## 介绍
他是一个功能强大的JMeter Grpc插件,可用于测试任何gRPC服务器,它不需要生成gRPC类或编译服务的protos二进制文件,只是一个非常简单的输入:

- gRPC服务的主机和端口
- 需要测试的RPC方法
- proto文件路径
- 格式化的JSON请求数据

## 特性
- 支持压测阻塞等调用方式
- 支持在运行时解析proto文件
- 支持TLS连接
- 支持元数据认证(JWT/Token)
- 支持JSON格式的请求数据
- 支持运行在Windows、Mac、Linux中
- 支持自动列出proto文件中的所有完整方法
- 支持根据proto文件自动生成请求Mock
- 支持各种报告生成
- 支持自动化测试

## 如何使用
<p align="center"><img src="./dist/asset/jmeter-grpc-create-testscript.gif" width="820px" alt="jmeter-create-testscript-grpc" /></p>

### 插件安装
你需要将 **jmeter-grpc-request** 插件的 `jar` 包复制到JMeter的 `lib/ext` 目录下面,然后重启你的JMeter工具。

**jmeter-grpc-request** 插件的 `jar` 包,可以从 [Releases Page](https://github.com/zalopay-oss/jmeter-grpc-request/releases) 获得,也可以 在 [JMeter Plugins Manager](https://jmeter-plugins.org/?search=jmeter-grpc-request) 中找到

### 使用 JMeter 发出 gRPC 请求
创建测试脚本:

- 添加线程组:右键单击测试计划 → 添加 → 线程(用户) → 线程组
- 添加GRPC Request:右键单击新建的线程组 → 添加 → 取样器 → GRPC Request
- 填写请求信息:主机、端口、proto文件夹、rpc方法、请求数据
- 保存测试脚本

运行测试:

- 通过JMeter GUI在顶部栏点击启动按钮
- 通过命令行:`bin/jmeter -n -t <test JMX file>.jmx -l <test JMX result>.csv -j <test log file>.log -e -o <Path to output folder>`

### 使用说明
| 序号 | 选项 | 描述 |
|----- |:----------------------------------- |:--------------------------------------------------------------------- |
| 1 | Server Name or IP | gRPC服务器地址(域名或IP) |
| 2 | Port Number | gRPC服务器端口 (80/443) |
| 3 | SSL/TLS | 开启SSL/TLS认证(https) |
| 4 | Disable SSL/TLS Cert Verification | 禁用SSL/TLS证书校验(自签证书需开启) |
| 5 | Proto Root Directory | proto文件的根路径 |
| 6 | Library Directory (Optional) | proto文件解析需要依赖的额外库的文件夹路径 (googleapis) |
| 7 | Full Method | 用于请求测试的RPC方法 |
| 8 | Metadata | Metadata可以用于token身份验证等方式,支持以下两种方式传输(UTF-8):<br/>1. 使用键值对(Key: Value):<br/>&nbsp; - key1: value1, key2: value2<br/>2. 使用 JSON String:<br/>&nbsp; - {"key1":"Value1", "key2":"value2"} |
| 9 | Deadline | 请求超时时间(单位:毫秒) |
| 10 | Send JSON Format With the Request | 格式化的JSON请求数据 |

## 运行示例
运行示例说明见 [./dist/example](./dist/example) 目录

## 基准测试
通过基准测试验证,jmeter-grpc-request 插件在对gRPC系统进行负载测试时是稳定的。

了解更多 [Benchmark: jmter-grpc-request](./dist/benchmark)

- CCU: 120 user
- Duration: 30 min

<img src= "./dist/asset/report-120-1800s.jpg" />

## 开发构建
### 构建环境
从源码构建 JMeter GRPC Request 插件,进行开发调试,必须拥有以下环境:

- [Java 8](https://www.oracle.com/downloads/index.html)
- [Apache Maven 3](https://maven.apache.org/)

### 构建命令
```
mvn clean package
```
19 changes: 12 additions & 7 deletions src/main/java/vn/zalopay/benchmark/GRPCSampler.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,18 +109,23 @@ private String whoAmI() {
}

private void errorResult(GrpcResponse grpcResponse, SampleResult sampleResult, Exception e) {
sampleResult.sampleEnd();
sampleResult.setSuccessful(false);
sampleResult.setResponseData(String.format("Exception: %s. %s", e.getCause().getMessage(), grpcResponse.getGrpcMessageString()), "UTF-8");
sampleResult.setResponseMessage("Exception: " + e.getCause().getMessage());
sampleResult.setDataType(SampleResult.TEXT);
sampleResult.setResponseCode("500");
try {
sampleResult.setSuccessful(false);
sampleResult.setResponseCode("500");
sampleResult.setDataType(SampleResult.TEXT);
sampleResult.sampleEnd();
sampleResult.setResponseMessage("Exception: " + e.getCause().getMessage());
sampleResult.setResponseData(String.format("Exception: %s. %s", e.getCause().getMessage(), grpcResponse.getGrpcMessageString()), "UTF-8");
} catch (Exception ex) {
// Prints exceptions that occur before the request is initiated
e.printStackTrace();
log.error("GrpcMessage: {}", grpcResponse.getGrpcMessageString());
}
}

/**
* GETTER AND SETTER
*/

public String getMetadata() {
return getPropertyAsString(METADATA);
}
Expand Down
100 changes: 73 additions & 27 deletions src/main/java/vn/zalopay/benchmark/GRPCSamplerGui.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import kg.apc.jmeter.JMeterPluginsUtils;
import kg.apc.jmeter.gui.BrowseAction;
import kg.apc.jmeter.gui.GuiBuilderHelper;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.gui.util.HorizontalPanel;
import org.apache.jmeter.gui.util.JSyntaxTextArea;
Expand All @@ -28,6 +29,7 @@
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;

Expand All @@ -44,6 +46,7 @@ public class GRPCSamplerGui extends AbstractSamplerGui {
private static final String WIKI_PAGE = "https://github.com/zalopay-oss/jmeter-grpc-request";

private GRPCSampler grpcSampler;
private String[] protoMethods;

private JTextField protoFolderField;
private JButton protoBrowseButton;
Expand Down Expand Up @@ -166,7 +169,6 @@ private void initGui() {
/**
* Helper function
*/

private void addToPanel(JPanel panel, GridBagConstraints constraints, int col, int row,
JComponent component) {
constraints.gridx = col;
Expand Down Expand Up @@ -197,6 +199,13 @@ private JPanel getWebServerPanel() {
private JPanel getRequestJSONPanel() {
requestJsonArea = JSyntaxTextArea.getInstance(30, 50);
requestJsonArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JSON);
requestJsonArea.setBracketMatchingEnabled(true);
requestJsonArea.setPaintMatchedBracketPair(true);
requestJsonArea.setAutoIndentEnabled(true);
requestJsonArea.setMarkOccurrences(true);
requestJsonArea.setPaintMarkOccurrencesBorder(true);
requestJsonArea.setPaintTabLines(true);
requestJsonArea.setShowMatchedBracketPopup(true);

JPanel webServerPanel = new JPanel(new BorderLayout());
webServerPanel.setBorder(BorderFactory.createCompoundBorder(
Expand Down Expand Up @@ -267,13 +276,14 @@ private JPanel getGRPCRequestPanel() {
addToPanel(requestPanel, labelConstraints, 0, row, new JLabel("Full Method: ", JLabel.RIGHT));
addToPanel(requestPanel, editConstraints, 1, row, fullMethodField = new JComboBox<>());
fullMethodField.setEditable(true);
fullMethodField.setMaximumRowCount(12);
addToPanel(requestPanel, labelConstraints, 2, row, fullMethodButton = new JButton("Listing..."));

fullMethodButton.addActionListener(new ActionListener() {
// fullMethodButton click listener
@Override
public void actionPerformed(ActionEvent e) {
getMethods(fullMethodField);
reloadProtoMethods();
}
});
fullMethodField.addPopupMenuListener(new PopupMenuListener() {
Expand All @@ -283,6 +293,7 @@ public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
// fullMethod list checked listener
@Override
public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
// Request Mock Auto Create
requestMock();
}
@Override
Expand All @@ -294,6 +305,30 @@ public void popupMenuCanceled(PopupMenuEvent e) {
@Override
public void actionPerformed(ActionEvent e) {
if ("comboBoxEdited".equals(e.getActionCommand())) {
// fullMethodField Drop - down box edit auto-complement
String fullMethod = fullMethodField.getSelectedItem().toString();
if (StringUtils.isBlank(fullMethod)) {
return;
}

String[] protoMethods = getProtoMethods(false);
try {
for (String protoMethod : protoMethods) {
boolean startsWith = protoMethod.startsWith(fullMethod);
if (startsWith == true) {
fullMethodField.setSelectedItem(protoMethod);
if (protoMethod.equals(fullMethod) == false) {
fullMethodField.showPopup();
}

break;
}
}
} catch (Exception ex) {
ex.printStackTrace();
}

// Request Mock Auto Create
requestMock();
}
}
Expand All @@ -309,24 +344,34 @@ public void actionPerformed(ActionEvent e) {
return container;
}

private void getMethods(JComboBox<String> fullMethodField) {
if (StringUtils.isNotBlank(grpcSampler.getProtoFolder())) {
JMeterVariableUtils.undoVariableReplacement(grpcSampler);
ServiceResolver serviceResolver = ClientList.getServiceResolver(grpcSampler.getProtoFolder(), grpcSampler.getLibFolder(), true);
List<String> methods = ClientList.listServices(serviceResolver);

log.info("Full Methods: " + methods.toString());
String[] methodsArr = new String[methods.size()];
methods.toArray(methodsArr);

fullMethodField.setModel(new DefaultComboBoxModel<>(methodsArr));
try {
Object selectedItem = fullMethodField.getSelectedItem();
fullMethodField.setSelectedItem(selectedItem);
} catch (Exception e) {
fullMethodField.setSelectedIndex(0);
private void reloadProtoMethods() {
Object selectedItem = fullMethodField.getSelectedItem();
getProtoMethods(true);
try {
if (selectedItem != null && StringUtils.isBlank(selectedItem.toString())) {
selectedItem = fullMethodField.getSelectedItem();
}
fullMethodField.setSelectedItem(selectedItem);
} catch (Exception e) {
fullMethodField.setSelectedIndex(0);
} finally {
fullMethodField.showPopup();
}
}

private String[] getProtoMethods(boolean reload) {
if (StringUtils.isNotBlank(grpcSampler.getProtoFolder()) && (ArrayUtils.isEmpty(protoMethods) || reload == true)) {
JMeterVariableUtils.undoVariableReplacement(grpcSampler);
ServiceResolver serviceResolver = ClientList.getServiceResolver(grpcSampler.getProtoFolder(), grpcSampler.getLibFolder(), reload);
List<String> methodList = ClientList.listServices(serviceResolver);
protoMethods = new String[methodList.size()];
methodList.toArray(protoMethods);
Arrays.sort(protoMethods);
fullMethodField.setModel(new DefaultComboBoxModel<>(protoMethods));
log.info("Full Methods Length: {}", protoMethods.length);
}

return protoMethods;
}

private void requestMock() {
Expand All @@ -345,7 +390,7 @@ private void requestMock() {
JSONObject requestBody = new JSONObject(true);
for (Descriptors.FieldDescriptor field : fields) {
String name = field.getName();
Object defaultValue = getValue(field);
Object defaultValue = getMockValue(field);
requestBody.put(name, defaultValue);
}
String text = requestBody.toString(
Expand All @@ -360,25 +405,25 @@ private void requestMock() {
}
}

private Object getValue(Descriptors.FieldDescriptor field) {
private Object getMockValue(Descriptors.FieldDescriptor field) {
String name = field.getName();
String type = field.getType().name().toLowerCase();
if ("message".equals(type)) {
List<Descriptors.FieldDescriptor> fields = field.getMessageType().getFields();
JSONObject repeatedField = new JSONObject(true);
for (Descriptors.FieldDescriptor repeatedFieldDescriptor : fields) {
repeatedField.put(repeatedFieldDescriptor.getName(), this.getValue(repeatedFieldDescriptor));
repeatedField.put(repeatedFieldDescriptor.getName(), this.getMockValue(repeatedFieldDescriptor));
}
return repeatedField;
} else {
return getDefaultValue(name, type);
return getMockDefaultValue(name, type);
}
}

private Object getDefaultValue(String name, String type) {
private Object getMockDefaultValue(String name, String type) {
switch (type) {
case "string":
return interpretMockViaFieldName(name);
return fieldNameGenerateMock(name);
case "bool":
return true;
case "number":
Expand Down Expand Up @@ -412,10 +457,11 @@ private Object getDefaultValue(String name, String type) {
}

/**
* Tries to guess a mock value from the field name.
* Default Hello.
* Mock generation from fieldName.
*
* <p>Default: Hello</p>
*/
private String interpretMockViaFieldName(String fieldName) {
private String fieldNameGenerateMock(String fieldName) {
String fieldNameLower = fieldName.toLowerCase();

if (fieldNameLower.startsWith("id") || fieldNameLower.endsWith("id")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public void verifyCanPerformGetMethodName() throws NoSuchFieldException, Illegal
grpcSampler.setRequestJson("dummyRequest");
grpRequestPluginGUI.configure(grpcSampler);
fullMethodButton.doClick();
Assert.assertEquals(fullMethodComboBox.getSelectedItem(), "bookstore.Bookstore/ListShelves");
Assert.assertEquals(fullMethodComboBox.getSelectedItem(), "bookstore.Bookstore/CreateShelf");
Assert.assertNotNull(grpcSampler);
Assert.assertNotNull(grpRequestPluginGUI);
frame.dispose();
Expand Down