From 0d2ab95596586b2b902b788fa8c55bdb1d6775d7 Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Tue, 22 Jul 2025 16:57:39 +0300 Subject: [PATCH 1/2] feat: allow expressions for TestElement.enabled property Fixes https://github.com/apache/jmeter/issues/6006 --- .../apache/jmeter/threads/JMeterThread.java | 22 +++++-- .../jmeter/threads/ListenerNotifier.java | 3 + .../control/EnabledWithVariablesTest.kt | 61 +++++++++++++++++++ 3 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 src/functions/src/test/kotlin/org/apache/jmeter/control/EnabledWithVariablesTest.kt diff --git a/src/core/src/main/java/org/apache/jmeter/threads/JMeterThread.java b/src/core/src/main/java/org/apache/jmeter/threads/JMeterThread.java index 01a74581881..348ea43f0bd 100644 --- a/src/core/src/main/java/org/apache/jmeter/threads/JMeterThread.java +++ b/src/core/src/main/java/org/apache/jmeter/threads/JMeterThread.java @@ -264,7 +264,10 @@ public void run() { iterationListener = initRun(threadContext); while (running) { Sampler sam = threadGroupLoopController.next(); - while (running && sam != null) { + for (; running && sam != null; sam = threadGroupLoopController.next()) { + if (!sam.isEnabled()) { + continue; + } processSampler(sam, null, threadContext); threadContext.cleanAfterSample(); @@ -296,11 +299,8 @@ public void run() { } } threadContext.setTestLogicalAction(TestLogicalAction.CONTINUE); - sam = null; setLastSampleOk(threadContext.getVariables(), true); - } - else { - sam = threadGroupLoopController.next(); + break; } } @@ -898,6 +898,9 @@ private void stopThread() { private static void checkAssertions(List assertions, SampleResult parent, JMeterContext threadContext) { for (Assertion assertion : assertions) { + if (!((TestElement) assertion).isEnabled()) { + continue; + } TestBeanHelper.prepare((TestElement) assertion); if (assertion instanceof AbstractScopedAssertion scopedAssertion) { String scope = scopedAssertion.fetchScope(); @@ -965,6 +968,9 @@ private static void processAssertion(SampleResult result, Assertion assertion) { private static void runPostProcessors(List extractors) { for (PostProcessor ex : extractors) { + if (!((TestElement) ex).isEnabled()) { + continue; + } TestBeanHelper.prepare((TestElement) ex); ex.process(); } @@ -972,6 +978,9 @@ private static void runPostProcessors(List extractors) private static void runPreProcessors(List preProcessors) { for (PreProcessor ex : preProcessors) { + if (!((TestElement) ex).isEnabled()) { + continue; + } if (log.isDebugEnabled()) { log.debug("Running preprocessor: {}", ((AbstractTestElement) ex).getName()); } @@ -992,6 +1001,9 @@ private static void runPreProcessors(List preProcessors) private void delay(List timers) { long totalDelay = 0; for (Timer timer : timers) { + if (!((TestElement) timer).isEnabled()) { + continue; + } TestBeanHelper.prepare((TestElement) timer); long delay = timer.delay(); if (APPLY_TIMER_FACTOR && timer.isModifiable()) { diff --git a/src/core/src/main/java/org/apache/jmeter/threads/ListenerNotifier.java b/src/core/src/main/java/org/apache/jmeter/threads/ListenerNotifier.java index 22beba470f1..7cbdfc6eb98 100644 --- a/src/core/src/main/java/org/apache/jmeter/threads/ListenerNotifier.java +++ b/src/core/src/main/java/org/apache/jmeter/threads/ListenerNotifier.java @@ -54,6 +54,9 @@ public class ListenerNotifier implements Serializable { public void notifyListeners(SampleEvent res, List listeners) { for (SampleListener sampleListener : listeners) { try { + if (!((TestElement) sampleListener).isEnabled()) { + continue; + } TestBeanHelper.prepare((TestElement) sampleListener); sampleListener.sampleOccurred(res); } catch (RuntimeException e) { diff --git a/src/functions/src/test/kotlin/org/apache/jmeter/control/EnabledWithVariablesTest.kt b/src/functions/src/test/kotlin/org/apache/jmeter/control/EnabledWithVariablesTest.kt new file mode 100644 index 00000000000..3d9ebe257e3 --- /dev/null +++ b/src/functions/src/test/kotlin/org/apache/jmeter/control/EnabledWithVariablesTest.kt @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to you under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.jmeter.control + +import org.apache.jmeter.junit.JMeterTestCase +import org.apache.jmeter.sampler.DebugSampler +import org.apache.jmeter.test.assertions.executePlanAndCollectEvents +import org.apache.jmeter.threads.ThreadGroup +import org.apache.jmeter.treebuilder.TreeBuilder +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.seconds + +class EnabledWithVariablesTest : JMeterTestCase() { + fun TreeBuilder.oneThread(loops: Int, body: ThreadGroup.() -> Unit) { + ThreadGroup::class { + numThreads = 1 + rampUp = 0 + setSamplerController( + LoopController().apply { + this.loops = loops + } + ) + body() + } + } + + @Test + fun `sampler with conditional enable function`() { + val events = executePlanAndCollectEvents(5.seconds) { + oneThread(loops = 4) { + DebugSampler::class { + name = "Conditionally enabled: \${__jm____idx}" + props { + it[enabled] = "\${__javaScript(vars.get('__jm____idx')%2==1)}" + } + } + } + } + Assertions.assertEquals( + "[Conditionally enabled: 1, Conditionally enabled: 3]", + events.map { it.result.sampleLabel }.toString(), + "Test should complete within reasonable time, and the test has 2 debug samplers, so we expect 2 events" + ) + } +} From 1e4051e43110234314aa5bfdfc39dbffa9ba5869 Mon Sep 17 00:00:00 2001 From: Vladimir Sitnikov Date: Wed, 23 Jul 2025 19:29:31 +0300 Subject: [PATCH 2/2] WIP: UI --- .../gui/AbstractJMeterGuiComponent.java | 32 +++++++++---------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/src/core/src/main/java/org/apache/jmeter/gui/AbstractJMeterGuiComponent.java b/src/core/src/main/java/org/apache/jmeter/gui/AbstractJMeterGuiComponent.java index 4c6a79144e0..1df91cbdd21 100644 --- a/src/core/src/main/java/org/apache/jmeter/gui/AbstractJMeterGuiComponent.java +++ b/src/core/src/main/java/org/apache/jmeter/gui/AbstractJMeterGuiComponent.java @@ -41,6 +41,7 @@ import org.apache.jmeter.testelement.TestElementSchema; import org.apache.jmeter.util.JMeterUtils; import org.apache.jmeter.visualizers.Printable; +import org.apache.jorphan.gui.JEditableCheckBox; import org.apache.jorphan.gui.JFactory; import org.apiguardian.api.API; import org.slf4j.Logger; @@ -70,9 +71,6 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete /** Logging */ private static final Logger log = LoggerFactory.getLogger(AbstractJMeterGuiComponent.class); - /** Flag indicating whether this component is enabled. */ - private boolean enabled = true; - /** * A GUI panel containing the name of this component. * @deprecated use {@link #getName()} or {@link AbstractJMeterGuiComponent#createTitleLabel()} for better alignment of the fields @@ -84,6 +82,10 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete private final JTextArea commentField = JFactory.tabMovesFocus(new JTextArea()); + private final JBooleanPropertyEditor enabled = new JBooleanPropertyEditor( + TestElementSchema.INSTANCE.getEnabled(), + JMeterUtils.getResString("enable")); + /** * Stores a collection of property editors, so GuiCompoenent can have default implementations that * update the UI fields based on {@link TestElement} properties and vice versa. @@ -99,6 +101,7 @@ public abstract class AbstractJMeterGuiComponent extends JPanel implements JMete protected AbstractJMeterGuiComponent() { namePanel = new NamePanel(); init(); + bindingGroup.add(enabled); } /** @@ -127,7 +130,7 @@ public void setComment(String comment) { */ @Override public boolean isEnabled() { - return enabled; + return enabled.getValue().equals(JEditableCheckBox.Value.of(true)); } /** @@ -137,7 +140,7 @@ public boolean isEnabled() { @Override public void setEnabled(boolean enabled) { log.debug("Setting enabled: {}", enabled); - this.enabled = enabled; + this.enabled.setValue(JEditableCheckBox.Value.of(enabled)); } /** @@ -211,7 +214,6 @@ protected Component createTitleLabel() { @Override public void configure(TestElement element) { setName(element.getName()); - enabled = element.isEnabled(); commentField.setText(element.getComment()); bindingGroup.updateUi(element); } @@ -225,7 +227,6 @@ public void configure(TestElement element) { @Override public void clearGui() { initGui(); - enabled = true; } private void initGui() { @@ -241,7 +242,7 @@ private void init() { @API(status = EXPERIMENTAL, since = "5.6.3") public void modifyTestElement(TestElement element) { JMeterGUIComponent.super.modifyTestElement(element); - modifyTestElementEnabledAndComment(element); + modifyTestElementComment(element); bindingGroup.updateElement(element); } @@ -264,7 +265,10 @@ protected void configureTestElement(TestElement mc) { TestElementSchema schema = TestElementSchema.INSTANCE; mc.set(schema.getGuiClass(), getClass()); mc.set(schema.getTestClass(), mc.getClass()); - modifyTestElementEnabledAndComment(mc); + modifyTestElementComment(mc); + // This stores the state of the TestElement + log.debug("setting element to enabled: {}", enabled.getValue()); + enabled.updateElement(mc); } /** @@ -272,14 +276,7 @@ protected void configureTestElement(TestElement mc) { * * @param mc test element */ - private void modifyTestElementEnabledAndComment(TestElement mc) { - // This stores the state of the TestElement - log.debug("setting element to enabled: {}", enabled); - // We can skip storing "enabled" state if it's true, as it's default value. - // JMeter removes disabled elements early from the tree, so configuration elements - // with enabled=false (~HTTP Request Defaults) can't unexpectedly override the regular ones - // like HTTP Request. - mc.set(TestElementSchema.INSTANCE.getEnabled(), enabled ? null : Boolean.FALSE); + private void modifyTestElementComment(TestElement mc) { // Note: we can't use editors for "comments" as getComments() is not a final method, so plugins might // override it and provide a different implementation. mc.setComment(StringUtils.defaultIfEmpty(getComment(), null)); @@ -305,6 +302,7 @@ protected Container makeTitlePanel() { commentField.setWrapStyleWord(true); commentField.setLineWrap(true); titlePanel.add(commentField); + titlePanel.add(enabled, "span 2"); // Note: VerticalPanel has a workaround for Box layout which aligns elements, so we can't // use trivial JPanel.