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

Commit

Permalink
Merge pull request #98 from yue-open/master
Browse files Browse the repository at this point in the history
fullMethodField Drop - down box edit auto-complement
  • Loading branch information
huynhminhtan authored Mar 7, 2022
2 parents 4f0539e + 135cfaf commit ffe0c10
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 41 deletions.
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

0 comments on commit ffe0c10

Please sign in to comment.