Skip to content

Commit 1ed58b7

Browse files
Add support for specifying default follow redirections in HttpDefaults and use it in recorded test plans
1 parent 6862dec commit 1ed58b7

File tree

4 files changed

+244
-46
lines changed

4 files changed

+244
-46
lines changed

jmeter-java-dsl-recorder/src/test/resources/RecordedTestPlan.template.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
testPlan(
22
threadGroup(1, 1,
33
httpDefaults()
4-
.encoding(StandardCharsets.UTF_8),
4+
.encoding(StandardCharsets.UTF_8)
5+
.followRedirects(false),
56
httpSampler("/-1", "http://localhost:{{\d+}}"),
67
httpSampler("/home-{{\d+}}", "http://localhost:{{\d+}}/home")
78
.children(
@@ -11,7 +12,7 @@
1112
httpSampler("/cart-{{\d+}}", "http://localhost:{{\d+}}/cart")
1213
.method(HTTPConstants.POST)
1314
.contentType(ContentType.APPLICATION_FORM_URLENCODED)
14-
.rawParam("productId", "${productId#2}"),
15+
.param("productId", "${productId#2}"),
1516
httpSampler("/cart-{{\d+}}", "http://localhost:{{\d+}}/cart")
1617
.children(
1718
regexExtractor("productId#4", "name=\"productId\" value=\"([^\"]+)\"")

jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/http/DslHttpDefaults.java

Lines changed: 197 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,20 @@
99
import org.apache.jmeter.config.ConfigTestElement;
1010
import org.apache.jmeter.protocol.http.config.gui.HttpDefaultsGui;
1111
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
12+
import org.apache.jmeter.protocol.http.sampler.HTTPSamplerProxy;
1213
import org.apache.jmeter.testelement.TestElement;
1314
import org.apache.jmeter.testelement.property.TestElementProperty;
15+
import org.apache.jorphan.collections.HashTree;
1416
import us.abstracta.jmeter.javadsl.codegeneration.MethodCall;
1517
import us.abstracta.jmeter.javadsl.codegeneration.MethodCallContext;
1618
import us.abstracta.jmeter.javadsl.codegeneration.MethodCallContext.MethodCallContextEndListener;
1719
import us.abstracta.jmeter.javadsl.codegeneration.MethodParam;
1820
import us.abstracta.jmeter.javadsl.codegeneration.SingleGuiClassCallBuilder;
1921
import us.abstracta.jmeter.javadsl.codegeneration.TestElementParamBuilder;
22+
import us.abstracta.jmeter.javadsl.codegeneration.params.BoolParam;
2023
import us.abstracta.jmeter.javadsl.codegeneration.params.EncodingParam;
24+
import us.abstracta.jmeter.javadsl.core.BuildTreeContext;
25+
import us.abstracta.jmeter.javadsl.core.BuildTreeContext.TreeContextEndListener;
2126
import us.abstracta.jmeter.javadsl.core.configs.BaseConfigElement;
2227
import us.abstracta.jmeter.javadsl.http.DslBaseHttpSampler.BaseHttpSamplerCodeBuilder;
2328
import us.abstracta.jmeter.javadsl.http.DslHttpSampler.HttpClientImpl;
@@ -48,6 +53,7 @@ public class DslHttpDefaults extends BaseConfigElement {
4853
protected String proxyUser;
4954
protected String proxyPassword;
5055
protected HttpClientImpl clientImpl;
56+
protected Boolean followRedirects;
5157

5258
public DslHttpDefaults() {
5359
super("HTTP Request Defaults", HttpDefaultsGui.class);
@@ -169,6 +175,20 @@ public DslHttpDefaults encoding(Charset encoding) {
169175
return this;
170176
}
171177

178+
/**
179+
* Specifies if by default HTTP redirects should be automatically followed (a new request
180+
* automatically created) when detected, or not.
181+
*
182+
* @param enable specifies whether to enable or disable automatic redirections by defaults. When
183+
* not set then the default is true.
184+
* @return the config element for further configuration or usage.
185+
* @since 1.9
186+
*/
187+
public DslHttpDefaults followRedirects(boolean enable) {
188+
this.followRedirects = enable;
189+
return this;
190+
}
191+
172192
/**
173193
* Allows enabling automatic download of HTML embedded resources (images, iframes, etc) by
174194
* default.
@@ -339,7 +359,8 @@ public DslHttpDefaults clientImpl(HttpClientImpl clientImpl) {
339359
* <p>
340360
* When using reset connection for each thread consider tuning OS like explained in "Configure
341361
* your environment" section of
342-
* <a href="https://medium.com/@chientranthien/how-to-generate-high-load-benchmark-with-jmeter-80e828a67592">this article</a>.
362+
* <a href="https://medium.com/@chientranthien/how-to-generate-high-load-benchmark-with-jmeter-80e828a67592">this
363+
* article</a>.
343364
* <p>
344365
* <b>Warning:</b> This setting is applied at JVM level, which means that it will affect the
345366
* entire test plan and potentially other test plans running in the same JVM instance.
@@ -403,6 +424,62 @@ protected TestElement buildTestElement() {
403424
return ret;
404425
}
405426

427+
@Override
428+
public HashTree buildTreeUnder(HashTree parent, BuildTreeContext context) {
429+
HashTree ret = super.buildTreeUnder(parent, context);
430+
if (followRedirects != null) {
431+
buildEndListener(context.getParent()).followRedirects = followRedirects;
432+
}
433+
return ret;
434+
}
435+
436+
private static DefaultsTreeContextEndListener buildEndListener(BuildTreeContext parentCtx) {
437+
return parentCtx.getOrCreateEntry(DslHttpDefaults.class.getName(),
438+
() -> new DefaultsTreeContextEndListener(parentCtx));
439+
}
440+
441+
/* Set as protected to avoid this method to appear to users while creating a test plan,
442+
but still be visible for DslHttpSampler */
443+
protected static void addPendingFollowRedirectsElement(HTTPSamplerProxy element,
444+
BuildTreeContext context) {
445+
buildEndListener(context.getRoot()).pendingFollowRedirectsElements.add(element);
446+
}
447+
448+
private static class DefaultsTreeContextEndListener implements TreeContextEndListener {
449+
450+
private Boolean followRedirects;
451+
private final List<HTTPSamplerProxy> pendingFollowRedirectsElements = new ArrayList<>();
452+
453+
private DefaultsTreeContextEndListener(BuildTreeContext context) {
454+
context.addEndListener(this);
455+
}
456+
457+
@Override
458+
public void execute(BuildTreeContext context, HashTree tree) {
459+
if (followRedirects != null) {
460+
setChildrenToFollowRedirects(tree);
461+
} else {
462+
pendingFollowRedirectsElements.stream()
463+
.filter(e -> e.getPropertyAsString(HTTPSamplerBase.FOLLOW_REDIRECTS).isEmpty())
464+
.forEach(e -> e.setFollowRedirects(true));
465+
}
466+
}
467+
468+
private void setChildrenToFollowRedirects(HashTree tree) {
469+
tree.forEach((key, value) -> {
470+
if (key instanceof HTTPSamplerProxy) {
471+
HTTPSamplerProxy sampler = (HTTPSamplerProxy) key;
472+
if (sampler.getPropertyAsString(HTTPSamplerBase.FOLLOW_REDIRECTS).isEmpty()) {
473+
sampler.setFollowRedirects(followRedirects);
474+
}
475+
} else {
476+
setChildrenToFollowRedirects(value);
477+
}
478+
});
479+
}
480+
481+
}
482+
406483
public static class CodeBuilder extends SingleGuiClassCallBuilder {
407484

408485
public CodeBuilder(List<Method> builderMethods) {
@@ -437,95 +514,144 @@ protected MethodCall buildMethodCall(MethodCallContext context) {
437514

438515
public void registerDependency(MethodCallContext context) {
439516
MethodCallContext parentCtx = context.getParent();
440-
BuildContextEndListener endListener = parentCtx.computeEntryIfAbsent(DslHttpDefaults.class,
441-
() -> new BuildContextEndListener(parentCtx));
442-
String encoding = context.getTestElement()
443-
.getPropertyAsString(HTTPSamplerBase.CONTENT_ENCODING);
444-
boolean isDefaultCandidate = context.getTestElement()
445-
.getPropertyAsString(TestElement.GUI_CLASS)
517+
DefaultsMethodContextEndListener endListener = parentCtx.computeEntryIfAbsent(
518+
DslHttpDefaults.class, () -> new DefaultsMethodContextEndListener(parentCtx));
519+
TestElement testElement = context.getTestElement();
520+
String encoding = testElement.getPropertyAsString(HTTPSamplerBase.CONTENT_ENCODING);
521+
boolean isDefaultCandidate = testElement.getPropertyAsString(TestElement.GUI_CLASS)
446522
.equals(HttpDefaultsGui.class.getName());
447523
endListener.registerEncoding(encoding, context, isDefaultCandidate);
524+
boolean followRedirects =
525+
isDefaultCandidate || testElement.getPropertyAsBoolean(HTTPSamplerBase.FOLLOW_REDIRECTS);
526+
endListener.registerFollowRedirect(followRedirects, context, isDefaultCandidate);
448527
}
449528

450-
private static class BuildContextEndListener implements MethodCallContextEndListener {
529+
private static class DefaultsMethodContextEndListener implements MethodCallContextEndListener {
451530

452-
private final List<EncodedCall> calls = new ArrayList<>();
531+
private final List<EncodedCall> encodedCalls = new ArrayList<>();
532+
private final List<RedirectableCall> redirectableCalls = new ArrayList<>();
453533

454-
private BuildContextEndListener(MethodCallContext parentCtx) {
534+
private DefaultsMethodContextEndListener(MethodCallContext parentCtx) {
455535
parentCtx.addEndListener(this);
456536
}
457537

458538
public void registerEncoding(String encoding, MethodCallContext ret,
459539
boolean isDefaultCandidate) {
460-
calls.add(new EncodedCall(encoding, ret, isDefaultCandidate));
540+
encodedCalls.add(new EncodedCall(encoding, ret, isDefaultCandidate));
541+
}
542+
543+
public void registerFollowRedirect(boolean followRedirects, MethodCallContext ret,
544+
boolean isDefaultCandidate) {
545+
redirectableCalls.add(new RedirectableCall(followRedirects, ret, isDefaultCandidate));
461546
}
462547

463548
@Override
464549
public void execute(MethodCallContext ctx, MethodCall ret) {
465-
final EncodedCall defaultsCall = findDefaultsCall(ctx);
550+
solveDefaultEncoding(ctx);
551+
solveDefaultFollowRedirects(ctx);
552+
}
553+
554+
private void solveDefaultEncoding(MethodCallContext ctx) {
555+
EncodedCall defaultsCall = findDefaultEncodingCall(ctx);
466556
if (defaultsCall != null) {
467-
calls.stream()
557+
encodedCalls.stream()
468558
.filter(c -> c != defaultsCall && !c.isDefaultCandidate
469559
&& c.encoding.equals(defaultsCall.encoding))
470560
.forEach(EncodedCall::removeEncoding);
471561
}
472-
MethodCallContext parentCtx = ctx.getParent();
473-
if (parentCtx != null) {
474-
BuildContextEndListener parentListener = parentCtx.computeEntryIfAbsent(
475-
DslHttpDefaults.class, () -> new BuildContextEndListener(parentCtx));
562+
DefaultsMethodContextEndListener parentListener = buildParentContextEndListener(ctx);
563+
if (parentListener != null) {
476564
parentListener.registerEncoding(defaultsCall != null ? defaultsCall.encoding : "",
477565
defaultsCall != null ? defaultsCall.ctx : null, false);
478566
}
479567
}
480568

481-
private EncodedCall findDefaultsCall(MethodCallContext ctx) {
482-
if (calls.size() == 1) {
483-
return calls.get(0);
569+
private static DefaultsMethodContextEndListener buildParentContextEndListener(
570+
MethodCallContext ctx) {
571+
MethodCallContext parentCtx = ctx.getParent();
572+
return (parentCtx != null) ? parentCtx.computeEntryIfAbsent(DslHttpDefaults.class,
573+
() -> new DefaultsMethodContextEndListener(parentCtx)) : null;
574+
}
575+
576+
private EncodedCall findDefaultEncodingCall(MethodCallContext ctx) {
577+
if (encodedCalls.size() == 1) {
578+
return encodedCalls.get(0);
484579
}
485-
EncodedCall defaultsCall = calls.stream()
580+
EncodedCall ret = encodedCalls.stream()
486581
.filter(c -> c.isDefaultCandidate && !c.encoding.isEmpty())
487582
.findAny()
488583
.orElse(null);
489-
if (defaultsCall != null) {
490-
return defaultsCall;
584+
if (ret != null) {
585+
return ret;
491586
}
492-
String someEncoding = calls.stream()
587+
String someEncoding = encodedCalls.stream()
493588
.filter(c -> !c.encoding.isEmpty())
494589
.map(c -> c.encoding)
495590
.findAny()
496591
.orElse(null);
497-
if (someEncoding != null && calls.stream()
592+
if (someEncoding != null && encodedCalls.stream()
498593
.allMatch(c -> c.isDefaultCandidate || c.encoding.equals(someEncoding))) {
499-
defaultsCall = calls.stream()
594+
ret = encodedCalls.stream()
500595
.filter(c -> c.isDefaultCandidate)
501596
.findAny()
502597
.orElse(null);
503-
if (defaultsCall == null) {
504-
defaultsCall = buildDefaultsCall(ctx);
598+
if (ret == null) {
599+
ret = new EncodedCall("", buildDefaultsCall(ctx), true);
505600
}
506-
defaultsCall.setEncoding(someEncoding);
507-
return defaultsCall;
601+
ret.setEncoding(someEncoding);
602+
return ret;
508603
}
509604
return null;
510605
}
511606

512-
private EncodedCall buildDefaultsCall(MethodCallContext ctx) {
513-
MethodCallContext child = ctx.prependChild(
514-
new DslHttpDefaults().buildConfiguredTestElement(), null);
515-
return new EncodedCall("", child, true);
607+
private MethodCallContext buildDefaultsCall(MethodCallContext ctx) {
608+
return ctx.prependChild(new DslHttpDefaults().buildConfiguredTestElement(), null);
609+
}
610+
611+
private void solveDefaultFollowRedirects(MethodCallContext ctx) {
612+
RedirectableCall defaultsCall = findDefaultRedirectableCall(ctx);
613+
if (defaultsCall != null) {
614+
redirectableCalls.stream()
615+
.filter(c -> c != defaultsCall)
616+
.forEach(RedirectableCall::removeFollowRedirects);
617+
}
618+
DefaultsMethodContextEndListener parentListener = buildParentContextEndListener(ctx);
619+
if (parentListener != null) {
620+
parentListener.registerFollowRedirect(
621+
defaultsCall == null || defaultsCall.followRedirects,
622+
defaultsCall != null ? defaultsCall.ctx : null, false);
623+
}
624+
}
625+
626+
private RedirectableCall findDefaultRedirectableCall(MethodCallContext ctx) {
627+
if (redirectableCalls.size() == 1) {
628+
return redirectableCalls.get(0);
629+
}
630+
if (redirectableCalls.stream().allMatch(c -> c.isDefaultCandidate || !c.followRedirects)) {
631+
RedirectableCall ret = redirectableCalls.stream()
632+
.filter(c -> c.isDefaultCandidate)
633+
.findAny()
634+
.orElse(null);
635+
if (ret == null) {
636+
ret = new RedirectableCall(false, buildDefaultsCall(ctx), true);
637+
}
638+
ret.setFollowRedirects(false);
639+
return ret;
640+
}
641+
return null;
516642
}
517643

518644
}
519645

520646
}
521647

522-
public static class EncodedCall {
648+
private static class EncodedCall {
523649

524650
private String encoding;
525651
private final MethodCallContext ctx;
526652
private final boolean isDefaultCandidate;
527653

528-
public EncodedCall(String encoding, MethodCallContext ctx, boolean isDefaultCandidate) {
654+
private EncodedCall(String encoding, MethodCallContext ctx, boolean isDefaultCandidate) {
529655
this.encoding = encoding;
530656
this.ctx = ctx;
531657
this.isDefaultCandidate = isDefaultCandidate;
@@ -537,12 +663,41 @@ public void setEncoding(String encoding) {
537663
}
538664

539665
public void removeEncoding() {
540-
MethodCall methodCall = ctx.getMethodCall();
541-
methodCall.unchain("encoding");
542-
if (methodCall.getReturnType() == DslHttpDefaults.class && methodCall.chainSize() == 0) {
543-
ctx.getParent().getMethodCall()
544-
.replaceChild(methodCall, MethodCall.emptyCall());
545-
}
666+
ctx.getMethodCall().unchain("encoding");
667+
removeEmptyDefaultsCall(ctx);
668+
}
669+
670+
}
671+
672+
private static void removeEmptyDefaultsCall(MethodCallContext ctx) {
673+
MethodCall methodCall = ctx.getMethodCall();
674+
if (methodCall.getReturnType() == DslHttpDefaults.class && methodCall.chainSize() == 0) {
675+
ctx.getParent().getMethodCall()
676+
.replaceChild(methodCall, MethodCall.emptyCall());
677+
}
678+
}
679+
680+
private static class RedirectableCall {
681+
682+
private boolean followRedirects;
683+
private final MethodCallContext ctx;
684+
private final boolean isDefaultCandidate;
685+
686+
private RedirectableCall(boolean followRedirects, MethodCallContext ctx,
687+
boolean isDefaultCandidate) {
688+
this.followRedirects = followRedirects;
689+
this.ctx = ctx;
690+
this.isDefaultCandidate = isDefaultCandidate;
691+
}
692+
693+
public void setFollowRedirects(boolean followRedirects) {
694+
this.followRedirects = followRedirects;
695+
ctx.getMethodCall().chain("followRedirects", new BoolParam(followRedirects, true));
696+
}
697+
698+
public void removeFollowRedirects() {
699+
ctx.getMethodCall().unchain("followRedirects");
700+
removeEmptyDefaultsCall(ctx);
546701
}
547702

548703
}

0 commit comments

Comments
 (0)