diff --git a/jmeter-java-dsl-cli/pom.xml b/jmeter-java-dsl-cli/pom.xml index 67b13a00..a5a4cc5d 100644 --- a/jmeter-java-dsl-cli/pom.xml +++ b/jmeter-java-dsl-cli/pom.xml @@ -70,6 +70,11 @@ jmeter-java-dsl-websocket ${project.version} + + us.abstracta.jmeter + jmeter-java-dsl-http2 + ${project.version} + org.slf4j jul-to-slf4j @@ -180,6 +185,7 @@ org.seleniumhq.selenium:* com.google.guava:guava com.blazemeter:jmeter-bzm-correlation-recorder + com.blazemeter:jmeter-bzm-http2 com.fasterxml.jackson.core:jackson-annotations org.apache.commons:commons-text dev.failsafe:failsafe diff --git a/jmeter-java-dsl-cli/src/main/java/us/abstracta/jmeter/javadsl/cli/Jmx2DslCommand.java b/jmeter-java-dsl-cli/src/main/java/us/abstracta/jmeter/javadsl/cli/Jmx2DslCommand.java index 744f14be..6b8275d5 100644 --- a/jmeter-java-dsl-cli/src/main/java/us/abstracta/jmeter/javadsl/cli/Jmx2DslCommand.java +++ b/jmeter-java-dsl-cli/src/main/java/us/abstracta/jmeter/javadsl/cli/Jmx2DslCommand.java @@ -9,6 +9,7 @@ import us.abstracta.jmeter.javadsl.datadog.DatadogBackendListener; import us.abstracta.jmeter.javadsl.elasticsearch.listener.ElasticsearchBackendListener; import us.abstracta.jmeter.javadsl.graphql.DslGraphqlSampler; +import us.abstracta.jmeter.javadsl.http2.Http2JmeterDsl; import us.abstracta.jmeter.javadsl.jdbc.JdbcJmeterDsl; import us.abstracta.jmeter.javadsl.parallel.ParallelController; import us.abstracta.jmeter.javadsl.websocket.WebsocketJMeterDsl; @@ -50,6 +51,7 @@ public Integer call() throws Exception { codeGenerator); addBuildersFrom(DatadogBackendListener.class, "jmeter-java-dsl-datadog", codeGenerator); addBuildersFrom(WebsocketJMeterDsl.class, "jmeter-java-dsl-websocket", codeGenerator); + addBuildersFrom(Http2JmeterDsl.class, "jmeter-java-dsl-http2", codeGenerator); System.out.println(codeGenerator.generateCodeFromJmx(jmxFile)); return 0; } diff --git a/jmeter-java-dsl-http2/README.md b/jmeter-java-dsl-http2/README.md new file mode 100644 index 00000000..2d941613 --- /dev/null +++ b/jmeter-java-dsl-http2/README.md @@ -0,0 +1,52 @@ +# jmeter-java-dsl-http2 + +Module for [BlazeMeter HTTP plugin](https://github.com/Blazemeter/jmeter-http2-plugin) support (HTTP/1.1, HTTP/2, HTTP/3). + +## Requirements + +- Java 17+ at runtime (required by the BlazeMeter HTTP plugin) +- BlazeMeter HTTP plugin JAR on the classpath + +## Plugin dependency setup + +Version `3.2.0` of `com.blazemeter:jmeter-bzm-http2` is not published on Maven Central. Install it locally before building: + +```bash +curl -L -o jmeter-bzm-http2-3.2.0.jar \ + https://github.com/Blazemeter/jmeter-http2-plugin/releases/download/v3.2.0/jmeter-bzm-http2-3.2.0.jar + +mvn install:install-file \ + -Dfile=jmeter-bzm-http2-3.2.0.jar \ + -DgroupId=com.blazemeter \ + -DartifactId=jmeter-bzm-http2 \ + -Dversion=3.2.0 \ + -Dpackaging=jar +``` + +## Usage + +```java +import static us.abstracta.jmeter.javadsl.JmeterDsl.*; +import static us.abstracta.jmeter.javadsl.http2.Http2JmeterDsl.*; + +testPlan( + threadGroup(1, 1, + http2Sampler("https://example.com") + .http2Only(), + httpAsyncController( + http2Sampler("https://api.example.com/a"), + http2Sampler("https://api.example.com/b") + ) + ) +).run(); +``` + +## Maven dependency + +```xml + + us.abstracta.jmeter + jmeter-java-dsl-http2 + 2.3-SNAPSHOT + +``` diff --git a/jmeter-java-dsl-http2/pom.xml b/jmeter-java-dsl-http2/pom.xml new file mode 100644 index 00000000..bfa6d59d --- /dev/null +++ b/jmeter-java-dsl-http2/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + + us.abstracta.jmeter + jmeter-java-dsl-parent + 2.3-SNAPSHOT + ../pom.xml + + + jmeter-java-dsl-http2 + ${project.artifactId} + Module which includes BlazeMeter HTTP plugin for HTTP/2 and HTTP/3 support. + + + 3.2.0 + + 17 + 17 + + + + + us.abstracta.jmeter + jmeter-java-dsl + ${project.version} + + + com.blazemeter + jmeter-bzm-http2 + ${blazemeter-http2.version} + + + us.abstracta.jmeter + jmeter-java-dsl + test-jar + ${project.version} + test + + + javax.servlet + javax.servlet-api + 4.0.1 + test + + + + diff --git a/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/DslHttp2Sampler.java b/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/DslHttp2Sampler.java new file mode 100644 index 00000000..5cd63ac8 --- /dev/null +++ b/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/DslHttp2Sampler.java @@ -0,0 +1,379 @@ +package us.abstracta.jmeter.javadsl.http2; + +import com.blazemeter.jmeter.http2.sampler.HTTP2Sampler; +import com.blazemeter.jmeter.http2.sampler.gui.HTTP2SamplerGui; +import java.lang.reflect.Method; +import java.util.List; +import org.apache.jmeter.config.Arguments; +import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase; +import org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy; +import org.apache.jmeter.protocol.http.util.HTTPArgument; +import org.apache.jmeter.testelement.TestElement; +import org.apache.jmeter.util.JMeterUtils; +import org.apache.jorphan.collections.HashTree; +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.EnumPropertyValue; +import us.abstracta.jmeter.javadsl.core.BuildTreeContext; +import us.abstracta.jmeter.javadsl.core.testelements.BaseTestElement; +import us.abstracta.jmeter.javadsl.http.DslBaseHttpSampler; +import us.abstracta.jmeter.javadsl.http.DslCacheManager; +import us.abstracta.jmeter.javadsl.http.DslCookieManager; +import us.abstracta.jmeter.javadsl.http.DslHttpSampler; +import us.abstracta.jmeter.javadsl.http.HttpElementHelper; + +/** + * Allows configuring a BlazeMeter HTTP sampler with support for HTTP/1.1, HTTP/2, and HTTP/3. + *

