listeners = Collections.emptyList();
+ private final int queueSize = 5000;
+
+ }
+
public static class SetUpTearDownSettings {
}
@@ -102,6 +127,8 @@ public static class BrowserSettings {
private final String userAgent = "";
private final CacheManager cache = new CacheManager();
private final CookiesManager cookies = new CookiesManager();
+ private final Boolean downloadResources = null;
+ private final Boolean keepAlive = null;
}
diff --git a/jmeter-java-dsl-octoperf/src/test/java/us/abstracta/jmeter/javadsl/octoperf/OctoPerfEngineTest.java b/jmeter-java-dsl-octoperf/src/test/java/us/abstracta/jmeter/javadsl/octoperf/OctoPerfEngineTest.java
index 54d4ae52..502c44b8 100644
--- a/jmeter-java-dsl-octoperf/src/test/java/us/abstracta/jmeter/javadsl/octoperf/OctoPerfEngineTest.java
+++ b/jmeter-java-dsl-octoperf/src/test/java/us/abstracta/jmeter/javadsl/octoperf/OctoPerfEngineTest.java
@@ -6,8 +6,6 @@
import static us.abstracta.jmeter.javadsl.JmeterDsl.threadGroup;
import java.time.Duration;
-import java.util.HashMap;
-import java.util.Map;
import org.junit.jupiter.api.Test;
import us.abstracta.jmeter.javadsl.core.TestPlanStats;
diff --git a/jmeter-java-dsl-websocket/pom.xml b/jmeter-java-dsl-websocket/pom.xml
new file mode 100644
index 00000000..7855080d
--- /dev/null
+++ b/jmeter-java-dsl-websocket/pom.xml
@@ -0,0 +1,44 @@
+
+
+ 4.0.0
+
+ us.abstracta.jmeter
+ jmeter-java-dsl-parent
+ 2.2-SNAPSHOT
+ ../pom.xml
+
+
+ jmeter-java-dsl-websocket
+
+
+
+ us.abstracta.jmeter
+ jmeter-java-dsl
+ ${project.version}
+
+
+ net.luminis.jmeter
+ jmeter-websocket-samplers
+ 1.3.1
+
+
+ org.apache.jmeter
+ ApacheJMeter_core
+
+
+ org.apache.jmeter
+ jorphan
+
+
+
+
+ org.java-websocket
+ Java-WebSocket
+ 1.6.0
+ test
+
+
+
+
\ No newline at end of file
diff --git a/jmeter-java-dsl-websocket/src/main/java/us/abstracta/jmeter/javadsl/websocket/WebsocketJMeterDsl.java b/jmeter-java-dsl-websocket/src/main/java/us/abstracta/jmeter/javadsl/websocket/WebsocketJMeterDsl.java
new file mode 100644
index 00000000..1a82411e
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/main/java/us/abstracta/jmeter/javadsl/websocket/WebsocketJMeterDsl.java
@@ -0,0 +1,545 @@
+package us.abstracta.jmeter.javadsl.websocket;
+
+import eu.luminis.jmeter.wssampler.CloseWebSocketSampler;
+import eu.luminis.jmeter.wssampler.CloseWebSocketSamplerGui;
+import eu.luminis.jmeter.wssampler.DataPayloadType;
+import eu.luminis.jmeter.wssampler.OpenWebSocketSampler;
+import eu.luminis.jmeter.wssampler.OpenWebSocketSamplerGui;
+import eu.luminis.jmeter.wssampler.SingleReadWebSocketSampler;
+import eu.luminis.jmeter.wssampler.SingleReadWebSocketSampler.DataType;
+import eu.luminis.jmeter.wssampler.SingleReadWebSocketSamplerGui;
+import eu.luminis.jmeter.wssampler.SingleWriteWebSocketSampler;
+import eu.luminis.jmeter.wssampler.SingleWriteWebSocketSamplerGui;
+import java.lang.reflect.Method;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.List;
+import org.apache.jmeter.testelement.TestElement;
+import us.abstracta.jmeter.javadsl.codegeneration.MethodCall;
+import us.abstracta.jmeter.javadsl.codegeneration.MethodCallContext;
+import us.abstracta.jmeter.javadsl.codegeneration.MethodParam;
+import us.abstracta.jmeter.javadsl.codegeneration.SingleTestElementCallBuilder;
+import us.abstracta.jmeter.javadsl.codegeneration.TestElementParamBuilder;
+import us.abstracta.jmeter.javadsl.codegeneration.params.BoolParam;
+import us.abstracta.jmeter.javadsl.codegeneration.params.EnumParam;
+import us.abstracta.jmeter.javadsl.codegeneration.params.StringParam;
+import us.abstracta.jmeter.javadsl.core.samplers.BaseSampler;
+
+/**
+ * Provides factory methods to create WebSocket samplers for performance
+ * testing.
+ *
+ * This class serves as the entry point for creating different types of
+ * WebSocket operations:
+ *
+ * - {@link #websocketConnect(String)} - Establish a WebSocket connection
+ * - {@link #websocketWrite(String)} - Send messages to the server
+ * - {@link #websocketRead()} - Read responses from the server
+ * - {@link #websocketDisconnect()} - Close the WebSocket connection
+ *
+ *
+ * Example usage:
+ *
+ *
{@code
+ *import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+ *import static us.abstracta.jmeter.javadsl.websocket.DslWebsocketSampler.webSocketSampler;
+ *import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+ *
+ *public class Test {
+ *
+ * public static void main(String[] args) throws Exception {
+ * TestPlanStats stats = testPlan(
+ * threadGroup(1, 1,
+ * webSocketSampler().connect("wss://server.com/websocket")
+ * webSocketSampler().write("Hello WebSocket!")
+ * webSocketSampler().read()
+ * .children(
+ * responseAssertion()
+ * .equalsToStrings("Hello WebSocket!")),
+ * webSocketSampler().disconnect()
+ * )
+ * ).run();
+ * }
+ * }
+ * }
+ *
+ * @since 2.2
+ */
+public class WebsocketJMeterDsl {
+
+ private WebsocketJMeterDsl() {
+ }
+
+ /**
+ * Creates a WebSocket connect sampler to establish a connection to the server.
+ *
+ * After establishing the connection, use {@link #websocketWrite(String)} and
+ * {@link #websocketRead()}
+ * samplers to send and receive messages. Remember to close the connection using
+ * {@link #websocketDisconnect()} when finished.
+ *
+ * It could be also used alone to test the connection to the server.
+ *
+ * URL Format: {@code ws://host:port/path?query} or
+ * {@code wss://host:port/path?query}
+ *
+ *
+ * @param url the WebSocket server URL. Supported schemes: {@code ws://} (plain)
+ * and {@code wss://} (TLS)
+ * @return the connect sampler for further configuration or usage
+ * @since 2.2
+ */
+ public static DslConnectSampler websocketConnect(String url) {
+ return new DslConnectSampler(url);
+ }
+
+ /**
+ * Creates WebSocket disconnect sampler to gracefully close the connection to
+ * the
+ *
+ * This sampler sends a close frame to the server and waits for the server's
+ * close frame response. It is recommended to always close connections
+ * explicitly to properly release resources.
+ *
+ * @return the disconnect sampler for further configuration or usage
+ * @since 2.2
+ */
+ public static DslDisconnectSampler websocketDisconnect() {
+ return new DslDisconnectSampler();
+ }
+
+ /**
+ * Creates a WebSocket write sampler to send a text message to the server.
+ *
+ * Requires an active WebSocket connection established via
+ * {@link #websocketConnect(String)}.
+ *
+ * @param requestData the message to send to the WebSocket server
+ * @return the write sampler for further configuration or usage
+ * @since 2.2
+ */
+ public static DslWriteSampler websocketWrite(String requestData) {
+ return new DslWriteSampler(requestData);
+ }
+
+ /**
+ * Creates a WebSocket read sampler to receive a message from the server.
+ *
+ * By default, this sampler blocks execution until a response is received or
+ * the timeout is reached. This behavior can be changed using
+ * {@link DslReadSampler#waitForResponse(boolean)}.
+ *
+ * Requires an active WebSocket connection established via
+ * {@link #websocketConnect(String)}.
+ *
+ * @return the read sampler for further configuration or usage
+ * @since 2.2
+ */
+ public static DslReadSampler websocketRead() {
+ return new DslReadSampler();
+ }
+
+ public static class DslConnectSampler extends BaseSampler {
+ private String connectionTimeoutMillis;
+ private String responseTimeoutMillis;
+ private String server;
+ private String port;
+ private String path;
+ private boolean tls = false;
+
+ private DslConnectSampler(String url) {
+ super("WebSocket Open Connection", OpenWebSocketSamplerGui.class);
+ try {
+ URI uri = new URI(url);
+
+ String scheme = uri.getScheme();
+ if (scheme == null || (!"ws".equals(scheme) && !"wss".equals(scheme))) {
+ throw new IllegalArgumentException(
+ "Invalid WebSocket URL. Must start with 'ws://' or 'wss://'");
+ }
+
+ this.tls = "wss".equals(scheme);
+ this.server = uri.getHost();
+ if (this.server == null) {
+ throw new IllegalArgumentException("Invalid WebSocket URL. Host is required");
+ }
+
+ int port = uri.getPort();
+ if (port == -1) {
+ this.port = this.tls ? "443" : "80";
+ } else {
+ this.port = String.valueOf(port);
+ }
+
+ String path = uri.getPath();
+ if (path == null || path.isEmpty()) {
+ this.path = "/";
+ } else {
+ this.path = path;
+ }
+
+ String query = uri.getQuery();
+ if (query != null && !query.isEmpty()) {
+ this.path = this.path + "?" + query;
+ }
+
+ } catch (URISyntaxException e) {
+ throw new IllegalArgumentException("Invalid WebSocket URL: " + url, e);
+ }
+ }
+
+ @Override
+ protected TestElement buildTestElement() {
+ OpenWebSocketSampler ret = new OpenWebSocketSampler();
+ if (connectionTimeoutMillis != null) {
+ ret.setConnectTimeout(connectionTimeoutMillis);
+ }
+ if (responseTimeoutMillis != null) {
+ ret.setReadTimeout(responseTimeoutMillis);
+ }
+ ret.setTLS(tls);
+ ret.setServer(server);
+ ret.setPort(port);
+ ret.setPath(path);
+ return ret;
+ }
+
+ /**
+ * Sets the connection timeout for the WebSocket connection creation.
+ *
+ * @param timeoutMillis the connection timeout in milliseconds (default value is
+ * 20000 milliseconds)
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslConnectSampler connectionTimeout(int timeoutMillis) {
+ this.connectionTimeoutMillis = String.valueOf(timeoutMillis);
+ return this;
+ }
+
+ /**
+ * Same as {@link #connectionTimeout(int)} but allowing to use JMeter
+ * expressions
+ * (variables or
+ * functions) to solve the actual parameter values.
+ *
+ * @param timeoutMillis a JMeter expression that returns timeout in milliseconds
+ * (default value is 20000 milliseconds)
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslConnectSampler connectionTimeout(String timeoutMillis) {
+ this.connectionTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Sets the response timeout for the WebSocket negotiation.
+ *
+ * @param timeoutMillis the response timeout in milliseconds (default value is
+ * 6000 milliseconds)
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslConnectSampler responseTimeout(int timeoutMillis) {
+ this.responseTimeoutMillis = String.valueOf(timeoutMillis);
+ return this;
+ }
+
+ /**
+ * Same as {@link #responseTimeout(int)} but allowing to use JMeter expressions
+ * (variables or functions) to solve the actual parameter values.
+ *
+ * @param timeoutMillis a JMeter expression that returns timeout in milliseconds
+ * (default value is 6000 milliseconds)
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslConnectSampler responseTimeout(String timeoutMillis) {
+ this.responseTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ public static class CodeBuilder extends SingleTestElementCallBuilder {
+
+ public CodeBuilder(List builderMethods) {
+ super(OpenWebSocketSampler.class, builderMethods);
+ }
+
+ @Override
+ protected MethodCall buildMethodCall(OpenWebSocketSampler testElement,
+ MethodCallContext context) {
+ TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement);
+ MethodParam server = paramBuilder.stringParam("server", "");
+ MethodParam port = paramBuilder.stringParam("port", "");
+ MethodParam path = paramBuilder.stringParam("path", "");
+ MethodParam tls = paramBuilder.boolParam("TLS", false);
+ String protocol = tls.getExpression().equals("true") ? "wss" : "ws";
+ String url = protocol + "://" + server.getExpression() + ":" + port.getExpression()
+ + path.getExpression();
+ return new MethodCall("websocketConnect", DslConnectSampler.class,
+ new StringParam(url))
+ .chain("connectionTimeout", paramBuilder.intParam("connectTimeout", 20000))
+ .chain("responseTimeout", paramBuilder.intParam("readTimeout", 6000));
+ }
+ }
+ }
+
+ public enum StatusCode implements EnumParam.EnumPropertyValue {
+ NORMAL_CLOSURE("1000"),
+ GOING_AWAY("1001"),
+ PROTOCOL_ERROR("1002"),
+ UNSUPPORTED_DATA("1003"),
+ NO_STATUS_CODE_PRESENT("1005"),
+ MESSAGE_TYPE_ERROR("1007"),
+ POLICY_VIOLATION("1008"),
+ MESSAGE_TOO_BIG_ERROR("1009"),
+ TLS_HANDSHAKE_ERROR("1015");
+
+ private final String propertyValue;
+
+ StatusCode(String propertyValue) {
+ this.propertyValue = propertyValue;
+ }
+
+ @Override
+ public String propertyValue() {
+ return propertyValue;
+ }
+
+ public static boolean isValidStatusCode(String value) {
+ return Arrays.stream(StatusCode.values())
+ .anyMatch(code -> code.propertyValue().equals(value));
+ }
+
+ }
+
+ public static class DslDisconnectSampler extends BaseSampler {
+ private String responseTimeoutMillis;
+ private String statusCode;
+
+ private DslDisconnectSampler() {
+ super("WebSocket Close", CloseWebSocketSamplerGui.class);
+ }
+
+ @Override
+ protected TestElement buildTestElement() {
+ CloseWebSocketSampler close = new CloseWebSocketSampler();
+ if (responseTimeoutMillis != null) {
+ close.setReadTimeout(responseTimeoutMillis);
+ }
+ if (statusCode != null) {
+ close.setStatusCode(statusCode);
+ }
+ return close;
+ }
+
+ /**
+ * Sets the response timeout for the close frame from server to be received.
+ *
+ * @param timeoutMillis the response timeout in milliseconds (default value is
+ * 6000 milliseconds)
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslDisconnectSampler responseTimeout(int timeoutMillis) {
+ this.responseTimeoutMillis = String.valueOf(timeoutMillis);
+ return this;
+ }
+
+ /**
+ * Same as {@link #responseTimeout(int)} but allowing to use JMeter expressions
+ * (variables or functions) to solve the actual parameter values.
+ *
+ * @param timeoutMillis a JMeter expression that returns timeout in milliseconds
+ * (default value is 6000 milliseconds)
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslDisconnectSampler responseTimeout(String timeoutMillis) {
+ this.responseTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Sets the status code to indicate the reason for closing the connection.
+ *
+ * Common status codes:
+ *
+ * - 1000 - Normal closure (default)
+ * - 1001 - Going away
+ * - 1002 - Protocol error
+ * - 1003 - Unsupported data
+ *
+ *
+ * For a complete list, see
+ * RFC
+ * 6455 Section 7.4
+ *
+ * @param statusCode the status code for the WebSocket disconnect
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslDisconnectSampler statusCode(StatusCode statusCode) {
+ this.statusCode = statusCode.propertyValue();
+ return this;
+ }
+
+ /**
+ * Same as {@link #statusCode(StatusCode)} but allowing to use JMeter
+ * expressions
+ * (variables or functions) to solve the actual parameter values.
+ *
+ * @param statusCode a JMeter expression that returns the status code for the
+ * WebSocket disconnect
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslDisconnectSampler statusCode(String statusCode) {
+ this.statusCode = statusCode;
+ return this;
+ }
+
+ public static class CodeBuilder extends SingleTestElementCallBuilder {
+
+ public CodeBuilder(List builderMethods) {
+ super(CloseWebSocketSampler.class, builderMethods);
+ }
+
+ @Override
+ protected MethodCall buildMethodCall(CloseWebSocketSampler testElement,
+ MethodCallContext context) {
+ TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement);
+ MethodParam statusCode = paramBuilder.stringParam("statusCode", "1000");
+ if (StatusCode.isValidStatusCode(statusCode.getExpression())) {
+ statusCode = paramBuilder.enumParam("statusCode", StatusCode.NORMAL_CLOSURE);
+ }
+ return new MethodCall("websocketDisconnect", DslDisconnectSampler.class)
+ .chain("responseTimeout", paramBuilder.intParam("readTimeout", 6000))
+ .chain("statusCode", statusCode);
+ }
+ }
+ }
+
+ public static class DslWriteSampler extends BaseSampler {
+ private String requestData;
+
+ private DslWriteSampler(String requestData) {
+ super("WebSocket Single Write", SingleWriteWebSocketSamplerGui.class);
+ this.requestData = requestData;
+ }
+
+ @Override
+ protected TestElement buildTestElement() {
+ SingleWriteWebSocketSampler write = new SingleWriteWebSocketSampler();
+ write.setType(DataPayloadType.Text);
+ write.setRequestData(requestData);
+ write.setCreateNewConnection(false);
+ return write;
+ }
+
+ public static class CodeBuilder
+ extends SingleTestElementCallBuilder {
+
+ public CodeBuilder(List builderMethods) {
+ super(SingleWriteWebSocketSampler.class, builderMethods);
+ }
+
+ @Override
+ protected MethodCall buildMethodCall(SingleWriteWebSocketSampler testElement,
+ MethodCallContext context) {
+ TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement);
+ MethodParam requestData = paramBuilder.stringParam("requestData", "");
+ return new MethodCall("websocketWrite", DslWriteSampler.class,
+ new StringParam(requestData.getExpression()));
+ }
+ }
+ }
+
+ public static class DslReadSampler extends BaseSampler {
+ private String responseTimeoutMillis;
+ private boolean waitForResponse = true;
+
+ private DslReadSampler() {
+ super("WebSocket Single Read", SingleReadWebSocketSamplerGui.class);
+ }
+
+ @Override
+ protected TestElement buildTestElement() {
+ SingleReadWebSocketSampler read = new SingleReadWebSocketSampler();
+ if (responseTimeoutMillis != null) {
+ read.setReadTimeout(responseTimeoutMillis);
+ }
+ read.setDataType(DataType.Text);
+ read.setOptional(!waitForResponse);
+ read.setCreateNewConnection(false);
+ return read;
+ }
+
+ /**
+ * Specifies whether the sampler should block execution until a response is
+ * received.
+ *
+ * When set to {@code true} (default), the sampler waits for a server response
+ * or until the timeout expires. When set to {@code false}, the sampler returns
+ * immediately if no message is available.
+ *
+ * @param waitForResponse {@code true} to block until response is received,
+ * {@code false} to return immediately
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslReadSampler waitForResponse(boolean waitForResponse) {
+ this.waitForResponse = waitForResponse;
+ return this;
+ }
+
+ /**
+ * Same as {@link #responseTimeout(int)} but allowing to use JMeter expressions
+ * (variables or functions) to solve the actual parameter values.
+ *
+ * @param timeoutMillis a JMeter expression that returns timeout in milliseconds
+ * (default value is 6000 milliseconds)
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslReadSampler responseTimeout(String timeoutMillis) {
+ this.responseTimeoutMillis = timeoutMillis;
+ return this;
+ }
+
+ /**
+ * Sets the response timeout for the WebSocket response to be received.
+ *
+ * @param timeoutMillis the response timeout in milliseconds (default value is
+ * 6000 milliseconds)
+ * @return the sampler for further configuration or usage
+ * @since 2.2
+ */
+ public DslReadSampler responseTimeout(int timeoutMillis) {
+ this.responseTimeoutMillis = String.valueOf(timeoutMillis);
+ return this;
+ }
+
+ public static class CodeBuilder
+ extends SingleTestElementCallBuilder {
+
+ public CodeBuilder(List builderMethods) {
+ super(SingleReadWebSocketSampler.class, builderMethods);
+ }
+
+ @Override
+ protected MethodCall buildMethodCall(SingleReadWebSocketSampler testElement,
+ MethodCallContext context) {
+ TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement);
+ boolean optionalParam = !paramBuilder.boolParam("optional", false)
+ .getExpression().equals("true");
+ return new MethodCall("websocketRead", DslReadSampler.class)
+ .chain("responseTimeout", paramBuilder.intParam("readTimeout", 6000))
+ .chain("waitForResponse", new BoolParam(optionalParam, true))
+ .chain("createNewConnection", paramBuilder.boolParam("createNewConnection", false));
+ }
+ }
+ }
+}
diff --git a/jmeter-java-dsl-websocket/src/test/java/DslWebsocketSamplerTest.java b/jmeter-java-dsl-websocket/src/test/java/DslWebsocketSamplerTest.java
new file mode 100644
index 00000000..a1122587
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/java/DslWebsocketSamplerTest.java
@@ -0,0 +1,86 @@
+import java.util.concurrent.TimeUnit;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.responseAssertion;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.testPlan;
+import static us.abstracta.jmeter.javadsl.JmeterDsl.threadGroup;
+import static us.abstracta.jmeter.javadsl.websocket.WebsocketJMeterDsl.*;
+
+import org.junit.jupiter.api.Test;
+import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+
+public class DslWebsocketSamplerTest {
+
+ @Test
+ public void shouldConnectAndEchoMessageWhenWebSocketTestPlanWithEchoServer() throws Exception {
+ WebSocketEchoServer echoServer = new WebSocketEchoServer(0);
+ echoServer.start();
+ echoServer.awaitStart(5, TimeUnit.SECONDS);
+ String wsUri = echoServer.getUri();
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ websocketConnect(wsUri),
+ websocketWrite("Hello WebSocket Test!"),
+ websocketRead()
+ .children(
+ responseAssertion()
+ .containsSubstrings("Hello WebSocket Test!")),
+ websocketDisconnect()))
+ .run();
+ assertThat(stats.overall().errorsCount()).isEqualTo(0);
+ }
+
+ @Test
+ public void shouldThrowIllegalArgumentExceptionWhenConnectWithInvalidUrl() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ websocketConnect("http://localhost:80/test");
+ });
+ }
+
+ @Test
+ public void shouldErrorSamplerWhenConnectToUnavailableServer() throws Exception {
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ websocketConnect("ws://localhost:9999/nonexistent")))
+ .run();
+ assertThat(stats.overall().errorsCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldErrorSamplerWhenConnectWithVeryShortTimeout() throws Exception {
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ websocketConnect("ws://localhost:8080/test")
+ .connectionTimeout(1)
+ .responseTimeout(1)))
+ .run();
+ assertThat(stats.overall().errorsCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldErrorSamplerWhenWriteOperationWhenNoPreviousConnection() throws Exception {
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ websocketWrite("Test message")))
+ .run();
+ assertThat(stats.overall().errorsCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldErrorSamplerWhenReadOperationWhenNoPreviousConnection() throws Exception {
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ websocketRead()))
+ .run();
+ assertThat(stats.overall().errorsCount()).isEqualTo(1);
+ }
+
+ @Test
+ public void shouldErrorSamplerWhenDisconnectOperationWhenNoPreviousConnection() throws Exception {
+ TestPlanStats stats = testPlan(
+ threadGroup(1, 1,
+ websocketDisconnect()))
+ .run();
+ assertThat(stats.overall().errorsCount()).isEqualTo(1);
+ }
+}
\ No newline at end of file
diff --git a/jmeter-java-dsl-websocket/src/test/java/WebSocketEchoServer.java b/jmeter-java-dsl-websocket/src/test/java/WebSocketEchoServer.java
new file mode 100644
index 00000000..6337ba4f
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/java/WebSocketEchoServer.java
@@ -0,0 +1,48 @@
+import java.net.InetSocketAddress;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.java_websocket.WebSocket;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.server.WebSocketServer;
+
+public class WebSocketEchoServer extends WebSocketServer {
+
+ private final CountDownLatch startLatch = new CountDownLatch(1);
+
+ public WebSocketEchoServer(int port) {
+ super(new InetSocketAddress(port));
+ }
+
+ @Override
+ public void onOpen(WebSocket conn, ClientHandshake handshake) {
+ }
+
+ @Override
+ public void onClose(WebSocket conn, int code, String reason, boolean remote) {
+ }
+
+ @Override
+ public void onMessage(WebSocket conn, String message) {
+ conn.send(message);
+ }
+
+ @Override
+ public void onError(WebSocket conn, Exception ex) {
+ ex.printStackTrace();
+ }
+
+ @Override
+ public void onStart() {
+ startLatch.countDown();
+ }
+
+ public void awaitStart(long timeout, TimeUnit unit) throws InterruptedException {
+ if (!startLatch.await(timeout, unit)) {
+ throw new RuntimeException("WebSocket server failed to start within timeout");
+ }
+ }
+
+ public String getUri() {
+ return "ws://localhost:" + getPort();
+ }
+}
\ No newline at end of file
diff --git a/jmeter-java-dsl-websocket/src/test/java/WebsocketCodeGeneratorTest.java b/jmeter-java-dsl-websocket/src/test/java/WebsocketCodeGeneratorTest.java
new file mode 100644
index 00000000..9dd3d1bd
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/java/WebsocketCodeGeneratorTest.java
@@ -0,0 +1,70 @@
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.Set;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import us.abstracta.jmeter.javadsl.codegeneration.DslCodeGenerator;
+import us.abstracta.jmeter.javadsl.codegeneration.TestClassTemplate;
+import us.abstracta.jmeter.javadsl.core.util.StringTemplate;
+import us.abstracta.jmeter.javadsl.util.TestResource;
+import us.abstracta.jmeter.javadsl.websocket.WebsocketJMeterDsl;
+
+public class WebsocketCodeGeneratorTest {
+
+ private static final String RESOURCES_FOLDER = "websocket-codegeneration";
+
+ @Test
+ public void shouldGenerateExpectedCodeWhenSimpleWebSocketJmxIsProvided(@TempDir Path tempDir)
+ throws Exception {
+ File solvedTemplate = solveTemplateResource("websocket-test-plan.template.jmx", tempDir);
+ assertThat(new DslCodeGenerator().addBuildersFrom(WebsocketJMeterDsl.class).generateCodeFromJmx(solvedTemplate))
+ .isEqualToNormalizingNewlines(
+ solveTestClassTemplate(Collections.emptySet(),
+ "SimpleWebSocketTest.java"));
+ }
+
+ @Test
+ public void shouldGenerateExpectedCodeWhenComplexWebSocketJmxIsProvided(@TempDir Path tempDir) throws Exception {
+ File solvedTemplate = solveTemplateResource("/complex-websocket.jmx", tempDir);
+ assertThat(new DslCodeGenerator().addBuildersFrom(WebsocketJMeterDsl.class).generateCodeFromJmx(solvedTemplate))
+ .isEqualToNormalizingNewlines(
+ solveTestClassTemplate(Collections.emptySet(),
+ "ComplexWebSocketTest.java"));
+ }
+
+ @Test
+ public void shouldGenerateExpectedCodeWhenWebSocketWithVariablesJmxIsProvided(@TempDir Path tempDir)
+ throws Exception {
+ assertThat(new DslCodeGenerator()
+
+ .addBuildersFrom(WebsocketJMeterDsl.class)
+ .addDependency(WebsocketJMeterDsl.class, "us.abstracta.jmeter:jmeter-java-dsl-websocket")
+ .generateCodeFromJmx(new TestResource(RESOURCES_FOLDER + "/websocket-with-variables.jmx").file()))
+ .isEqualToNormalizingNewlines(solveTestClassTemplate(Collections.emptySet(),
+ "WebSocketWithVariablesTest.java"));
+ }
+
+ private File solveTemplateResource(String resourcePath, Path tempDir) throws IOException {
+ String templateContents = new StringTemplate(new TestResource(RESOURCES_FOLDER + "/" + resourcePath).rawContents())
+ .solve();
+ Path solvedTemplate = tempDir.resolve("websocket-test-plan.jmx");
+ Files.write(solvedTemplate, templateContents.getBytes(StandardCharsets.UTF_8));
+ return solvedTemplate.toFile();
+ }
+
+ private String solveTestClassTemplate(Set imports, String testPlanCodeResource)
+ throws IOException {
+ return new TestClassTemplate()
+ .dependencies(Collections.singleton("us.abstracta.jmeter:jmeter-java-dsl"))
+ .imports(imports)
+ .testPlan(new TestResource(RESOURCES_FOLDER + "/" + testPlanCodeResource).rawContents())
+ .solve();
+ }
+
+}
diff --git a/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/ComplexWebSocketTest.java b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/ComplexWebSocketTest.java
new file mode 100644
index 00000000..48647b41
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/ComplexWebSocketTest.java
@@ -0,0 +1,14 @@
+testPlan(
+ threadGroup(1, 1,
+ websocketConnect("ws://echo.websocket.org:80/")
+ .connectionTimeout(15000)
+ .responseTimeout(10000),
+ websocketWrite("Hello from JMeter WebSocket Test"),
+ websocketRead()
+ .responseTimeout(10000)
+ .waitForResponse(false),
+ websocketDisconnect()
+ .responseTimeout(2000)
+ .statusCode("3000")
+ )
+ )
\ No newline at end of file
diff --git a/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/SimpleWebSocketTest.java b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/SimpleWebSocketTest.java
new file mode 100644
index 00000000..ea6f7bc2
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/SimpleWebSocketTest.java
@@ -0,0 +1,8 @@
+testPlan(
+ threadGroup(1, 1,
+ websocketConnect("ws://ws.postman-echo.com:80/raw"),
+ websocketWrite("Hello WebSocket Test!"),
+ websocketRead(),
+ websocketDisconnect()
+ )
+ )
\ No newline at end of file
diff --git a/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/WebSocketWithVariablesTest.java b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/WebSocketWithVariablesTest.java
new file mode 100644
index 00000000..7f46b58c
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/WebSocketWithVariablesTest.java
@@ -0,0 +1,13 @@
+testPlan(
+ threadGroup(1, 1,
+ websocketConnect("ws://${WEBSOCKET_SERVER}:${WEBSOCKET_PORT}/raw")
+ .connectionTimeout("${timeout}")
+ .responseTimeout("${timeout}"),
+ websocketWrite("${MESSAGE}"),
+ websocketRead()
+ .responseTimeout("${timeout}"),
+ websocketDisconnect()
+ .responseTimeout("${timeout}")
+ .statusCode("${statusCode}")
+ )
+ )
\ No newline at end of file
diff --git a/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/complex-websocket.jmx b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/complex-websocket.jmx
new file mode 100644
index 00000000..17ce5036
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/complex-websocket.jmx
@@ -0,0 +1,64 @@
+
+
+
+
+ true
+
+
+
+
+
+
+ 1
+ 1
+ true
+ continue
+
+ 1
+ false
+
+
+
+
+ 15000
+ 10000
+ false
+ echo.websocket.org
+ 80
+ /
+
+
+
+ 20000
+ Text
+ Hello from JMeter WebSocket Test
+ false
+ false
+
+ 80
+
+ false
+
+
+
+
+ 20000
+ 10000
+ Text
+ true
+ false
+ false
+
+ 80
+
+
+
+
+ 2000
+ 3000
+
+
+
+
+
+
diff --git a/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/websocket-test-plan.template.jmx b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/websocket-test-plan.template.jmx
new file mode 100644
index 00000000..7fede633
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/websocket-test-plan.template.jmx
@@ -0,0 +1,68 @@
+
+
+
+
+
+ false
+ true
+ false
+
+
+
+
+
+
+
+ 1
+ 1
+ true
+ continue
+
+ 1
+ false
+
+
+
+
+
+
+ false
+ ws.postman-echo.com
+ 80
+ /raw
+
+
+
+ 20000
+ Text
+ Hello WebSocket Test!
+ false
+ false
+
+ 80
+
+ false
+
+
+
+
+ 20000
+
+ Text
+ false
+ false
+ false
+
+ 80
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/websocket-with-variables.jmx b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/websocket-with-variables.jmx
new file mode 100644
index 00000000..776f6ad8
--- /dev/null
+++ b/jmeter-java-dsl-websocket/src/test/resources/websocket-codegeneration/websocket-with-variables.jmx
@@ -0,0 +1,82 @@
+
+
+
+
+ true
+
+
+
+ WEBSOCKET_SERVER
+ ws.postman-echo.com
+ =
+
+
+ WEBSOCKET_PORT
+ 80
+ =
+
+
+ MESSAGE
+ Hello from variable
+ =
+
+
+
+ false
+ false
+
+
+
+ 1
+ 1
+ true
+ continue
+
+ 1
+ false
+
+
+
+
+ ${timeout}
+ ${timeout}
+ false
+ ${WEBSOCKET_SERVER}
+ ${WEBSOCKET_PORT}
+ /raw
+
+
+
+ 20000
+ Text
+ ${MESSAGE}
+ false
+ false
+
+ 80
+
+ false
+
+
+
+
+ 20000
+ ${timeout}
+ Text
+ false
+ false
+ false
+
+ 80
+
+
+
+
+ ${timeout}
+ ${statusCode}
+
+
+
+
+
+
diff --git a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/FragmentMethodCall.java b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/FragmentMethodCall.java
index 85eba4c8..d0aceb5b 100644
--- a/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/FragmentMethodCall.java
+++ b/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/controllers/FragmentMethodCall.java
@@ -51,8 +51,8 @@ private static String solveMethodName(TestElement element, MethodCallContext con
}
private static Map getDefinedMethods(MethodCallContext context) {
- // Since JMeter 5.6.3, TestElement hash calculation changed from object identity
- // to property-based hashing. IdentityHashMap is used here to prevent hash collisions
+ // Since JMeter 5.6.3, TestElement hash calculation changed from object identity
+ // to property-based hashing. IdentityHashMap is used here to prevent hash collisions
// that occur when multiple TestElements share identical property values.
return context.getRoot().computeEntryIfAbsent(FragmentMethodCall.class, IdentityHashMap::new);
}
diff --git a/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/codegeneration/MethodCallBuilderTest.java b/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/codegeneration/MethodCallBuilderTest.java
index 5032b2bb..0c01d14a 100644
--- a/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/codegeneration/MethodCallBuilderTest.java
+++ b/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/codegeneration/MethodCallBuilderTest.java
@@ -68,7 +68,7 @@ private Map extractCodeBuilderTestCodes() {
.getMethods().stream()
.collect(
Collectors.toMap(NodeWithSimpleName::getNameAsString,
- m -> m.getBody().map(Objects::toString).orElse("").replace("\r\n", "\n")));
+ m -> m.getBody().map(Objects::toString).orElse("")));
}
@ParameterizedTest(name = "{0}")
diff --git a/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/core/StringTemplateAssert.java b/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/core/StringTemplateAssert.java
index 649be002..3ed8e133 100644
--- a/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/core/StringTemplateAssert.java
+++ b/jmeter-java-dsl/src/test/java/us/abstracta/jmeter/javadsl/core/StringTemplateAssert.java
@@ -43,7 +43,7 @@ public static StringTemplateAssertString assertThat(String actual) {
protected abstract ErrorMessageFactory getErrorMessageFactory(List> diffs);
public SELF matches(TestResource template) throws IOException {
- return matches(template.rawContents().replace("\r\n", "\n"));
+ return matches(template.rawContents());
}
public SELF matches(String templateContents) throws IOException {
diff --git a/pom.xml b/pom.xml
index 105ad5fa..ee9121e4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -72,6 +72,7 @@
jmeter-java-dsl-datadog
jmeter-java-dsl-bridge
jmeter-java-dsl-prometheus
+ jmeter-java-dsl-websocket