+ * This sampler uses the + * BlazeMeter HTTP plugin and + * requires Java 17+ at runtime. It supports the same HTTP request features as + * {@link us.abstracta.jmeter.javadsl.http.DslHttpSampler} (headers, body, cookies, cache, etc.) + * plus multi-protocol client configuration. + * + * @since 2.3 + */ +public class DslHttp2Sampler extends DslHttpSampler { + + private static final String DEFAULT_NAME = "bzm - HTTP Sampler"; + private static final String PROFILE_PROPERTY = "HTTP2Sampler.profile"; + + protected ClientProfile clientProfile; + protected Boolean enableHttp3; + protected Boolean enableHttp2; + protected Boolean enableHttp1; + protected Boolean alpnEnabled; + protected Boolean automaticFallback; + protected Boolean protocolErrorFallback; + protected Boolean h2cUpgrade; + protected Boolean http2PriorKnowledge; + + public DslHttp2Sampler(String name, String url) { + super(name != null ? name : DEFAULT_NAME, url); + } + + /** + * Specifies the client profile which defines default protocol negotiation behavior. + * + * @param profile the client profile to use. When none is specified, the plugin default + * ({@link ClientProfile#BROWSER_LIKE}) is used. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler clientProfile(ClientProfile profile) { + this.clientProfile = profile; + return this; + } + + /** + * Enables or disables HTTP/3 (QUIC) support for this sampler. + * + * @param enable when {@code true}, allows HTTP/3 when the origin advertises it via Alt-Svc. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler enableHttp3(boolean enable) { + this.enableHttp3 = enable; + return this; + } + + /** + * Enables or disables HTTP/2 support for this sampler. + * + * @param enable when {@code true}, allows HTTP/2 negotiation via ALPN (HTTPS) or h2c (HTTP). + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler enableHttp2(boolean enable) { + this.enableHttp2 = enable; + return this; + } + + /** + * Enables or disables HTTP/1.1 support for this sampler. + * + * @param enable when {@code true}, allows HTTP/1.1 requests and fallback. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler enableHttp1(boolean enable) { + this.enableHttp1 = enable; + return this; + } + + /** + * Enables or disables TLS ALPN for HTTPS connections. + * + * @param enable when {@code true}, enables ALPN for HTTP/1.1 and HTTP/2 protocol negotiation. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler alpnEnabled(boolean enable) { + this.alpnEnabled = enable; + return this; + } + + /** + * Enables or disables automatic fallback between protocols (e.g. HTTP/3 to HTTP/2 to HTTP/1.1). + * + * @param enable when {@code true}, enables automatic protocol fallback. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler automaticFallback(boolean enable) { + this.automaticFallback = enable; + return this; + } + + /** + * Enables or disables fallback to HTTP/1.1 when an HTTP/2 {@code protocol_error} occurs. + * + * @param enable when {@code true}, retries with HTTP/1.1 on protocol errors. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler protocolErrorFallback(boolean enable) { + this.protocolErrorFallback = enable; + return this; + } + + /** + * Enables HTTP/1.1 to HTTP/2 cleartext (h2c) upgrade for {@code http://} origins. + * + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler h2cUpgrade() { + return h2cUpgrade(true); + } + + /** + * Same as {@link #h2cUpgrade()} but allowing to enable or disable the setting. + * + * @param enable when {@code true}, enables h2c upgrade negotiation. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler h2cUpgrade(boolean enable) { + this.h2cUpgrade = enable; + return this; + } + + /** + * Forces HTTP/2 prior knowledge (h2c) for cleartext origins, skipping the upgrade negotiation. + *

+ * Use only when the server is known to support h2c prior knowledge. + * + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler http2PriorKnowledge() { + return http2PriorKnowledge(true); + } + + /** + * Same as {@link #http2PriorKnowledge()} but allowing to enable or disable the setting. + * + * @param enable when {@code true}, forces h2c prior knowledge. + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler http2PriorKnowledge(boolean enable) { + this.http2PriorKnowledge = enable; + return this; + } + + /** + * Configures the sampler to use HTTP/2 only, disabling HTTP/1.1 and HTTP/3. + *

+ * For {@code https://} origins, ALPN is enabled to negotiate HTTP/2 over TLS. + * + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler http2Only() { + return clientProfile(ClientProfile.BROWSER_LIKE_CUSTOM) + .enableHttp1(false) + .enableHttp2(true) + .enableHttp3(false) + .alpnEnabled(true) + .automaticFallback(false); + } + + /** + * Configures the sampler to use HTTP/1.1 only, disabling HTTP/2 and HTTP/3. + * + * @return the sampler for further configuration or usage. + */ + public DslHttp2Sampler http1Only() { + return clientProfile(ClientProfile.BROWSER_LIKE_CUSTOM) + .enableHttp1(true) + .enableHttp2(false) + .enableHttp3(false); + } + + @Override + protected TestElement buildConfiguredTestElement() { + TestElement ret = buildTestElement(); + return BaseTestElement.configureTestElement(ret, name, HTTP2SamplerGui.class); + } + + @Override + protected TestElement buildTestElement() { + if (JMeterUtils.getProperty(DslBaseHttpSampler.RESET_CONNECTIONS_BETWEEN_ITERATIONS_PROP) + == null) { + JMeterUtils.setProperty(DslBaseHttpSampler.RESET_CONNECTIONS_BETWEEN_ITERATIONS_PROP, + String.valueOf(false)); + } + HTTP2Sampler ret = new HTTP2Sampler(); + HttpElementHelper.modifyTestElementUrl(ret, protocol, host, port, path); + HttpElementHelper.modifyTestElementTimeouts(ret, connectionTimeout, responseTimeout); + HttpElementHelper.modifyTestElementProxy(ret, proxyUrl, proxyUser, proxyPassword); + configureHttp2Sampler(ret); + return ret; + } + + private void configureHttp2Sampler(HTTP2Sampler elem) { + HTTPSamplerProxy proxy = configureHttpTestElement(new HTTPSamplerProxy()); + copyHttpSamplerProperties(proxy, elem); + if (clientProfile != null) { + elem.setProfile(clientProfile.propertyValue); + } + if (enableHttp3 != null) { + elem.setEnableHttp3(enableHttp3); + } + if (enableHttp2 != null) { + elem.setEnableHttp2(enableHttp2); + } + if (enableHttp1 != null) { + elem.setEnableHttp1(enableHttp1); + } + if (alpnEnabled != null) { + elem.setAlpnEnabled(alpnEnabled); + } + if (automaticFallback != null) { + elem.setFallbackEnabled(automaticFallback); + } + if (protocolErrorFallback != null) { + elem.setProtocolErrorFallbackEnabled(protocolErrorFallback); + } + if (h2cUpgrade != null) { + elem.setHttp1UpgradeEnabled(h2cUpgrade); + } + if (http2PriorKnowledge != null) { + elem.setHttp2PriorKnowledgeEnabled(http2PriorKnowledge); + } + } + + private void copyHttpSamplerProperties(HTTPSamplerBase source, HTTP2Sampler target) { + target.setMethod(source.getMethod()); + target.setArguments(cloneArguments(source.getArguments())); + target.setDoMultipart(source.getUseMultipart()); + target.setHTTPFiles(source.getHTTPFiles()); + if (source.getContentEncoding() != null) { + target.setContentEncoding(source.getContentEncoding()); + } + target.setFollowRedirects(source.getFollowRedirects()); + target.setUseKeepAlive(source.getUseKeepAlive()); + if (source.getPropertyAsBoolean(HTTPSamplerBase.IMAGE_PARSER, false)) { + HttpElementHelper.modifyTestElementEmbeddedResources(target, true, + source.getPropertyAsString(HTTPSamplerBase.EMBEDDED_URL_RE), + source.getPropertyAsString(HTTPSamplerBase.EMBEDDED_URL_EXCLUDE_RE)); + } + } + + private Arguments cloneArguments(Arguments args) { + Arguments ret = new Arguments(); + for (int i = 0; i < args.getArgumentCount(); i++) { + ret.addArgument((HTTPArgument) args.getArgument(i).clone()); + } + return ret; + } + + @Override + public HashTree buildTreeUnder(HashTree parent, BuildTreeContext context) { + files.forEach(f -> f.setPath(context.processAssetFile(f.getPath()))); + if (path == null && urlBuilder != null) { + path = urlBuilder.build(); + } + HashTree ret = parent.add(buildConfiguredTestElement()); + if (!headers.isEmpty()) { + context.buildChild(headers, ret); + } + new DslCookieManager().registerDependency(context); + new DslCacheManager().registerDependency(context); + return ret; + } + + /** + * Defines client profiles that bundle default protocol negotiation settings. + */ + public enum ClientProfile implements EnumPropertyValue { + /** + * Browser-like profile with HTTP/3, HTTP/2, HTTP/1.1 and automatic fallback enabled. + */ + BROWSER_LIKE("browser-like"), + /** + * Browser-compatible profile for less common browsers. + */ + BROWSER_COMPATIBLE("browser-compatible"), + /** + * Legacy profile oriented to older systems. + */ + LEGACY("legacy"), + /** + * Custom profile where per-sampler protocol toggles are used. + */ + BROWSER_LIKE_CUSTOM("browser-like-custom"); + + public final String propertyValue; + + ClientProfile(String propertyValue) { + this.propertyValue = propertyValue; + } + + @Override + public String propertyValue() { + return propertyValue; + } + } + + public static class CodeBuilder extends SingleTestElementCallBuilder { + + public CodeBuilder(List builderMethods) { + super(HTTP2Sampler.class, builderMethods); + } + + @Override + protected MethodCall buildMethodCall(HTTP2Sampler testElement, MethodCallContext context) { + TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement); + MethodParam name = paramBuilder.nameParam(DEFAULT_NAME); + MethodParam protocol = paramBuilder.stringParam(HTTPSamplerBase.PROTOCOL); + MethodParam domain = paramBuilder.stringParam(HTTPSamplerBase.DOMAIN); + MethodParam port = paramBuilder.intParam(HTTPSamplerBase.PORT); + MethodParam path = paramBuilder.stringParam(HTTPSamplerBase.PATH, "/"); + MethodParam url = HttpElementHelper.buildUrlParam(protocol, domain, port, path); + MethodCall ret = buildMethodCall(name, url); + context.findBuilder(DslCacheManager.CodeBuilder.class).registerDependency(context, ret); + context.findBuilder(DslCookieManager.CodeBuilder.class).registerDependency(context, ret); + if (url.equals(path)) { + ret.chain("protocol", protocol) + .chain("host", domain) + .chain("port", port); + } + ret.chain("clientProfile", + paramBuilder.enumParam(PROFILE_PROPERTY, ClientProfile.BROWSER_LIKE)); + chainOptionalBool(ret, testElement.getEnableHttp3(), "enableHttp3"); + chainOptionalBool(ret, testElement.getEnableHttp2(), "enableHttp2"); + chainOptionalBool(ret, testElement.getEnableHttp1(), "enableHttp1"); + chainOptionalBool(ret, testElement.getAlpnEnabled(), "alpnEnabled"); + chainOptionalBool(ret, testElement.getFallbackEnabled(), "automaticFallback"); + chainOptionalBool(ret, testElement.getProtocolErrorFallbackEnabled(), + "protocolErrorFallback"); + if (testElement.isHttp1UpgradeEnabled()) { + ret.chain("h2cUpgrade", new BoolParam(true, false)); + } + chainOptionalBool(ret, testElement.getHttp2PriorKnowledgeEnabled(), "http2PriorKnowledge"); + return ret; + } + + private void chainOptionalBool(MethodCall ret, Boolean value, String methodName) { + if (value != null) { + ret.chain(methodName, new BoolParam(value, null)); + } + } + } + +} diff --git a/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/DslHttpAsyncController.java b/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/DslHttpAsyncController.java new file mode 100644 index 00000000..a6db38e3 --- /dev/null +++ b/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/DslHttpAsyncController.java @@ -0,0 +1,173 @@ +package us.abstracta.jmeter.javadsl.http2; + +import com.blazemeter.jmeter.http2.control.HTTP2Controller; +import com.blazemeter.jmeter.http2.control.gui.HTTP2ControllerGUI; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +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.SingleTestElementCallBuilder; +import us.abstracta.jmeter.javadsl.codegeneration.TestElementParamBuilder; +import us.abstracta.jmeter.javadsl.codegeneration.params.ChildrenParam; +import us.abstracta.jmeter.javadsl.core.controllers.BaseController; +import us.abstracta.jmeter.javadsl.core.threadgroups.BaseThreadGroup.ThreadGroupChild; + +/** + * Allows grouping BlazeMeter HTTP samplers that need to execute concurrently within a thread + * iteration. + *

+ * This element uses the + * BlazeMeter HTTP plugin + * {@code bzm - HTTP Async Controller}. Check the plugin documentation for more details. + *

+ * By default, this element does not generate a parent sample result. Check provided methods to + * change this behavior. + * + * @since 2.3 + */ +public class DslHttpAsyncController extends BaseController { + + public static final String DEFAULT_NAME = "bzm - HTTP Async Controller"; + private static final String GENERATE_PARENT_SAMPLE_PROPERTY = + "blazemeter.http.controller.generateParentSample"; + private static final String LIMIT_MAX_PARALLEL_PROPERTY = + "blazemeter.http.controller.limitMaxParallel"; + private static final String MAX_CONCURRENT_PROPERTY = + "blazemeter.http.controller.maxConcurrentAsyncInController"; + private static final String INCLUDE_TIMERS_PROPERTY = "TransactionController.includeTimers"; + + protected boolean generateParentSample; + protected boolean limitMaxParallel; + protected Integer maxParallelExecutions; + protected boolean includeTimers; + + public DslHttpAsyncController(String name, List children) { + super(name == null ? DEFAULT_NAME : name, HTTP2ControllerGUI.class, children); + } + + /** + * Builds an HTTP Async Controller executing the given children elements concurrently. + * + * @param children test elements to execute concurrently. + * @return the controller for further configuration or usage. + */ + public static DslHttpAsyncController httpAsyncController(ThreadGroupChild... children) { + return httpAsyncController(null, children); + } + + /** + * Same as {@link #httpAsyncController(ThreadGroupChild...)} but allowing to set a name on the + * controller. + * + * @param name is the label assigned to the controller in collected metrics when + * {@link #generateParentSample()} is used. + * @param children test elements to execute concurrently. + * @return the controller for further configuration or usage. + */ + public static DslHttpAsyncController httpAsyncController(String name, + ThreadGroupChild... children) { + return new DslHttpAsyncController(name, Arrays.asList(children)); + } + + /** + * Same as {@link #httpAsyncController(ThreadGroupChild...)} but postponing children specification + * to allow specifying additional settings first. + * + * @return the controller for further configuration or usage. + */ + public static DslHttpAsyncController httpAsyncController() { + return httpAsyncController((String) null); + } + + /** + * Same as {@link #httpAsyncController(String, ThreadGroupChild...)} but postponing children + * specification to allow specifying additional settings first. + * + * @return the controller for further configuration or usage. + */ + public static DslHttpAsyncController httpAsyncController(String name) { + return new DslHttpAsyncController(name, Collections.emptyList()); + } + + /** + * Specifies whether to generate a sample result containing children elements results as sub + * results. + * + * @return the controller for further configuration or usage. + */ + public DslHttpAsyncController generateParentSample() { + return generateParentSample(true); + } + + /** + * Same as {@link #generateParentSample()} but allowing to enable and disable the setting. + * + * @param enable specifies to enable or disable the setting. By default, it is set to false. + * @return the controller for further configuration or usage. + */ + public DslHttpAsyncController generateParentSample(boolean enable) { + this.generateParentSample = enable; + return this; + } + + /** + * Enables limiting the number of concurrent BlazeMeter HTTP sampler executions. + * + * @param maxParallelExecutions maximum number of concurrent executions. Must be at least 1. + * @return the controller for further configuration or usage. + */ + public DslHttpAsyncController maxParallelExecutions(int maxParallelExecutions) { + this.limitMaxParallel = true; + this.maxParallelExecutions = maxParallelExecutions; + return this; + } + + /** + * Specifies whether to include timer and pre/post-processor duration in the generated parent + * sample. Only applies when {@link #generateParentSample()} is enabled. + * + * @param enable when {@code true}, includes timer and pre/post-processor time in parent sample. + * @return the controller for further configuration or usage. + */ + public DslHttpAsyncController includeTimers(boolean enable) { + this.includeTimers = enable; + return this; + } + + @Override + protected TestElement buildTestElement() { + HTTP2Controller ret = new HTTP2Controller(); + ret.setGenerateParentSample(generateParentSample); + ret.setLimitMaxParallel(limitMaxParallel); + if (maxParallelExecutions != null) { + ret.setMaxConcurrentAsyncInController(maxParallelExecutions); + } + ret.setIncludeTimers(includeTimers); + return ret; + } + + public static class CodeBuilder extends SingleTestElementCallBuilder { + + public CodeBuilder(List builderMethods) { + super(HTTP2Controller.class, builderMethods); + } + + @Override + protected MethodCall buildMethodCall(HTTP2Controller testElement, MethodCallContext context) { + TestElementParamBuilder paramBuilder = new TestElementParamBuilder(testElement); + MethodCall ret = buildMethodCall(paramBuilder.nameParam(DEFAULT_NAME), + new ChildrenParam<>(ThreadGroupChild[].class)) + .chain("generateParentSample", + paramBuilder.boolParam(GENERATE_PARENT_SAMPLE_PROPERTY, false)); + if (!paramBuilder.boolParam(LIMIT_MAX_PARALLEL_PROPERTY, false).isDefault()) { + ret.chain("maxParallelExecutions", paramBuilder.intParam(MAX_CONCURRENT_PROPERTY)); + } + ret.chain("includeTimers", paramBuilder.boolParam(INCLUDE_TIMERS_PROPERTY, false)); + return ret; + } + } + +} diff --git a/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/Http2JmeterDsl.java b/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/Http2JmeterDsl.java new file mode 100644 index 00000000..8acfd45b --- /dev/null +++ b/jmeter-java-dsl-http2/src/main/java/us/abstracta/jmeter/javadsl/http2/Http2JmeterDsl.java @@ -0,0 +1,90 @@ +package us.abstracta.jmeter.javadsl.http2; + +import us.abstracta.jmeter.javadsl.core.threadgroups.BaseThreadGroup.ThreadGroupChild; + +/** + * Provides factory methods for HTTP/2 samplers and controllers. + *

+ * This module uses the + * BlazeMeter HTTP plugin. + * It requires Java 17+ at runtime. + *

+ * Example usage: + * + *

{@code
+ * import static us.abstracta.jmeter.javadsl.JmeterDsl.*;
+ * import static us.abstracta.jmeter.javadsl.http2.Http2JmeterDsl.*;
+ * import us.abstracta.jmeter.javadsl.core.TestPlanStats;
+ *
+ * public class Test {
+ *
+ *   public static void main(String[] args) throws Exception {
+ *     TestPlanStats stats = testPlan(
+ *         threadGroup(1, 1,
+ *             http2Sampler("https://example.com")
+ *                 .http2Only()
+ *         )
+ *     ).run();
+ *   }
+ * }
+ * }
+ * + * @since 2.3 + */ +public class Http2JmeterDsl { + + private Http2JmeterDsl() { + } + + /** + * Builds an HTTP/2 sampler pointing to the given URL. + *

+ * By default, the sampler uses the {@link DslHttp2Sampler.ClientProfile#BROWSER_LIKE} + * profile which enables HTTP/3, HTTP/2, and HTTP/1.1 with automatic fallback. + * + * @param url specifies URL to send HTTP requests to (e.g. {@code https://example.com/path}). + * @return the sampler for further configuration or usage. + * @see DslHttp2Sampler + */ + public static DslHttp2Sampler http2Sampler(String url) { + return http2Sampler(null, url); + } + + /** + * Same as {@link #http2Sampler(String)} but allowing to set a name on the sampler. + * + * @param name is the label assigned to the sampler in collected metrics. + * @param url specifies URL to send HTTP requests to. + * @return the sampler for further configuration or usage. + * @see #http2Sampler(String) + */ + public static DslHttp2Sampler http2Sampler(String name, String url) { + return new DslHttp2Sampler(name, url); + } + + /** + * Builds an HTTP Async Controller to run HTTP/2 samplers concurrently within a thread + * iteration. + * + * @param children test elements to execute concurrently. + * @return the controller for further configuration or usage. + * @see DslHttpAsyncController + */ + public static DslHttpAsyncController httpAsyncController(ThreadGroupChild... children) { + return DslHttpAsyncController.httpAsyncController(children); + } + + /** + * Same as {@link #httpAsyncController(ThreadGroupChild...)} but allowing to set a name on the + * controller. + * + * @param name is the label assigned to the controller in collected metrics. + * @param children test elements to execute concurrently. + * @return the controller for further configuration or usage. + */ + public static DslHttpAsyncController httpAsyncController(String name, + ThreadGroupChild... children) { + return DslHttpAsyncController.httpAsyncController(name, children); + } + +} diff --git a/jmeter-java-dsl-http2/src/test/java/DslHttp2SamplerTest.java b/jmeter-java-dsl-http2/src/test/java/DslHttp2SamplerTest.java new file mode 100644 index 00000000..b469df09 --- /dev/null +++ b/jmeter-java-dsl-http2/src/test/java/DslHttp2SamplerTest.java @@ -0,0 +1,48 @@ +import static org.assertj.core.api.Assertions.assertThat; +import static us.abstracta.jmeter.javadsl.JmeterDsl.testPlan; +import static us.abstracta.jmeter.javadsl.JmeterDsl.threadGroup; +import static us.abstracta.jmeter.javadsl.http2.Http2JmeterDsl.http2Sampler; + +import org.apache.http.entity.ContentType; +import org.junit.jupiter.api.Test; +import us.abstracta.jmeter.javadsl.JmeterDslTest; +import us.abstracta.jmeter.javadsl.core.TestPlanStats; + +public class DslHttp2SamplerTest extends JmeterDslTest { + + @Test + public void shouldSendHttpRequestWhenHttp2Sampler() throws Exception { + TestPlanStats stats = testPlan( + threadGroup(1, 1, + http2Sampler(wiremockUri) + ) + ).run(); + assertThat(stats.overall().samplesCount()).isEqualTo(1); + assertThat(stats.overall().errorsCount()).isZero(); + } + + @Test + public void shouldSendPostWhenHttp2SamplerWithPost() throws Exception { + TestPlanStats stats = testPlan( + threadGroup(1, 1, + http2Sampler(wiremockUri) + .post(JSON_BODY, ContentType.APPLICATION_JSON) + ) + ).run(); + assertThat(stats.overall().samplesCount()).isEqualTo(1); + assertThat(stats.overall().errorsCount()).isZero(); + } + + @Test + public void shouldUseHttp2OnlyProfileWhenHttp2Only() throws Exception { + TestPlanStats stats = testPlan( + threadGroup(1, 1, + http2Sampler(wiremockUri) + .http2Only() + ) + ).run(); + assertThat(stats.overall().samplesCount()).isEqualTo(1); + assertThat(stats.overall().errorsCount()).isZero(); + } + +} diff --git a/jmeter-java-dsl-http2/src/test/resources/jtls/custom-sample-jtl.xml b/jmeter-java-dsl-http2/src/test/resources/jtls/custom-sample-jtl.xml new file mode 100644 index 00000000..28007e9a --- /dev/null +++ b/jmeter-java-dsl-http2/src/test/resources/jtls/custom-sample-jtl.xml @@ -0,0 +1,7 @@ + + + + Tested + + + diff --git a/pom.xml b/pom.xml index 9a35d118..7f387616 100644 --- a/pom.xml +++ b/pom.xml @@ -72,6 +72,7 @@ jmeter-java-dsl-bridge jmeter-java-dsl-prometheus jmeter-java-dsl-websocket + jmeter-java-dsl-http2